alchemylab 1.4.2 → 1.4.4
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 +175 -141
- package/dist/http-server.js +89 -69
- package/dist/index.js +89 -69
- package/dist/mcp-direct.js +617 -597
- package/package.json +1 -1
- package/dist/640.js +0 -14
package/dist/cli.js
CHANGED
|
@@ -27,7 +27,7 @@ const{sessionId:s,hookId:n}=e.params,a=await o.post(`/api/projects/${r}/hook-ite
|
|
|
27
27
|
t.json(a)}catch(e){n.debug("Failed to increment hook iteration",{error:(0,r.getErrorMessage)(e)}),t.json({success:!0,data:{iterationCount:0}})}}(e,s,t)))}
|
|
28
28
|
const r=t(8824)},441(e,exports){"use strict"
|
|
29
29
|
Object.defineProperty(exports,"__esModule",{value:!0}),exports.BAD_REQUEST=exports.INTERNAL_ERROR=exports.NOT_FOUND=exports.FORBIDDEN=exports.UNAUTHORIZED=exports.VALIDATION_ERROR=exports.MCP_PROTOCOL_ERROR=exports.REQUEST_VALIDATION_FAILED=exports.REQUEST_ID_INVALID_FORMAT=exports.REQUEST_NOT_FOUND=exports.CONNECTION_ERROR=exports.SERVICE_UNAVAILABLE=exports.CONNECTION_TIMEOUT=exports.SERVER_ERROR=exports.NETWORK_ERROR=exports.TASK_VALIDATION_FAILED=exports.TASK_ID_INVALID_FORMAT=exports.TASK_NOT_FOUND=void 0,exports.TASK_NOT_FOUND="TASK_NOT_FOUND",exports.TASK_ID_INVALID_FORMAT="TASK_ID_INVALID_FORMAT",exports.TASK_VALIDATION_FAILED="TASK_VALIDATION_FAILED",exports.NETWORK_ERROR="NETWORK_ERROR",exports.SERVER_ERROR="SERVER_ERROR",exports.CONNECTION_TIMEOUT="CONNECTION_TIMEOUT",exports.SERVICE_UNAVAILABLE="SERVICE_UNAVAILABLE",exports.CONNECTION_ERROR="CONNECTION_ERROR",exports.REQUEST_NOT_FOUND="REQUEST_NOT_FOUND",exports.REQUEST_ID_INVALID_FORMAT="REQUEST_ID_INVALID_FORMAT",exports.REQUEST_VALIDATION_FAILED="REQUEST_VALIDATION_FAILED",exports.MCP_PROTOCOL_ERROR="MCP_PROTOCOL_ERROR",exports.VALIDATION_ERROR="VALIDATION_ERROR",exports.UNAUTHORIZED="UNAUTHORIZED",exports.FORBIDDEN="FORBIDDEN",exports.NOT_FOUND="NOT_FOUND",exports.INTERNAL_ERROR="INTERNAL_ERROR",exports.BAD_REQUEST="BAD_REQUEST"},489(e,exports,t){"use strict"
|
|
30
|
-
function r(e,t){return(0,h.join)(e,p,m,"pool-"+t)}function s(){const e=
|
|
30
|
+
function r(e,t){return(0,h.join)(e,p,m,"pool-"+t)}function s(){const e=S.shift()
|
|
31
31
|
e&&setImmediate(e)}async function o(e,t){const o=Date.now()
|
|
32
32
|
!function(e){let t=!1
|
|
33
33
|
for(const r of E.values())r.busy&&r.busySince&&e-r.busySince>b&&(r.busy=!1,r.busySince=void 0,t=!0)
|
|
@@ -42,13 +42,13 @@ try{n=await d.promises.readdir(o,{withFileTypes:!0})}catch{return}for(const e of
|
|
|
42
42
|
return new Set(t)}catch{return null}}Object.defineProperty(exports,"__esModule",{value:!0}),exports.getSlotPath=r,exports.acquireSlot=async function(e,t,r=I){return await o(e,t)||new Promise(((i,a)=>{let c=!1
|
|
43
43
|
const l=Date.now(),d=setTimeout((()=>{if(c)return
|
|
44
44
|
c=!0
|
|
45
|
-
const e=
|
|
46
|
-
0>e||
|
|
45
|
+
const e=S.indexOf(u)
|
|
46
|
+
0>e||S.splice(e,1),a(Error(`All ${t} pool slot(s) busy after waiting ${Math.round((Date.now()-l)/1e3)}s`))}),r)
|
|
47
47
|
d.unref()
|
|
48
48
|
const u=async()=>{if(c)return
|
|
49
49
|
const r=await o(e,t)
|
|
50
|
-
c?r&&n(r.path):r?(c=!0,clearTimeout(d),i(r)):(
|
|
51
|
-
|
|
50
|
+
c?r&&n(r.path):r?(c=!0,clearTimeout(d),i(r)):(S.unshift(u),s())}
|
|
51
|
+
S.push(u)}))},exports.releaseSlot=n,exports.scheduleSlotRelease=i,exports.watchPidAndRelease=function(e,t,r,s=5e3){if(i(t,r),!e||0>=e)return
|
|
52
52
|
const o=setInterval((()=>{try{process.kill(e,0)}catch{n(t),clearInterval(o)}}),s)
|
|
53
53
|
o.unref()},exports.readSlotFingerprint=a,exports.writeSlotFingerprint=async function(e,t){try{await d.promises.writeFile((0,h.join)(e,f),t,"utf-8")}catch(e){console.warn("worktree-pool: writeSlotFingerprint failed (non-fatal):",(0,g.getErrorMessage)(e))}},exports.computeFingerprint=async function(e,t){if(!t||0===t.length)return""
|
|
54
54
|
const r=(0,u.createHash)("sha256")
|
|
@@ -58,8 +58,8 @@ await d.promises.writeFile((0,h.join)(e,y),t.join("\n")+"\n","utf-8")}catch(e){c
|
|
|
58
58
|
if(!t)return{removed:0}
|
|
59
59
|
const r=await c(e)
|
|
60
60
|
let s=0
|
|
61
|
-
for(const o of r)if(!t.has(o))try{await d.promises.rm((0,h.join)(e,o),{force:!0}),s++}catch{}return{removed:s}},exports._resetForTests=function(){E.clear(),
|
|
62
|
-
const d=t(9896),u=t(6982),h=t(6928),g=t(8824),p=".alchemy_lab",m="test-worktrees",f=".fingerprint.sha256",y=".alchemy-warm-snapshot.txt",v=new Set([f,y]),w=new Set([".git"]),b=12e5,I=6e5,E=new Map,
|
|
61
|
+
for(const o of r)if(!t.has(o))try{await d.promises.rm((0,h.join)(e,o),{force:!0}),s++}catch{}return{removed:s}},exports._resetForTests=function(){E.clear(),S.length=0}
|
|
62
|
+
const d=t(9896),u=t(6982),h=t(6928),g=t(8824),p=".alchemy_lab",m="test-worktrees",f=".fingerprint.sha256",y=".alchemy-warm-snapshot.txt",v=new Set([f,y]),w=new Set([".git"]),b=12e5,I=6e5,E=new Map,S=[]},833(e,exports,t){"use strict"
|
|
63
63
|
var r=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}}
|
|
64
64
|
Object.defineProperty(exports,"__esModule",{value:!0}),exports.getOpenCodeUrlFromConfig=function(e){const t=o.default.join(e,".alchemy_lab","opencode.json")
|
|
65
65
|
try{if(!s.default.existsSync(t))return null
|
|
@@ -270,7 +270,7 @@ let f=!1
|
|
|
270
270
|
try{await m("create report",(()=>d.post(`/api/projects/${u}/session-messages/reports`,{fromSessionId:n,toSessionId:p,status:"success",summary:"Plan complete. Created requests: "+(a||[]).join(", "),details:c||"Plan complete"}))),f=!0}catch(e){l.error("Failed to create report after retry",{error:(0,r.getErrorMessage)(e)})}let y=!1
|
|
271
271
|
const v=(0,o.getOpenCodeServerUrl)(h,l)
|
|
272
272
|
if(v){const e=`The planner has finished creating the plan.\n\nCreated requests: ${(a||[]).join(", ")}\n\nPlease begin working on these requests.`
|
|
273
|
-
try{const r=await m("notify parent",(()=>fetch(`${v}/session/${p}/prompt_async`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({parts:[{type:"text",text:e,metadata:(0,s.partMeta)("
|
|
273
|
+
try{const r=await m("notify parent",(()=>fetch(`${v}/session/${p}/prompt_async`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({parts:[{type:"text",text:e,metadata:(0,s.partMeta)("plan-complete",{plannerSessionId:n,summary:c,requestIds:a})}],...t.cachedDefaultModel?(()=>{const[e,...r]=t.cachedDefaultModel.split("/")
|
|
274
274
|
return{model:{providerID:e,modelID:r.join("/")}}})():{}})})))
|
|
275
275
|
r.ok?(y=!0,l.info("Notified parent session to start building",{parentSessionId:p})):l.error("Parent session prompt returned non-OK",{status:r.status,parentSessionId:p})}catch(e){l.error("Could not notify parent session after retry",{error:(0,r.getErrorMessage)(e)})}}else l.error("Could not resolve OpenCode server URL \u2014 parent prompt skipped")
|
|
276
276
|
if(f&&y){const e=g?.id
|
|
@@ -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),S=t(6563),C=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 S.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 C.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
|
|
@@ -867,9 +886,9 @@ break}await s(500)}for(;Date.now()<n;){const o=await r(e,t)
|
|
|
867
886
|
if("error"===o)return{status:"error"}
|
|
868
887
|
if(("idle"===o||"completed"===o)&&i)return{status:"completed"}
|
|
869
888
|
"busy"!==o&&"running"!==o||(i=!0),await s(2e3)}return{status:"timeout"}}(y,w,g),I=Date.now()-v,E=await async function(e,t){try{const r=await fetch(`${e}/session/${t}/message?limit=200`,{signal:AbortSignal.timeout(1e4)})
|
|
870
|
-
return r.ok?(await r.json()).map((e=>({role:e.info?.role||"unknown",text:(e.parts||[]).filter((e=>"text"===e.type)).map((e=>e.text)).join("\n"),toolCalls:(e.parts||[]).filter((e=>"tool_use"===e.type)).map((e=>({name:e.name,id:e.id}))),timestamp:e.info?.time?.created}))):[]}catch{return[]}}(y,w),
|
|
889
|
+
return r.ok?(await r.json()).map((e=>({role:e.info?.role||"unknown",text:(e.parts||[]).filter((e=>"text"===e.type)).map((e=>e.text)).join("\n"),toolCalls:(e.parts||[]).filter((e=>"tool_use"===e.type)).map((e=>({name:e.name,id:e.id}))),timestamp:e.info?.time?.created}))):[]}catch{return[]}}(y,w),S=function(e){return e.reduce(((e,t)=>e+(t.toolCalls?.length||0)),0)}(E),C=function(e){for(let t=e.length-1;t>=0;t--)if("assistant"===e[t].role&&e[t].text)return e[t].text
|
|
871
890
|
return""}(E)
|
|
872
|
-
return{success:!0,message:"Prompt completed",data:{sessionId:w,status:b.status,messages:E,toolCallCount:
|
|
891
|
+
return{success:!0,message:"Prompt completed",data:{sessionId:w,status:b.status,messages:E,toolCallCount:S,elapsedMs:I,finalResponse:C}}}
|
|
873
892
|
const o=t(8148)},4769(e,exports,t){"use strict"
|
|
874
893
|
function r(e){return(0,o.getOpenCodeServerUrl)(e.projectPath,e.logger)}async function s(e,t,s,o){const n=r(s)
|
|
875
894
|
if(!n||!t)return!1
|
|
@@ -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:{}}
|
|
@@ -999,21 +1026,21 @@ let I
|
|
|
999
1026
|
if(u?.length)I=(await Promise.all(u.map((e=>f.get(`/api/projects/${y}/test-processes/${e}`))))).filter((e=>e.success&&e.data)).map((e=>e.data))
|
|
1000
1027
|
else{const e=await f.get(`/api/projects/${y}/test-processes/active`)
|
|
1001
1028
|
I=e.success&&e.data?Array.isArray(e.data)?e.data:[e.data]:[]}if(0===I.length)return{success:!1,message:"No test processes configured for this project. Set up testing first.",error:"No test processes"}
|
|
1002
|
-
const E=Math.max(1,Math.min(8,I[0]?.config?.poolSize??2)),
|
|
1003
|
-
let
|
|
1004
|
-
try{
|
|
1029
|
+
const E=Math.max(1,Math.min(8,I[0]?.config?.poolSize??2)),S=I[0]?.config?.fingerprint||[]
|
|
1030
|
+
let C
|
|
1031
|
+
try{C=await(0,c.acquireSlot)(v,E)}catch(e){return{success:!1,message:"Could not acquire test worktree slot: "+(0,i.getErrorMessage)(e),error:(0,i.getErrorMessage)(e)}}const k=C.path,T=(0,n.simpleGit)(v)
|
|
1005
1032
|
try{if((0,s.existsSync)(k))try{const e=(0,n.simpleGit)(k)
|
|
1006
|
-
await e.raw(["reset","--hard",b]),m.info("Reset pool slot to latest branch HEAD",{worktreePath:k,branchName:b,slotIndex:
|
|
1033
|
+
await e.raw(["reset","--hard",b]),m.info("Reset pool slot to latest branch HEAD",{worktreePath:k,branchName:b,slotIndex:C.index})}catch(e){m.warn("Could not reset slot, recreating",{error:(0,i.getErrorMessage)(e)})
|
|
1007
1034
|
try{await T.raw(["worktree","remove","--force",k])}catch{}}if(!(0,s.existsSync)(k)){(0,s.mkdirSync)((0,o.join)(v,".alchemy_lab","test-worktrees"),{recursive:!0})
|
|
1008
1035
|
try{if(!(await T.branch()).all.includes(b))return(0,c.releaseSlot)(k),{success:!1,message:`Branch ${b} not found`,error:"Branch not found"}
|
|
1009
|
-
await T.raw(["worktree","add","--detach",k,b]),m.info("Created pool slot worktree (detached)",{worktreePath:k,branchName:b,slotIndex:
|
|
1010
|
-
m.info("Pool slot worktree already exists, using as-is",{worktreePath:k})}}}catch(e){throw(0,c.releaseSlot)(k),e}const _=await(0,c.computeFingerprint)(k,
|
|
1011
|
-
j||m.info("Pool slot fingerprint matches; skipping setup",{worktreePath:k,slotIndex:
|
|
1036
|
+
await T.raw(["worktree","add","--detach",k,b]),m.info("Created pool slot worktree (detached)",{worktreePath:k,branchName:b,slotIndex:C.index})}catch(e){if(!(e+"").includes("already exists"))return(0,c.releaseSlot)(k),{success:!1,message:"Failed to create pool slot worktree: "+(0,i.getErrorMessage)(e),error:(0,i.getErrorMessage)(e)}
|
|
1037
|
+
m.info("Pool slot worktree already exists, using as-is",{worktreePath:k})}}}catch(e){throw(0,c.releaseSlot)(k),e}const _=await(0,c.computeFingerprint)(k,S),j=0===S.length||_!==C.storedFingerprint,A=0===S.length?null:_===C.storedFingerprint
|
|
1038
|
+
j||m.info("Pool slot fingerprint matches; skipping setup",{worktreePath:k,slotIndex:C.index,fingerprintFiles:S})
|
|
1012
1039
|
const{execSync:P}=await Promise.resolve().then(t.t.bind(t,5317,23))
|
|
1013
1040
|
if(j){for(const e of I){const t=e.config?.setup||[]
|
|
1014
|
-
if(0!==t.length){m.info("Running setup commands for test process",{processName:e.name,commandCount:t.length,slotIndex:
|
|
1015
|
-
for(const r of t)try{m.info("Setup command",{command:r,worktreePath:k}),P(r,{cwd:k,timeout:12e4,encoding:"utf-8",stdio:["pipe","pipe","pipe"],shell:"/bin/sh",env:{...process.env,FORCE_COLOR:"0"}})}catch(t){m.warn("Setup command failed",{command:r,processName:e.name,exitCode:t.status,stderr:(t.stderr||"").slice(-500)})}}}
|
|
1016
|
-
e>0&&m.info("Pruned drift from warm slot",{worktreePath:k,slotIndex:
|
|
1041
|
+
if(0!==t.length){m.info("Running setup commands for test process",{processName:e.name,commandCount:t.length,slotIndex:C.index})
|
|
1042
|
+
for(const r of t)try{m.info("Setup command",{command:r,worktreePath:k}),P(r,{cwd:k,timeout:12e4,encoding:"utf-8",stdio:["pipe","pipe","pipe"],shell:"/bin/sh",env:{...process.env,FORCE_COLOR:"0"}})}catch(t){m.warn("Setup command failed",{command:r,processName:e.name,exitCode:t.status,stderr:(t.stderr||"").slice(-500)})}}}S.length>0&&await(0,c.writeSlotFingerprint)(k,_),await(0,c.writeWarmSnapshot)(k)}else{const{removed:e}=await(0,c.restoreToWarmSnapshot)(k)
|
|
1043
|
+
e>0&&m.info("Pruned drift from warm slot",{worktreePath:k,slotIndex:C.index,removed:e})}let M
|
|
1017
1044
|
try{const e=(0,n.simpleGit)(k)
|
|
1018
1045
|
M=(await e.revparse(["HEAD"])).trim()}catch(e){m.warn("Failed to capture worktree HEAD; commit_sha will be NULL on this run",{worktreePath:k,error:(0,i.getErrorMessage)(e)})}const O=[],R=[],D=[]
|
|
1019
1046
|
let x
|
|
@@ -1112,14 +1139,21 @@ const o=s(t(9278))},6895(module){"use strict"
|
|
|
1112
1139
|
module.exports=require("chalk")},6928(module){"use strict"
|
|
1113
1140
|
module.exports=require("path")},6982(module){"use strict"
|
|
1114
1141
|
module.exports=require("crypto")},7078(e,exports,t){"use strict"
|
|
1115
|
-
Object.defineProperty(exports,"__esModule",{value:!0}),exports.handleSpawnAutoPlannerAction=async function(e,t){const{requestId:o,parentSessionId:
|
|
1116
|
-
if(!o)return{success:!1,message:"requestId is required",error:"Missing
|
|
1117
|
-
|
|
1118
|
-
try{
|
|
1119
|
-
if(
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1142
|
+
Object.defineProperty(exports,"__esModule",{value:!0}),exports.handleSpawnAutoPlannerAction=async function(e,t){const{requestId:o,prompt:n,title:i,parentSessionId:a,originMessageCode:c}=e,{logger:l,apiClient:d,projectId:u,projectPath:h,trackChildPid:g}=t
|
|
1143
|
+
if(!o&&!n)return{success:!1,message:"Either requestId or prompt is required",error:"Missing payload"}
|
|
1144
|
+
l.info("Starting spawn-auto-planner action",{requestId:o,hasPrompt:!!n})
|
|
1145
|
+
try{let e,t,p
|
|
1146
|
+
if(o){const r=await d.get(`/api/projects/${u}/requests/${o}`)
|
|
1147
|
+
if(!r.success||!r.data)return{success:!1,message:`Request ${o} not found`,error:"Request not found"}
|
|
1148
|
+
const s=r.data,n=Array.isArray(s.acceptanceCriteria)?s.acceptanceCriteria.map((e=>e.description||e)).join("\n- "):""
|
|
1149
|
+
e=`Create a plan for request ${o}: ${s.title}\n\nDescription: ${s.description||"No description provided."}\n\n${n?"Acceptance Criteria:\n- "+n:""}\n\nResearch the codebase and create a comprehensive implementation plan.`,t=`[${o}] Plan: ${s.title}`,p=s.title||"New Plan for "+o}else e=n,t=i||"Auto-planner",p=i||"Auto-planner Draft"
|
|
1150
|
+
const m=`PLAN-${p.replace(/\.md$/i,"")}.md`
|
|
1151
|
+
let f
|
|
1152
|
+
try{const e=await d.post(`/api/projects/${u}/documents`,{filename:m,content:" ",type:"Plan"})
|
|
1153
|
+
f=e?.data?.id,l.info("Pre-created plan document for auto-planner",{planId:f,filename:m})}catch(e){l.warn("Failed to pre-create plan document; auto-planner will fall back to plan_write create",{error:(0,r.getErrorMessage)(e)})}const y=f?`[Session Context]\n- planId: ${f} \u2014 this is YOUR associated plan document. It already exists with a placeholder filename.\n- Use plan_write (update / append / replace) with planId "${f}" to fill it in.\n- On your FIRST plan_write call, include a "name" field with a short descriptive title so the document gets a meaningful filename.\n- Do NOT call plan_write "create" \u2014 always modify the document above.\n- Do NOT call plan_read "get-active" \u2014 your associated plan is given here, not via the active-plan slot.\n\n${e}`:e,v=await(0,s.spawnActionAgent)({agent:"auto-planner",prompt:y,cwd:h,projectPath:h,projectId:u,title:t,requestId:o,parentSessionId:a,apiClient:d,logger:l,trackChildPid:g})
|
|
1154
|
+
if(!v.success)return{success:!1,message:"Failed to spawn auto-planner: "+v.error,error:v.error||"Spawn failed"}
|
|
1155
|
+
if(l.info("Spawned auto-planner",{requestId:o,sessionId:v.sessionId,pid:v.pid,planId:f}),f&&v.sessionId)try{await d.post(`/api/projects/${u}/session-plan-associations`,{opencodeSessionId:v.sessionId,documentId:f})}catch(e){l.warn("Failed to create session-plan association at spawn time",{error:(0,r.getErrorMessage)(e)})}return c&&d.patch(`/api/projects/${u}/session-messages/${c}/response`,{partial:{plannerSessionId:v.sessionId,planId:f}}).catch((e=>l.warn("Failed to record plannerSessionId on origin message",{originMessageCode:c,error:(0,r.getErrorMessage)(e)}))),{success:!0,message:o?"Auto-planner spawned for request "+o:"Auto-planner spawned: "+(i||v.sessionId),data:{sessionId:v.sessionId,pid:v.pid}}}catch(e){const t=(0,r.getErrorMessage)(e)
|
|
1156
|
+
return l.error("spawn-auto-planner action failed",{requestId:o,error:t}),{success:!1,message:"Failed to spawn auto-planner: "+t,error:t}}}
|
|
1123
1157
|
const r=t(8824),s=t(8148)},7163(e,exports){"use strict"
|
|
1124
1158
|
Object.defineProperty(exports,"__esModule",{value:!0}),exports.debugTokenAccess=function(){},exports.compareTokens=function(){},exports.debugTokenStorage=function(){},exports.disableTokenDebugging=function(){},exports.enableTokenDebugging=function(){},new WeakMap},7210(e,exports,t){"use strict"
|
|
1125
1159
|
Object.defineProperty(exports,"__esModule",{value:!0}),exports.tryMarkMessageProcessed=exports.createToolCallHash=exports.isDuplicateToolCall=exports.determineEventToEmit=exports.interpolatePrompt=exports.evaluateCondition=exports.TimerConditionEvaluator=exports.TimerHookScheduler=exports.HookCache=exports.HookExecutor=void 0
|
|
@@ -1493,13 +1527,13 @@ if(!i){try{const e=await r.readFile(w,"utf-8"),t=JSON.parse(e)
|
|
|
1493
1527
|
t?.openCodeTools?(g=t.openCodeTools,this.logger.info("Found tool settings in project config",{projectConfigTools:g})):v=!0,t?.openCodeModel&&(p=t.openCodeModel)}catch(e){this.logger.debug("No project config found, will create with defaults"),v=!0}try{const e=await r.readFile(h,"utf-8"),t=JSON.parse(e)
|
|
1494
1528
|
t?.mode?.alchemy?.tools&&(m=t.mode.alchemy.tools,this.logger.info("Found existing tool settings in opencode.json",{existingTools:m})),t?.model&&(f=t.model),t?.mode?.journal&&(y=!0)}catch(e){this.logger.info("No existing opencode.json found, will use defaults")}}const b=i?.openCodeTools||g||m||{glob:!1,grep:!1,ls:!1,list:!1,view:!1,read:!1,write:!1,edit:!1,patch:!1,diagnostics:!0,bash:!0===i?.enableBash,fetch:!1,sourcegraph:!0,agent:!1!==a,todoread:!1,todowrite:!1},I={tools:b},E=i?.openCodeModel||p||f||"anthropic/claude-opus-4-5"
|
|
1495
1529
|
i?.openCodeModel?I.model=i.openCodeModel:p?I.model=p:f&&(I.model=f)
|
|
1496
|
-
const
|
|
1497
|
-
await r.mkdir(
|
|
1498
|
-
const
|
|
1530
|
+
const S=c.join(s,".opencode","agent")
|
|
1531
|
+
await r.mkdir(S,{recursive:!0})
|
|
1532
|
+
const C=E||"anthropic/claude-opus-4-5-20251101"
|
|
1499
1533
|
let k,T=null,_=null
|
|
1500
1534
|
if(i?.projectId)if(await this.seedAgentsFromRemote(i.projectId),_=await this.fetchProjectFromRemote(i.projectId),k=_?.metadata?.globalSystemPrompt,k&&this.logger.info("Using global system prompt from project metadata",{projectId:i.projectId,promptLength:k.length}),T=await this.fetchAgentsFromRemote(i.projectId),T&&T.length>0){this.logger.info("Using agents from remote database",{count:T.length})
|
|
1501
1535
|
for(const e of T){if(!e.enabled)continue
|
|
1502
|
-
const t=this.generateAgentMarkdown(e,d,
|
|
1536
|
+
const t=this.generateAgentMarkdown(e,d,C,k),s=c.join(S,e.name+".md")
|
|
1503
1537
|
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
1538
|
else this.logger.warn("No projectId provided - agents must be configured via database")
|
|
1505
1539
|
const j={$schema:"https://opencode.ai/config.json",model:E,mcp:{[d]:{type:"local",command:[n,u,s],enabled:!0,timeout:6e5}}}
|
|
@@ -1512,7 +1546,7 @@ e.name in r&&(t.bash=!!r[e.name]),t.skill=!1,("subagent"===e.mode||"tester"===e.
|
|
|
1512
1546
|
let s=""
|
|
1513
1547
|
k&&(s+=k.trim()+"\n\n"),e.prompt&&(s+=e.prompt),e.promptAddition&&(s+="\n\n"+e.promptAddition),j.agent[e.name]={tools:t,...s?{prompt:s}:{},...e.description?{description:e.description}:{},mode:"utility"===e.mode?"primary":e.mode||"primary",...e.color?{color:e.color}:{}}}j.agent.general={...j.agent.general,disable:!0},j.agent.explore={...j.agent.explore,tools:{grep:!1,glob:!1,list:!1,read:!1,bash:!1,[d+"_search"]:!0,[d+"_read_files"]:!0,[d+"_view_asset"]:!0,[d+"_query_history"]:!0,webfetch:!0,websearch:!0,codesearch:!0}}
|
|
1514
1548
|
const e=`---\ndescription: Fast codebase exploration and research subagent\nmode: subagent\n---\n\n${k?k.trim()+"\n\n":""}You are a codebase exploration agent. Your job is to find specific information and return concise, targeted answers.\n\n## Response Guidelines\n\n- **Be concise.** Return only what was asked for. A typical answer is 100-500 words.\n- **Lead with the answer.** State findings first, then supporting details.\n- **Reference specific files and lines.** Use \`file_path:line_number\` format.\n- **Don't exhaustively document everything you find.** Focus on what's relevant to the question.\n- **Use structured output** when listing multiple items (bullet points, not prose).\n\n## Strategy\n\n1. Start with \`search\` to find relevant files by name or content patterns\n2. Use \`read_files\` to read specific sections (use line ranges, don't read entire large files)\n3. Use \`query_history\` for understanding why code changed or tracing decisions\n\n## Anti-patterns to Avoid\n\n- Don't produce multi-thousand-word reports for simple questions\n- Don't read every file in a directory when a targeted search would suffice\n- Don't explain how tools work \u2014 just use them and report findings\n`
|
|
1515
|
-
await r.writeFile(c.join(
|
|
1549
|
+
await r.writeFile(c.join(S,"explore.md"),e,"utf8"),this.logger.info("Created explore agent override with research prompt"),this.logger.info("Added agent tools to opencode.json",{agentCount:Object.keys(j.agent).length})}const A=e=>"string"==typeof e&&e.includes(".opencode/plugin/"),P=(await this.fetchPluginManifest())["opencode-claude-auth"],M=j.plugin,O=Array.isArray(M)?M.filter((e=>!A(e))):[]
|
|
1516
1550
|
P&&this.applyAuthPluginPin(O,P),O.length>0?j.plugin=O:delete j.plugin,await r.writeFile(h,JSON.stringify(j,null,2),"utf8"),this.logger.info("OpenCode configured successfully",{configPath:h})
|
|
1517
1551
|
try{const e=await Promise.resolve().then(t.t.bind(t,857,23)),s=c.join(e.homedir(),".alchemy_lab","opencode","config"),o=c.join(s,"opencode.json")
|
|
1518
1552
|
let n=null
|
|
@@ -1635,12 +1669,12 @@ if(i.ok){const e=await i.json()
|
|
|
1635
1669
|
return e.data?.id||e.id}s.info(`Project registration failed (HTTP ${i.status})`)}catch(e){s.info("Project registration failed",{error:(0,b.getErrorMessage)(e)})}}Object.defineProperty(i,"__esModule",{value:!0})
|
|
1636
1670
|
const l=e(3444),d=e(9896),u=e(1943),h=e(6928),g=e(5317),p=e(857),m=e(1253),f=e(8984),y=e(2366),v=e(3962),w=e(1187),b=e(8824),I=e(6800),E=e(4139);(async function(){23===parseInt(process.version.replace("v","").split(".")[0],10)&&(console.error(`\n\u274c Node.js ${process.version} is not supported.`),console.error(" AlchemyLab requires Node.js 22 or 24+ (Node 23 has a broken sqlite API)."),console.error("\n Switch with: nvm use 22 or nvm use 24\n"),process.exit(1))
|
|
1637
1671
|
const i=process.argv.slice(2)
|
|
1638
|
-
let
|
|
1639
|
-
const
|
|
1640
|
-
const k=i.filter(((e,t)=>"--port"!==e&&(-1===
|
|
1672
|
+
let S
|
|
1673
|
+
const C=i.indexOf("--port");-1!==C&&i[C+1]&&(S=parseInt(i[C+1],10),isNaN(S)&&(console.error("\u274c Invalid port number"),process.exit(1)))
|
|
1674
|
+
const k=i.filter(((e,t)=>"--port"!==e&&(-1===C||t!==C+1)))
|
|
1641
1675
|
if(0===k.length)return void await async function(r={}){const i=process.cwd(),l=(0,h.join)(i,E.ALCHEMY_DIR_NAME)
|
|
1642
1676
|
await(0,u.mkdir)(l,{recursive:!0})
|
|
1643
|
-
const
|
|
1677
|
+
const S=(0,h.join)(l,"server.log"),{createLogger:C}=await Promise.resolve().then(e.t.bind(e,2782,23)),k=C({serviceName:"alchemy-server",console:!1,logToFile:!0,logFilePath:S,baseDir:i}),T=await a(k),_=new m.TokenManager(k,i,{remoteServerUrl:T,allowInteractiveAuth:!1})
|
|
1644
1678
|
await _.initialize()
|
|
1645
1679
|
let j=await _.getToken()
|
|
1646
1680
|
if(!j?.token){console.log(""),console.log("\u{1f511} Not logged in. Opening browser to authenticate...")
|
|
@@ -1689,7 +1723,7 @@ u=setInterval((async()=>{if(!y)try{if(await _.hasValidToken())return
|
|
|
1689
1723
|
y=!0,console.log(""),console.log("\u26a0\ufe0f Auth token expired. Opening browser to re-authenticate...")
|
|
1690
1724
|
const e=await _.startAuthenticationFlow()
|
|
1691
1725
|
if(console.log("\u{1f310} "+e.authUrl),o(e.authUrl),console.log("\u23f1\ufe0f Waiting for re-authentication..."),await n(_)){const e=await _.getToken()
|
|
1692
|
-
e?.token&&(A=e.token,D.updateAuthToken(A)),console.log("\u2705 Re-authenticated successfully.")}else console.log("\u274c Re-authentication timed out. Some features may not work until you restart.")}catch(e){k.warn("Token watchdog check failed",{error:(0,b.getErrorMessage)(e)})}finally{y=!1}}),3e5),await m.start(g)}catch(e){k.error("Failed to start server",{error:(0,b.getErrorMessage)(e)}),console.error("\u274c Failed to start: "+(0,b.getErrorMessage)(e)),process.exit(1)}}({port:
|
|
1726
|
+
e?.token&&(A=e.token,D.updateAuthToken(A)),console.log("\u2705 Re-authenticated successfully.")}else console.log("\u274c Re-authentication timed out. Some features may not work until you restart.")}catch(e){k.warn("Token watchdog check failed",{error:(0,b.getErrorMessage)(e)})}finally{y=!1}}),3e5),await m.start(g)}catch(e){k.error("Failed to start server",{error:(0,b.getErrorMessage)(e)}),console.error("\u274c Failed to start: "+(0,b.getErrorMessage)(e)),process.exit(1)}}({port:S})
|
|
1693
1727
|
const T=k[0].toLowerCase()
|
|
1694
1728
|
switch("--help"!==T&&"-h"!==T||(r(),process.exit(0)),"--version"!==T&&"-v"!==T||(console.log("alchemy v"+t()),process.exit(0)),T){case"serve":await async function(e={}){const r=process.cwd()
|
|
1695
1729
|
s(r)||(console.log("\u{1f4c1} Current directory is not an AlchemyLab project."),console.log(""),console.log("\u{1f4a1} Run `alchemy` to initialize this project."),console.log(""),process.exit(1))
|
|
@@ -1703,7 +1737,7 @@ const h=e.port||await(0,I.findAvailablePort)(3456),g=new y.AlchemyActionServer(o
|
|
|
1703
1737
|
try{await g.start(),console.log(""),console.log("\u2705 Action server running!"),console.log(` Health: http://127.0.0.1:${g.getPort()}/health`),console.log(""),console.log("Press Ctrl+C to stop the server."),console.log("")
|
|
1704
1738
|
let e=!1
|
|
1705
1739
|
const t=async()=>{e||(e=!0,console.log(""),console.log("Shutting down action server..."),await g.stop(),process.exit(0))}
|
|
1706
|
-
process.on("SIGTERM",t),process.on("SIGINT",t),await new Promise((()=>{}))}catch(e){o.error("Failed to start server",{error:(0,b.getErrorMessage)(e)}),console.error("\u274c Failed to start: "+(0,b.getErrorMessage)(e)),process.exit(1)}}({port:
|
|
1740
|
+
process.on("SIGTERM",t),process.on("SIGINT",t),await new Promise((()=>{}))}catch(e){o.error("Failed to start server",{error:(0,b.getErrorMessage)(e)}),console.error("\u274c Failed to start: "+(0,b.getErrorMessage)(e)),process.exit(1)}}({port:S})
|
|
1707
1741
|
break
|
|
1708
1742
|
case"logout":await async function(){const e=process.cwd(),t=(0,l.createEnvLogger)({serviceName:"alchemy-cli",console:!1,baseDir:e}),r=await a(t),s=new m.TokenManager(t,e,{remoteServerUrl:r,allowInteractiveAuth:!1})
|
|
1709
1743
|
await s.initialize(),await s.hasValidToken()?await s.removeToken()?console.log("Logged out successfully."):(console.error("Failed to remove auth token."),process.exit(1)):console.log("Not currently logged in.")}()
|