alchemylab 1.4.1 → 1.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +130 -103
- package/dist/http-server.js +63 -46
- package/dist/index.js +621 -604
- package/dist/mcp-direct.js +591 -574
- package/package.json +2 -2
- package/dist/640.js +0 -14
package/dist/cli.js
CHANGED
|
@@ -298,28 +298,35 @@ Object.defineProperty(exports,"__esModule",{value:!0}),exports.partMeta=function
|
|
|
298
298
|
return{...e,_meta:{...s,alchemy:{...o,...r,kind:t}}}},exports.readAlchemyMeta=function(e){return e?._meta?.alchemy}},2366(e,exports,t){"use strict"
|
|
299
299
|
var r=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}}
|
|
300
300
|
Object.defineProperty(exports,"__esModule",{value:!0}),exports.AlchemyActionServer=void 0
|
|
301
|
-
const s=r(t(7252)),o=r(t(8577)),n=t(
|
|
302
|
-
class
|
|
303
|
-
const r=(0,
|
|
304
|
-
this.apiClient=new
|
|
305
|
-
return e.use(((e,t,r)=>{t.setHeader("Access-Control-Allow-Private-Network","true"),r()})),e.use((0,o.default)()),e.use(s.default.json({limit:"10mb"})),e.get("/health",this.handleHealth.bind(this)),e.get("/api/
|
|
301
|
+
const s=r(t(7252)),o=r(t(8577)),n=t(9896),i=t(5959),a=t(2640),c=t(4192),l=t(1152),d=r(t(6928)),u=r(t(9896)),h=r(t(857)),g=t(5607),p=t(8984),m=t(8524),f=t(8824),y=t(2096),v=t(7210),w=t(7227),b=t(5419),I=t(3962),E=t(2928),C=t(6563),S=t(1401),k=t(1292),T=t(8148),_=t(8463),j=t(489),A=t(3807),P=t(7478),M=t(5317),O=t(355),R=t(833)
|
|
302
|
+
class D{updateAuthToken(e){this.options.authToken=e,this.apiClient.authToken=e,this.configurer.authToken=e}constructor(e,t){this.directReportDedup=new Map,this.activeMcpCalls=new Map,this.pendingAgentResync=!1,this.settingsSyncCounter=0,this.versionWarnings=[],this.updateBlocked=!1,this._pendingUpdateBlock=!1,this.isPolling=!1,this.isProcessingAction=!1,this.openCodeUrlCache=null,this.openCodePort=null,this.proxyAbortControllers=new Map,this.trackedChildPids=new Set,this.sseEventBuffer=[],this.droppedEventCount=0,this.firstDropAt=0,this.hasRemoteEventConsumers=!1,this.cachedProjectConfig=null,this.logger=e,this.options=t,this.port=t.port||3456,this.startTime=new Date
|
|
303
|
+
const r=(0,l.createMetricsCollector)(!1)
|
|
304
|
+
this.apiClient=new c.BaseApiClient(t.remoteServerUrl,e,r,t.authToken,{timeout:35e3}),this.configurer=new p.MCPClientConfigurer(e,t.remoteServerUrl,t.authToken),this.cliBridge=new m.CLIBridgeWebSocket(e),(0,m.setCLIBridge)(this.cliBridge),this.hookExecutor=new v.HookExecutor(e,"",t.projectPath,this.apiClient,{cachedDefaultModel:this.cachedDefaultModel,trackChildPid:e=>this.trackedChildPids.add(e)}),this.app=this.createApp()}createApp(){const e=(0,s.default)()
|
|
305
|
+
return e.use(((e,t,r)=>{t.setHeader("Access-Control-Allow-Private-Network","true"),r()})),e.use((0,o.default)()),e.use(s.default.json({limit:"10mb"})),e.get("/health",this.handleHealth.bind(this)),e.get("/api/commands/running",(async(e,t)=>{try{const e=16384,r=await Promise.all((0,i.listEntries)().map((async t=>{let r,s="",o=0,i=t.finishedAt??t.startedAt
|
|
306
|
+
try{const a=await n.promises.stat(t.logPath)
|
|
307
|
+
o=a.size,i=a.mtime.toISOString(),r=await n.promises.open(t.logPath,"r")
|
|
308
|
+
const c=Math.max(0,o-e),l=Buffer.alloc(o-c)
|
|
309
|
+
await r.read(l,0,l.length,c),s=l.toString("utf-8").split("\n").slice(-50).join("\n")}catch{}finally{await(r?.close().catch((()=>{})))}return{...t,tail:s,sizeBytes:o,modifiedAt:i}})))
|
|
310
|
+
t.json({success:!0,data:r})}catch(e){t.status(500).json({success:!1,error:(0,f.getErrorMessage)(e)})}})),e.post("/api/commands/start",(async(e,t)=>{try{const{id:r,name:s,command:o,cwd:n,sessionId:a,startedAt:c,logPath:l}=e.body??{}
|
|
311
|
+
if(!(r&&s&&o&&n&&c&&l))return void t.status(400).json({success:!1,error:"missing required fields"});(0,i.addStart)({id:r,name:s,command:o,cwd:n,sessionId:a,startedAt:c,logPath:l}),t.json({success:!0})}catch(e){t.status(500).json({success:!1,error:(0,f.getErrorMessage)(e)})}})),e.post("/api/commands/end",(async(e,t)=>{try{const{id:r,exitCode:s,timedOut:o,finishedAt:n}=e.body??{}
|
|
312
|
+
if(!r||"number"!=typeof s||!n)return void t.status(400).json({success:!1,error:"missing required fields"});(0,i.markEnd)({id:r,exitCode:s,timedOut:!!o,finishedAt:n}),t.json({success:!0})}catch(e){t.status(500).json({success:!1,error:(0,f.getErrorMessage)(e)})}})),e.get("/api/auth-status",this.handleAuthStatus.bind(this)),e.get("/status",this.handleStatus.bind(this)),e.get("/opencode",this.handleOpenCodeConfig.bind(this)),e.post("/sync-agents",this.handleSyncAgents.bind(this)),e.post("/api/generate-journal",this.handleGenerateJournal.bind(this)),(0,O.registerHooksRoutes)(e,{apiClient:this.apiClient,logger:this.logger,getProjectId:()=>this.projectId}),e.get("/api/project-settings",((e,t)=>{t.json({success:!0,data:{defaultModel:this.cachedDefaultModel||null}})})),e.post("/api/sync-settings",(async(e,t)=>{await this.syncProjectSettings(),t.json({success:!0,data:{defaultModel:this.cachedDefaultModel||null}})})),e.get("/api/agents/by-name/:agentName",this.handleGetAgentByName.bind(this)),e.get("/api/sessions/:sessionId/agent-type",this.handleGetSessionAgentType.bind(this)),e.get("/api/sessions/:sessionId/parent",this.handleGetSessionParent.bind(this)),e.get("/api/sessions/:sessionId/ancestors",(async(e,t)=>{try{const{sessionId:r}=e.params,s=this.projectId
|
|
306
313
|
if(!s)return void t.status(400).json({success:!1,error:"No project"})
|
|
307
314
|
const o=await this.apiClient.get(`/api/projects/${s}/cli-sessions/external/opencode/${encodeURIComponent(r)}/ancestors`),n=(o?.data||o||[]).map((e=>({externalSessionId:e.externalSessionId,agentType:e.agentType,title:e.title})))
|
|
308
|
-
t.json({success:!0,ancestors:n})}catch(e){t.status(500).json({success:!1,error:(0,
|
|
309
|
-
r.json({success:!0,data:e()})}catch(e){r.status(500).json({success:!1,error:(0,
|
|
315
|
+
t.json({success:!0,ancestors:n})}catch(e){t.status(500).json({success:!1,error:(0,f.getErrorMessage)(e)})}})),e.get("/api/registry/projects",(async(e,r)=>{try{const{listLive:e}=await Promise.resolve().then(t.bind(t,2640))
|
|
316
|
+
r.json({success:!0,data:e()})}catch(e){r.status(500).json({success:!1,error:(0,f.getErrorMessage)(e)})}})),e.post("/api/mcp/tool-call-event",((e,t)=>{const{action:r,callId:s}=e.body||{}
|
|
310
317
|
!s||"start"!==r&&"end"!==r?t.status(400).json({success:!1,error:"action must be start|end, callId required"}):(this.handleMcpToolCallEvent(r,s+""),t.json({success:!0}))})),e.post("/api/hook-event",(async(e,t)=>{try{const{eventType:r,sessionId:s,variables:o,timing:n}=e.body
|
|
311
318
|
if(!r)return void t.status(400).json({success:!1,error:"eventType is required"})
|
|
312
319
|
const i=await this.hookExecutor.processEvent({eventType:r,sessionId:s||"",variables:o||{},timing:n})
|
|
313
|
-
t.json({success:!0,data:i})}catch(e){this.logger.error("Hook event processing failed",{error:(0,
|
|
320
|
+
t.json({success:!0,data:i})}catch(e){this.logger.error("Hook event processing failed",{error:(0,f.getErrorMessage)(e)}),t.status(500).json({success:!1,error:(0,f.getErrorMessage)(e)})}})),e.post("/api/tool-event",(async(e,t)=>{try{const{toolName:r,sessionId:s,input:o,output:n,timing:i}=e.body||{}
|
|
314
321
|
if(!r||"before"!==i&&"after"!==i)return void t.status(400).json({success:!1,error:"toolName and timing ('before'|'after') are required"})
|
|
315
|
-
const a=(0,
|
|
322
|
+
const a=(0,w.parseToolEvents)({toolName:r,sessionId:s||"",input:o,output:n,timing:i})
|
|
316
323
|
let c=0
|
|
317
324
|
for(const e of a)c+=(await this.hookExecutor.processEvent({eventType:e.eventType,sessionId:s||"",variables:e.variables,timing:e.timing})).triggered
|
|
318
|
-
this.logger.info("tool-event received",{toolName:r,timing:i,sessionId:s||"",events:a.length,triggered:c}),t.json({success:!0,data:{events:a.length,triggered:c}})}catch(e){this.logger.error("tool-event processing failed",{error:(0,
|
|
325
|
+
this.logger.info("tool-event received",{toolName:r,timing:i,sessionId:s||"",events:a.length,triggered:c}),t.json({success:!0,data:{events:a.length,triggered:c}})}catch(e){this.logger.error("tool-event processing failed",{error:(0,f.getErrorMessage)(e)}),t.status(500).json({success:!1,error:(0,f.getErrorMessage)(e)})}})),e.post("/api/direct-report",this.handleDirectReport.bind(this)),e.post("/api/cli-bridge/command",this.handleCLIBridgeCommand.bind(this)),e.get("/api/cli-bridge/status",this.handleCLIBridgeStatus.bind(this)),e.post("/api/spawn-session",this.handleSpawnSession.bind(this)),e.post("/api/setup-tests",this.handleSetupTests.bind(this)),e.post("/api/setup-commands",this.handleSetupCommands.bind(this)),e.post("/api/run-tests-now",this.handleRunTestsNow.bind(this)),e.get("/api/branch-head",(async(e,t)=>{try{const r="string"==typeof e.query.requestId?e.query.requestId:void 0,s=("string"==typeof e.query.branch?e.query.branch:void 0)||(r?(0,P.generateRequestBranchName)(r,""):void 0)
|
|
319
326
|
if(!s)return void t.status(400).json({success:!1,error:"requestId or branch required"})
|
|
320
|
-
const o=(await(0,
|
|
321
|
-
t.json({success:!0,data:{branch:s,sha:o}})}catch(e){this.logger.debug("branch-head lookup failed",{error:(0,
|
|
322
|
-
if(!r||"string"!=typeof r)return void t.status(400).json({success:!1,error:"worktreePath required"});(0,
|
|
327
|
+
const o=(await(0,A.simpleGit)(this.options.projectPath).revparse([s])).trim()
|
|
328
|
+
t.json({success:!0,data:{branch:s,sha:o}})}catch(e){this.logger.debug("branch-head lookup failed",{error:(0,f.getErrorMessage)(e)}),t.json({success:!0,data:{branch:null,sha:null}})}})),e.post("/api/release-pool-slot",(async(e,t)=>{try{const{worktreePath:r}=e.body||{}
|
|
329
|
+
if(!r||"string"!=typeof r)return void t.status(400).json({success:!1,error:"worktreePath required"});(0,j.releaseSlot)(r),this.logger.info("Released pool slot via report-back",{worktreePath:r}),t.json({success:!0})}catch(e){this.logger.warn("release-pool-slot failed (non-fatal)",{error:(0,f.getErrorMessage)(e)}),t.status(500).json({success:!1,error:(0,f.getErrorMessage)(e)})}})),e.get("/api/timer-hooks/status",((e,t)=>{const r=this.timerHookScheduler?.getStatus()??{running:!1,trackedHooks:0,inFlight:[],hooks:[]}
|
|
323
330
|
t.json({success:!0,data:r})})),e.post("/api/timer-hooks/:hookId/pause",((e,t)=>{if(!this.timerHookScheduler)return void t.status(503).json({success:!1,error:"Scheduler not running"})
|
|
324
331
|
const r=this.timerHookScheduler.pauseHook(e.params.hookId)
|
|
325
332
|
t.json({success:r})})),e.post("/api/timer-hooks/:hookId/resume",((e,t)=>{if(!this.timerHookScheduler)return void t.status(503).json({success:!1,error:"Scheduler not running"})
|
|
@@ -327,27 +334,27 @@ const r=this.timerHookScheduler.resumeHook(e.params.hookId)
|
|
|
327
334
|
t.json({success:r})})),e.post("/api/timer-hooks/:hookId/stop",((e,t)=>{if(!this.timerHookScheduler)return void t.status(503).json({success:!1,error:"Scheduler not running"})
|
|
328
335
|
const r=this.timerHookScheduler.stopHook(e.params.hookId)
|
|
329
336
|
t.json({success:r})})),e.get("/api/stuck-sessions",this.handleGetStuckSessions.bind(this)),e.post("/api/stuck-sessions/test-inject",this.handleTestInjectStuckSession.bind(this)),e.post("/api/stuck-sessions/:sessionId/retry",this.handleRetryStuckSession.bind(this)),e.get("/api/version-warnings",((e,t)=>{t.json({success:!0,data:this.versionWarnings})})),e.post("/api/self-update",(async(e,r)=>{try{const{execSync:e}=await Promise.resolve().then(t.t.bind(t,5317,23)),s=e("npm root -g",{encoding:"utf8"}).trim()
|
|
330
|
-
try{
|
|
331
|
-
r.json({success:!0,message:"Update started. The CLI will restart when complete."}),o("npm install -g alchemylab@latest",{timeout:12e4},((e,t,r)=>{e?this.logger.error("Self-update failed",{error:e.message,stderr:r}):(this.logger.info("Self-update complete, shutting down for restart",{stdout:t.trim()}),setTimeout((()=>{console.log("\n\x1b[32mUpdate complete. Please restart alchemylab.\x1b[0m\n"),process.exit(0)}),1e3))}))}catch(e){r.json({success:!1,error:(0,
|
|
337
|
+
try{u.default.accessSync(s,u.default.constants.W_OK)}catch{return void r.json({success:!1,error:"Permission denied. Run manually: sudo npm install -g alchemylab@latest"})}const{exec:o}=await Promise.resolve().then(t.t.bind(t,5317,23))
|
|
338
|
+
r.json({success:!0,message:"Update started. The CLI will restart when complete."}),o("npm install -g alchemylab@latest",{timeout:12e4},((e,t,r)=>{e?this.logger.error("Self-update failed",{error:e.message,stderr:r}):(this.logger.info("Self-update complete, shutting down for restart",{stdout:t.trim()}),setTimeout((()=>{console.log("\n\x1b[32mUpdate complete. Please restart alchemylab.\x1b[0m\n"),process.exit(0)}),1e3))}))}catch(e){r.json({success:!1,error:(0,f.getErrorMessage)(e)})}})),e.use("/api/opencode",((e,t)=>this.handleOpenCodeProxy(e,t))),e}handleHealth(e,t){t.json({status:"healthy",uptime:Math.floor((Date.now()-this.startTime.getTime())/1e3),timestamp:(new Date).toISOString()})}async handleAuthStatus(e,t){const r=this.options.tokenManager
|
|
332
339
|
if(r)try{const e=await r.hasValidToken(),s=r.getAuthStatus(),o=e?await r.getToken():null
|
|
333
|
-
t.json({authenticated:e,isPendingAuth:s.isPendingAuth,authUrl:e?void 0:s.authUrl,expiresAt:o?.expiresAt??null})}catch(e){this.logger.warn("auth-status check failed",{error:(0,
|
|
334
|
-
try{if(!
|
|
335
|
-
const e=
|
|
336
|
-
t.json({port:s.port,pid:s.pid,startedAt:s.startedAt,baseUrl:"http://localhost:"+s.port,directory:this.options.projectPath,proxyEnabled:!0})}catch(e){this.logger.error("Failed to read OpenCode config",{error:(0,
|
|
340
|
+
t.json({authenticated:e,isPendingAuth:s.isPendingAuth,authUrl:e?void 0:s.authUrl,expiresAt:o?.expiresAt??null})}catch(e){this.logger.warn("auth-status check failed",{error:(0,f.getErrorMessage)(e)}),t.json({authenticated:!1,isPendingAuth:!1})}else t.json({authenticated:!0,isPendingAuth:!1})}handleStatus(e,t){t.json({status:"running",uptime:Math.floor((Date.now()-this.startTime.getTime())/1e3),startTime:this.startTime.toISOString(),projectId:this.projectId,serverId:this.serverId,isPolling:this.isPolling,port:this.port,timestamp:(new Date).toISOString()})}handleOpenCodeConfig(e,t){const r=d.default.join(this.options.projectPath,".alchemy_lab","opencode.json")
|
|
341
|
+
try{if(!u.default.existsSync(r))return void t.status(404).json({error:"OpenCode config not found",message:"OpenCode serve may not be running"})
|
|
342
|
+
const e=u.default.readFileSync(r,"utf-8"),s=JSON.parse(e)
|
|
343
|
+
t.json({port:s.port,pid:s.pid,startedAt:s.startedAt,baseUrl:"http://localhost:"+s.port,directory:this.options.projectPath,proxyEnabled:!0})}catch(e){this.logger.error("Failed to read OpenCode config",{error:(0,f.getErrorMessage)(e)}),t.status(500).json({error:"Failed to read OpenCode config",message:(0,f.getErrorMessage)(e)})}}updateDefaultModel(e){void 0!==e&&e!==this.cachedDefaultModel&&(this.cachedDefaultModel=e,this.hookExecutor.setDefaultModel(e||""),e&&this.projectId&&this.resyncAgentMarkdowns().catch((e=>this.logger.debug("Background agent markdown re-sync failed",{error:(0,f.getErrorMessage)(e)}))))}handleMcpToolCallEvent(e,t){"start"!==e?(this.activeMcpCalls.delete(t),0===this.activeMcpCalls.size&&this.pendingAgentResync&&(this.pendingAgentResync=!1,this.logger.info("Firing deferred agent resync now that MCP is idle"),this.resyncAgentMarkdowns().catch((e=>this.logger.debug("Deferred resync failed",{error:(0,f.getErrorMessage)(e)}))))):this.activeMcpCalls.set(t,Date.now())}startMcpStaleCleanup(){this.mcpStaleCleanupInterval||(this.mcpStaleCleanupInterval=setInterval((()=>{const e=Date.now()-12e4
|
|
337
344
|
let t=0
|
|
338
345
|
for(const[r,s]of this.activeMcpCalls)e>s&&(this.activeMcpCalls.delete(r),t++)
|
|
339
|
-
t>0&&(this.logger.warn("Cleaned up stale MCP tool-call tracking entries",{removed:t}),0===this.activeMcpCalls.size&&this.pendingAgentResync&&(this.pendingAgentResync=!1,this.resyncAgentMarkdowns().catch((e=>this.logger.debug("Deferred resync after cleanup failed",{error:(0,
|
|
346
|
+
t>0&&(this.logger.warn("Cleaned up stale MCP tool-call tracking entries",{removed:t}),0===this.activeMcpCalls.size&&this.pendingAgentResync&&(this.pendingAgentResync=!1,this.resyncAgentMarkdowns().catch((e=>this.logger.debug("Deferred resync after cleanup failed",{error:(0,f.getErrorMessage)(e)})))))}),6e4))}async resyncAgentMarkdowns(){if(!this.projectId)return
|
|
340
347
|
if(this.activeMcpCalls.size>0)return this.pendingAgentResync=!0,void this.logger.info("Deferring agent resync until MCP tool calls complete",{inFlight:this.activeMcpCalls.size})
|
|
341
348
|
const e=await this.configurer.fetchAgentsFromRemote(this.projectId)
|
|
342
349
|
if(!e?.length)return
|
|
343
|
-
const t=await this.configurer.fetchProjectFromRemote(this.projectId),r=t?.metadata?.globalSystemPrompt,s=
|
|
344
|
-
await
|
|
350
|
+
const t=await this.configurer.fetchProjectFromRemote(this.projectId),r=t?.metadata?.globalSystemPrompt,s=d.default.join(this.options.projectPath,".opencode","agent")
|
|
351
|
+
await u.default.promises.mkdir(s,{recursive:!0})
|
|
345
352
|
const o=this.cachedDefaultModel||"anthropic/claude-opus-4-5"
|
|
346
353
|
for(const t of e){if(!t.enabled)continue
|
|
347
354
|
const e=this.configurer.generateAgentMarkdown(t,"alchemy",o,r)
|
|
348
|
-
await
|
|
355
|
+
await u.default.promises.writeFile(d.default.join(s,t.name+".md"),e,"utf8")}this.logger.info("Re-synced agent markdowns after model change",{model:o,count:e.length})
|
|
349
356
|
try{const e=this.getOpenCodeUrl()
|
|
350
|
-
e&&(await fetch(e+"/config/reload",{method:"POST",signal:AbortSignal.timeout(5e3)})).ok&&this.logger.info("Triggered OpenCode config reload")}catch(e){this.logger.debug("Failed to trigger OpenCode config reload",{error:(0,
|
|
357
|
+
e&&(await fetch(e+"/config/reload",{method:"POST",signal:AbortSignal.timeout(5e3)})).ok&&this.logger.info("Triggered OpenCode config reload")}catch(e){this.logger.debug("Failed to trigger OpenCode config reload",{error:(0,f.getErrorMessage)(e)})}}async handleSyncAgents(e,t){try{const e=this.projectId
|
|
351
358
|
if(!e)return void t.status(400).json({success:!1,error:"Project not registered"})
|
|
352
359
|
this.logger.info("Syncing agents from remote",{projectId:e})
|
|
353
360
|
const r=await this.configurer.fetchAgentsFromRemote(e)
|
|
@@ -356,22 +363,22 @@ const s=await this.configurer.fetchProjectFromRemote(e)
|
|
|
356
363
|
this.logger.debug("Fetched project for global prompt",{projectId:e,hasProject:!!s,hasGlobalSystemPrompt:!!s?.metadata?.globalSystemPrompt})
|
|
357
364
|
const o=s?.metadata?.globalSystemPrompt
|
|
358
365
|
o?this.logger.info("Using global system prompt from project metadata",{projectId:e,promptLength:o.length}):this.logger.debug("No global system prompt found in project metadata")
|
|
359
|
-
const n=
|
|
360
|
-
await
|
|
361
|
-
const i="alchemy",
|
|
362
|
-
return
|
|
363
|
-
|
|
364
|
-
try{const e=
|
|
365
|
-
if(
|
|
366
|
+
const n=d.default.join(this.options.projectPath,".opencode","agent")
|
|
367
|
+
await u.default.promises.mkdir(n,{recursive:!0})
|
|
368
|
+
const i="alchemy",a=this.cachedDefaultModel||"anthropic/claude-opus-4-5",c=r.filter((e=>e.enabled)),l=await Promise.allSettled(c.map((e=>{const t=this.configurer.generateAgentMarkdown(e,i,a,o),r=d.default.join(n,e.name+".md")
|
|
369
|
+
return u.default.promises.writeFile(r,t,"utf8").then((()=>{this.logger.info("Synced agent file",{name:e.name,path:r,mode:e.mode})}))}))),h=l.filter((e=>"fulfilled"===e.status)).length,g=l.filter((e=>"rejected"===e.status)).length
|
|
370
|
+
g>0&&this.logger.warn("Some agent file writes failed",{writeFails:g})
|
|
371
|
+
try{const e=d.default.join(this.options.projectPath,"opencode.json")
|
|
372
|
+
if(u.default.existsSync(e)){const t=u.default.readFileSync(e,"utf-8"),o=JSON.parse(t),n=s?.metadata?.bashToolAgents||{}
|
|
366
373
|
if(o.agent){for(const e of r)e.enabled&&o.agent[e.name]&&e.name in n&&(o.agent[e.name].tools.bash=!!n[e.name])
|
|
367
|
-
|
|
374
|
+
u.default.writeFileSync(e,JSON.stringify(o,null,2),"utf-8"),this.logger.info("Updated opencode.json agent tools",{bashOverrides:Object.keys(n).length})}}}catch(e){this.logger.warn("Failed to update opencode.json agent tools",{error:(0,f.getErrorMessage)(e)})}let p=!1
|
|
368
375
|
try{const e=this.getOpenCodeUrl()
|
|
369
|
-
e&&(await fetch(e+"/config/reload",{method:"POST",signal:AbortSignal.timeout(5e3)})).ok&&(this.logger.info("Triggered OpenCode config reload after agent sync"),
|
|
376
|
+
e&&(await fetch(e+"/config/reload",{method:"POST",signal:AbortSignal.timeout(5e3)})).ok&&(this.logger.info("Triggered OpenCode config reload after agent sync"),p=!0)}catch(e){this.logger.warn("Could not reload OpenCode config",{error:(0,f.getErrorMessage)(e)})}t.json({success:!0,message:`Synced ${h} agent(s) to local files${p?" - config reloaded":""}`,count:h,reloaded:p})}catch(e){this.logger.error("Failed to sync agents",{error:(0,f.getErrorMessage)(e)}),t.status(500).json({success:!1,error:(0,f.getErrorMessage)(e)})}}async handleGenerateJournal(e,r){try{const{requestId:s}=e.body
|
|
370
377
|
if(!s)return void r.status(400).json({success:!1,error:"requestId is required"})
|
|
371
378
|
if(!/^req\d+$/.test(s))return void r.status(400).json({success:!1,error:"Invalid requestId format"})
|
|
372
379
|
this.logger.info("Generating journal for request",{requestId:s})
|
|
373
|
-
const o=`Generate a journal entry for request ${s}.\n\nSteps:\n1. Use the generate_request_journal tool with requestId: ${s}\n2. Write a concise narrative journal entry based on the returned data covering:\n - What was implemented and why\n - Key decisions and trade-offs made\n - Challenges encountered and solutions\n - Files modified and their purposes\n - How acceptance criteria were met\n3. Save as JOURNAL-${s}.md using document_operations (operation: create, type: Report)\n\nKeep it story-focused and informative.`,{resolveRunCommand:n}=await Promise.resolve().then(t.bind(t,9068)),i=n(),a=(0,
|
|
374
|
-
a.pid&&this.trackChildPid(a.pid),a.unref(),r.json({success:!0,message:"Journal generation started for "+s})}catch(e){this.logger.error("Failed to generate journal",{error:(0,
|
|
380
|
+
const o=`Generate a journal entry for request ${s}.\n\nSteps:\n1. Use the generate_request_journal tool with requestId: ${s}\n2. Write a concise narrative journal entry based on the returned data covering:\n - What was implemented and why\n - Key decisions and trade-offs made\n - Challenges encountered and solutions\n - Files modified and their purposes\n - How acceptance criteria were met\n3. Save as JOURNAL-${s}.md using document_operations (operation: create, type: Report)\n\nKeep it story-focused and informative.`,{resolveRunCommand:n}=await Promise.resolve().then(t.bind(t,9068)),i=n(),a=(0,M.spawn)(i.command,[...i.prependArgs,"run","--agent","journal",o],{detached:!0,stdio:"ignore",cwd:this.options.projectPath,env:{...process.env,...(0,b.getOpencodeSpawnEnv)(this.options.projectPath,(0,I.isDevMode)())}})
|
|
381
|
+
a.pid&&this.trackChildPid(a.pid),a.unref(),r.json({success:!0,message:"Journal generation started for "+s})}catch(e){this.logger.error("Failed to generate journal",{error:(0,f.getErrorMessage)(e)}),r.status(500).json({success:!1,error:(0,f.getErrorMessage)(e)})}}async handleDirectReport(e,t){try{const r=this.projectId
|
|
375
382
|
if(!r)return void t.status(400).json({success:!1,error:"Project not registered"})
|
|
376
383
|
const{sessionId:s,requestId:o,summary:n,status:i}=e.body
|
|
377
384
|
if(!s)return void t.status(400).json({success:!1,error:"sessionId is required"})
|
|
@@ -379,26 +386,26 @@ this.logger.info("Direct report creation",{sessionId:s,requestId:o,status:i})
|
|
|
379
386
|
const a=[]
|
|
380
387
|
try{const e=await this.apiClient.get(`/api/projects/${r}/cli-sessions/external/opencode/${encodeURIComponent(s)}/ancestors`),t=e?.data||e||[]
|
|
381
388
|
let o=s
|
|
382
|
-
for(const e of t)a.push({ancestorId:e.externalSessionId,childId:o}),o=e.externalSessionId}catch(e){this.logger.warn("Failed to fetch ancestor chain",{sessionId:s,error:(0,
|
|
389
|
+
for(const e of t)a.push({ancestorId:e.externalSessionId,childId:o}),o=e.externalSessionId}catch(e){this.logger.warn("Failed to fetch ancestor chain",{sessionId:s,error:(0,f.getErrorMessage)(e)})}if(0===a.length)return void t.status(404).json({success:!1,error:"No parent session found"})
|
|
383
390
|
let c,l=n
|
|
384
391
|
if(!l&&o)try{const e=await this.apiClient.get(`/api/projects/${r}/requests/${o}`),t=e?.data||e
|
|
385
392
|
c=t?.title
|
|
386
393
|
const s=t?.completionSummary
|
|
387
|
-
s?l=s:c&&(l="Done: "+c)}catch(e){this.logger.debug("Could not fetch request details for summary",{requestId:o,error:(0,
|
|
394
|
+
s?l=s:c&&(l="Done: "+c)}catch(e){this.logger.debug("Could not fetch request details for summary",{requestId:o,error:(0,f.getErrorMessage)(e)})}l||(l="Completed request "+(o||"unknown"))
|
|
388
395
|
const d=o||s
|
|
389
396
|
if(this.directReportDedup.size>=1e3){const e=this.directReportDedup.keys().next().value
|
|
390
397
|
void 0!==e&&this.directReportDedup.delete(e)}this.directReportDedup.has(d)||this.directReportDedup.set(d,new Set)
|
|
391
|
-
const u=this.directReportDedup.get(d),h=a.filter((({ancestorId:e})=>!u.has(e)||(this.logger.info("Direct report dedup: already notified",{ancestorId:e,dedupKey:d}),!1))),
|
|
392
|
-
return u.add(e),this.logger.info("Direct report created for ancestor",{messageCode:o?.messageCode,ancestorId:e,fromChild:t}),{externalSessionId:e,messageCode:o?.messageCode,fromSessionId:t,content:o?.content}}))),
|
|
393
|
-
for(const e of
|
|
394
|
-
for(const e of
|
|
398
|
+
const u=this.directReportDedup.get(d),h=a.filter((({ancestorId:e})=>!u.has(e)||(this.logger.info("Direct report dedup: already notified",{ancestorId:e,dedupKey:d}),!1))),g=await Promise.allSettled(h.map((async({ancestorId:e,childId:t})=>{const s=await this.apiClient.post(`/api/projects/${r}/session-messages/reports`,{fromSessionId:t,toSessionId:e,status:i||"success",summary:l}),o=s?.data||s
|
|
399
|
+
return u.add(e),this.logger.info("Direct report created for ancestor",{messageCode:o?.messageCode,ancestorId:e,fromChild:t}),{externalSessionId:e,messageCode:o?.messageCode,fromSessionId:t,content:o?.content}}))),p=[]
|
|
400
|
+
for(const e of g)"fulfilled"===e.status&&p.push(e.value)
|
|
401
|
+
for(const e of g)"rejected"===e.status&&this.logger.warn("Failed to create report for ancestor",{error:(0,f.getErrorMessage)(e.reason)})
|
|
395
402
|
if(setTimeout((()=>{this.directReportDedup.delete(d)}),3e5),o)try{const e=await this.apiClient.get(`/api/projects/${r}/session-request-associations/by-session/${encodeURIComponent(s)}?activeOnly=true`),t=e?.data||e||[],o=(await Promise.allSettled(t.map((e=>this.apiClient.post(`/api/projects/${r}/session-request-associations/end`,{sessionId:s,requestLocalId:e.requestLocalId}))))).filter((e=>"rejected"===e.status)).length
|
|
396
|
-
this.logger.info("Released request associations",{sessionId:s,count:t.length,failCount:o})}catch(e){this.logger.warn("Failed to release associations",{sessionId:s,error:(0,
|
|
403
|
+
this.logger.info("Released request associations",{sessionId:s,count:t.length,failCount:o})}catch(e){this.logger.warn("Failed to release associations",{sessionId:s,error:(0,f.getErrorMessage)(e)})}t.json({success:!0,data:{notifiedSessions:p,report:p[0]?{messageCode:p[0].messageCode}:void 0,parentExternalSessionId:a[0]?.ancestorId||null}})}catch(e){this.logger.error("Direct report creation failed",{error:(0,f.getErrorMessage)(e)}),t.status(500).json({success:!1,error:(0,f.getErrorMessage)(e)})}}async handleGetSessionAgentType(e,t){try{const{sessionId:r}=e.params,s=this.projectId
|
|
397
404
|
if(s)try{const e=await this.apiClient.get(`/api/projects/${s}/cli-sessions/external/opencode/${encodeURIComponent(r)}`),o=e?.data||e
|
|
398
|
-
if(this.logger.info("Agent type DB lookup",{sessionId:r,dbAgentType:o?.agentType||null,dbId:o?.id||o?.localId||null}),o?.agentType)return void t.json({success:!0,agentType:o.agentType,source:"db"})}catch(e){this.logger.info("Agent type DB lookup failed",{sessionId:r,error:(0,
|
|
399
|
-
if(this.logger.info("Agent type bridge lookup",{sessionId:r,bridgeAgentType:e?.data?.agentType||null,bridgeSuccess:e?.success}),e.success&&e.data?.agentType)return void t.json({success:!0,agentType:e.data.agentType,source:"bridge"})}catch(e){this.logger.info("Agent type bridge lookup failed",{sessionId:r,error:(0,
|
|
405
|
+
if(this.logger.info("Agent type DB lookup",{sessionId:r,dbAgentType:o?.agentType||null,dbId:o?.id||o?.localId||null}),o?.agentType)return void t.json({success:!0,agentType:o.agentType,source:"db"})}catch(e){this.logger.info("Agent type DB lookup failed",{sessionId:r,error:(0,f.getErrorMessage)(e)})}if(this.cliBridge.isUIConnected())try{const e=await this.cliBridge.sendCommand("get-session-status",{sessionId:r},5e3)
|
|
406
|
+
if(this.logger.info("Agent type bridge lookup",{sessionId:r,bridgeAgentType:e?.data?.agentType||null,bridgeSuccess:e?.success}),e.success&&e.data?.agentType)return void t.json({success:!0,agentType:e.data.agentType,source:"bridge"})}catch(e){this.logger.info("Agent type bridge lookup failed",{sessionId:r,error:(0,f.getErrorMessage)(e)})}this.logger.info("Agent type not found",{sessionId:r}),t.json({success:!0,agentType:null,source:"unknown"})}catch(e){this.logger.debug("Failed to get session agent type",{error:(0,f.getErrorMessage)(e)}),t.status(500).json({success:!1,error:(0,f.getErrorMessage)(e)})}}async handleGetSessionParent(e,t){try{const{sessionId:r}=e.params,s=this.projectId
|
|
400
407
|
if(s)try{const e=await this.apiClient.get(`/api/projects/${s}/cli-sessions/resolve/${encodeURIComponent(r)}`),o=e?.data?.parent?.externalSessionId||""
|
|
401
|
-
return void t.json({success:!0,parentSessionId:o})}catch(e){this.logger.info("Parent session DB lookup failed",{sessionId:r,error:(0,
|
|
408
|
+
return void t.json({success:!0,parentSessionId:o})}catch(e){this.logger.info("Parent session DB lookup failed",{sessionId:r,error:(0,f.getErrorMessage)(e)})}t.json({success:!0,parentSessionId:""})}catch(e){this.logger.debug("Failed to get parent session",{error:(0,f.getErrorMessage)(e)}),t.status(500).json({success:!1,error:(0,f.getErrorMessage)(e)})}}async handleOpenCodeProxy(e,t){try{const r=this.getOpenCodeUrl()
|
|
402
409
|
if(!r)return void t.status(502).json({error:"OpenCode not running"})
|
|
403
410
|
const s=e.path.replace(/^\//,""),o=e.originalUrl.includes("?")?e.originalUrl.substring(e.originalUrl.indexOf("?")):"",n=`${r}/${s}${o}`,i=e.headers.accept?.includes("text/event-stream")
|
|
404
411
|
if(i){const r=new AbortController,s=()=>{this.proxyAbortControllers.delete(r)},o=(async()=>{try{const e=await fetch(n,{headers:{Accept:"text/event-stream"},signal:r.signal})
|
|
@@ -418,49 +425,49 @@ if(!t||t===r)throw e
|
|
|
418
425
|
d=`${t}/${s}${o}`,this.logger.info("OpenCode proxy: retrying with fresh port",{oldUrl:r,newUrl:t}),l=await fetch(d,c)}if(204===l.status||"0"===l.headers.get("content-length"))return void t.status(l.status).end()
|
|
419
426
|
const u=l.headers.get("content-type")||""
|
|
420
427
|
let h
|
|
421
|
-
try{h=u.includes("application/json")?await l.json():await l.text()}catch{return void t.status(l.status).end()}t.status(l.status),u&&t.setHeader("Content-Type",u),"string"==typeof h?t.send(h):t.json(h)}catch(r){this.openCodePort=null,this.openCodeUrlCache=null,this.logger.error("OpenCode proxy error",{error:(0,
|
|
428
|
+
try{h=u.includes("application/json")?await l.json():await l.text()}catch{return void t.status(l.status).end()}t.status(l.status),u&&t.setHeader("Content-Type",u),"string"==typeof h?t.send(h):t.json(h)}catch(r){this.openCodePort=null,this.openCodeUrlCache=null,this.logger.error("OpenCode proxy error",{error:(0,f.getErrorMessage)(r),path:e.params[0]}),t.status(502).json({error:"OpenCode proxy error",message:(0,f.getErrorMessage)(r)})}}async handleSpawnSession(e,t){try{const{agent:r,prompt:s,requestId:o,parentSessionId:n,title:i,specialization:a,specializationHeadline:c,allowParallel:l,callerAgentType:d}=e.body
|
|
422
429
|
if(!r||!s)return void t.status(400).json({success:!1,error:"agent and prompt are required"})
|
|
423
430
|
if(!this.projectId)return void t.status(400).json({success:!1,error:"No project configured"})
|
|
424
|
-
const u=this.projectId,h=new
|
|
425
|
-
t.json({success:!0,data:g})}catch(e){this.logger.error("Spawn session failed",{error:(0,
|
|
431
|
+
const u=this.projectId,h=new k.SessionSpawnService({apiClient:this.apiClient,logger:this.logger,projectId:u,getDefaultModel:()=>this.cachedDefaultModel||void 0}),g=await h.prepareSession({agent:r,prompt:s,projectPath:this.options.projectPath,requestId:o,parentSessionId:n,title:i,specialization:a,specializationHeadline:c,allowParallel:l,callerAgentType:d})
|
|
432
|
+
t.json({success:!0,data:g})}catch(e){this.logger.error("Spawn session failed",{error:(0,f.getErrorMessage)(e)}),t.status(500).json({success:!1,error:(0,f.getErrorMessage)(e)})}}async handleRunTestsNow(e,r){try{const{requestId:s,parentSessionId:o,testProcessIds:n,commandNames:i}=e.body||{}
|
|
426
433
|
if(!this.projectId)return void r.status(400).json({success:!1,error:"No project configured"})
|
|
427
434
|
if(!s)return void r.status(400).json({success:!1,error:"requestId is required"})
|
|
428
435
|
const{handleRunTestsAction:a}=await Promise.resolve().then(t.bind(t,6555)),c={projectId:this.projectId,projectPath:this.options.projectPath,logger:this.logger,apiClient:this.apiClient,trackChildPid:e=>this.trackedChildPids.add(e),cachedDefaultModel:this.cachedDefaultModel??null},l=await a({requestId:s,parentSessionId:o,testProcessIds:n,commandNames:i,mode:"auto"},c)
|
|
429
436
|
if(!l.success)return void r.status(500).json({success:!1,error:l.error||l.message})
|
|
430
|
-
r.json({success:!0,data:l.data})}catch(e){this.logger.error("Run tests now failed",{error:(0,
|
|
437
|
+
r.json({success:!0,data:l.data})}catch(e){this.logger.error("Run tests now failed",{error:(0,f.getErrorMessage)(e)}),r.status(500).json({success:!1,error:(0,f.getErrorMessage)(e)})}}async handleSetupTests(e,t){try{const{requestId:r,parentSessionId:s,branchName:o}=e.body||{}
|
|
431
438
|
if(!this.projectId)return void t.status(400).json({success:!1,error:"No project configured"})
|
|
432
439
|
const n=this.projectId,i=this.options.projectPath
|
|
433
|
-
await(0,
|
|
434
|
-
const
|
|
435
|
-
try{await(0,
|
|
436
|
-
return this.logger.error("Failed to create setup worktree",{branch:
|
|
440
|
+
await(0,_.sweepStaleSetupWorktrees)(i,this.logger)
|
|
441
|
+
const a=await(0,_.resolveSetupBranch)(this.apiClient,n,o,r,this.logger,i),c=(0,_.generateSetupId)(),l=d.default.join((0,_.getSetupWorktreesBase)(i),c)
|
|
442
|
+
try{await(0,_.createSetupWorktree)(i,l,a,this.logger)}catch(e){const r=(0,f.getErrorMessage)(e)
|
|
443
|
+
return this.logger.error("Failed to create setup worktree",{branch:a,error:r}),void t.status(500).json({success:!1,error:"Failed to create setup worktree: "+r})}const u=(0,_.buildSetupPrompt)(i,r),h=await(0,T.spawnActionAgent)({agent:"test-architect",prompt:u,cwd:l,projectPath:i,projectId:n,title:r?`[${r}] Test setup`:"Test setup",requestId:r,parentSessionId:s,allowParallel:!0,apiClient:this.apiClient,logger:this.logger,trackChildPid:e=>this.trackedChildPids.add(e),getDefaultModel:()=>this.cachedDefaultModel||void 0})
|
|
437
444
|
if(!h.success)return void t.status(500).json({success:!1,error:h.error||"Spawn failed"})
|
|
438
|
-
t.json({success:!0,data:{sessionId:h.sessionId,worktreePath:
|
|
445
|
+
t.json({success:!0,data:{sessionId:h.sessionId,worktreePath:l,branch:a}})}catch(e){this.logger.error("Setup tests failed",{error:(0,f.getErrorMessage)(e)}),t.status(500).json({success:!1,error:(0,f.getErrorMessage)(e)})}}async handleSetupCommands(e,t){try{const{requestId:r,parentSessionId:s,branchName:o,context:n}=e.body||{}
|
|
439
446
|
if(!this.projectId)return void t.status(400).json({success:!1,error:"No project configured"})
|
|
440
|
-
const i=this.projectId,
|
|
441
|
-
await(0,
|
|
442
|
-
const
|
|
443
|
-
try{await(0,
|
|
444
|
-
return this.logger.error("Failed to create setup worktree (commands)",{branch:
|
|
445
|
-
if(!
|
|
446
|
-
t.json({success:!0,data:{sessionId:
|
|
447
|
+
const i=this.projectId,a=this.options.projectPath
|
|
448
|
+
await(0,_.sweepStaleSetupWorktrees)(a,this.logger)
|
|
449
|
+
const c=await(0,_.resolveSetupBranch)(this.apiClient,i,o,r,this.logger,a),l=(0,_.generateSetupId)(),u=d.default.join((0,_.getSetupWorktreesBase)(a),l)
|
|
450
|
+
try{await(0,_.createSetupWorktree)(a,u,c,this.logger)}catch(e){const r=(0,f.getErrorMessage)(e)
|
|
451
|
+
return this.logger.error("Failed to create setup worktree (commands)",{branch:c,error:r}),void t.status(500).json({success:!1,error:"Failed to create setup worktree: "+r})}const h=(0,_.buildCommandSetupPrompt)(a,r,n),g=await(0,T.spawnActionAgent)({agent:"command-architect",prompt:h,cwd:u,projectPath:a,projectId:i,title:r?`[${r}] Commands setup`:"Commands setup",requestId:r,parentSessionId:s,allowParallel:!0,apiClient:this.apiClient,logger:this.logger,trackChildPid:e=>this.trackedChildPids.add(e),getDefaultModel:()=>this.cachedDefaultModel||void 0})
|
|
452
|
+
if(!g.success)return void t.status(500).json({success:!1,error:g.error||"Spawn failed"})
|
|
453
|
+
t.json({success:!0,data:{sessionId:g.sessionId,worktreePath:u,branch:c}})}catch(e){this.logger.error("Setup commands failed",{error:(0,f.getErrorMessage)(e)}),t.status(500).json({success:!1,error:(0,f.getErrorMessage)(e)})}}async handleGetAgentByName(e,t){try{const r=this.projectId
|
|
447
454
|
if(!r)return void t.status(400).json({success:!1,error:"Project not registered"})
|
|
448
455
|
const{agentName:s}=e.params,o=await this.apiClient.get(`/api/projects/${r}/agents`),n=(o?.data||o||[]).find((e=>e.name===s))
|
|
449
456
|
if(!n)return void t.status(404).json({success:!1,error:`Agent '${s}' not found`})
|
|
450
|
-
t.json({success:!0,data:n})}catch(e){this.logger.debug("Failed to get agent by name",{error:(0,
|
|
451
|
-
const e=
|
|
452
|
-
try{if(!
|
|
453
|
-
const t=
|
|
454
|
-
return this.cachedProjectConfig={config:r,timestamp:Date.now()},r}catch(e){return this.logger.error("Failed to read project config",{error:(0,
|
|
457
|
+
t.json({success:!0,data:n})}catch(e){this.logger.debug("Failed to get agent by name",{error:(0,f.getErrorMessage)(e)}),t.status(500).json({success:!1,error:(0,f.getErrorMessage)(e)})}}readProjectConfig(){if(this.cachedProjectConfig&&Date.now()-this.cachedProjectConfig.timestamp<D.PROJECT_CONFIG_TTL)return this.cachedProjectConfig.config
|
|
458
|
+
const e=d.default.join(this.options.projectPath,".alchemy_lab","project.json")
|
|
459
|
+
try{if(!u.default.existsSync(e))return this.cachedProjectConfig={config:null,timestamp:Date.now()},null
|
|
460
|
+
const t=u.default.readFileSync(e,"utf-8"),r=JSON.parse(t)
|
|
461
|
+
return this.cachedProjectConfig={config:r,timestamp:Date.now()},r}catch(e){return this.logger.error("Failed to read project config",{error:(0,f.getErrorMessage)(e)}),this.cachedProjectConfig={config:null,timestamp:Date.now()},null}}setOpenCodePort(e){this.openCodePort=e
|
|
455
462
|
const t="http://127.0.0.1:"+e
|
|
456
|
-
this.openCodeUrlCache={url:t,timestamp:Date.now()},this.logger.info("OpenCode port set directly",{port:e}),this.cachedDefaultModel||this.seedDefaultModelFromZen(e).catch((e=>this.logger.debug("Failed to seed zen default model",{error:(0,
|
|
463
|
+
this.openCodeUrlCache={url:t,timestamp:Date.now()},this.logger.info("OpenCode port set directly",{port:e}),this.cachedDefaultModel||this.seedDefaultModelFromZen(e).catch((e=>this.logger.debug("Failed to seed zen default model",{error:(0,f.getErrorMessage)(e)})))}async seedDefaultModelFromZen(e){const t=await fetch(`http://localhost:${e}/config/providers`,{signal:AbortSignal.timeout(5e3)})
|
|
457
464
|
if(!t.ok)return
|
|
458
465
|
const r=await t.json(),s=r?.default||{},[o,n]=Object.entries(s)[0]||[]
|
|
459
466
|
if(!o||!n)return
|
|
460
467
|
const i=`${o}/${n}`
|
|
461
|
-
this.updateDefaultModel(i),this.logger.info("Seeded defaultModel from OpenCode zen default",{defaultModel:i}),this.apiClient.post("/api/user/preferences",{defaultModel:i}).catch((e=>this.logger.debug("Failed to seed user preference",{error:(0,
|
|
468
|
+
this.updateDefaultModel(i),this.logger.info("Seeded defaultModel from OpenCode zen default",{defaultModel:i}),this.apiClient.post("/api/user/preferences",{defaultModel:i}).catch((e=>this.logger.debug("Failed to seed user preference",{error:(0,f.getErrorMessage)(e)})))}getOpenCodeUrl(){if(this.openCodePort)return"http://127.0.0.1:"+this.openCodePort
|
|
462
469
|
if(this.openCodeUrlCache&&3e4>Date.now()-this.openCodeUrlCache.timestamp)return this.openCodeUrlCache.url
|
|
463
|
-
const e=(0,
|
|
470
|
+
const e=(0,R.getOpenCodeUrlFromConfig)(this.options.projectPath)
|
|
464
471
|
return e&&(this.openCodeUrlCache={url:e,timestamp:Date.now()}),e}async handleSessionCommand(e,t){const r=this.getOpenCodeUrl()
|
|
465
472
|
if(!r)return{success:!1,error:"OpenCode server not available. Is 'opencode serve' running?"}
|
|
466
473
|
try{switch(e){case"list-sessions":{const[e,s]=await Promise.all([fetch(r+"/session"),fetch(r+"/session/status")])
|
|
@@ -491,14 +498,14 @@ const[s,o]=await Promise.all([fetch(r+"/session/status"),fetch(r+"/session")]),n
|
|
|
491
498
|
let i,a
|
|
492
499
|
if(o.ok){const t=(await o.json()).find((t=>t.id===e))
|
|
493
500
|
i=t?.title}if(this.projectId)try{const t=await this.apiClient.get(`/api/projects/${this.projectId}/cli-sessions/external/opencode/${encodeURIComponent(e)}`),r=t?.data||t
|
|
494
|
-
r?.agentType&&(a=r.agentType)}catch{}return{success:!0,data:{id:e,title:i||"(untitled)",status:n[e]?.type||"idle",agentType:a}}}default:return{success:!1,error:"Unknown session action: "+e}}}catch(e){return{success:!1,error:(0,
|
|
501
|
+
r?.agentType&&(a=r.agentType)}catch{}return{success:!0,data:{id:e,title:i||"(untitled)",status:n[e]?.type||"idle",agentType:a}}}default:return{success:!1,error:"Unknown session action: "+e}}}catch(e){return{success:!1,error:(0,f.getErrorMessage)(e)}}}async register(){const e=this.readProjectConfig()
|
|
495
502
|
if(!e?.id)throw Error("No project configuration found")
|
|
496
503
|
this.projectId=e.id,this.hookExecutor.setProjectId(this.projectId),this.timerHookScheduler?.setProjectId(this.projectId)
|
|
497
|
-
try{const e=
|
|
504
|
+
try{const e=d.default.resolve(__dirname,"../../package.json"),t=JSON.parse(u.default.readFileSync(e,"utf8"))
|
|
498
505
|
this.clientVersion=t.version}catch{}try{const e=t(45)
|
|
499
506
|
this.opencodeVersion=e.version}catch{try{const e=process.env.ALCHEMY_OPENCODE_DIR
|
|
500
|
-
if(e){const t=JSON.parse(
|
|
501
|
-
this.opencodeVersion=t.version}}catch{}}const r={projectId:this.projectId,projectPath:this.options.projectPath,serverPort:this.port,hostname:
|
|
507
|
+
if(e){const t=JSON.parse(u.default.readFileSync(d.default.join(e,"package.json"),"utf8"))
|
|
508
|
+
this.opencodeVersion=t.version}}catch{}}const r={projectId:this.projectId,projectPath:this.options.projectPath,serverPort:this.port,hostname:h.default.hostname(),platform:process.platform,clientVersion:this.clientVersion,opencodeVersion:this.opencodeVersion}
|
|
502
509
|
this.logger.info("Registering with remote",{projectId:this.projectId})
|
|
503
510
|
const s=await this.apiClient.post("/api/servers/register",r)
|
|
504
511
|
if(s.success&&s.data?.serverId){this.serverId=s.data.serverId,this.logger.info("Registered successfully",{serverId:this.serverId})
|
|
@@ -506,9 +513,8 @@ const e=s.data.versionWarnings
|
|
|
506
513
|
if(e&&e.length>0){const t=e.some((e=>"required"===e.severity))
|
|
507
514
|
for(const t of e)"required"===t.severity?(this.logger.error("[UPDATE REQUIRED] "+t.message),console.error("\n\x1b[31m\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"),console.error("\u2551 UPDATE REQUIRED \u2551"),console.error("\x1b[31m\u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255d\x1b[0m"),console.error(`\x1b[31m${t.message}\x1b[0m\n`)):(this.logger.warn("[UPDATE RECOMMENDED] "+t.message),console.warn(`\n\x1b[33m[UPDATE RECOMMENDED] ${t.message}\x1b[0m\n`))
|
|
508
515
|
this.versionWarnings=e,t&&(this._pendingUpdateBlock=!0)}}else this.logger.warn("Registration failed, continuing without remote connection",{error:s.error?.message||"Unknown error"})}async applyVersionWarnings(e){if(e.map((e=>`${e.component}:${e.severity}`)).join(",")!==this.versionWarnings.map((e=>`${e.component}:${e.severity}`)).join(",")){for(const t of e)"required"===t.severity?(this.logger.error("[UPDATE REQUIRED] "+t.message),console.error(`\n\x1b[31m[UPDATE REQUIRED] ${t.message}\x1b[0m\n`)):(this.logger.warn("[UPDATE RECOMMENDED] "+t.message),console.warn(`\n\x1b[33m[UPDATE RECOMMENDED] ${t.message}\x1b[0m\n`))
|
|
509
|
-
this.versionWarnings=e,!e.some((e=>"required"===e.severity))&&!this._pendingUpdateBlock||this.updateBlocked||await this.enterUpdateMode()}}startHeartbeat(){this.serverId&&(
|
|
510
|
-
|
|
511
|
-
await this.applyVersionWarnings(t)}catch(e){this.logger.warn("Heartbeat failed",{error:(0,g.getErrorMessage)(e)})}}),3e5))}startSSEEventForwarder(){this.logger.info("Starting SSE event forwarder"),this.connectToOpenCodeSSE(),this.sseForwarderInterval=setInterval((async()=>{await this.flushSSEEvents()}),2e3)}stopSSEEventForwarder(){this.sseForwarderInterval&&(clearInterval(this.sseForwarderInterval),this.sseForwarderInterval=void 0),this.sseAbortController&&(this.sseAbortController.abort(),this.sseAbortController=void 0),this.flushSSEEvents().catch((e=>this.logger.warn("Failed to flush remaining SSE events",{error:(0,g.getErrorMessage)(e)})))}async connectToOpenCodeSSE(){const e=this.getOpenCodeUrl()
|
|
516
|
+
this.versionWarnings=e,!e.some((e=>"required"===e.severity))&&!this._pendingUpdateBlock||this.updateBlocked||await this.enterUpdateMode()}}startHeartbeat(){this.serverId&&(setInterval((()=>{if(this.projectId)try{(0,a.touchHeartbeat)(this.projectId)}catch{}}),3e4),this.heartbeatInterval=setInterval((async()=>{try{const e=await this.apiClient.post(`/api/servers/${this.serverId}/heartbeat`,{timestamp:(new Date).toISOString(),clientVersion:this.clientVersion,opencodeVersion:this.opencodeVersion}),t=e?.data?.versionWarnings??[]
|
|
517
|
+
await this.applyVersionWarnings(t)}catch(e){this.logger.warn("Heartbeat failed",{error:(0,f.getErrorMessage)(e)})}}),3e5))}startSSEEventForwarder(){this.logger.info("Starting SSE event forwarder"),this.connectToOpenCodeSSE(),this.sseForwarderInterval=setInterval((async()=>{await this.flushSSEEvents()}),2e3)}stopSSEEventForwarder(){this.sseForwarderInterval&&(clearInterval(this.sseForwarderInterval),this.sseForwarderInterval=void 0),this.sseAbortController&&(this.sseAbortController.abort(),this.sseAbortController=void 0),this.flushSSEEvents().catch((e=>this.logger.warn("Failed to flush remaining SSE events",{error:(0,f.getErrorMessage)(e)})))}async connectToOpenCodeSSE(){const e=this.getOpenCodeUrl()
|
|
512
518
|
if(!e)return this.logger.debug("SSE forwarder: OpenCode URL unavailable, will retry"),void setTimeout((()=>this.connectToOpenCodeSSE()),5e3)
|
|
513
519
|
const t=e+"/event"
|
|
514
520
|
this.logger.info("SSE forwarder connecting",{url:t}),this.sseAbortController=new AbortController
|
|
@@ -521,23 +527,23 @@ o+=s.decode(t,{stream:!0})
|
|
|
521
527
|
const n=o.split("\n")
|
|
522
528
|
o=n.pop()||""
|
|
523
529
|
for(const e of n)if(e.startsWith("data: "))try{const t=JSON.parse(e.slice(6)),r=t.type||"unknown"
|
|
524
|
-
this.sseEventBuffer.push({type:r,data:t}),this.handleSSEHookEvent(t).catch((e=>this.logger.warn("SSE hook event processing failed",{type:t?.type,error:(0,
|
|
530
|
+
this.sseEventBuffer.push({type:r,data:t}),this.handleSSEHookEvent(t).catch((e=>this.logger.warn("SSE hook event processing failed",{type:t?.type,error:(0,f.getErrorMessage)(e)}))),"session.idle"===r&&this.flushSSEEvents().catch((e=>this.logger.debug("Immediate SSE flush failed",{error:(0,f.getErrorMessage)(e)})))}catch{}}}catch(e){"AbortError"===e?.name||(e?.message+"").includes("terminated")||this.logger.debug("SSE forwarder stream ended",{error:(0,f.getErrorMessage)(e)})}this.sseForwarderInterval&&(this.logger.info("SSE forwarder reconnecting in 3s"),setTimeout((()=>this.connectToOpenCodeSSE()),3e3))})()}catch(e){"AbortError"===e?.name||(e?.message+"").includes("terminated")||(this.logger.debug("SSE forwarder connection failed",{error:(0,f.getErrorMessage)(e)}),this.sseForwarderInterval&&setTimeout((()=>this.connectToOpenCodeSSE()),5e3))}}async handleSSEHookEvent(e){const t=e?.type
|
|
525
531
|
if(t&&this.projectId)if("session.idle"===t){const t=e?.properties?.sessionID||e?.sessionID||""
|
|
526
532
|
if(!t)return
|
|
527
533
|
this.logger.debug("SSE hook: session_idle",{sessionId:t}),await this.hookExecutor.processEvent({eventType:"session_idle",sessionId:t,variables:{sessionId:t}})}else if("session.created"===t){const t=e?.properties?.info||e?.info,r=t?.id||""
|
|
528
534
|
if(!r)return
|
|
529
535
|
const s=t?.metadata
|
|
530
536
|
if(s?.agentType&&this.projectId){const e=t?.parentID||null
|
|
531
|
-
this.logger.info("SSE-driven cli-sessions upsert",{sessionId:r,agentType:s.agentType,parentExternalIdFromOpenCode:e,note:"no parent fields included in body; preserves existing parent if record exists"}),this.apiClient.post(`/api/projects/${this.projectId}/cli-sessions`,{externalSessionId:r,providerId:"opencode",agentType:s.agentType,status:"active",specialization:s.specialization||void 0}).catch((e=>{this.logger.debug("SSE session registration (idempotent)",{sessionId:r,error:(0,
|
|
537
|
+
this.logger.info("SSE-driven cli-sessions upsert",{sessionId:r,agentType:s.agentType,parentExternalIdFromOpenCode:e,note:"no parent fields included in body; preserves existing parent if record exists"}),this.apiClient.post(`/api/projects/${this.projectId}/cli-sessions`,{externalSessionId:r,providerId:"opencode",agentType:s.agentType,status:"active",specialization:s.specialization||void 0}).catch((e=>{this.logger.debug("SSE session registration (idempotent)",{sessionId:r,error:(0,f.getErrorMessage)(e)})}))}setTimeout((async()=>{this.logger.debug("SSE hook: session_created (delayed)",{sessionId:r}),await this.hookExecutor.processEvent({eventType:"session_created",sessionId:r,variables:{sessionId:r}})}),500)}}async flushSSEEvents(){if(!this.projectId)return
|
|
532
538
|
if(0===this.sseEventBuffer.length&&0===this.droppedEventCount)return
|
|
533
539
|
if(!this.hasRemoteEventConsumers){if(this.sseEventBuffer.length>500){const e=this.sseEventBuffer.length-500
|
|
534
540
|
this.sseEventBuffer.splice(0,e),0===this.droppedEventCount&&(this.firstDropAt=Date.now()),this.droppedEventCount+=e}return}const e=this.sseEventBuffer.splice(0),t=this.droppedEventCount>0?{type:"alchemy.events.dropped",data:{count:this.droppedEventCount,firstDropAt:this.firstDropAt,reportedAt:Date.now()}}:null
|
|
535
541
|
t&&e.unshift(t)
|
|
536
|
-
try{await this.apiClient.post(`/api/projects/${this.projectId}/opencode-events`,{events:e}),t&&(this.logger.warn("Reported dropped SSE events to remote",{count:t.data.count,firstDropAt:t.data.firstDropAt}),this.droppedEventCount=0,this.firstDropAt=0)}catch(r){this.logger.debug("SSE event flush failed",{error:(0,
|
|
542
|
+
try{await this.apiClient.post(`/api/projects/${this.projectId}/opencode-events`,{events:e}),t&&(this.logger.warn("Reported dropped SSE events to remote",{count:t.data.count,firstDropAt:t.data.firstDropAt}),this.droppedEventCount=0,this.firstDropAt=0)}catch(r){this.logger.debug("SSE event flush failed",{error:(0,f.getErrorMessage)(r),count:e.length})
|
|
537
543
|
const s=t?e.slice(1):e
|
|
538
544
|
if(this.sseEventBuffer.unshift(...s),this.sseEventBuffer.length>500){const e=this.sseEventBuffer.length-500
|
|
539
545
|
this.sseEventBuffer.splice(0,e),0===this.droppedEventCount&&(this.firstDropAt=Date.now()),this.droppedEventCount+=e}}}startPolling(){this.projectId&&(this.isPolling=!0,this.pollAbortController=new AbortController,this.logger.info("Starting action long-polling",{projectId:this.projectId}),this.longPollLoop())}async longPollLoop(){for(;this.isPolling;)try{await this.pollOnce()}catch(e){if(!this.isPolling)return
|
|
540
|
-
this.logger.debug("Long-poll error, retrying in 5s",{error:(0,
|
|
546
|
+
this.logger.debug("Long-poll error, retrying in 5s",{error:(0,f.getErrorMessage)(e)}),await new Promise((e=>setTimeout(e,5e3)))}}async pollOnce(){if(this.isPolling&&this.projectId&&!this.updateBlocked)try{const e=`${this.options.remoteServerUrl}/api/projects/${this.projectId}/actions/pending?timeout=30000`,t={"Content-Type":"application/json",Accept:"application/json"}
|
|
541
547
|
this.apiClient.authToken&&(t.Authorization="Bearer "+this.apiClient.authToken)
|
|
542
548
|
const r=process.env.SERVICE_SHARED_SECRET||"dev-service-secret-change-in-production"
|
|
543
549
|
t["X-Service-Secret"]=r,this.clientVersion&&(t["X-Client-Version"]=this.clientVersion),this.opencodeVersion&&(t["X-OpenCode-Version"]=this.opencodeVersion)
|
|
@@ -546,54 +552,54 @@ if(!s.ok)throw Error("Poll failed with status "+s.status)
|
|
|
546
552
|
const o=await s.json(),n={success:!0,data:o.data||o}
|
|
547
553
|
if(n.success&&n.data&&void 0!==n.data.hasRemoteEventConsumers&&(this.hasRemoteEventConsumers=n.data.hasRemoteEventConsumers),n.success&&n.data?.settings){const{defaultModel:e,autoJournal:t}=n.data.settings
|
|
548
554
|
if(void 0!==e&&this.updateDefaultModel(e),void 0!==t&&t!==this.cachedAutoJournal){this.logger.info("autoJournal setting changed",{oldValue:this.cachedAutoJournal,newValue:t})
|
|
549
|
-
const e=new
|
|
555
|
+
const e=new y.ProjectConfigService(this.logger)
|
|
550
556
|
await e.saveProjectConfig(this.options.projectPath,t),this.cachedAutoJournal=t}}if(n.success&&Array.isArray(n.data?.invalidations)&&n.data.invalidations.length>0){const e=n.data.invalidations
|
|
551
557
|
this.logger.info("Received cache invalidations from remote",{invalidations:e}),e.includes("hooks")&&this.timerHookScheduler?.invalidateCache(),e.includes("quests")&&this.questTickScheduler?.invalidateCache(),e.includes("alchemist")&&this.questTickScheduler?.invalidateAlchemistCache()}if(n.success&&Array.isArray(n.data?.versionWarnings)&&this.applyVersionWarnings(n.data.versionWarnings),n.success&&n.data?.actions?.length>0){const e=[],t=[]
|
|
552
558
|
for(const r of n.data.actions)"opencode-proxy"===r.type||"run-tests"===r.type?e.push(r):t.push(r)
|
|
553
|
-
for(const t of e)this.processAction(t).catch((e=>this.logger.warn("Proxy action failed",{actionId:t.id,error:(0,
|
|
559
|
+
for(const t of e)this.processAction(t).catch((e=>this.logger.warn("Proxy action failed",{actionId:t.id,error:(0,f.getErrorMessage)(e)})))
|
|
554
560
|
if(t.length>0&&!this.isProcessingAction){this.isProcessingAction=!0
|
|
555
561
|
try{for(const e of t)await this.processAction(e)}finally{this.isProcessingAction=!1}}}}catch(e){throw e}}async syncProjectSettings(){try{const e=await this.apiClient.get("/api/projects/"+this.projectId)
|
|
556
562
|
if(e.success&&e.data?.metadata){const{autoJournal:t,defaultModel:r}=e.data.metadata
|
|
557
563
|
if(void 0!==r&&this.updateDefaultModel(r),void 0!==t&&t!==this.cachedAutoJournal){this.logger.info("autoJournal setting changed, syncing to local config",{oldValue:this.cachedAutoJournal,newValue:t})
|
|
558
|
-
const e=new
|
|
559
|
-
await e.saveProjectConfig(this.options.projectPath,t),this.cachedAutoJournal=t}}}catch(e){this.logger.debug("Settings sync failed",{error:(0,
|
|
564
|
+
const e=new y.ProjectConfigService(this.logger)
|
|
565
|
+
await e.saveProjectConfig(this.options.projectPath,t),this.cachedAutoJournal=t}}}catch(e){this.logger.debug("Settings sync failed",{error:(0,f.getErrorMessage)(e)})}}async processAction(e){this.logger.info("Processing action",{actionId:e.id,type:e.type})
|
|
560
566
|
const t=Date.now()
|
|
561
|
-
try{const r={projectPath:this.options.projectPath,projectId:this.projectId,logger:this.logger,apiClient:this.apiClient,trackChildPid:e=>this.trackChildPid(e),cachedDefaultModel:this.cachedDefaultModel},s=await(0,
|
|
562
|
-
await this.apiClient.post(`/api/actions/${e.id}/result`,{success:s.success,result:s.data,error:s.message||s.error,duration:Date.now()-t}),this.logger.info("Action processed",{actionId:e.id,type:e.type,success:s.success,duration:Date.now()-t})}catch(r){const s=(0,
|
|
567
|
+
try{const r={projectPath:this.options.projectPath,projectId:this.projectId,logger:this.logger,apiClient:this.apiClient,trackChildPid:e=>this.trackChildPid(e),cachedDefaultModel:this.cachedDefaultModel},s=await(0,g.executeAction)(e.type,e.payload||{},r)
|
|
568
|
+
await this.apiClient.post(`/api/actions/${e.id}/result`,{success:s.success,result:s.data,error:s.message||s.error,duration:Date.now()-t}),this.logger.info("Action processed",{actionId:e.id,type:e.type,success:s.success,duration:Date.now()-t})}catch(r){const s=(0,f.getErrorMessage)(r)
|
|
563
569
|
await this.apiClient.post(`/api/actions/${e.id}/result`,{success:!1,error:s,duration:Date.now()-t}),this.logger.error("Action failed",{actionId:e.id,type:e.type,error:s})}}async handleCLIBridgeCommand(e,t){try{const{action:r,params:s,timeout:o}=e.body
|
|
564
570
|
if(!r)return void t.status(400).json({success:!1,error:"action is required"})
|
|
565
571
|
if(["list-notifications","get-notification","respond-to-notification"].includes(r)){const e=await this.handleNotificationCommand(r,s||{})
|
|
566
572
|
return void t.json(e)}if(["list-sessions","get-messages","send-message","create-session","get-session-status"].includes(r)){const e=await this.handleSessionCommand(r,s||{})
|
|
567
573
|
return void t.json(e)}if(["list-requests","get-request"].includes(r)){const e=await this.handleRequestCommand(r,s||{})
|
|
568
574
|
return void t.json(e)}const n=await this.cliBridge.sendCommand(r,s||{},o)
|
|
569
|
-
t.json(n)}catch(e){t.status(500).json({success:!1,error:(0,
|
|
575
|
+
t.json(n)}catch(e){t.status(500).json({success:!1,error:(0,f.getErrorMessage)(e)})}}async handleNotificationCommand(e,t){if(!this.projectId)return{success:!1,error:"Project not registered yet"}
|
|
570
576
|
const r=this.projectId
|
|
571
577
|
try{switch(e){case"list-notifications":{const e={limit:t.limit||30}
|
|
572
578
|
return t.sessionId&&(e.sessionId=t.sessionId),t.messageType&&(e.messageType=t.messageType),t.status&&(e.status=t.status),t.readStatus&&(e.readStatus=t.readStatus),{success:!0,data:(await this.apiClient.get(`/api/projects/${r}/session-messages`,e)).data}}case"get-notification":return t.messageCode?{success:!0,data:(await this.apiClient.get(`/api/projects/${r}/session-messages/by-code/${encodeURIComponent(t.messageCode)}`)).data}:{success:!1,error:"messageCode is required"}
|
|
573
579
|
case"respond-to-notification":{if(!t.messageCode)return{success:!1,error:"messageCode is required"}
|
|
574
580
|
const e=t.messageCode
|
|
575
|
-
return{success:!0,data:(await this.apiClient.post(`/api/projects/${r}/session-messages/${encodeURIComponent(e)}/answer`,{respondedBySessionId:t.respondedBySessionId||"cli-bridge",text:t.responseText,selectedOptions:t.selectedOptions,notes:t.notes})).data}}default:return{success:!1,error:"Unknown notification action: "+e}}}catch(e){return{success:!1,error:(0,
|
|
581
|
+
return{success:!0,data:(await this.apiClient.post(`/api/projects/${r}/session-messages/${encodeURIComponent(e)}/answer`,{respondedBySessionId:t.respondedBySessionId||"cli-bridge",text:t.responseText,selectedOptions:t.selectedOptions,notes:t.notes})).data}}default:return{success:!1,error:"Unknown notification action: "+e}}}catch(e){return{success:!1,error:(0,f.getErrorMessage)(e)}}}async handleRequestCommand(e,t){if(!this.projectId)return{success:!1,error:"Project not registered yet"}
|
|
576
582
|
const r=this.projectId
|
|
577
583
|
try{switch(e){case"list-requests":{const e={}
|
|
578
584
|
return t.status&&(e.status=t.status),t.type&&(e.type=t.type),t.limit&&(e.limit=t.limit),t.offset&&(e.offset=t.offset),t.sortBy&&(e.sortBy=t.sortBy),t.sortDirection&&(e.sortDirection=t.sortDirection),{success:!0,data:(await this.apiClient.get(`/api/projects/${r}/requests`,e)).data}}case"get-request":{if(!t.requestId)return{success:!1,error:"requestId is required"}
|
|
579
585
|
const e=encodeURIComponent(t.requestId),[s,o]=await Promise.all([this.apiClient.get(`/api/projects/${r}/requests/${e}`),this.apiClient.get(`/api/projects/${r}/tasks`,{requestId:t.requestId})])
|
|
580
|
-
return{success:!0,data:{...s.data,tasks:o.data||[]}}}default:return{success:!1,error:"Unknown request action: "+e}}}catch(e){return{success:!1,error:(0,
|
|
586
|
+
return{success:!0,data:{...s.data,tasks:o.data||[]}}}default:return{success:!1,error:"Unknown request action: "+e}}}catch(e){return{success:!1,error:(0,f.getErrorMessage)(e)}}}handleCLIBridgeStatus(e,t){t.json({uiConnected:this.cliBridge.isUIConnected()})}async start(){this.logger.info("Starting AlchemyActionServer",{projectPath:this.options.projectPath,port:this.port})
|
|
581
587
|
for(let e=0;10>e;e++)try{await new Promise(((e,t)=>{try{this.server=this.app.listen(this.port,"127.0.0.1",(()=>{this.logger.info("HTTP server started",{port:this.port}),this.cliBridge.attach(this.server),e()})),this.server.on("error",(e=>{e.code,t(e)}))}catch(e){t(e)}}))
|
|
582
588
|
break}catch(t){if("EADDRINUSE"===t?.code&&9>e){this.logger.info(`Port ${this.port} in use, trying ${this.port+1}`),this.port++
|
|
583
|
-
continue}throw Error(`Port ${this.port} is already in use (tried 10 ports)`)}try{const e=
|
|
584
|
-
|
|
585
|
-
if(e?.id){const{upsertEntry:r}=await
|
|
586
|
-
r({projectId:e.id,projectPath:this.options.projectPath,port:this.port,pid:process.pid,startedAt:(new Date).toISOString(),lastHeartbeat:(new Date).toISOString()}),this.logger.info("Wrote running-projects registry entry",{projectId:e.id,port:this.port})}}catch(e){this.logger.warn("Failed to write project registry entry",{error:(0,
|
|
589
|
+
continue}throw Error(`Port ${this.port} is already in use (tried 10 ports)`)}try{const e=d.default.join(this.options.projectPath,".alchemy_lab","action-server.json")
|
|
590
|
+
u.default.mkdirSync(d.default.dirname(e),{recursive:!0}),u.default.writeFileSync(e,JSON.stringify({port:this.port},null,2),"utf-8"),this.logger.info("Wrote action-server port file",{port:this.port,path:e})}catch(e){this.logger.warn("Failed to write action-server port file",{error:(0,f.getErrorMessage)(e)})}try{const e=this.readProjectConfig()
|
|
591
|
+
if(e?.id){const{upsertEntry:r}=await Promise.resolve().then(t.bind(t,2640))
|
|
592
|
+
r({projectId:e.id,projectPath:this.options.projectPath,port:this.port,pid:process.pid,startedAt:(new Date).toISOString(),lastHeartbeat:(new Date).toISOString()}),this.logger.info("Wrote running-projects registry entry",{projectId:e.id,port:this.port})}}catch(e){this.logger.warn("Failed to write project registry entry",{error:(0,f.getErrorMessage)(e)})}try{await this.register(),this.startHeartbeat()}catch(e){const t=(0,f.getErrorMessage)(e)
|
|
587
593
|
t.includes("Required update"),this.logger.warn("Remote registration failed, continuing in local-only mode",{error:t})}try{const e=`${this.options.remoteServerUrl}/api/projects/${this.projectId}/actions/pending?timeout=5000`,t={"Content-Type":"application/json",Accept:"application/json"}
|
|
588
594
|
this.apiClient.authToken&&(t.Authorization="Bearer "+this.apiClient.authToken)
|
|
589
595
|
const r=process.env.SERVICE_SHARED_SECRET||"dev-service-secret-change-in-production"
|
|
590
596
|
t["X-Service-Secret"]=r
|
|
591
597
|
const s=await fetch(e,{headers:t,signal:AbortSignal.timeout(8e3)})
|
|
592
598
|
if(s.ok){const e=await s.json(),t=e?.data?.settings||e?.settings
|
|
593
|
-
void 0!==t?.defaultModel&&(this.updateDefaultModel(t.defaultModel),this.logger.info("Fetched defaultModel on startup via quick poll",{defaultModel:this.cachedDefaultModel}))}}catch(e){this.logger.debug("Quick poll for startup settings failed",{error:(0,
|
|
594
|
-
try{const e=
|
|
595
|
-
if(
|
|
596
|
-
if(r.pid)try{process.kill(-r.pid,"SIGTERM"),this.logger.info("Killed OpenCode process group from config",{pid:r.pid})}catch(e){if("ESRCH"!==e?.code){try{process.kill(r.pid,"SIGTERM")}catch{}this.logger.debug("Could not kill OpenCode group, fell back to single kill",{pid:r.pid,error:(0,
|
|
599
|
+
void 0!==t?.defaultModel&&(this.updateDefaultModel(t.defaultModel),this.logger.info("Fetched defaultModel on startup via quick poll",{defaultModel:this.cachedDefaultModel}))}}catch(e){this.logger.debug("Quick poll for startup settings failed",{error:(0,f.getErrorMessage)(e)})}this.startPolling(),this.projectId&&this.startSSEEventForwarder(),this.projectId&&this.startStuckSessionWatchdog(),this.projectId&&this.startTimerHookScheduler(),this.projectId&&this.startQuestTickScheduler(),this.startMcpStaleCleanup()}trackChildPid(e){this.trackedChildPids.add(e)}killTrackedProcesses(){for(const e of this.trackedChildPids)try{process.kill(-e,"SIGTERM"),this.logger.info("Killed tracked process group",{pid:e})}catch(t){if("ESRCH"!==t?.code){try{process.kill(e,"SIGTERM")}catch{}this.logger.debug("Could not kill process group, fell back to single kill",{pid:e,error:(0,f.getErrorMessage)(t)})}}this.trackedChildPids.clear()
|
|
600
|
+
try{const e=d.default.join(this.options.projectPath,".alchemy_lab","opencode.json")
|
|
601
|
+
if(u.default.existsSync(e)){const t=u.default.readFileSync(e,"utf-8"),r=JSON.parse(t)
|
|
602
|
+
if(r.pid)try{process.kill(-r.pid,"SIGTERM"),this.logger.info("Killed OpenCode process group from config",{pid:r.pid})}catch(e){if("ESRCH"!==e?.code){try{process.kill(r.pid,"SIGTERM")}catch{}this.logger.debug("Could not kill OpenCode group, fell back to single kill",{pid:r.pid,error:(0,f.getErrorMessage)(e)})}}}}catch{}}startTimerHookScheduler(){this.timerHookScheduler=new C.TimerHookScheduler({hookExecutor:this.hookExecutor,apiClient:this.apiClient,logger:this.logger,getOpenCodeUrl:()=>this.getOpenCodeUrl(),getDefaultModel:()=>this.cachedDefaultModel||void 0,projectId:this.projectId||"",projectPath:this.options.projectPath}),this.timerHookScheduler.start()}startQuestTickScheduler(){this.questTickScheduler=new S.QuestTickScheduler({apiClient:this.apiClient,logger:this.logger,getOpenCodeUrl:()=>this.getOpenCodeUrl(),getDefaultModel:()=>this.cachedDefaultModel||void 0,projectId:this.projectId||"",projectPath:this.options.projectPath}),this.questTickScheduler.start()}startStuckSessionWatchdog(){this.stuckSessionWatchdog=new E.StuckSessionWatchdog({getOpenCodeUrl:()=>this.getOpenCodeUrl(),getDefaultModel:()=>this.cachedDefaultModel||void 0,logger:this.logger,onStuckDetected:e=>this.handleStuckSessionDetected(e),onStuckResolved:e=>this.handleStuckSessionResolved(e)}),this.stuckSessionWatchdog.start()}handleStuckSessionDetected(e){this.sseEventBuffer.push({type:"session.stuck",data:{type:"session.stuck",properties:{sessionId:e.sessionId,messageId:e.messageId,detectedAt:e.detectedAt,sessionTitle:e.sessionTitle,retryCount:e.retryCount}}}),this.logger.warn("Stuck session detected",{sessionId:e.sessionId,title:e.sessionTitle})}handleStuckSessionResolved(e){this.sseEventBuffer.push({type:"session.unstuck",data:{type:"session.unstuck",properties:{sessionId:e}}})}handleGetStuckSessions(e,t){const r=this.stuckSessionWatchdog?.getStuckSessions()??[]
|
|
597
603
|
t.json({success:!0,data:r})}handleTestInjectStuckSession(e,t){if(!this.stuckSessionWatchdog)return void t.status(503).json({success:!1,error:"Watchdog not running"})
|
|
598
604
|
const{sessionId:r,title:s}=e.body||{},o=this.stuckSessionWatchdog.injectTestStuck(r,s)
|
|
599
605
|
this.logger.info("Test-injected stuck session",{sessionId:o.sessionId}),t.json({success:!0,data:o})}async handleRetryStuckSession(e,t){const{sessionId:r}=e.params
|
|
@@ -604,10 +610,10 @@ for(const e of this.versionWarnings)console.error(`\x1b[31m ${e.message}\x1b[0m
|
|
|
604
610
|
console.error("\x1b[31m\n Functionality disabled. Update via UI or run:\n npm update -g alchemylab\x1b[0m\n"),this.killTrackedProcesses(),this.stuckSessionWatchdog?.stop(),this.timerHookScheduler?.stop(),this.questTickScheduler?.stop(),this.stopSSEEventForwarder(),this.isPolling=!1,this.pollAbortController&&this.pollAbortController.abort(),this.pollingInterval&&clearInterval(this.pollingInterval)
|
|
605
611
|
for(const[e]of this.proxyAbortControllers)try{e.abort()}catch{}this.proxyAbortControllers.clear(),this.cliBridge.close(),this.logger.info("Update-required mode active. HTTP server still running on port "+this.port)}}async stop(){this.logger.info("Stopping AlchemyActionServer...")
|
|
606
612
|
for(const[e]of this.proxyAbortControllers)try{e.abort()}catch{}this.proxyAbortControllers.size>0&&await Promise.allSettled([...this.proxyAbortControllers.values()]),this.proxyAbortControllers.clear(),this.killTrackedProcesses(),this.stuckSessionWatchdog?.stop(),this.timerHookScheduler?.stop(),this.questTickScheduler?.stop(),this.stopSSEEventForwarder(),this.mcpStaleCleanupInterval&&(clearInterval(this.mcpStaleCleanupInterval),this.mcpStaleCleanupInterval=void 0)
|
|
607
|
-
try{const e=
|
|
608
|
-
|
|
613
|
+
try{const e=d.default.join(this.options.projectPath,".alchemy_lab","action-server.json")
|
|
614
|
+
u.default.existsSync(e)&&u.default.unlinkSync(e)}catch{}if(this.projectId)try{const{removeEntry:e}=await Promise.resolve().then(t.bind(t,2640))
|
|
609
615
|
e(this.projectId)}catch{}if(this.isPolling=!1,this.pollAbortController&&this.pollAbortController.abort(),this.pollingInterval&&clearInterval(this.pollingInterval),this.heartbeatInterval&&clearInterval(this.heartbeatInterval),this.serverId)try{await this.apiClient.post(`/api/servers/${this.serverId}/disconnect`)}catch{}this.cliBridge.close(),await new Promise((e=>{if(this.server){const t=setTimeout((()=>{this.logger.warn("Graceful shutdown timed out after 10s, forcing close"),e()}),1e4)
|
|
610
|
-
this.server.close((()=>{clearTimeout(t),e()}))}else e()})),this.logger.info("AlchemyActionServer stopped")}getPort(){return this.port}isRunning(){return!!this.server}}exports.AlchemyActionServer=
|
|
616
|
+
this.server.close((()=>{clearTimeout(t),e()}))}else e()})),this.logger.info("AlchemyActionServer stopped")}getPort(){return this.port}isRunning(){return!!this.server}}exports.AlchemyActionServer=D,D.PROJECT_CONFIG_TTL=3e4},2424(e,exports,t){"use strict"
|
|
611
617
|
Object.defineProperty(exports,"__esModule",{value:!0}),exports.ensureRequestWorktree=async function(e,t,i){const a="requests/"+t,c=(0,s.join)(e,n.ALCHEMY_DIR_NAME,"worktrees"),l=(0,s.join)(c,t)
|
|
612
618
|
if((0,r.existsSync)(l))return void i.info("Worktree already exists on disk, skipping pre-spawn creation",{requestId:t,worktreePath:l})
|
|
613
619
|
const d=(0,o.simpleGit)(e);(0,r.mkdirSync)(c,{recursive:!0})
|
|
@@ -616,7 +622,20 @@ try{if(h)await d.raw(["worktree","add",l,a]),i.info("Pre-spawn: created worktree
|
|
|
616
622
|
else{const e=u.all.includes("main")?"main":"master"
|
|
617
623
|
await d.raw(["worktree","add","-b",a,l,e]),i.info("Pre-spawn: created worktree with new branch from latest main",{requestId:t,branchName:a,mainBranch:e,worktreePath:l})}}catch(e){const r=e instanceof Error?e.message:e+""
|
|
618
624
|
i.warn("Pre-spawn: failed to create worktree (MCP process will retry)",{requestId:t,error:r})}}
|
|
619
|
-
const r=t(9896),s=t(6928),o=t(3807),n=t(4139)},
|
|
625
|
+
const r=t(9896),s=t(6928),o=t(3807),n=t(4139)},2640(e,exports,t){"use strict"
|
|
626
|
+
function r(){if(!(0,o.existsSync)(c))return[]
|
|
627
|
+
try{const e=(0,o.readFileSync)(c,"utf-8"),t=JSON.parse(e)
|
|
628
|
+
return Array.isArray(t)?t:[]}catch{return[]}}function s(e){(0,o.existsSync)(a)||(0,o.mkdirSync)(a,{recursive:!0})
|
|
629
|
+
const t=`${c}.${process.pid}.tmp`;(0,o.writeFileSync)(t,JSON.stringify(e,null,2),"utf-8"),(0,o.renameSync)(t,c)}Object.defineProperty(exports,"__esModule",{value:!0}),exports.upsertEntry=function(e){try{const t=r().filter((t=>t.projectId!==e.projectId))
|
|
630
|
+
t.push(e),s(t)}catch{}},exports.removeEntry=function(e){try{const t=r().filter((t=>t.projectId!==e))
|
|
631
|
+
if(0===t.length&&(0,o.existsSync)(c))return void(0,o.unlinkSync)(c)
|
|
632
|
+
s(t)}catch{}},exports.touchHeartbeat=function(e){try{const t=r(),o=t.findIndex((t=>t.projectId===e))
|
|
633
|
+
if(-1===o)return
|
|
634
|
+
t[o]={...t[o],lastHeartbeat:(new Date).toISOString()},s(t)}catch{}},exports.listLive=function(){const e=r(),t=Date.now()-l
|
|
635
|
+
return e.filter((e=>{const r=Date.parse(e.lastHeartbeat)
|
|
636
|
+
if(!isFinite(r)||t>r)return!1
|
|
637
|
+
try{process.kill(e.pid,0)}catch(e){if("ESRCH"===e?.code)return!1}return!0}))}
|
|
638
|
+
const o=t(9896),n=t(6928),i=t(857),a=(0,n.join)((0,i.homedir)(),".alchemy_lab"),c=(0,n.join)(a,"running-projects.json"),l=6e4},2782(e,exports,t){"use strict"
|
|
620
639
|
function r(){return"true"===process.env.ENABLE_DEBUG_TOOLS?.toLowerCase()}function s(e={}){const t={...function(e){const t={}
|
|
621
640
|
if(!r())return t.level=(0,l.isDevBuild)()?c.LogLevel.INFO:c.LogLevel.ERROR,t.logToFile=!1,t
|
|
622
641
|
if("true"===process.env.DEBUG_LOGGING?.toLowerCase()){t.level=c.LogLevel.DEBUG,t.logToFile=!0
|
|
@@ -948,7 +967,15 @@ await s.default.mkdir(t,{recursive:!0})
|
|
|
948
967
|
const a=JSON.stringify({timestamp:(new Date).toISOString(),reason:i},null,2)
|
|
949
968
|
return await s.default.writeFile(e,a,"utf-8"),r.info("MCP restart triggered",{triggerPath:e,reason:i}),{success:!0,message:"MCP restart triggered: "+i,data:{triggerPath:e}}}catch(e){const t=e instanceof Error?e.message:e+""
|
|
950
969
|
return r.error("Failed to trigger MCP restart",{error:t}),{success:!1,message:"Failed to trigger MCP restart: "+t,error:t}}}
|
|
951
|
-
const s=r(t(1943)),o=r(t(6928))},
|
|
970
|
+
const s=r(t(1943)),o=r(t(6928))},5959(e,exports){"use strict"
|
|
971
|
+
function t(){const e=Date.now()
|
|
972
|
+
for(const[t,s]of o)"running"!==s.status&&s.finishedAt&&e-Date.parse(s.finishedAt)>r&&o.delete(t)
|
|
973
|
+
if(o.size>s){const e=[...o.values()].filter((e=>"running"!==e.status)).sort(((e,t)=>(e.finishedAt??"").localeCompare(t.finishedAt??"")))
|
|
974
|
+
for(const t of e){if(o.size<=s)break
|
|
975
|
+
o.delete(t.id)}}}Object.defineProperty(exports,"__esModule",{value:!0}),exports.addStart=function(e){const r={...e,status:"running"}
|
|
976
|
+
o.set(e.id,r),t()},exports.markEnd=function(e){const r=o.get(e.id)
|
|
977
|
+
r&&(r.exitCode=e.exitCode,r.timedOut=e.timedOut,r.finishedAt=e.finishedAt,r.status=e.timedOut?"timed-out":0===e.exitCode?"completed":"failed",t())},exports.listEntries=function(){return t(),[...o.values()].sort(((e,t)=>t.startedAt.localeCompare(e.startedAt)))},exports._resetForTest=function(){o.clear()}
|
|
978
|
+
const r=6e5,s=100,o=new Map},6175(e,exports){"use strict"
|
|
952
979
|
Object.defineProperty(exports,"__esModule",{value:!0}),exports.TimerConditionEvaluator=void 0
|
|
953
980
|
const t=new Set(["coder","reviewer","tester","journal","planner","researcher"])
|
|
954
981
|
exports.TimerConditionEvaluator=class{constructor(e){this.logger=e}async evaluate(e,t,r){if(!e||0===e.length)return{passed:!0,variables:{}}
|
|
@@ -1502,7 +1529,7 @@ for(const e of T){if(!e.enabled)continue
|
|
|
1502
1529
|
const t=this.generateAgentMarkdown(e,d,S,k),s=c.join(C,e.name+".md")
|
|
1503
1530
|
await r.writeFile(s,t,"utf8"),this.logger.info(`Created ${e.name} agent file from remote config`,{agentPath:s,mode:e.mode})}}else this.logger.warn("No agents found - check remote server or enable agents via UI")
|
|
1504
1531
|
else this.logger.warn("No projectId provided - agents must be configured via database")
|
|
1505
|
-
const j={$schema:"https://opencode.ai/config.json",model:E,mcp:{[d]:{type:"local",command:[n,u,s],enabled:!0}}}
|
|
1532
|
+
const j={$schema:"https://opencode.ai/config.json",model:E,mcp:{[d]:{type:"local",command:[n,u,s],enabled:!0,timeout:6e5}}}
|
|
1506
1533
|
if(l&&process.env.REMOTE_SERVER_URL&&(j.mcp[d].environment={REMOTE_SERVER_URL:process.env.REMOTE_SERVER_URL}),T&&T.length>0){j.agent={}
|
|
1507
1534
|
for(const e of T){if(!e.enabled)continue
|
|
1508
1535
|
const t={}
|