@automagik/genie 4.260406.1 → 4.260406.2

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.
@@ -10,7 +10,7 @@
10
10
  "plugins": [
11
11
  {
12
12
  "name": "genie",
13
- "version": "4.260406.1",
13
+ "version": "4.260406.2",
14
14
  "source": "./plugins/genie",
15
15
  "description": "Human-AI partnership for Claude Code. Share a terminal, orchestrate workers, evolve together. Brainstorm ideas, wish them into plans, make with parallel agents, ship as one team. A coding genie that grows with your project."
16
16
  }
package/dist/genie.js CHANGED
@@ -720,7 +720,7 @@ Sync complete: ${total} active agent(s), ${result2.archived.length} removed.`)}a
720
720
  ${sql.json(input.metadata??{})}
721
721
  )
722
722
  RETURNING *
723
- `,task=mapTask(rows[0]);if(input.metadata?.brain)try{let brain=await import("@automagik/genie-brain");if(brain.taskBrain)await brain.taskBrain({taskId:String(task.id),workdir:repo})}catch{}return task}async function getTask(idOrSeq,repoPath){let sql=await getConnection(),repo=repoPath??getRepoPath(),id=await resolveTaskId(idOrSeq,repo);if(!id)return null;let rows=await sql`SELECT * FROM tasks WHERE id = ${id}`;return rows.length>0?mapTask(rows[0]):null}function buildScopeConditions(filters,conditions,values2,startIdx){let paramIdx=startIdx;if(filters.projectName)conditions.push(`project_id = (SELECT id FROM projects WHERE name = $${paramIdx++})`),values2.push(filters.projectName);else if(filters.allProjects);else conditions.push(`repo_path = $${paramIdx++}`),values2.push(filters.repoPath??getRepoPath());return paramIdx}function buildFieldConditions(filters,conditions,values2,startIdx,colPrefix=""){let paramIdx=startIdx,simple=[["stage",filters.stage],["status",filters.status],["priority",filters.priority],["type_id",filters.typeId],["release_id",filters.releaseId]];for(let[col,val]of simple)if(val)conditions.push(`${colPrefix}${col} = $${paramIdx++}`),values2.push(val);if(!filters.status&&!filters.includeArchived)conditions.push(`${colPrefix}status != 'archived'`);if(filters.parentId!==void 0)if(filters.parentId===null)conditions.push("parent_id IS NULL");else conditions.push(`parent_id = $${paramIdx++}`),values2.push(filters.parentId);if(filters.boardId)conditions.push(`board_id = $${paramIdx++}`),values2.push(filters.boardId);if(filters.boardName)conditions.push(`board_id IN (SELECT id FROM boards WHERE name = $${paramIdx++})`),values2.push(filters.boardName);if(filters.externalId)conditions.push(`external_id = $${paramIdx++}`),values2.push(filters.externalId);if(filters.dueBefore)conditions.push(`due_date <= $${paramIdx++}`),values2.push(new Date(filters.dueBefore));return paramIdx}async function listTasks(filters={}){let sql=await getConnection(),conditions=[],values2=[],paramIdx=buildScopeConditions(filters,conditions,values2,1);paramIdx=buildFieldConditions(filters,conditions,values2,paramIdx);let where=conditions.length>0?conditions.join(" AND "):"1=1",limit=filters.limit??100,offset=filters.offset??0,query=`SELECT * FROM tasks WHERE ${where} ORDER BY created_at DESC LIMIT $${paramIdx++} OFFSET $${paramIdx++}`;return values2.push(limit,offset),(await sql.unsafe(query,values2)).map(mapTask)}async function updateTask(idOrSeq,updates,repoPath,comment){let sql=await getConnection(),repo=repoPath??getRepoPath(),id=await resolveTaskId(idOrSeq,repo);if(!id)return null;let sets=["updated_at = now()"],values2=[],paramIdx=1,fieldMap={title:["title",updates.title],description:["description",updates.description],acceptanceCriteria:["acceptance_criteria",updates.acceptanceCriteria],stage:["stage",updates.stage],status:["status",updates.status],priority:["priority",updates.priority],parentId:["parent_id",updates.parentId],wishFile:["wish_file",updates.wishFile],groupName:["group_name",updates.groupName],estimatedEffort:["estimated_effort",updates.estimatedEffort],blockedReason:["blocked_reason",updates.blockedReason],releaseId:["release_id",updates.releaseId],externalId:["external_id",updates.externalId],externalUrl:["external_url",updates.externalUrl]};for(let[key,[col,val]]of Object.entries(fieldMap))if(key in updates&&val!==void 0)sets.push(`${col} = $${paramIdx++}`),values2.push(val);if(updates.startDate!==void 0)sets.push(`start_date = $${paramIdx++}`),values2.push(updates.startDate?new Date(updates.startDate):null);if(updates.dueDate!==void 0)sets.push(`due_date = $${paramIdx++}`),values2.push(updates.dueDate?new Date(updates.dueDate):null);if(updates.metadata!==void 0)sets.push(`metadata = $${paramIdx++}`),values2.push(JSON.stringify(updates.metadata));values2.push(id);let query=`UPDATE tasks SET ${sets.join(", ")} WHERE id = $${paramIdx} RETURNING *`,rows=await sql.unsafe(query,values2);if(rows.length===0)return null;if(comment)await commentOnTask(id,comment.actor,comment.body,repo);return mapTask(rows[0])}async function linkTask(idOrSeq,externalId,externalUrl,repoPath){return updateTask(idOrSeq,{externalId,externalUrl},repoPath)}async function moveTask(idOrSeq,toStage,actor,comment,repoPath){let sql=await getConnection(),repo=repoPath??getRepoPath(),id=await resolveTaskId(idOrSeq,repo);if(!id)throw Error(`Task not found: ${idOrSeq}`);let current=await sql`SELECT id, stage, type_id, board_id FROM tasks WHERE id = ${id}`;if(current.length===0)throw Error(`Task not found: ${idOrSeq}`);let fromStage=current[0].stage,boardId=current[0].board_id,columnId=boardId?await resolveColumnId(sql,boardId,toStage):null;try{let result2=await sql.begin(async(tx)=>{let rows=boardId?await tx`
723
+ `,task=mapTask(rows[0]);if(input.metadata?.brain)try{let brain=await import("@khal-os/brain");if(brain.taskBrain)await brain.taskBrain({taskId:String(task.id),workdir:repo})}catch{}return task}async function getTask(idOrSeq,repoPath){let sql=await getConnection(),repo=repoPath??getRepoPath(),id=await resolveTaskId(idOrSeq,repo);if(!id)return null;let rows=await sql`SELECT * FROM tasks WHERE id = ${id}`;return rows.length>0?mapTask(rows[0]):null}function buildScopeConditions(filters,conditions,values2,startIdx){let paramIdx=startIdx;if(filters.projectName)conditions.push(`project_id = (SELECT id FROM projects WHERE name = $${paramIdx++})`),values2.push(filters.projectName);else if(filters.allProjects);else conditions.push(`repo_path = $${paramIdx++}`),values2.push(filters.repoPath??getRepoPath());return paramIdx}function buildFieldConditions(filters,conditions,values2,startIdx,colPrefix=""){let paramIdx=startIdx,simple=[["stage",filters.stage],["status",filters.status],["priority",filters.priority],["type_id",filters.typeId],["release_id",filters.releaseId]];for(let[col,val]of simple)if(val)conditions.push(`${colPrefix}${col} = $${paramIdx++}`),values2.push(val);if(!filters.status&&!filters.includeArchived)conditions.push(`${colPrefix}status != 'archived'`);if(filters.parentId!==void 0)if(filters.parentId===null)conditions.push("parent_id IS NULL");else conditions.push(`parent_id = $${paramIdx++}`),values2.push(filters.parentId);if(filters.boardId)conditions.push(`board_id = $${paramIdx++}`),values2.push(filters.boardId);if(filters.boardName)conditions.push(`board_id IN (SELECT id FROM boards WHERE name = $${paramIdx++})`),values2.push(filters.boardName);if(filters.externalId)conditions.push(`external_id = $${paramIdx++}`),values2.push(filters.externalId);if(filters.dueBefore)conditions.push(`due_date <= $${paramIdx++}`),values2.push(new Date(filters.dueBefore));return paramIdx}async function listTasks(filters={}){let sql=await getConnection(),conditions=[],values2=[],paramIdx=buildScopeConditions(filters,conditions,values2,1);paramIdx=buildFieldConditions(filters,conditions,values2,paramIdx);let where=conditions.length>0?conditions.join(" AND "):"1=1",limit=filters.limit??100,offset=filters.offset??0,query=`SELECT * FROM tasks WHERE ${where} ORDER BY created_at DESC LIMIT $${paramIdx++} OFFSET $${paramIdx++}`;return values2.push(limit,offset),(await sql.unsafe(query,values2)).map(mapTask)}async function updateTask(idOrSeq,updates,repoPath,comment){let sql=await getConnection(),repo=repoPath??getRepoPath(),id=await resolveTaskId(idOrSeq,repo);if(!id)return null;let sets=["updated_at = now()"],values2=[],paramIdx=1,fieldMap={title:["title",updates.title],description:["description",updates.description],acceptanceCriteria:["acceptance_criteria",updates.acceptanceCriteria],stage:["stage",updates.stage],status:["status",updates.status],priority:["priority",updates.priority],parentId:["parent_id",updates.parentId],wishFile:["wish_file",updates.wishFile],groupName:["group_name",updates.groupName],estimatedEffort:["estimated_effort",updates.estimatedEffort],blockedReason:["blocked_reason",updates.blockedReason],releaseId:["release_id",updates.releaseId],externalId:["external_id",updates.externalId],externalUrl:["external_url",updates.externalUrl]};for(let[key,[col,val]]of Object.entries(fieldMap))if(key in updates&&val!==void 0)sets.push(`${col} = $${paramIdx++}`),values2.push(val);if(updates.startDate!==void 0)sets.push(`start_date = $${paramIdx++}`),values2.push(updates.startDate?new Date(updates.startDate):null);if(updates.dueDate!==void 0)sets.push(`due_date = $${paramIdx++}`),values2.push(updates.dueDate?new Date(updates.dueDate):null);if(updates.metadata!==void 0)sets.push(`metadata = $${paramIdx++}`),values2.push(JSON.stringify(updates.metadata));values2.push(id);let query=`UPDATE tasks SET ${sets.join(", ")} WHERE id = $${paramIdx} RETURNING *`,rows=await sql.unsafe(query,values2);if(rows.length===0)return null;if(comment)await commentOnTask(id,comment.actor,comment.body,repo);return mapTask(rows[0])}async function linkTask(idOrSeq,externalId,externalUrl,repoPath){return updateTask(idOrSeq,{externalId,externalUrl},repoPath)}async function moveTask(idOrSeq,toStage,actor,comment,repoPath){let sql=await getConnection(),repo=repoPath??getRepoPath(),id=await resolveTaskId(idOrSeq,repo);if(!id)throw Error(`Task not found: ${idOrSeq}`);let current=await sql`SELECT id, stage, type_id, board_id FROM tasks WHERE id = ${id}`;if(current.length===0)throw Error(`Task not found: ${idOrSeq}`);let fromStage=current[0].stage,boardId=current[0].board_id,columnId=boardId?await resolveColumnId(sql,boardId,toStage):null;try{let result2=await sql.begin(async(tx)=>{let rows=boardId?await tx`
724
724
  UPDATE tasks SET stage = ${toStage}, column_id = ${columnId}, updated_at = now()
725
725
  WHERE id = ${id}
726
726
  RETURNING *
@@ -1128,7 +1128,7 @@ Stopping inbox watcher...`),stopInboxWatcher2(handle),process.exit(0)};process.o
1128
1128
  WHERE id = ANY(${ids})
1129
1129
  `,await sql`
1130
1130
  DELETE FROM task_actors WHERE task_id = ANY(${ids}) AND role = 'assignee'
1131
- `,ids.length}var GroupStatusSchema,GroupStateSchema,WishStateSchema;var init_wish_state=__esm(()=>{init_zod();init_db();GroupStatusSchema=exports_external.enum(["blocked","ready","in_progress","done"]),GroupStateSchema=exports_external.object({status:GroupStatusSchema,assignee:exports_external.string().optional(),dependsOn:exports_external.array(exports_external.string()).default([]),startedAt:exports_external.string().optional(),completedAt:exports_external.string().optional()}),WishStateSchema=exports_external.object({wish:exports_external.string(),groups:exports_external.record(exports_external.string(),GroupStateSchema),createdAt:exports_external.string(),updatedAt:exports_external.string()})});var exports_protocol_router_spawn={};__export(exports_protocol_router_spawn,{spawnWorkerFromTemplate:()=>spawnWorkerFromTemplate,injectResumeContext:()=>injectResumeContext,_deps:()=>_deps});import{exec as exec2}from"child_process";import{readFile as readFile6}from"fs/promises";import{join as join30}from"path";import{promisify as promisify2}from"util";async function resolveParentSession(_repoPath,team){let teamConfig=await getTeam(team);if(teamConfig?.nativeTeamParentSessionId)return teamConfig.nativeTeamParentSessionId;return await discoverClaudeParentSessionId()??`genie-${team}`}function buildSpawnParams(template,parentSessionId,spawnColor,resumeSessionId){let isClaude=template.provider==="claude"||template.provider==="claude-sdk",sessionName=template.role?`${template.team}-${template.role}`:void 0,newSessionId=isClaude&&!resumeSessionId?crypto.randomUUID():void 0,params={provider:template.provider,team:template.team,role:template.role,skill:template.skill,extraArgs:template.extraArgs,sessionId:newSessionId,resume:isClaude?resumeSessionId:void 0,name:sessionName};if(isClaude)params.nativeTeam={enabled:!0,parentSessionId,color:spawnColor,agentType:template.role??"general-purpose",agentName:template.role};return params}function buildFullCommand(launch){if(launch.env&&Object.keys(launch.env).length>0)return`env ${Object.entries(launch.env).map(([k,v])=>`${k}=${v}`).join(" ")} ${launch.command}`;return launch.command}async function generateWorkerId(team,role){let base=role?`${team}-${role}`:team;return(await list()).some((w)=>w.id===base)?`${base}-${crypto.randomUUID().slice(0,8)}`:base}async function createExecutorForAutoSpawn(template,paneId,session,repoPath,effectiveSessionId,spawnColor,teamWindow){let agentName=template.role??"worker",agentIdentity=await findOrCreateAgent(agentName,template.team,template.role);await(await getConnection()).begin(async(tx)=>{await tx`SELECT pg_advisory_xact_lock(hashtext(${agentIdentity.id}))`;let currentExec=await getCurrentExecutor(agentIdentity.id);if(currentExec&&currentExec.state!=="terminated"&&currentExec.state!=="done"){let providerImpl=getProvider(currentExec.provider);if(providerImpl)try{await providerImpl.terminate(currentExec)}catch{}await terminateActiveExecutor(agentIdentity.id)}let pid=null;try{let{stdout:pidOut}=await execAsync(genieTmuxCmd(`display -t '${paneId}' -p '#{pane_pid}'`)),parsed=Number.parseInt(pidOut.trim(),10);if(parsed>0)pid=parsed}catch{}let executorTransport=template.provider==="codex"?"api":template.provider==="claude-sdk"?"process":"tmux",executor=await createExecutor(agentIdentity.id,template.provider,executorTransport,{pid,tmuxSession:session,tmuxPaneId:paneId,tmuxWindow:teamWindow?.windowName??null,tmuxWindowId:teamWindow?.windowId??null,claudeSessionId:effectiveSessionId??null,state:"spawning",repoPath,paneColor:spawnColor??null});await setCurrentExecutor(agentIdentity.id,executor.id)})}async function spawnPaneInSession(session,team,repoPath,fullCommand){let teamWindow=null;try{teamWindow=await ensureTeamWindow(session,team,repoPath)}catch{}let splitTarget=teamWindow?`-t '${teamWindow.windowId}'`:"",escapedCmd=fullCommand.replace(/'/g,"'\\''"),{stdout}=await execAsync(genieTmuxCmd(`split-window -d ${splitTarget} -P -F '#{pane_id}' '${escapedCmd}'`)),paneId=stdout.trim(),layoutTarget=`${session}:${teamWindow?.windowName??""}`;if(!teamWindow){let wins=await listWindows(session);layoutTarget=wins[0]?wins[0].id:`${session}:`}try{await execAsync(genieTmuxCmd(buildLayoutCommand(layoutTarget,resolveLayoutMode())))}catch{}return{paneId,teamWindow}}async function spawnWorkerFromTemplate(template,resumeSessionId){let repoPath=template.cwd??process.cwd(),team=template.team,parentSessionId=await resolveParentSession(repoPath,team);await ensureNativeTeam(team,`Genie team: ${team}`,parentSessionId);let spawnColor=await assignColor(team),params=buildSpawnParams(template,parentSessionId,spawnColor,resumeSessionId),launch=buildLaunchCommand(validateSpawnParams(params)),fullCommand=buildFullCommand(launch),workerId=await generateWorkerId(team,template.role),session=(await getTeam(team))?.tmuxSessionName??await resolveRepoSession(repoPath)??await getCurrentSessionName()??team,{paneId,teamWindow}=await spawnPaneInSession(session,team,repoPath,fullCommand),now=new Date().toISOString(),agentName=template.role??"worker",isClaude=template.provider==="claude"||template.provider==="claude-sdk",effectiveSessionId=resumeSessionId??params.sessionId,workerEntry={id:workerId,paneId,session,provider:template.provider,transport:"tmux",role:template.role,skill:template.skill,team,worktree:null,startedAt:now,state:"spawning",lastStateChange:now,repoPath,claudeSessionId:effectiveSessionId,nativeTeamEnabled:isClaude,nativeAgentId:`${agentName}@${team}`,nativeColor:spawnColor,parentSessionId,window:teamWindow?.windowName,windowName:teamWindow?.windowName,windowId:teamWindow?.windowId};await register(workerEntry);try{await createExecutorForAutoSpawn(template,paneId,session,repoPath,effectiveSessionId,spawnColor,teamWindow)}catch{}if(isClaude){await registerNativeMember(team,{agentName,agentType:template.role??"general-purpose",color:spawnColor??"blue",tmuxPaneId:paneId,cwd:repoPath});let leaderInboxTarget;try{let{resolveLeaderName:resolveLeaderName2}=await Promise.resolve().then(() => (init_team_manager(),exports_team_manager));leaderInboxTarget=await resolveLeaderName2(team)}catch{leaderInboxTarget=team}try{await _deps.writeNativeInbox(team,leaderInboxTarget,{from:agentName,text:`Worker ${agentName} (${template.provider}) auto-spawned${resumeSessionId?" with --resume":""}. Ready for tasks.`,summary:`${agentName} auto-spawned`,timestamp:now,color:spawnColor??"blue",read:!1})}catch(err){let msg=err instanceof Error?err.message:String(err);console.warn(`[protocol-router] Native inbox write failed for team="${team}" target="${leaderInboxTarget}": ${msg}`)}}if(spawnColor)await applyPaneColor(paneId,spawnColor,teamWindow?.windowId);await injectResumeContext(repoPath,workerId,agentName,team);try{let brain=await import("@automagik/genie-brain");if(brain.autoBrain)await brain.autoBrain({agentId:workerId,workdir:repoPath})}catch{}return{worker:workerEntry,paneId,workerId}}function extractGroupSection(content,groupName){let escaped=groupName.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),pattern=new RegExp(`^### Group ${escaped}:`,"m"),match=content.match(pattern);if(!match||match.index===void 0)return null;let start=match.index,nextBoundary=content.slice(start).slice(1).search(/^### Group \d|^---$/m),end=nextBoundary!==-1?start+1+nextBoundary:content.length;return content.slice(start,end).trim()}async function getRecentGitLog(repoPath,count=3){try{let{stdout}=await execAsync(`git -C '${repoPath}' log --oneline -${count} 2>/dev/null`);return stdout.trim()}catch{return""}}async function getGitStatus(repoPath){try{let{stdout}=await execAsync(`git -C '${repoPath}' status --short 2>/dev/null`);return stdout.trim()}catch{return""}}async function injectResumeContext(repoPath,workerId,agentName,_team){try{let match=await _deps.findAnyGroupByAssignee(workerId,repoPath)??await _deps.findAnyGroupByAssignee(agentName,repoPath);if(!match)return;let{slug,groupName,group}=match,wishPath=join30(repoPath,".genie","wishes",slug,"WISH.md"),groupSection="";try{let wishContent=await readFile6(wishPath,"utf-8");groupSection=extractGroupSection(wishContent,groupName)??""}catch{}let gitLog=await getRecentGitLog(repoPath),gitStatus=await getGitStatus(repoPath),resumePrompt=[`RESUME CONTEXT: You were working on wish "${slug}", group "${groupName}".`,`Status: ${group.status}. Started at: ${group.startedAt??"unknown"}.`,`Wish file: .genie/wishes/${slug}/WISH.md`,"",groupSection?`Group section:
1131
+ `,ids.length}var GroupStatusSchema,GroupStateSchema,WishStateSchema;var init_wish_state=__esm(()=>{init_zod();init_db();GroupStatusSchema=exports_external.enum(["blocked","ready","in_progress","done"]),GroupStateSchema=exports_external.object({status:GroupStatusSchema,assignee:exports_external.string().optional(),dependsOn:exports_external.array(exports_external.string()).default([]),startedAt:exports_external.string().optional(),completedAt:exports_external.string().optional()}),WishStateSchema=exports_external.object({wish:exports_external.string(),groups:exports_external.record(exports_external.string(),GroupStateSchema),createdAt:exports_external.string(),updatedAt:exports_external.string()})});var exports_protocol_router_spawn={};__export(exports_protocol_router_spawn,{spawnWorkerFromTemplate:()=>spawnWorkerFromTemplate,injectResumeContext:()=>injectResumeContext,_deps:()=>_deps});import{exec as exec2}from"child_process";import{readFile as readFile6}from"fs/promises";import{join as join30}from"path";import{promisify as promisify2}from"util";async function resolveParentSession(_repoPath,team){let teamConfig=await getTeam(team);if(teamConfig?.nativeTeamParentSessionId)return teamConfig.nativeTeamParentSessionId;return await discoverClaudeParentSessionId()??`genie-${team}`}function buildSpawnParams(template,parentSessionId,spawnColor,resumeSessionId){let isClaude=template.provider==="claude"||template.provider==="claude-sdk",sessionName=template.role?`${template.team}-${template.role}`:void 0,newSessionId=isClaude&&!resumeSessionId?crypto.randomUUID():void 0,params={provider:template.provider,team:template.team,role:template.role,skill:template.skill,extraArgs:template.extraArgs,sessionId:newSessionId,resume:isClaude?resumeSessionId:void 0,name:sessionName};if(isClaude)params.nativeTeam={enabled:!0,parentSessionId,color:spawnColor,agentType:template.role??"general-purpose",agentName:template.role};return params}function buildFullCommand(launch){if(launch.env&&Object.keys(launch.env).length>0)return`env ${Object.entries(launch.env).map(([k,v])=>`${k}=${v}`).join(" ")} ${launch.command}`;return launch.command}async function generateWorkerId(team,role){let base=role?`${team}-${role}`:team;return(await list()).some((w)=>w.id===base)?`${base}-${crypto.randomUUID().slice(0,8)}`:base}async function createExecutorForAutoSpawn(template,paneId,session,repoPath,effectiveSessionId,spawnColor,teamWindow){let agentName=template.role??"worker",agentIdentity=await findOrCreateAgent(agentName,template.team,template.role);await(await getConnection()).begin(async(tx)=>{await tx`SELECT pg_advisory_xact_lock(hashtext(${agentIdentity.id}))`;let currentExec=await getCurrentExecutor(agentIdentity.id);if(currentExec&&currentExec.state!=="terminated"&&currentExec.state!=="done"){let providerImpl=getProvider(currentExec.provider);if(providerImpl)try{await providerImpl.terminate(currentExec)}catch{}await terminateActiveExecutor(agentIdentity.id)}let pid=null;try{let{stdout:pidOut}=await execAsync(genieTmuxCmd(`display -t '${paneId}' -p '#{pane_pid}'`)),parsed=Number.parseInt(pidOut.trim(),10);if(parsed>0)pid=parsed}catch{}let executorTransport=template.provider==="codex"?"api":template.provider==="claude-sdk"?"process":"tmux",executor=await createExecutor(agentIdentity.id,template.provider,executorTransport,{pid,tmuxSession:session,tmuxPaneId:paneId,tmuxWindow:teamWindow?.windowName??null,tmuxWindowId:teamWindow?.windowId??null,claudeSessionId:effectiveSessionId??null,state:"spawning",repoPath,paneColor:spawnColor??null});await setCurrentExecutor(agentIdentity.id,executor.id)})}async function spawnPaneInSession(session,team,repoPath,fullCommand){let teamWindow=null;try{teamWindow=await ensureTeamWindow(session,team,repoPath)}catch{}let splitTarget=teamWindow?`-t '${teamWindow.windowId}'`:"",escapedCmd=fullCommand.replace(/'/g,"'\\''"),{stdout}=await execAsync(genieTmuxCmd(`split-window -d ${splitTarget} -P -F '#{pane_id}' '${escapedCmd}'`)),paneId=stdout.trim(),layoutTarget=`${session}:${teamWindow?.windowName??""}`;if(!teamWindow){let wins=await listWindows(session);layoutTarget=wins[0]?wins[0].id:`${session}:`}try{await execAsync(genieTmuxCmd(buildLayoutCommand(layoutTarget,resolveLayoutMode())))}catch{}return{paneId,teamWindow}}async function spawnWorkerFromTemplate(template,resumeSessionId){let repoPath=template.cwd??process.cwd(),team=template.team,parentSessionId=await resolveParentSession(repoPath,team);await ensureNativeTeam(team,`Genie team: ${team}`,parentSessionId);let spawnColor=await assignColor(team),params=buildSpawnParams(template,parentSessionId,spawnColor,resumeSessionId),launch=buildLaunchCommand(validateSpawnParams(params)),fullCommand=buildFullCommand(launch),workerId=await generateWorkerId(team,template.role),session=(await getTeam(team))?.tmuxSessionName??await resolveRepoSession(repoPath)??await getCurrentSessionName()??team,{paneId,teamWindow}=await spawnPaneInSession(session,team,repoPath,fullCommand),now=new Date().toISOString(),agentName=template.role??"worker",isClaude=template.provider==="claude"||template.provider==="claude-sdk",effectiveSessionId=resumeSessionId??params.sessionId,workerEntry={id:workerId,paneId,session,provider:template.provider,transport:"tmux",role:template.role,skill:template.skill,team,worktree:null,startedAt:now,state:"spawning",lastStateChange:now,repoPath,claudeSessionId:effectiveSessionId,nativeTeamEnabled:isClaude,nativeAgentId:`${agentName}@${team}`,nativeColor:spawnColor,parentSessionId,window:teamWindow?.windowName,windowName:teamWindow?.windowName,windowId:teamWindow?.windowId};await register(workerEntry);try{await createExecutorForAutoSpawn(template,paneId,session,repoPath,effectiveSessionId,spawnColor,teamWindow)}catch{}if(isClaude){await registerNativeMember(team,{agentName,agentType:template.role??"general-purpose",color:spawnColor??"blue",tmuxPaneId:paneId,cwd:repoPath});let leaderInboxTarget;try{let{resolveLeaderName:resolveLeaderName2}=await Promise.resolve().then(() => (init_team_manager(),exports_team_manager));leaderInboxTarget=await resolveLeaderName2(team)}catch{leaderInboxTarget=team}try{await _deps.writeNativeInbox(team,leaderInboxTarget,{from:agentName,text:`Worker ${agentName} (${template.provider}) auto-spawned${resumeSessionId?" with --resume":""}. Ready for tasks.`,summary:`${agentName} auto-spawned`,timestamp:now,color:spawnColor??"blue",read:!1})}catch(err){let msg=err instanceof Error?err.message:String(err);console.warn(`[protocol-router] Native inbox write failed for team="${team}" target="${leaderInboxTarget}": ${msg}`)}}if(spawnColor)await applyPaneColor(paneId,spawnColor,teamWindow?.windowId);await injectResumeContext(repoPath,workerId,agentName,team);try{let brain=await import("@khal-os/brain");if(brain.autoBrain)await brain.autoBrain({agentId:workerId,workdir:repoPath})}catch{}return{worker:workerEntry,paneId,workerId}}function extractGroupSection(content,groupName){let escaped=groupName.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),pattern=new RegExp(`^### Group ${escaped}:`,"m"),match=content.match(pattern);if(!match||match.index===void 0)return null;let start=match.index,nextBoundary=content.slice(start).slice(1).search(/^### Group \d|^---$/m),end=nextBoundary!==-1?start+1+nextBoundary:content.length;return content.slice(start,end).trim()}async function getRecentGitLog(repoPath,count=3){try{let{stdout}=await execAsync(`git -C '${repoPath}' log --oneline -${count} 2>/dev/null`);return stdout.trim()}catch{return""}}async function getGitStatus(repoPath){try{let{stdout}=await execAsync(`git -C '${repoPath}' status --short 2>/dev/null`);return stdout.trim()}catch{return""}}async function injectResumeContext(repoPath,workerId,agentName,_team){try{let match=await _deps.findAnyGroupByAssignee(workerId,repoPath)??await _deps.findAnyGroupByAssignee(agentName,repoPath);if(!match)return;let{slug,groupName,group}=match,wishPath=join30(repoPath,".genie","wishes",slug,"WISH.md"),groupSection="";try{let wishContent=await readFile6(wishPath,"utf-8");groupSection=extractGroupSection(wishContent,groupName)??""}catch{}let gitLog=await getRecentGitLog(repoPath),gitStatus=await getGitStatus(repoPath),resumePrompt=[`RESUME CONTEXT: You were working on wish "${slug}", group "${groupName}".`,`Status: ${group.status}. Started at: ${group.startedAt??"unknown"}.`,`Wish file: .genie/wishes/${slug}/WISH.md`,"",groupSection?`Group section:
1132
1132
  ${groupSection}`:"","",gitLog?`Last git log:
1133
1133
  ${gitLog}`:"","",gitStatus?`Uncommitted changes:
1134
1134
  ${gitStatus}`:"","","Pick up where you left off. Read the wish file for full context."].filter(Boolean).join(`
@@ -1991,10 +1991,33 @@ ${answer}
1991
1991
  ON CONFLICT (id) DO NOTHING
1992
1992
  RETURNING id`,null,{executorId,chatId:""}))return null;return sessionId}async function recordTurn(safePgCall,sessionId,turnIndex,role,content,toolName,timestamp2){let ts3=timestamp2??new Date().toISOString();await safePgCall("sdk-session-turn",(sql)=>sql`INSERT INTO session_content (session_id, turn_index, role, content, tool_name, timestamp)
1993
1993
  VALUES (${sessionId}, ${turnIndex}, ${role}, ${content}, ${toolName??null}, ${ts3})
1994
- ON CONFLICT (session_id, turn_index) DO NOTHING`,void 0,{executorId:"",chatId:""})}async function updateTurnCount(safePgCall,sessionId,totalTurns){await safePgCall("sdk-session-turn-count",(sql)=>sql`UPDATE sessions SET total_turns = ${totalTurns}, updated_at = now() WHERE id = ${sessionId}`,void 0,{executorId:"",chatId:""})}async function endSession(safePgCall,sessionId,status="completed"){await safePgCall("sdk-session-end",(sql)=>sql`UPDATE sessions SET ended_at = now(), status = ${status}, updated_at = now() WHERE id = ${sessionId}`,void 0,{executorId:"",chatId:""})}async function loadSystemPrompt(entry){let identityPath=loadIdentity(entry);if(!identityPath)return;let{readFileSync:readFileSync22}=await import("fs");try{return readFileSync22(identityPath,"utf-8")}catch{return}}function extractTextFromAssistant(msg){if(!msg.message)return[];return msg.message.content.filter((b2)=>b2.type==="text"&&b2.text).map((b2)=>b2.text)}async function collectQueryResult(queryMessages){let textParts=[],sessionId;try{for await(let msg of queryMessages){if(msg.type==="assistant")textParts.push(...extractTextFromAssistant(msg));if(msg.type==="result"&&msg.subtype==="success")sessionId=msg.session_id}}catch(err){if(err.name==="AbortError")return{text:""};throw err}return{text:textParts.join(`
1995
- `).trim(),sessionId}}function buildReplyPayload(agent,chatId,instanceId,extra={}){return JSON.stringify({content:"",agent,chat_id:chatId,instance_id:instanceId,timestamp:new Date().toISOString(),...extra})}function resolveAction(params,env){if(params.skip)return{type:"skip",extra:{reason:params.reason},label:"Turn closed (skip)."};if(params.react)return{type:"react",extra:{react:String(params.react),message_id:env.OMNI_MESSAGE??""},label:`Reacted ${params.react} + turn closed.`};if(params.media)return{type:"media",extra:{content:params.caption?String(params.caption):params.text?String(params.text):"",media:String(params.media)},label:"Media sent + turn closed."};if(params.text)return{type:"text",extra:{content:String(params.text)},label:"Turn closed. Message delivered."};return{type:"skip",extra:{},label:"Turn closed (skip)."}}function handleDoneTool(params,env,natsPublish){let instanceId=env.OMNI_INSTANCE??"",chatId=env.OMNI_CHAT??"",agent=env.OMNI_AGENT??"",action=resolveAction(params,env);if(action.type==="skip"){if(natsPublish&&instanceId&&chatId)natsPublish(`omni.turn.done.${instanceId}.${chatId}`,JSON.stringify({action:"skip",...action.extra}));return action.label}if(!natsPublish||!instanceId||!chatId)return console.warn("[claude-sdk] No NATS publish available \u2014 reply dropped"),"Turn close attempted but NATS publish not available.";return natsPublish(`omni.reply.${instanceId}.${chatId}`,buildReplyPayload(agent,chatId,instanceId,action.extra)),action.label}async function createDoneMcpServer(env,natsPublish){let{createSdkMcpServer,tool}=await import("@anthropic-ai/claude-agent-sdk");return createSdkMcpServer({name:"genie-omni-tools",tools:[tool("done","Close this turn. REQUIRED after processing the user message. Sends a final response, reacts, or skips. Call exactly once per turn.",{text:exports_external.string().optional().describe("Final message to the user"),media:exports_external.string().optional().describe("File path for media attachment"),caption:exports_external.string().optional().describe("Caption for media"),react:exports_external.string().optional().describe("Emoji reaction (instead of text)"),skip:exports_external.boolean().optional().describe("Close turn without sending anything"),reason:exports_external.string().optional().describe("Internal reason for skipping")},async(args)=>{return{content:[{type:"text",text:handleDoneTool(args,env,natsPublish)}]}})]})}class ClaudeSdkOmniExecutor{sessions=new Map;safePgCall=null;natsPublish=null;deliveryQueues=new Map;pendingNudges=new Map;setSafePgCall(fn){this.safePgCall=fn}setNatsPublish(fn){this.natsPublish=fn}async injectNudge(session,text){if(!this.sessions.has(session.id))return;this.pendingNudges.set(session.id,text)}async spawn(agentName,chatId,env){if(!await resolve3(agentName))throw Error(`Agent "${agentName}" not found in genie directory`);let provider=new ClaudeSdkProvider,abortController=new AbortController,sessionId=`${agentName}:${chatId}`,registration=await this.registerInWorldA(agentName,chatId,env.OMNI_INSTANCE??"");if(this.sessions.set(sessionId,{abortController,running:!0,provider,executorId:registration?.executorId??null,claudeSessionId:registration?.claudeSessionId,dbSessionId:null,turnIndex:0,env}),registration?.executorId)await this.updateState(registration.executorId,"running",chatId);let now=Date.now();return{id:sessionId,agentName,chatId,tmuxSession:"",tmuxWindow:"",paneId:`sdk-${chatId}`,createdAt:now,lastActivityAt:now}}async registerInWorldA(agentName,chatId,instanceId){if(!this.safePgCall)return null;let agent=await this.safePgCall("sdk-find-or-create-agent",()=>findOrCreateAgent(agentName,"omni","omni"),null,{chatId});if(!agent)return null;let existing=await this.safePgCall("sdk-find-existing-executor",()=>findLatestByMetadata({agentId:agent.id,source:"omni",chatId}),null,{chatId});if(existing)return await this.safePgCall("sdk-relink-executor",()=>relinkExecutorToAgent(existing.id,agent.id),void 0,{executorId:existing.id,chatId}),await recordAuditEvent2(this.safePgCall,"session.resumed",{executor_id:existing.id,agent_id:agentName,chat_id:chatId,claude_session_id:existing.claudeSessionId}),{executorId:existing.id,claudeSessionId:existing.claudeSessionId??void 0};let executor=await this.safePgCall("sdk-create-executor",()=>createAndLinkExecutor(agent.id,"claude","api",{claudeSessionId:void 0,metadata:{source:"omni",chat_id:chatId,instance_id:instanceId}}),null,{chatId});if(executor)await recordAuditEvent2(this.safePgCall,"session.created_fresh",{executor_id:executor.id,agent_id:agentName,chat_id:chatId});return executor?{executorId:executor.id}:null}async updateState(executorId,state,chatId){if(!this.safePgCall)return;await this.safePgCall("sdk-update-executor-state",()=>updateExecutorState(executorId,state),void 0,{executorId,chatId})}async deliver(session,message){let state=this.sessions.get(session.id);if(!state)throw Error(`No SDK session found for ${session.id}`);let current=(this.deliveryQueues.get(session.id)??Promise.resolve()).then(()=>this._processDelivery(session,state,message));this.deliveryQueues.set(session.id,current.catch(()=>{}))}async _processDelivery(session,state,message){let resolved=await resolve3(session.agentName);if(!resolved)throw Error(`Agent "${session.agentName}" not found in genie directory`);let entry=resolved.entry,permissionConfig=resolvePermissionConfig(entry.permissions),systemPrompt=await loadSystemPrompt(entry);if(state.executorId)await this.updateState(state.executorId,"working",session.chatId);if(this.safePgCall)await recordAuditEvent2(this.safePgCall,"deliver.start",{executor_id:state.executorId??session.id,agent_id:session.agentName,chat_id:message.chatId,instance_id:message.instanceId});let doneMcp=await createDoneMcpServer(state.env,this.natsPublish),extraOptions={abortController:state.abortController,mcpServers:{"genie-omni-tools":doneMcp}};if(state.claudeSessionId)extraOptions.resume=state.claudeSessionId;let queryContent=message.content,pendingNudge=this.pendingNudges.get(session.id);if(pendingNudge)queryContent=`[system] ${pendingNudge}
1994
+ ON CONFLICT (session_id, turn_index) DO NOTHING`,void 0,{executorId:"",chatId:""})}async function updateTurnCount(safePgCall,sessionId,totalTurns){await safePgCall("sdk-session-turn-count",(sql)=>sql`UPDATE sessions SET total_turns = ${totalTurns}, updated_at = now() WHERE id = ${sessionId}`,void 0,{executorId:"",chatId:""})}async function endSession(safePgCall,sessionId,status="completed"){await safePgCall("sdk-session-end",(sql)=>sql`UPDATE sessions SET ended_at = now(), status = ${status}, updated_at = now() WHERE id = ${sessionId}`,void 0,{executorId:"",chatId:""})}function buildTurnBasedPrompt(senderName,instanceId,chatId){return`
1995
+ # WhatsApp Turn-Based Conversation
1996
1996
 
1997
- ${message.content}`,this.pendingNudges.delete(session.id);let{messages:queryMessages}=state.provider.runQuery({agentId:session.agentName,executorId:session.id,team:"",role:session.agentName,cwd:entry.dir||process.cwd(),model:entry.model,systemPrompt:state.claudeSessionId?void 0:systemPrompt},queryContent,permissionConfig,extraOptions,entry.sdk);await this.captureUserTurn(state,session.agentName,session.id,message.content);let result2=await collectQueryResult(queryMessages);if(result2.sessionId)await this.reconcileSessionId(state,session,result2.sessionId);if(await this.captureAssistantTurn(state,session.id,result2.sessionId,session.agentName,result2.text),session.lastActivityAt=Date.now(),this.safePgCall)await recordAuditEvent2(this.safePgCall,"deliver.end",{executor_id:state.executorId??session.id,agent_id:session.agentName,chat_id:message.chatId,instance_id:message.instanceId,turn_count:state.turnIndex});if(state.executorId)await this.updateState(state.executorId,"idle",session.chatId)}async reconcileSessionId(state,session,returnedSessionId){if(state.claudeSessionId&&returnedSessionId!==state.claudeSessionId&&this.safePgCall)await recordAuditEvent2(this.safePgCall,"session.resume_rejected",{executor_id:state.executorId??session.id,agent_id:session.agentName,chat_id:session.chatId,old_session_id:state.claudeSessionId,new_session_id:returnedSessionId});let execId=state.executorId;if(execId&&this.safePgCall&&returnedSessionId!==state.claudeSessionId)await this.safePgCall("sdk-update-claude-session",()=>updateClaudeSessionId(execId,returnedSessionId),void 0,{executorId:execId,chatId:session.chatId});state.claudeSessionId=returnedSessionId}async captureUserTurn(state,agentName,_sessionKey,content){if(!this.safePgCall)return;if(!state.dbSessionId&&state.executorId)state.dbSessionId=await startSession(this.safePgCall,state.executorId,state.claudeSessionId,agentName);if(state.dbSessionId)await recordTurn(this.safePgCall,state.dbSessionId,state.turnIndex++,"user",content)}async captureAssistantTurn(state,sessionKey2,claudeSessionId,agentName,replyText){if(!this.safePgCall)return;if(claudeSessionId&&state.dbSessionId?.startsWith("sdk-")){let newId=await startSession(this.safePgCall,state.executorId??sessionKey2,claudeSessionId,agentName);if(newId)state.dbSessionId=newId}if(state.dbSessionId&&replyText)await recordTurn(this.safePgCall,state.dbSessionId,state.turnIndex++,"assistant",replyText),await updateTurnCount(this.safePgCall,state.dbSessionId,state.turnIndex)}async waitForDeliveries(sessionId){if(sessionId)await this.deliveryQueues.get(sessionId);else await Promise.all([...this.deliveryQueues.values()])}async shutdown(session){let state=this.sessions.get(session.id);if(!state)return;if(state.abortController.abort(),state.running=!1,state.dbSessionId&&this.safePgCall)await endSession(this.safePgCall,state.dbSessionId,"completed");if(state.executorId&&this.safePgCall)await this.safePgCall("sdk-terminate-executor",()=>terminateExecutor(state.executorId),void 0,{executorId:state.executorId,chatId:session.chatId});this.sessions.delete(session.id),this.deliveryQueues.delete(session.id)}async isAlive(session){let state=this.sessions.get(session.id);if(!state)return!1;return state.running&&!state.abortController.signal.aborted}}var init_claude_sdk2=__esm(()=>{init_zod();init_agent_directory();init_agent_registry();init_executor_registry();init_claude_sdk_permissions();init_claude_sdk()});class TurnTracker{turns=new Map;open(sessionKey2,turnId,messageId){this.turns.set(sessionKey2,{turnId,sessionKey:sessionKey2,messageId,startedAt:Date.now(),closed:!1})}close(sessionKey2,action){let turn=this.turns.get(sessionKey2);if(turn&&!turn.closed)turn.closed=!0,turn.closedAction=action}isOpen(sessionKey2){let turn=this.turns.get(sessionKey2);return turn!==void 0&&!turn.closed}getTurnId(sessionKey2){return this.turns.get(sessionKey2)?.turnId}getByTurnId(turnId){for(let turn of this.turns.values())if(turn.turnId===turnId)return turn;return}delete(sessionKey2){this.turns.delete(sessionKey2)}}var exports_omni_bridge={};__export(exports_omni_bridge,{getBridge:()=>getBridge,OmniBridge:()=>OmniBridge});function getBridge(){return bridgeInstance}function withTimeout(p,ms,label){return new Promise((resolve9,reject)=>{let timer2=setTimeout(()=>reject(Error(`${label} timed out after ${ms}ms`)),ms);timer2.unref?.(),p.then((v)=>{clearTimeout(timer2),resolve9(v)},(err)=>{clearTimeout(timer2),reject(err)})})}function isPgConnectionError(err){if(!err||typeof err!=="object")return!1;let e=err,code=e.code??"";if(["ECONNREFUSED","ECONNRESET","ETIMEDOUT","ENOTFOUND","EPIPE","EHOSTUNREACH"].includes(code))return!0;let msg=e.message??String(err);return/ECONNREFUSED|ECONNRESET|ETIMEDOUT|ENOTFOUND|EPIPE|connection terminated|connection closed|server closed the connection|the database system is shutting down/i.test(msg)}class OmniBridge{nc=null;sub=null;executor;turnTracker=new TurnTracker;sessions=new Map;messageQueue=[];idleCheckTimer=null;sc=import_nats3.StringCodec();sql=null;pgAvailable=!1;pgProvider;natsConnectFn;natsUrl;idleTimeoutMs;maxConcurrent;executorType;constructor(config={}){if(this.natsUrl=config.natsUrl??process.env.GENIE_NATS_URL??DEFAULT_NATS_URL,this.idleTimeoutMs=config.idleTimeoutMs??(process.env.GENIE_IDLE_TIMEOUT_MS?Number(process.env.GENIE_IDLE_TIMEOUT_MS):DEFAULT_IDLE_TIMEOUT_MS2),this.maxConcurrent=config.maxConcurrent??(process.env.GENIE_MAX_CONCURRENT?Number(process.env.GENIE_MAX_CONCURRENT):DEFAULT_MAX_CONCURRENT),this.pgProvider=config.pgProvider??(async()=>{let{getConnection:getConnection2}=await Promise.resolve().then(() => (init_db(),exports_db));return await getConnection2()}),this.natsConnectFn=config.natsConnectFn??import_nats3.connect,this.executorType=resolveExecutorType(config.executorType),this.executorType==="sdk")this.executor=new ClaudeSdkOmniExecutor;else this.executor=new ClaudeCodeOmniExecutor}async start(){if(this.nc){console.log("[omni-bridge] Already running");return}console.log(`[omni-bridge] Connecting to NATS at ${this.natsUrl}...`),this.nc=await this.natsConnectFn({servers:this.natsUrl,name:"genie-omni-bridge",reconnect:!0,maxReconnectAttempts:-1,reconnectTimeWait:2000}),console.log("[omni-bridge] Connected to NATS"),await this.probePg(),this.executor.setSafePgCall(this.safePgCall.bind(this));let sc3=this.sc,nc2=this.nc;this.executor.setNatsPublish((topic,payload)=>{nc2.publish(topic,sc3.encode(payload))}),this.sub=this.nc.subscribe("omni.message.>"),this.processSubscription();let turnSubs=["omni.turn.open.>","omni.turn.done.>","omni.turn.nudge.>","omni.turn.timeout.>"];for(let topic of turnSubs){let sub=this.nc.subscribe(topic);this.processTurnEvents(sub)}this.idleCheckTimer=setInterval(()=>this.checkIdleSessions(),IDLE_CHECK_INTERVAL_MS),bridgeInstance=this,console.log(`[omni-bridge] Listening on omni.message.> (max_concurrent=${this.maxConcurrent}, idle_timeout=${this.idleTimeoutMs}ms)`)}async stop(){if(!this.nc){console.log("[omni-bridge] Not running");return}if(console.log("[omni-bridge] Shutting down..."),this.idleCheckTimer)clearInterval(this.idleCheckTimer),this.idleCheckTimer=null;for(let[key,entry]of this.sessions){if(entry.idleTimer)clearTimeout(entry.idleTimer);if(!entry.spawning&&entry.session)try{await this.executor.shutdown(entry.session)}catch(err){console.warn(`[omni-bridge] Error shutting down session ${key}:`,err)}}if(this.sessions.clear(),this.sub)this.sub.unsubscribe(),this.sub=null;try{await this.nc.drain()}catch{}this.nc=null,this.sql=null,this.pgAvailable=!1,bridgeInstance=null,console.log("[omni-bridge] Stopped")}async status(){let now=Date.now(),activeFromPg=null,executorIds=[];if(this.pgAvailable&&this.sql){let rows=await this.safePgCall("status_active_count",async(sql)=>sql`
1997
+ You are responding to a WhatsApp message from ${senderName}.
1998
+ Your context is pre-set (instance: ${instanceId}, chat: ${chatId}) \u2014 do NOT use \`omni use\` or \`omni open\`.
1999
+
2000
+ ## Available Commands
2001
+
2002
+ - omni say 'text' \u2014 reply with a text message
2003
+ - omni speak 'text' \u2014 reply with a voice note
2004
+ - omni imagine 'prompt' \u2014 generate and send an image
2005
+ - omni react 'emoji' --message <id> \u2014 react to a message
2006
+ - omni history \u2014 see recent messages for context
2007
+ - omni done \u2014 end your turn (REQUIRED as the last action)
2008
+
2009
+ ## Rules
2010
+
2011
+ 1. Use \`omni say\` to send your response. You can send multiple messages.
2012
+ 2. Use \`omni history\` to see recent messages if you need context.
2013
+ 3. ALWAYS call \`omni done\` as your LAST action to close the turn.
2014
+ 4. Do NOT generate bare text as your reply \u2014 it will go nowhere. Use omni say or omni done.
2015
+ `.trim()}async function loadSystemPrompt(entry){let identityPath=loadIdentity(entry);if(!identityPath)return;let{readFileSync:readFileSync22}=await import("fs");try{return readFileSync22(identityPath,"utf-8")}catch{return}}async function resolveSystemPrompt(entry,state,message,chatId){let prompt2=await loadSystemPrompt(entry),isTurnBased=Boolean(state.env.OMNI_INSTANCE);if(isTurnBased){let turnPrompt=buildTurnBasedPrompt(message.sender,state.env.OMNI_INSTANCE,state.env.OMNI_CHAT??chatId);prompt2=prompt2?`${turnPrompt}
2016
+
2017
+ ${prompt2}`:turnPrompt}return{prompt:prompt2,isTurnBased}}function extractTextFromAssistant(msg){if(!msg.message)return[];return msg.message.content.filter((b2)=>b2.type==="text"&&b2.text).map((b2)=>b2.text)}async function collectQueryResult(queryMessages){let textParts=[],sessionId;try{for await(let msg of queryMessages){if(msg.type==="assistant")textParts.push(...extractTextFromAssistant(msg));if(msg.type==="result"&&msg.subtype==="success")sessionId=msg.session_id}}catch(err){if(err.name==="AbortError")return{text:""};throw err}return{text:textParts.join(`
2018
+ `).trim(),sessionId}}function buildReplyPayload(agent,chatId,instanceId,extra={}){return JSON.stringify({content:"",agent,chat_id:chatId,instance_id:instanceId,timestamp:new Date().toISOString(),...extra})}function resolveAction(params,env){if(params.skip)return{type:"skip",extra:{reason:params.reason},label:"Turn closed (skip)."};if(params.react)return{type:"react",extra:{react:String(params.react),message_id:env.OMNI_MESSAGE??""},label:`Reacted ${params.react} + turn closed.`};if(params.media)return{type:"media",extra:{content:params.caption?String(params.caption):params.text?String(params.text):"",media:String(params.media)},label:"Media sent + turn closed."};if(params.text)return{type:"text",extra:{content:String(params.text)},label:"Turn closed. Message delivered."};return{type:"skip",extra:{},label:"Turn closed (skip)."}}function handleDoneTool(params,env,natsPublish){let instanceId=env.OMNI_INSTANCE??"",chatId=env.OMNI_CHAT??"",agent=env.OMNI_AGENT??"",action=resolveAction(params,env);if(action.type==="skip"){if(natsPublish&&instanceId&&chatId)natsPublish(`omni.turn.done.${instanceId}.${chatId}`,JSON.stringify({action:"skip",...action.extra}));return action.label}if(!natsPublish||!instanceId||!chatId)return console.warn("[claude-sdk] No NATS publish available \u2014 reply dropped"),"Turn close attempted but NATS publish not available.";return natsPublish(`omni.reply.${instanceId}.${chatId}`,buildReplyPayload(agent,chatId,instanceId,action.extra)),action.label}async function createDoneMcpServer(env,natsPublish){let{createSdkMcpServer,tool}=await import("@anthropic-ai/claude-agent-sdk");return createSdkMcpServer({name:"genie-omni-tools",tools:[tool("done","Close this turn. REQUIRED after processing the user message. Sends a final response, reacts, or skips. Call exactly once per turn.",{text:exports_external.string().optional().describe("Final message to the user"),media:exports_external.string().optional().describe("File path for media attachment"),caption:exports_external.string().optional().describe("Caption for media"),react:exports_external.string().optional().describe("Emoji reaction (instead of text)"),skip:exports_external.boolean().optional().describe("Close turn without sending anything"),reason:exports_external.string().optional().describe("Internal reason for skipping")},async(args)=>{return{content:[{type:"text",text:handleDoneTool(args,env,natsPublish)}]}})]})}class ClaudeSdkOmniExecutor{sessions=new Map;safePgCall=null;natsPublish=null;deliveryQueues=new Map;pendingNudges=new Map;setSafePgCall(fn){this.safePgCall=fn}setNatsPublish(fn){this.natsPublish=fn}async injectNudge(session,text){if(!this.sessions.has(session.id))return;this.pendingNudges.set(session.id,text)}async spawn(agentName,chatId,env){if(!await resolve3(agentName))throw Error(`Agent "${agentName}" not found in genie directory`);let provider=new ClaudeSdkProvider,abortController=new AbortController,sessionId=`${agentName}:${chatId}`,registration=await this.registerInWorldA(agentName,chatId,env.OMNI_INSTANCE??"");if(this.sessions.set(sessionId,{abortController,running:!0,provider,executorId:registration?.executorId??null,claudeSessionId:registration?.claudeSessionId,dbSessionId:null,turnIndex:0,env}),registration?.executorId)await this.updateState(registration.executorId,"running",chatId);let now=Date.now();return{id:sessionId,agentName,chatId,tmuxSession:"",tmuxWindow:"",paneId:`sdk-${chatId}`,createdAt:now,lastActivityAt:now}}async registerInWorldA(agentName,chatId,instanceId){if(!this.safePgCall)return null;let agent=await this.safePgCall("sdk-find-or-create-agent",()=>findOrCreateAgent(agentName,"omni","omni"),null,{chatId});if(!agent)return null;let existing=await this.safePgCall("sdk-find-existing-executor",()=>findLatestByMetadata({agentId:agent.id,source:"omni",chatId}),null,{chatId});if(existing)return await this.safePgCall("sdk-relink-executor",()=>relinkExecutorToAgent(existing.id,agent.id),void 0,{executorId:existing.id,chatId}),await recordAuditEvent2(this.safePgCall,"session.resumed",{executor_id:existing.id,agent_id:agentName,chat_id:chatId,claude_session_id:existing.claudeSessionId}),{executorId:existing.id,claudeSessionId:existing.claudeSessionId??void 0};let executor=await this.safePgCall("sdk-create-executor",()=>createAndLinkExecutor(agent.id,"claude","api",{claudeSessionId:void 0,metadata:{source:"omni",chat_id:chatId,instance_id:instanceId}}),null,{chatId});if(executor)await recordAuditEvent2(this.safePgCall,"session.created_fresh",{executor_id:executor.id,agent_id:agentName,chat_id:chatId});return executor?{executorId:executor.id}:null}async updateState(executorId,state,chatId){if(!this.safePgCall)return;await this.safePgCall("sdk-update-executor-state",()=>updateExecutorState(executorId,state),void 0,{executorId,chatId})}async deliver(session,message){let state=this.sessions.get(session.id);if(!state)throw Error(`No SDK session found for ${session.id}`);let current=(this.deliveryQueues.get(session.id)??Promise.resolve()).then(()=>this._processDelivery(session,state,message));this.deliveryQueues.set(session.id,current.catch(()=>{}))}async _processDelivery(session,state,message){let resolved=await resolve3(session.agentName);if(!resolved)throw Error(`Agent "${session.agentName}" not found in genie directory`);let entry=resolved.entry,permissionConfig=resolvePermissionConfig(entry.permissions),{prompt:systemPrompt,isTurnBased}=await resolveSystemPrompt(entry,state,message,session.chatId);if(state.executorId)await this.updateState(state.executorId,"working",session.chatId);if(this.safePgCall)await recordAuditEvent2(this.safePgCall,"deliver.start",{executor_id:state.executorId??session.id,agent_id:session.agentName,chat_id:message.chatId,instance_id:message.instanceId});let doneMcp=await createDoneMcpServer(state.env,this.natsPublish),extraOptions={abortController:state.abortController,mcpServers:{"genie-omni-tools":doneMcp}};if(state.claudeSessionId)extraOptions.resume=state.claudeSessionId;let queryContent=message.content,pendingNudge=this.pendingNudges.get(session.id);if(pendingNudge)queryContent=`[system] ${pendingNudge}
2019
+
2020
+ ${message.content}`,this.pendingNudges.delete(session.id);let{messages:queryMessages}=state.provider.runQuery({agentId:session.agentName,executorId:session.id,team:"",role:session.agentName,cwd:entry.dir||process.cwd(),model:entry.model,systemPrompt:state.claudeSessionId&&!isTurnBased?void 0:systemPrompt},queryContent,permissionConfig,extraOptions,entry.sdk);await this.captureUserTurn(state,session.agentName,session.id,message.content);let result2=await collectQueryResult(queryMessages);if(result2.sessionId)await this.reconcileSessionId(state,session,result2.sessionId);if(await this.captureAssistantTurn(state,session.id,result2.sessionId,session.agentName,result2.text),session.lastActivityAt=Date.now(),this.safePgCall)await recordAuditEvent2(this.safePgCall,"deliver.end",{executor_id:state.executorId??session.id,agent_id:session.agentName,chat_id:message.chatId,instance_id:message.instanceId,turn_count:state.turnIndex});if(state.executorId)await this.updateState(state.executorId,"idle",session.chatId)}async reconcileSessionId(state,session,returnedSessionId){if(state.claudeSessionId&&returnedSessionId!==state.claudeSessionId&&this.safePgCall)await recordAuditEvent2(this.safePgCall,"session.resume_rejected",{executor_id:state.executorId??session.id,agent_id:session.agentName,chat_id:session.chatId,old_session_id:state.claudeSessionId,new_session_id:returnedSessionId});let execId=state.executorId;if(execId&&this.safePgCall&&returnedSessionId!==state.claudeSessionId)await this.safePgCall("sdk-update-claude-session",()=>updateClaudeSessionId(execId,returnedSessionId),void 0,{executorId:execId,chatId:session.chatId});state.claudeSessionId=returnedSessionId}async captureUserTurn(state,agentName,_sessionKey,content){if(!this.safePgCall)return;if(!state.dbSessionId&&state.executorId)state.dbSessionId=await startSession(this.safePgCall,state.executorId,state.claudeSessionId,agentName);if(state.dbSessionId)await recordTurn(this.safePgCall,state.dbSessionId,state.turnIndex++,"user",content)}async captureAssistantTurn(state,sessionKey2,claudeSessionId,agentName,replyText){if(!this.safePgCall)return;if(claudeSessionId&&state.dbSessionId?.startsWith("sdk-")){let newId=await startSession(this.safePgCall,state.executorId??sessionKey2,claudeSessionId,agentName);if(newId)state.dbSessionId=newId}if(state.dbSessionId&&replyText)await recordTurn(this.safePgCall,state.dbSessionId,state.turnIndex++,"assistant",replyText),await updateTurnCount(this.safePgCall,state.dbSessionId,state.turnIndex)}async waitForDeliveries(sessionId){if(sessionId)await this.deliveryQueues.get(sessionId);else await Promise.all([...this.deliveryQueues.values()])}async shutdown(session){let state=this.sessions.get(session.id);if(!state)return;if(state.abortController.abort(),state.running=!1,state.dbSessionId&&this.safePgCall)await endSession(this.safePgCall,state.dbSessionId,"completed");if(state.executorId&&this.safePgCall)await this.safePgCall("sdk-terminate-executor",()=>terminateExecutor(state.executorId),void 0,{executorId:state.executorId,chatId:session.chatId});this.sessions.delete(session.id),this.deliveryQueues.delete(session.id)}async isAlive(session){let state=this.sessions.get(session.id);if(!state)return!1;return state.running&&!state.abortController.signal.aborted}}var init_claude_sdk2=__esm(()=>{init_zod();init_agent_directory();init_agent_registry();init_executor_registry();init_claude_sdk_permissions();init_claude_sdk()});class TurnTracker{turns=new Map;open(sessionKey2,turnId,messageId){this.turns.set(sessionKey2,{turnId,sessionKey:sessionKey2,messageId,startedAt:Date.now(),closed:!1})}close(sessionKey2,action){let turn=this.turns.get(sessionKey2);if(turn&&!turn.closed)turn.closed=!0,turn.closedAction=action}isOpen(sessionKey2){let turn=this.turns.get(sessionKey2);return turn!==void 0&&!turn.closed}getTurnId(sessionKey2){return this.turns.get(sessionKey2)?.turnId}getByTurnId(turnId){for(let turn of this.turns.values())if(turn.turnId===turnId)return turn;return}delete(sessionKey2){this.turns.delete(sessionKey2)}}var exports_omni_bridge={};__export(exports_omni_bridge,{getBridge:()=>getBridge,OmniBridge:()=>OmniBridge});function getBridge(){return bridgeInstance}function withTimeout(p,ms,label){return new Promise((resolve9,reject)=>{let timer2=setTimeout(()=>reject(Error(`${label} timed out after ${ms}ms`)),ms);timer2.unref?.(),p.then((v)=>{clearTimeout(timer2),resolve9(v)},(err)=>{clearTimeout(timer2),reject(err)})})}function isPgConnectionError(err){if(!err||typeof err!=="object")return!1;let e=err,code=e.code??"";if(["ECONNREFUSED","ECONNRESET","ETIMEDOUT","ENOTFOUND","EPIPE","EHOSTUNREACH"].includes(code))return!0;let msg=e.message??String(err);return/ECONNREFUSED|ECONNRESET|ETIMEDOUT|ENOTFOUND|EPIPE|connection terminated|connection closed|server closed the connection|the database system is shutting down/i.test(msg)}class OmniBridge{nc=null;sub=null;executor;turnTracker=new TurnTracker;sessions=new Map;messageQueue=[];idleCheckTimer=null;sc=import_nats3.StringCodec();sql=null;pgAvailable=!1;pgProvider;natsConnectFn;natsUrl;idleTimeoutMs;maxConcurrent;executorType;constructor(config={}){if(this.natsUrl=config.natsUrl??process.env.GENIE_NATS_URL??DEFAULT_NATS_URL,this.idleTimeoutMs=config.idleTimeoutMs??(process.env.GENIE_IDLE_TIMEOUT_MS?Number(process.env.GENIE_IDLE_TIMEOUT_MS):DEFAULT_IDLE_TIMEOUT_MS2),this.maxConcurrent=config.maxConcurrent??(process.env.GENIE_MAX_CONCURRENT?Number(process.env.GENIE_MAX_CONCURRENT):DEFAULT_MAX_CONCURRENT),this.pgProvider=config.pgProvider??(async()=>{let{getConnection:getConnection2}=await Promise.resolve().then(() => (init_db(),exports_db));return await getConnection2()}),this.natsConnectFn=config.natsConnectFn??import_nats3.connect,this.executorType=resolveExecutorType(config.executorType),this.executorType==="sdk")this.executor=new ClaudeSdkOmniExecutor;else this.executor=new ClaudeCodeOmniExecutor}async start(){if(this.nc){console.log("[omni-bridge] Already running");return}console.log(`[omni-bridge] Connecting to NATS at ${this.natsUrl}...`),this.nc=await this.natsConnectFn({servers:this.natsUrl,name:"genie-omni-bridge",reconnect:!0,maxReconnectAttempts:-1,reconnectTimeWait:2000}),console.log("[omni-bridge] Connected to NATS"),await this.probePg(),this.executor.setSafePgCall(this.safePgCall.bind(this));let sc3=this.sc,nc2=this.nc;this.executor.setNatsPublish((topic,payload)=>{nc2.publish(topic,sc3.encode(payload))}),this.sub=this.nc.subscribe("omni.message.>"),this.processSubscription();let turnSubs=["omni.turn.open.>","omni.turn.done.>","omni.turn.nudge.>","omni.turn.timeout.>"];for(let topic of turnSubs){let sub=this.nc.subscribe(topic);this.processTurnEvents(sub)}this.idleCheckTimer=setInterval(()=>this.checkIdleSessions(),IDLE_CHECK_INTERVAL_MS),bridgeInstance=this,console.log(`[omni-bridge] Listening on omni.message.> (max_concurrent=${this.maxConcurrent}, idle_timeout=${this.idleTimeoutMs}ms)`)}async stop(){if(!this.nc){console.log("[omni-bridge] Not running");return}if(console.log("[omni-bridge] Shutting down..."),this.idleCheckTimer)clearInterval(this.idleCheckTimer),this.idleCheckTimer=null;for(let[key,entry]of this.sessions){if(entry.idleTimer)clearTimeout(entry.idleTimer);if(!entry.spawning&&entry.session)try{await this.executor.shutdown(entry.session)}catch(err){console.warn(`[omni-bridge] Error shutting down session ${key}:`,err)}}if(this.sessions.clear(),this.sub)this.sub.unsubscribe(),this.sub=null;try{await this.nc.drain()}catch{}this.nc=null,this.sql=null,this.pgAvailable=!1,bridgeInstance=null,console.log("[omni-bridge] Stopped")}async status(){let now=Date.now(),activeFromPg=null,executorIds=[];if(this.pgAvailable&&this.sql){let rows=await this.safePgCall("status_active_count",async(sql)=>sql`
1998
2021
  SELECT id FROM executors
1999
2022
  WHERE ended_at IS NULL AND metadata->>'source' = 'omni'
2000
2023
  `,null);if(rows)activeFromPg=rows.length,executorIds=rows.map((r)=>r.id)}return{connected:this.nc!==null,natsUrl:this.natsUrl,pgAvailable:this.pgAvailable,activeSessions:activeFromPg??this.sessions.size,maxConcurrent:this.maxConcurrent,idleTimeoutMs:this.idleTimeoutMs,queueDepth:this.messageQueue.length,executorType:this.executorType,executorIds,sessions:Array.from(this.sessions.entries()).map(([key,entry])=>({id:key,agentName:entry.session.agentName,chatId:entry.session.chatId,instanceId:entry.instanceId,paneId:entry.session.paneId,spawning:entry.spawning,idleMs:now-entry.session.lastActivityAt,bufferSize:entry.buffer.length}))}}async probePg(){try{let sql=await withTimeout(this.pgProvider(),PG_STARTUP_PROBE_TIMEOUT_MS,"PG provider startup");await withTimeout(Promise.resolve(sql`SELECT 1`),PG_STARTUP_PROBE_TIMEOUT_MS,"PG SELECT 1 probe"),this.sql=sql,this.pgAvailable=!0,console.log("[omni-bridge] PG reachable \u2014 session recovery enabled")}catch(err){this.sql=null,this.pgAvailable=!1;let msg=err instanceof Error?err.message:String(err);if(isPgConnectionError(err)){console.warn(`[omni-bridge] PG unavailable \u2014 session recovery disabled (${msg})`);return}throw Error(`[omni-bridge] PG schema mismatch or setup error: ${msg}. ${"Run `bun run migrate` (or the equivalent migration command) and retry."}`)}}async safePgCall(op,fn,fallback,ctx){if(!this.pgAvailable||!this.sql)return fallback;let sql=this.sql;try{return await withTimeout(fn(sql),PG_RUNTIME_QUERY_TIMEOUT_MS,`safePgCall(${op})`)}catch(err){let msg=err instanceof Error?err.message:String(err),execPart=ctx?.executorId?` executor_id=${ctx.executorId}`:"",chatPart=ctx?.chatId?` chat_id=${ctx.chatId}`:"";if(console.warn(`[omni-bridge] safePgCall(${op}) failed${execPart}${chatPart}: ${msg}`),isPgConnectionError(err))this.pgAvailable=!1,this.sql=null,console.warn("[omni-bridge] PG connection lost \u2014 switching to degraded mode");return fallback}}async processSubscription(){if(!this.sub)return;for await(let msg of this.sub)try{let data=this.sc.decode(msg.data),parsed=JSON.parse(data),parts=msg.subject.split(".");if(parts.length>=4)parsed.instanceId=parsed.instanceId||parts[2],parsed.chatId=parsed.chatId||parts[3];if(!parsed.chatId||!parsed.agent){console.warn("[omni-bridge] Dropping message: missing chatId or agent",msg.subject);continue}await this.routeMessage(parsed)}catch(err){console.error("[omni-bridge] Error processing message:",err)}}async processTurnEvents(sub){for await(let msg of sub)try{let payload=JSON.parse(this.sc.decode(msg.data)),parts=msg.subject.split("."),eventType=parts[2],instanceId=parts[3],chatId=parts.slice(4).join("."),sessionKey2=this.findSessionKey(instanceId,chatId);if(sessionKey2)await this.routeTurnEvent(eventType,sessionKey2,payload)}catch(err){console.warn("[omni-bridge] Error processing turn event:",err)}}async routeTurnEvent(eventType,sessionKey2,payload){switch(eventType){case"open":this.turnTracker.open(sessionKey2,payload.turnId,payload.messageId);break;case"done":this.turnTracker.close(sessionKey2,payload.action);break;case"nudge":await this.handleTurnNudge(sessionKey2,payload.message);break;case"timeout":await this.handleTurnTimeout(sessionKey2);break}}findSessionKey(instanceId,chatId){for(let[key,entry]of this.sessions)if(entry.instanceId===instanceId&&entry.session?.chatId===chatId)return key;return}async handleTurnNudge(sessionKey2,nudgeText){let entry=this.sessions.get(sessionKey2);if(!entry?.session)return;try{await this.executor.injectNudge(entry.session,nudgeText)}catch(err){console.warn(`[omni-bridge] Failed to inject nudge for ${sessionKey2}:`,err)}}async handleTurnTimeout(sessionKey2){let entry=this.sessions.get(sessionKey2);if(!entry?.session)return;console.warn(`[omni-bridge] Turn timed out for ${sessionKey2}, evicting session`),this.turnTracker.close(sessionKey2,"timeout");try{await this.executor.shutdown(entry.session)}catch(err){console.warn(`[omni-bridge] Error shutting down timed-out session ${sessionKey2}:`,err)}this.sessions.delete(sessionKey2)}async routeMessage(message){let key=`${message.agent}:${message.chatId}`,entry=this.sessions.get(key);if(entry){if(entry.spawning){if(entry.buffer.length<MAX_BUFFER_PER_CHAT)entry.buffer.push(message);else console.warn(`[omni-bridge] Buffer full (${MAX_BUFFER_PER_CHAT}) for ${key}, dropping message from ${message.sender}`),await this.publishBufferFullReply(message);return}if(await this.executor.isAlive(entry.session)){await this.executor.deliver(entry.session,message),this.resetIdleTimer(key);return}this.removeSession(key)}await this.spawnSession(message)}async spawnSession(message){let key=`${message.agent}:${message.chatId}`;if(this.sessions.size>=this.maxConcurrent){this.messageQueue.push(message),await this.publishAutoReply(message),console.log(`[omni-bridge] Max concurrent (${this.maxConcurrent}) reached, queued message for ${key}`);return}let placeholder={session:null,instanceId:message.instanceId,spawning:!0,buffer:[message],idleTimer:null};this.sessions.set(key,placeholder);try{let raw=message,payloadEnv=raw.env,spawnEnv={OMNI_API_KEY:payloadEnv?.OMNI_API_KEY??process.env.OMNI_API_KEY??"",OMNI_INSTANCE:payloadEnv?.OMNI_INSTANCE??message.instanceId,OMNI_CHAT:payloadEnv?.OMNI_CHAT??message.chatId,OMNI_MESSAGE:payloadEnv?.OMNI_MESSAGE??raw.messageId??"",OMNI_TURN_ID:payloadEnv?.OMNI_TURN_ID??""};console.log(`[omni-bridge] Spawning session for ${key}...`);let session=await this.executor.spawn(message.agent,message.chatId,spawnEnv);placeholder.session=session,placeholder.spawning=!1;for(let buffered of placeholder.buffer)await this.executor.deliver(session,buffered);placeholder.buffer=[],this.resetIdleTimer(key),console.log(`[omni-bridge] Session active: ${key} (pane=${session.paneId})`)}catch(err){console.error(`[omni-bridge] Failed to spawn session for ${key}:`,err);let lostMessages=placeholder.buffer;if(lostMessages.length>0)console.warn(`[omni-bridge] Re-queuing ${lostMessages.length} buffered message(s) from failed spawn for ${key}`),this.messageQueue.push(...lostMessages);this.sessions.delete(key)}}resetIdleTimer(key){let entry=this.sessions.get(key);if(!entry)return;if(entry.idleTimer)clearTimeout(entry.idleTimer);entry.idleTimer=setTimeout(async()=>{console.log(`[omni-bridge] Idle timeout for ${key}, shutting down...`);try{await this.executor.shutdown(entry.session)}catch{}this.removeSession(key),await this.drainQueue()},this.idleTimeoutMs)}async checkIdleSessions(){let now=Date.now();for(let[key,entry]of this.sessions){if(entry.spawning)continue;if(!await this.executor.isAlive(entry.session)){console.log(`[omni-bridge] Dead session detected: ${key}`),this.removeSession(key);continue}let idleMs=now-entry.session.lastActivityAt;if(idleMs>this.idleTimeoutMs){console.log(`[omni-bridge] Forcing idle shutdown: ${key} (idle ${Math.round(idleMs/1000)}s)`);try{await this.executor.shutdown(entry.session)}catch{}this.removeSession(key)}}}removeSession(key){let entry=this.sessions.get(key);if(entry?.idleTimer)clearTimeout(entry.idleTimer);this.sessions.delete(key)}async drainQueue(){while(this.messageQueue.length>0){if(this.sessions.size>=this.maxConcurrent)break;let message=this.messageQueue.shift();if(message)await this.spawnSession(message)}}async publishBufferFullReply(message){if(!this.nc)return;let topic=`omni.reply.${message.instanceId}.${message.chatId}`,reply2={content:"Fila de mensagens cheia, por favor aguarde e tente novamente.",agent:message.agent,chat_id:message.chatId,instance_id:message.instanceId,timestamp:new Date().toISOString(),auto_reply:!0};this.nc.publish(topic,this.sc.encode(JSON.stringify(reply2)))}async publishAutoReply(message){if(!this.nc)return;let topic=`omni.reply.${message.instanceId}.${message.chatId}`,reply2={content:"Aguarde um momento, estou atendendo outros clientes.",agent:message.agent,chat_id:message.chatId,instance_id:message.instanceId,timestamp:new Date().toISOString(),auto_reply:!0};this.nc.publish(topic,this.sc.encode(JSON.stringify(reply2)))}}var import_nats3,DEFAULT_NATS_URL="localhost:4222",DEFAULT_IDLE_TIMEOUT_MS2=900000,DEFAULT_MAX_CONCURRENT=20,MAX_BUFFER_PER_CHAT=50,IDLE_CHECK_INTERVAL_MS=30000,PG_STARTUP_PROBE_TIMEOUT_MS=5000,PG_RUNTIME_QUERY_TIMEOUT_MS=2000,bridgeInstance=null;var init_omni_bridge=__esm(()=>{init_executor_config();init_claude_code2();init_claude_sdk2();import_nats3=__toESM(require_mod4(),1)});var exports_task_close_merged={};__export(exports_task_close_merged,{parseSinceDate:()=>parseSinceDate,matchPRsToSlugs:()=>matchPRsToSlugs,fetchMergedPRs:()=>fetchMergedPRs,extractWishSlug:()=>extractWishSlug,extractSlugFromBranch:()=>extractSlugFromBranch,extractSlugFromBody:()=>extractSlugFromBody,closeMergedTasks:()=>closeMergedTasks});import{execSync as execSync15}from"child_process";function extractSlugFromBody(body){if(!body)return null;let match=body.match(/(?:wish|slug):\s*(\S+)/i);return match?match[1]:null}function extractSlugFromBranch(branch){if(!branch)return null;let match=branch.match(/^(?:feat|fix|chore|docs|refactor|test|dream)\/(.+)$/);return match?match[1]:null}function extractWishSlug(pr){return extractSlugFromBody(pr.body)??extractSlugFromBranch(pr.headRefName)}function parseSinceDate(since){let match=since.match(/^(\d+)([hd])$/);if(!match)throw Error(`Invalid --since format: "${since}". Use e.g. "24h" or "7d".`);let amount=Number.parseInt(match[1],10),unit=match[2],now=new Date;if(unit==="h")now.setHours(now.getHours()-amount);else now.setDate(now.getDate()-amount);return now.toISOString()}function fetchMergedPRs(since,repo){let sinceDate=parseSinceDate(since),cmd=`gh pr list --state merged --json number,title,body,headRefName,mergedAt --limit 100 ${repo?`--repo ${repo}`:""}`.trim(),output;try{output=execSync15(cmd,{encoding:"utf-8",stdio:["pipe","pipe","pipe"]})}catch(err){let message=err instanceof Error?err.message:String(err);throw Error(`Failed to fetch merged PRs: ${message}`)}return JSON.parse(output).filter((pr)=>new Date(pr.mergedAt)>=new Date(sinceDate))}function matchPRsToSlugs(prs){let matches=[];for(let pr of prs){let slug=extractWishSlug(pr);if(slug)matches.push({prNumber:pr.number,slug,mergedAt:pr.mergedAt})}return matches}async function closeMergedTasks(options={}){let{since="24h",dryRun=!1,repo,repoPath}=options,ts3=await Promise.resolve().then(() => (init_task_service(),exports_task_service)),prs=fetchMergedPRs(since,repo),slugMatches=matchPRsToSlugs(prs),result2={closed:0,alreadyShipped:0,prsScanned:prs.length,details:[]};if(slugMatches.length===0)return result2;let actor={actorType:"local",actorId:process.env.GENIE_AGENT_NAME??"cli"},processedTaskIds=new Set;for(let{prNumber,slug}of slugMatches){let tasks=await findTasksByWishSlug(slug,repoPath);for(let task of tasks){if(processedTaskIds.has(task.id))continue;if(processedTaskIds.add(task.id),task.stage==="ship"){result2.alreadyShipped++;continue}if(!dryRun)await ts3.moveTask(task.id,"ship",actor,void 0,task.repoPath),await ts3.commentOnTask(task.id,actor,`Auto-closed: PR #${prNumber} merged to dev`,task.repoPath);result2.closed++,result2.details.push({taskSeq:task.seq,taskTitle:task.title,prNumber,slug})}}return result2}async function findTasksByWishSlug(slug,repoPath){let{getConnection:getConnection2}=await Promise.resolve().then(() => (init_db(),exports_db)),sql=await getConnection2(),repo=repoPath??resolveRepoPath(),pattern=`%${slug}%`;return(await sql`
@@ -2858,7 +2881,7 @@ Bus `);for(let i2=1;i2<parts.length;i2++){let usb2=parseLinuxUsb(parts[i2]);resu
2858
2881
  `),await killStalePostgres(),await cleanSharedMemory();let genieHome2=process.env.GENIE_HOME??join4(homedir4(),".genie"),pidFile=join4(genieHome2,"scheduler.pid");await stopExistingDaemon(pidFile),removeStaleFiles(genieHome2,pidFile),await restartDaemon(),console.log(`
2859
2882
  \x1B[2m${"\u2500".repeat(40)}\x1B[0m`),console.log(`\x1B[32mFix complete.\x1B[0m Run \x1B[36mgenie doctor\x1B[0m to verify.
2860
2883
  `)}init_setup();init_shortcuts();import{existsSync as existsSync7}from"fs";import{homedir as homedir8}from"os";import{join as join8}from"path";async function shortcutsShowCommand(){displayShortcuts();let home=homedir8(),tmuxConf=join8(home,".tmux.conf"),zshrc=join8(home,".zshrc"),bashrc=join8(home,".bashrc");if(console.log("Installation status:"),isShortcutsInstalled(tmuxConf))console.log(" \x1B[32m\u2713\x1B[0m tmux.conf");else console.log(" \x1B[33m-\x1B[0m tmux.conf");let shellRc=existsSync7(zshrc)?zshrc:bashrc;if(isShortcutsInstalled(shellRc))console.log(` \x1B[32m\u2713\x1B[0m ${shellRc.replace(home,"~")}`);else console.log(` \x1B[33m-\x1B[0m ${shellRc.replace(home,"~")}`);console.log(),console.log("Run \x1B[36mgenie shortcuts install\x1B[0m to install shortcuts."),console.log("Run \x1B[36mgenie shortcuts uninstall\x1B[0m to remove shortcuts."),console.log()}async function shortcutsInstallCommand(){await installShortcuts()}async function shortcutsUninstallCommand(){await uninstallShortcuts()}init_esm6();init_claude_settings();init_genie_config2();import{existsSync as existsSync8,lstatSync,rmSync as rmSync2,unlinkSync as unlinkSync4}from"fs";import{homedir as homedir9}from"os";import{join as join9}from"path";var ORCHESTRATION_RULES_PATH=join9(homedir9(),".claude","rules","genie-orchestration.md"),LOCAL_BIN=join9(homedir9(),".local","bin"),SYMLINKS=["genie","term"];function isGenieSymlink(path){try{if(!existsSync8(path))return!1;if(!lstatSync(path).isSymbolicLink())return!1;return!0}catch{return!1}}function removeSymlinks(){let removed=[];for(let name of SYMLINKS){let symlinkPath=join9(LOCAL_BIN,name);if(isGenieSymlink(symlinkPath))try{unlinkSync4(symlinkPath),removed.push(name)}catch{}}return removed}function tryRemoveStep(label,successMsg,fn){console.log(`\x1B[2m${label}\x1B[0m`);try{fn(),console.log(` \x1B[32m+\x1B[0m ${successMsg}`)}catch(error){let message=error instanceof Error?error.message:String(error);console.log(` \x1B[33m!\x1B[0m ${label.replace("...","")} failed: ${message}`)}}function performUninstall(hasHookScript,existingSymlinks,genieDir,hasGenieDir){if(hasHookScript)tryRemoveStep("Removing hook script...","Hook script removed",()=>removeHookScript());if(existingSymlinks.length>0){console.log("\x1B[2mRemoving symlinks...\x1B[0m");let removed=removeSymlinks();if(removed.length>0)console.log(` \x1B[32m+\x1B[0m Removed: ${removed.join(", ")}`)}if(existsSync8(ORCHESTRATION_RULES_PATH))tryRemoveStep("Removing orchestration rules...","Orchestration rules removed (~/.claude/rules/genie-orchestration.md)",()=>unlinkSync4(ORCHESTRATION_RULES_PATH));if(hasGenieDir)tryRemoveStep("Removing genie directory...","Directory removed",()=>rmSync2(genieDir,{recursive:!0,force:!0}))}async function uninstallCommand(){console.log(),console.log("\x1B[1m\x1B[33m Uninstall Genie CLI\x1B[0m"),console.log();let genieDir=getGenieDir(),hasGenieDir=existsSync8(genieDir),hasHookScript=hookScriptExists(),hasOrchestrationRules=existsSync8(ORCHESTRATION_RULES_PATH),existingSymlinks=SYMLINKS.filter((name)=>isGenieSymlink(join9(LOCAL_BIN,name)));if(console.log("\x1B[2mThis will remove:\x1B[0m"),hasHookScript)console.log(" \x1B[31m-\x1B[0m Hook script (~/.claude/hooks/genie-bash-hook.sh)");if(hasOrchestrationRules)console.log(" \x1B[31m-\x1B[0m Orchestration rules (~/.claude/rules/genie-orchestration.md)");if(hasGenieDir)console.log(` \x1B[31m-\x1B[0m Genie directory (${contractPath(genieDir)})`);if(existingSymlinks.length>0)console.log(` \x1B[31m-\x1B[0m Symlinks from ~/.local/bin: ${existingSymlinks.join(", ")}`);if(console.log(),!hasGenieDir&&!hasHookScript&&!hasOrchestrationRules&&existingSymlinks.length===0){console.log("\x1B[33mNothing to uninstall.\x1B[0m"),console.log();return}if(!await esm_default2({message:"Are you sure you want to uninstall Genie CLI?",default:!1})){console.log(),console.log("\x1B[2mUninstall cancelled.\x1B[0m"),console.log();return}console.log(),performUninstall(hasHookScript,existingSymlinks,genieDir,hasGenieDir),console.log(),console.log("\x1B[32m+\x1B[0m Genie CLI uninstalled."),console.log(),console.log("\x1B[2mNote: If you installed via npm/bun, also run:\x1B[0m"),console.log(" \x1B[36mbun remove -g @automagik/genie\x1B[0m"),console.log(" \x1B[2mor\x1B[0m"),console.log(" \x1B[36mnpm uninstall -g @automagik/genie\x1B[0m"),console.log()}init_genie_config2();import{execSync as execSync2,spawn}from"child_process";import{chmodSync as chmodSync2,copyFileSync as copyFileSync2,existsSync as existsSync9,mkdirSync as mkdirSync6,readFileSync as readFileSync5,readdirSync,rmSync as rmSync3,writeFileSync as writeFileSync6}from"fs";import{chmod,copyFile,mkdir,unlink}from"fs/promises";import{homedir as homedir10}from"os";import{join as join10}from"path";var GENIE_HOME=process.env.GENIE_HOME||join10(homedir10(),".genie"),GENIE_SRC=join10(GENIE_HOME,"src"),GENIE_BIN=join10(GENIE_HOME,"bin"),LOCAL_BIN2=join10(homedir10(),".local","bin");function log(message){console.log(`\x1B[32m\u25B8\x1B[0m ${message}`)}function success(message){console.log(`\x1B[32m\u2714\x1B[0m ${message}`)}function error(message){console.log(`\x1B[31m\u2716\x1B[0m ${message}`)}async function runCommand(command,args,cwd){return new Promise((resolve)=>{let output=[],child=spawn(command,args,{cwd,stdio:["inherit","pipe","pipe"],env:{...process.env,FORCE_COLOR:"1"}});child.stdout?.on("data",(data)=>{let str=data.toString();output.push(str),process.stdout.write(str)}),child.stderr?.on("data",(data)=>{let str=data.toString();output.push(str),process.stderr.write(str)}),child.on("close",(code)=>{resolve({success:code===0,output:output.join("")})}),child.on("error",(err)=>{error(err.message),resolve({success:!1,output:err.message})})})}async function getGitInfo(cwd){try{let branchResult=await runCommandSilent("git",["rev-parse","--abbrev-ref","HEAD"],cwd),commitResult=await runCommandSilent("git",["rev-parse","--short","HEAD"],cwd),dateResult=await runCommandSilent("git",["log","-1","--format=%ci"],cwd);if(branchResult.success&&commitResult.success&&dateResult.success)return{branch:branchResult.output.trim(),commit:commitResult.output.trim(),commitDate:dateResult.output.trim().split(" ")[0]}}catch{}return null}async function runCommandSilent(command,args,cwd,timeoutMs=4000){return new Promise((resolve)=>{let output=[],settled=!1,child=spawn(command,args,{cwd,stdio:["inherit","pipe","pipe"]}),timer=setTimeout(()=>{if(settled)return;settled=!0,child.kill("SIGTERM"),resolve({success:!1,output:`Timed out after ${timeoutMs}ms`})},timeoutMs);child.stdout?.on("data",(data)=>{output.push(data.toString())}),child.stderr?.on("data",(data)=>{output.push(data.toString())}),child.on("close",(code)=>{if(settled)return;settled=!0,clearTimeout(timer),resolve({success:code===0,output:output.join("")})}),child.on("error",(err)=>{if(settled)return;settled=!0,clearTimeout(timer),resolve({success:!1,output:err.message})})})}function detectFromBinaryPath(path){if(path.includes(".bun"))return"bun";if(path.includes("node_modules"))return"npm";if(path===join10(LOCAL_BIN2,"genie")||path.startsWith(GENIE_BIN))return"source";return null}async function detectInstallationType(){if(genieConfigExists())try{let config=await loadGenieConfig();if(config.installMethod)return config.installMethod}catch{}if(existsSync9(join10(GENIE_SRC,".git")))return"source";let result2=await runCommandSilent("which",["genie"]);if(!result2.success)return"unknown";let detected=detectFromBinaryPath(result2.output.trim());if(detected)return detected;return(await runCommandSilent("which",["bun"])).success?"bun":"npm"}async function updateViaBun(channel){try{__require("fs").unlinkSync(join10(homedir10(),".bun","install","global","bun.lock"))}catch{}if(log(`Updating via bun (channel: ${channel})...`),!(await runCommand("bun",["add","-g","--force","--no-cache",`@automagik/genie@${channel}`])).success)return error("Failed to update via bun"),!1;return console.log(),success(`Genie CLI updated via bun (${channel})!`),!0}async function updateViaNpm(channel){if(log(`Updating via npm (channel: ${channel})...`),!(await runCommand("npm",["install","-g",`@automagik/genie@${channel}`])).success)return error("Failed to update via npm"),!1;return console.log(),success(`Genie CLI updated via npm (${channel})!`),!0}async function detectGlobalInstalls(){let found=new Set,[npmResult,bunResult]=await Promise.all([runCommandSilent("npm",["list","-g","@automagik/genie"]),runCommandSilent("bun",["pm","ls","-g"])]);if(npmResult.success&&!npmResult.output.includes("(empty)"))found.add("npm");if(bunResult.success&&bunResult.output.includes("@automagik/genie"))found.add("bun");return found}async function updateSource(){let beforeInfo=await getGitInfo(GENIE_SRC);if(beforeInfo)console.log(`Current: \x1B[2m${beforeInfo.branch}@${beforeInfo.commit} (${beforeInfo.commitDate})\x1B[0m`),console.log();if(log("Fetching latest changes..."),!(await runCommand("git",["fetch","origin"],GENIE_SRC)).success)error("Failed to fetch from origin"),process.exit(1);if(log("Resetting to origin/main..."),!(await runCommand("git",["reset","--hard","origin/main"],GENIE_SRC)).success)error("Failed to reset to origin/main"),process.exit(1);console.log();let afterInfo=await getGitInfo(GENIE_SRC);if(beforeInfo&&afterInfo&&beforeInfo.commit===afterInfo.commit){success("Already up to date!"),console.log();return}if(log("Installing dependencies..."),!(await runCommand("bun",["install"],GENIE_SRC)).success)error("Failed to install dependencies"),process.exit(1);if(console.log(),log("Building..."),!(await runCommand("bun",["run","build"],GENIE_SRC)).success)error("Failed to build"),process.exit(1);console.log(),log("Installing binaries...");try{await mkdir(GENIE_BIN,{recursive:!0}),await mkdir(LOCAL_BIN2,{recursive:!0});let binaries=["genie.js","term.js"],names=["genie","term"];for(let i=0;i<binaries.length;i++){let src=join10(GENIE_SRC,"dist",binaries[i]),binDest=join10(GENIE_BIN,binaries[i]),linkDest=join10(LOCAL_BIN2,names[i]);await copyFile(src,binDest),await chmod(binDest,493),await symlinkOrCopy(binDest,linkDest)}for(let legacy of["claudio.js","claudio"]){let legacyBin=join10(GENIE_BIN,legacy),legacyLink=join10(LOCAL_BIN2,legacy);try{await unlink(legacyBin)}catch{}try{await unlink(legacyLink)}catch{}}success("Binaries installed")}catch(err){error(`Failed to install binaries: ${err}`),process.exit(1)}if(console.log(),console.log("\x1B[2m\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\x1B[0m"),success("Genie CLI updated successfully!"),console.log(),afterInfo)console.log(`Version: \x1B[36m${afterInfo.branch}@${afterInfo.commit}\x1B[0m (${afterInfo.commitDate})`),console.log()}async function symlinkOrCopy(src,dest){let{symlink,unlink:unlink2}=await import("fs/promises");try{if(existsSync9(dest))await unlink2(dest);await symlink(src,dest)}catch{await copyFile(src,dest)}}function copyDirSync(src,dest){mkdirSync6(dest,{recursive:!0});for(let entry of readdirSync(src,{withFileTypes:!0})){let srcPath=join10(src,entry.name),destPath=join10(dest,entry.name);if(entry.isDirectory())copyDirSync(srcPath,destPath);else copyFileSync2(srcPath,destPath)}}async function resolveGlobalPkgDir(installType){if(installType==="bun"){let bunPath=join10(homedir10(),".bun","install","global","node_modules","@automagik","genie");if(existsSync9(bunPath))return bunPath}if(installType==="npm"){let npmRootResult=await runCommandSilent("npm",["root","-g"]);if(npmRootResult.success){let npmPath=join10(npmRootResult.output.trim(),"@automagik","genie");if(existsSync9(npmPath))return npmPath}}let bunFallback=join10(homedir10(),".bun","install","global","node_modules","@automagik","genie");if(existsSync9(bunFallback))return bunFallback;let npmRootFallback=await runCommandSilent("npm",["root","-g"]);if(npmRootFallback.success){let npmPath=join10(npmRootFallback.output.trim(),"@automagik","genie");if(existsSync9(npmPath))return npmPath}return null}function updatePluginRegistry(claudePlugins,cacheDir,version){let registryPath=join10(claudePlugins,"installed_plugins.json");try{if(!existsSync9(registryPath))return;let registry=JSON.parse(readFileSync5(registryPath,"utf-8")),entries=registry.plugins?.["genie@automagik"];if(!Array.isArray(entries))return;for(let entry of entries)if(entry.scope==="user")entry.installPath=cacheDir,entry.version=version,entry.lastUpdated=new Date().toISOString();writeFileSync6(registryPath,JSON.stringify(registry,null,2))}catch(err){log(`Registry update failed (non-fatal): ${err}`)}}function syncTmuxConf(tmuxScriptsSrc){mkdirSync6(GENIE_HOME,{recursive:!0});let tmuxConfSrc=join10(tmuxScriptsSrc,"genie.tmux.conf"),tmuxConfDest=join10(GENIE_HOME,"tmux.conf");if(existsSync9(tmuxConfSrc))try{copyFileSync2(tmuxConfSrc,tmuxConfDest),success(`Installed tmux config to ${tmuxConfDest}`);try{let{tmuxBin:tmuxBin2}=(init_ensure_tmux(),__toCommonJS(exports_ensure_tmux));execSync2(`${tmuxBin2()} -L genie source-file '${tmuxConfDest}'`,{stdio:"ignore"}),success("Reloaded genie tmux server configuration")}catch{}}catch{}let tuiConfSrc=join10(tmuxScriptsSrc,"tui-tmux.conf"),tuiConfDest=join10(GENIE_HOME,"tui-tmux.conf");if(existsSync9(tuiConfSrc))try{copyFileSync2(tuiConfSrc,tuiConfDest),success(`Installed TUI tmux config to ${tuiConfDest}`)}catch{}let osc52Src=join10(tmuxScriptsSrc,"osc52-copy.sh"),osc52Dest=join10(GENIE_HOME,"osc52-copy.sh");if(existsSync9(osc52Src))try{copyFileSync2(osc52Src,osc52Dest),chmodSync2(osc52Dest,493),success(`Installed OSC 52 clipboard helper to ${osc52Dest}`)}catch{}}function syncTmuxScripts(globalPkgDir){let tmuxScriptsSrc=join10(globalPkgDir,"scripts","tmux");if(!existsSync9(tmuxScriptsSrc))return;let scriptsDir=join10(GENIE_HOME,"scripts");mkdirSync6(scriptsDir,{recursive:!0});let scriptCount=0;for(let entry of readdirSync(tmuxScriptsSrc))if(entry.endsWith(".sh")||entry==="genie.tmux.conf"||entry==="tui-tmux.conf"){let src=join10(tmuxScriptsSrc,entry),dest=join10(scriptsDir,entry);copyFileSync2(src,dest);try{chmodSync2(dest,entry.endsWith(".sh")?493:420)}catch{}scriptCount++}if(scriptCount>0)success(`Refreshed ${scriptCount} tmux scripts at ${scriptsDir}`);syncTmuxConf(tmuxScriptsSrc)}function syncMarketplaceVersion(claudePlugins,version){let marketplacePath=join10(claudePlugins,"marketplaces","automagik",".claude-plugin","marketplace.json");try{if(!existsSync9(marketplacePath))return;let data=JSON.parse(readFileSync5(marketplacePath,"utf-8"));if(Array.isArray(data.plugins)){for(let plugin of data.plugins)if(plugin.name==="genie")plugin.version=version}writeFileSync6(marketplacePath,JSON.stringify(data,null,2)),success(`Updated marketplace.json to v${version}`)}catch(err){log(`Marketplace version update failed (non-fatal): ${err}`)}}function syncPluginPackageVersion(claudePlugins,version){let pkgPath=join10(claudePlugins,"marketplaces","automagik","plugins","genie","package.json");try{if(!existsSync9(pkgPath))return;let data=JSON.parse(readFileSync5(pkgPath,"utf-8"));data.version=version,writeFileSync6(pkgPath,JSON.stringify(data,null,2)),success(`Updated plugin package.json to v${version}`)}catch(err){log(`Plugin package.json update failed (non-fatal): ${err}`)}}function syncSkillsSymlink(claudePlugins,version){let skillsLink=join10(claudePlugins,"marketplaces","automagik","plugins","genie","skills"),cacheSkills=join10("..","..","..","..","cache","automagik","genie",version,"skills");try{let{symlinkSync,unlinkSync:unlinkSync5,lstatSync:lstatSync2}=__require("fs");try{lstatSync2(skillsLink),unlinkSync5(skillsLink)}catch{}symlinkSync(cacheSkills,skillsLink),success(`Skills symlink \u2192 cache/${version}/skills`)}catch(err){log(`Skills symlink update failed (non-fatal): ${err}`)}}async function syncPlugin(installType){log("Syncing Claude Code plugin...");let globalPkgDir=await resolveGlobalPkgDir(installType);if(!globalPkgDir){log("Could not find installed package \u2014 skipping plugin sync");return}let pluginSrc=join10(globalPkgDir,"plugins","genie");if(!existsSync9(pluginSrc)){log("Plugin source not found in package \u2014 skipping plugin sync");return}let version;try{version=JSON.parse(readFileSync5(join10(globalPkgDir,"package.json"),"utf-8")).version}catch{log("Could not read package version \u2014 skipping plugin sync");return}let claudePlugins=join10(homedir10(),".claude","plugins"),cacheDir=join10(claudePlugins,"cache","automagik","genie",version);try{if(existsSync9(cacheDir))rmSync3(cacheDir,{recursive:!0,force:!0});copyDirSync(pluginSrc,cacheDir);let skillsSrc=join10(globalPkgDir,"skills");if(existsSync9(skillsSrc)&&!existsSync9(join10(cacheDir,"skills")))copyDirSync(skillsSrc,join10(cacheDir,"skills"))}catch(err){error(`Failed to copy plugin: ${err}`);return}updatePluginRegistry(claudePlugins,cacheDir,version),syncMarketplaceVersion(claudePlugins,version),syncPluginPackageVersion(claudePlugins,version),syncSkillsSymlink(claudePlugins,version),syncTmuxScripts(globalPkgDir),success(`Plugin synced to v${version}`)}async function resolveChannel(options){if(options.next)return"next";if(options.stable)return"latest";if(genieConfigExists())try{let config=await loadGenieConfig();if(config.updateChannel)return config.updateChannel}catch{}return"latest"}async function persistChannel(channel){try{let config=await loadGenieConfig();config.updateChannel=channel,await saveGenieConfig(config)}catch{}}async function updateCommand(options={}){console.log(),console.log("\x1B[1m\uD83E\uDDDE Genie CLI Update\x1B[0m"),console.log("\x1B[2m\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\x1B[0m"),console.log();let channel=await resolveChannel(options);if(options.next||options.stable)await persistChannel(channel);let installType=await detectInstallationType();if(log(`Detected installation: ${installType}`),log(`Channel: ${channel}${channel==="next"?" (dev builds)":" (stable)"}`),console.log(),installType==="unknown")error("No Genie CLI installation found"),console.log(),console.log("Install method not configured. Please reinstall genie:"),console.log("\x1B[36m curl -fsSL https://raw.githubusercontent.com/automagik-dev/genie/main/install.sh | bash\x1B[0m"),console.log(),process.exit(1);if(installType==="source"){await updateSource();return}let globalInstalls=await detectGlobalInstalls(),primaryMethod=installType;if(!(primaryMethod==="bun"?await updateViaBun(channel):await updateViaNpm(channel)))process.exit(1);let secondaryMethod=primaryMethod==="bun"?"npm":"bun";if(globalInstalls.has(secondaryMethod)){if(console.log(),log(`Also updating ${secondaryMethod}-global install...`),!(secondaryMethod==="bun"?await updateViaBun(channel):await updateViaNpm(channel)))error(`Secondary update via ${secondaryMethod} failed (non-blocking)`)}await syncPlugin(installType)}init_version();import{execSync as execSync3}from"child_process";var MAX_COMMITS=5;function getRecentGitHistory(filePath,cwd){try{let trimmed=execSync3(`git log --oneline -n ${MAX_COMMITS} -- ${JSON.stringify(filePath)}`,{encoding:"utf-8",timeout:5000,cwd,stdio:["pipe","pipe","pipe"]}).trim();if(!trimmed)return null;return trimmed}catch{return null}}async function auditContext(payload){let input=payload.tool_input;if(!input)return;let filePath=input.file_path;if(!filePath)return;let cwd=payload.cwd??process.cwd(),history=getRecentGitHistory(filePath,cwd);if(!history)return;return{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"allow",additionalContext:`[audit-context] Recent git history for ${filePath}:
2861
- ${history}`}}}function buildSearchNames(recipient,dirEntry){let names=new Set([recipient]);if(dirEntry){if(names.add(dirEntry.entry.name),dirEntry.entry.roles)for(let role of dirEntry.entry.roles)names.add(role)}return names}function buildSpawnArgs(template){let args=["spawn","--provider",template.provider,"--team",template.team];if(template.role)args.push("--role",template.role);if(template.skill)args.push("--skill",template.skill);if(template.cwd)args.push("--cwd",template.cwd);if(template.extraArgs)args.push(...template.extraArgs);return args}async function isRecipientLeader(recipient,teamName){try{let{getTeam:getTeam2}=await Promise.resolve().then(() => (init_team_manager(),exports_team_manager)),config=await getTeam2(teamName);return!!config?.leader&&recipient===config.leader}catch{return!1}}async function autoSpawn(payload){let input=payload.tool_input;if(!input||input.type!=="message")return;let recipient=input.recipient;if(!recipient)return;let teamName=process.env.GENIE_TEAM??payload.team_name;if(!teamName)return;if(recipient==="team-lead")return;if(await isRecipientLeader(recipient,teamName))return;try{let registryMod=await Promise.resolve().then(() => (init_agent_registry(),exports_agent_registry)),tmuxMod=await Promise.resolve().then(() => (init_tmux(),exports_tmux)),directoryMod=await Promise.resolve().then(() => (init_agent_directory(),exports_agent_directory)),existing=(await registryMod.list()).find((a)=>(a.role===recipient||a.id===recipient)&&a.team===teamName);if(existing&&await tmuxMod.isPaneAlive(existing.paneId))return;let dirEntry=await directoryMod.resolve(recipient),templates=await registryMod.listTemplates(),searchNames=buildSearchNames(recipient,dirEntry),template=templates.find((t)=>{if(t.team!==teamName)return!1;return[...searchNames].some((q)=>t.id===q||t.role===q)});if(!template){if(dirEntry)console.error(`[genie-hook] Agent "${recipient}" is registered in directory but has no spawn template in team "${teamName}".`);return}let{spawnSync}=__require("child_process");spawnSync("genie",buildSpawnArgs(template),{timeout:1e4,stdio:"ignore",env:{...process.env,GENIE_TEAM:teamName}}),console.error(`[genie-hook] Auto-spawned "${recipient}" in team "${teamName}"`)}catch(err){let msg=err instanceof Error?err.message:String(err);return console.error(`[genie-hook] Auto-spawn failed for "${recipient}": ${msg}`),{hookSpecificOutput:{hookEventName:"PreToolUse",additionalContext:`auto-spawn warning: failed to spawn "${recipient}": ${msg}`}}}}import{existsSync as existsSync19}from"fs";import{basename as basename3,join as join21}from"path";var BRAIN_PKG="@automagik/genie-brain",BRAIN_DIR="node_modules/@automagik/genie-brain",enrichedSessions=new Set;function sessionKey(payload){return payload.session_id??`${process.pid}`}function isBrainAvailable(){return existsSync19(join21(BRAIN_DIR,"package.json"))}async function queryBrain(cwd){try{let brain=await import(BRAIN_PKG);if(!brain.search)return null;let projectName=basename3(cwd),results=await brain.search({query:`context for ${projectName}`,limit:5,minScore:0.5});if(!results||results.length===0)return null;return results.map((r)=>{return`- ${(r.content??r.text??"").slice(0,200)}`}).join(`
2884
+ ${history}`}}}function buildSearchNames(recipient,dirEntry){let names=new Set([recipient]);if(dirEntry){if(names.add(dirEntry.entry.name),dirEntry.entry.roles)for(let role of dirEntry.entry.roles)names.add(role)}return names}function buildSpawnArgs(template){let args=["spawn","--provider",template.provider,"--team",template.team];if(template.role)args.push("--role",template.role);if(template.skill)args.push("--skill",template.skill);if(template.cwd)args.push("--cwd",template.cwd);if(template.extraArgs)args.push(...template.extraArgs);return args}async function isRecipientLeader(recipient,teamName){try{let{getTeam:getTeam2}=await Promise.resolve().then(() => (init_team_manager(),exports_team_manager)),config=await getTeam2(teamName);return!!config?.leader&&recipient===config.leader}catch{return!1}}async function autoSpawn(payload){let input=payload.tool_input;if(!input||input.type!=="message")return;let recipient=input.recipient;if(!recipient)return;let teamName=process.env.GENIE_TEAM??payload.team_name;if(!teamName)return;if(recipient==="team-lead")return;if(await isRecipientLeader(recipient,teamName))return;try{let registryMod=await Promise.resolve().then(() => (init_agent_registry(),exports_agent_registry)),tmuxMod=await Promise.resolve().then(() => (init_tmux(),exports_tmux)),directoryMod=await Promise.resolve().then(() => (init_agent_directory(),exports_agent_directory)),existing=(await registryMod.list()).find((a)=>(a.role===recipient||a.id===recipient)&&a.team===teamName);if(existing&&await tmuxMod.isPaneAlive(existing.paneId))return;let dirEntry=await directoryMod.resolve(recipient),templates=await registryMod.listTemplates(),searchNames=buildSearchNames(recipient,dirEntry),template=templates.find((t)=>{if(t.team!==teamName)return!1;return[...searchNames].some((q)=>t.id===q||t.role===q)});if(!template){if(dirEntry)console.error(`[genie-hook] Agent "${recipient}" is registered in directory but has no spawn template in team "${teamName}".`);return}let{spawnSync}=__require("child_process");spawnSync("genie",buildSpawnArgs(template),{timeout:1e4,stdio:"ignore",env:{...process.env,GENIE_TEAM:teamName}}),console.error(`[genie-hook] Auto-spawned "${recipient}" in team "${teamName}"`)}catch(err){let msg=err instanceof Error?err.message:String(err);return console.error(`[genie-hook] Auto-spawn failed for "${recipient}": ${msg}`),{hookSpecificOutput:{hookEventName:"PreToolUse",additionalContext:`auto-spawn warning: failed to spawn "${recipient}": ${msg}`}}}}import{existsSync as existsSync19}from"fs";import{basename as basename3,join as join21}from"path";var BRAIN_PKG="@khal-os/brain",BRAIN_DIR="node_modules/@khal-os/brain",enrichedSessions=new Set;function sessionKey(payload){return payload.session_id??`${process.pid}`}function isBrainAvailable(){return existsSync19(join21(BRAIN_DIR,"package.json"))}async function queryBrain(cwd){try{let brain=await import(BRAIN_PKG);if(!brain.search)return null;let projectName=basename3(cwd),results=await brain.search({query:`context for ${projectName}`,limit:5,minScore:0.5});if(!results||results.length===0)return null;return results.map((r)=>{return`- ${(r.content??r.text??"").slice(0,200)}`}).join(`
2862
2885
  `)}catch{return null}}async function brainInject(payload){let key=sessionKey(payload);if(enrichedSessions.has(key))return;if(enrichedSessions.add(key),!isBrainAvailable())return;let cwd=payload.cwd??process.cwd();try{let context=await queryBrain(cwd);if(!context)return;return{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"allow",additionalContext:`[brain-inject] Prior context from knowledge base:
2863
2886
  ${context}`}}}catch{return}}var DENY_PATTERNS=[{test:(cmd)=>/git\s+push\b/i.test(cmd)&&/(?:^|\s)(main|master)(?:\s|$)/.test(cmd),reason:"BLOCKED: Push to main/master is FORBIDDEN. Push to a feature branch and create a PR targeting dev."},{test:(cmd)=>/git\s+push\b/.test(cmd)&&/:(main|master)\b/.test(cmd),reason:"BLOCKED: Push refspec targeting main/master is FORBIDDEN."},{test:(cmd)=>/gh\s+pr\s+create\b/.test(cmd)&&!/(--base|-B)\s+\S/.test(cmd),reason:"BLOCKED: gh pr create requires explicit --base flag. Use: gh pr create --base dev (or --base main for releases)"},{test:(cmd)=>/gh\s+pr\s+merge\b/.test(cmd),reason:"BLOCKED: Agents may NOT merge PRs. Only humans merge via GitHub UI."},{test:(cmd)=>/git\s+checkout\s+(main|master)\s*[;&|]+\s*git\s+(commit|merge|cherry-pick|rebase|push|add)\b/.test(cmd),reason:"BLOCKED: Committing or mutating on main/master is FORBIDDEN. Work on feature branches."}];async function branchGuard(payload){let input=payload.tool_input;if(!input)return;let command=input.command;if(!command)return;if(!/\b(git|gh)\b/.test(command))return;for(let pattern of DENY_PATTERNS)if(pattern.test(command))return{decision:"deny",reason:pattern.reason};return}import{execSync as execSync5}from"child_process";import{statSync}from"fs";var STALENESS_THRESHOLD_SECS=120;function getLastCommitInfo(filePath,cwd){try{let trimmed=execSync5(`git log -1 --format="%at|%an|%s" -- ${JSON.stringify(filePath)}`,{encoding:"utf-8",timeout:5000,cwd,stdio:["pipe","pipe","pipe"]}).trim();if(!trimmed)return null;let[timestampStr,author,...messageParts]=trimmed.split("|"),timestamp2=Number.parseInt(timestampStr,10);if(Number.isNaN(timestamp2))return null;let age=Math.floor(Date.now()/1000)-timestamp2;return{author:author??"unknown",age,message:messageParts.join("|")}}catch{return null}}function getFileModAge(filePath){try{let stat2=statSync(filePath);return Math.floor((Date.now()-stat2.mtimeMs)/1000)}catch{return null}}function buildCommitWarning(filePath,commitInfo){return{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"allow",additionalContext:`[freshness] Stale read warning: ${filePath} was modified ${commitInfo.age}s ago by "${commitInfo.author}" (${commitInfo.message}). Contents may have changed since you last read it.`}}}function checkUncommittedChanges(filePath,cwd,diskAge){try{if(execSync5(`git status --porcelain -- ${JSON.stringify(filePath)}`,{encoding:"utf-8",timeout:5000,cwd,stdio:["pipe","pipe","pipe"]}).trim())return{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"allow",additionalContext:`[freshness] Stale read warning: ${filePath} has uncommitted changes (modified ${diskAge}s ago). Another agent may be editing this file concurrently.`}}}catch{}return}async function freshness(payload){let input=payload.tool_input;if(!input)return;let filePath=input.file_path;if(!filePath)return;let cwd=payload.cwd??process.cwd(),currentAgent=process.env.GENIE_AGENT_NAME,diskAge=getFileModAge(filePath);if(diskAge===null||diskAge>=STALENESS_THRESHOLD_SECS)return;let commitInfo=getLastCommitInfo(filePath,cwd);if(commitInfo&&commitInfo.age<STALENESS_THRESHOLD_SECS){if(currentAgent&&commitInfo.author.includes(currentAgent))return;return buildCommitWarning(filePath,commitInfo)}if(currentAgent)return checkUncommittedChanges(filePath,cwd,diskAge);return}async function identityInject(payload){let input=payload.tool_input;if(!input)return;let msgType=input.type;if(msgType&&msgType!=="message"&&msgType!=="broadcast")return;let agentName=process.env.GENIE_AGENT_NAME;if(!agentName)return;let contentField=input.content!==void 0?"content":"message",content=input[contentField];if(!content)return;if(content.startsWith(`[from:${agentName}]`))return;return{updatedInput:{...input,[contentField]:`[from:${agentName}] ${content}`}}}var NUDGE_PATTERNS=[{test:/tmux\s+capture-pane/,message:`If you're checking genie agent progress, use structured monitoring instead:
2864
2887
  `+` genie task status <slug> \u2014 wish progress from PG
@@ -2927,9 +2950,8 @@ Board: ${board.name} (${board.id})`),board.description)console.log(`Description:
2927
2950
  Columns:`);for(let i2=0;i2<sorted.length;i2++){let c=sorted[i2],count=countByColumn.get(c.name)??countByColumn.get(c.id)??0,gate=` [gate: ${c.gate}]`,action=c.action?` (action: ${c.action})`:"";console.log(` ${i2+1}. ${c.label??c.name}${gate}${action} \u2014 ${count} task${count===1?"":"s"}`)}console.log("")}function buildColumnUpdates(options){let updates={};if(options.gate)updates.gate=options.gate;if(options.action)updates.action=options.action;if(options.color)updates.color=options.color;if(options.rename)updates.name=options.rename,updates.label=options.rename;return updates}async function handleBoardEdit(name,options){let bs=await getBoardService(),board=await resolveBoard(name,options.project);if(options.column){let col=board.columns.find((c)=>c.name===options.column||c.label===options.column);if(!col)throw Error(`Column not found: ${options.column}`);let updates=buildColumnUpdates(options);if(!await bs.updateColumn(board.id,col.id,updates))throw Error(`Failed to update column: ${options.column}`);console.log(`Updated column "${options.column}" on board "${board.name}".`);return}let boardUpdates={};if(options.name)boardUpdates.name=options.name;if(options.description)boardUpdates.description=options.description;if(Object.keys(boardUpdates).length===0)console.error("Error: No updates specified. Use --column, --name, or --description."),process.exit(1);let updated=await bs.updateBoard(board.id,boardUpdates);if(!updated)throw Error(`Failed to update board: ${name}`);console.log(`Updated board "${updated.name}" (${updated.id}).`)}async function handleBoardDelete(name,options){let bs=await getBoardService(),board=await resolveBoard(name,options.project);if(!options.force)console.log(`Deleting board "${board.name}" (${board.id})...`);if(!await bs.deleteBoard(board.id))throw Error(`Failed to delete board: ${name}`);console.log(`Deleted board "${board.name}" (${board.id}).`)}async function handleBoardColumns(name,options){let board=await resolveBoard(name,options.project);if(options.json){console.log(JSON.stringify(board.columns,null,2));return}printColumnPipeline(board.columns,`Board: ${board.name} (${board.columns.length} columns)`)}async function handleBoardUse(name,options){let board=await resolveBoard(name,options.project),repoRoot=execSync10("git rev-parse --show-toplevel",{encoding:"utf-8"}).trim(),genieDir=join37(repoRoot,".genie"),configPath2=join37(genieDir,"config.json");if(!existsSync28(genieDir))mkdirSync12(genieDir,{recursive:!0});let config={};if(existsSync28(configPath2))try{config=JSON.parse(readFileSync16(configPath2,"utf-8"))}catch{}config.activeBoard=board.id,writeFileSync13(configPath2,`${JSON.stringify(config,null,2)}
2928
2951
  `),console.log(`Active board set to "${board.name}" (${board.id})`)}async function handleBoardExport(name,options){let bs=await getBoardService(),board=await resolveBoard(name,options.project),exported=await bs.exportBoard(board.id),json2=JSON.stringify(exported,null,2);if(options.output){let dir=dirname6(options.output);if(!existsSync28(dir))mkdirSync12(dir,{recursive:!0});writeFileSync13(options.output,`${json2}
2929
2952
  `),console.log(`Exported board "${board.name}" to ${options.output}`)}else console.log(json2)}async function handleBoardReconcile(name,options){let{reconcileBoard:reconcileBoard2}=await Promise.resolve().then(() => (init_board_service(),exports_board_service)),board=await resolveBoard(name,options.project),result2=await reconcileBoard2(board.id);if(options.json){console.log(JSON.stringify(result2,null,2));return}if(result2.fixed===0&&result2.orphaned===0){console.log(`Board "${board.name}": all tasks have valid column_ids.`);return}if(console.log(`Board "${board.name}" reconciliation:`),console.log(` Fixed: ${result2.fixed} task${result2.fixed===1?"":"s"}`),result2.orphaned>0){let count=result2.orphaned;console.log(` Still orphaned: ${count} task${count===1?"":"s"} (stage doesn't match any column)`)}}async function handleBoardImport(options){let bs=await getBoardService(),projectId=await resolveProjectId(options.project),raw=readFileSync16(options.json,"utf-8"),data=JSON.parse(raw),board=await bs.importBoard(data,projectId);console.log(`Imported board "${board.name}" (${board.id}) with ${board.columns.length} columns`)}async function handleTemplateList(options){let templates=await(await getTemplateService()).listTemplates();if(options.json){console.log(JSON.stringify(templates,null,2));return}printTemplateTable(templates)}async function handleTemplateShow(name,options){let template=await(await getTemplateService()).getTemplate(name);if(!template)throw Error(`Template not found: ${name}`);if(options.json){console.log(JSON.stringify(template,null,2));return}if(console.log(`
2930
- Template: ${template.name} (${template.id})`),template.description)console.log(`Description: ${template.description}`);if(template.icon)console.log(`Icon: ${template.icon}`);console.log(`Built-in: ${template.isBuiltin?"yes":"no"}`),printColumnPipeline(template.columns,`Pipeline (${template.columns.length} columns)`)}async function handleTemplateCreate(name,options){let tmpl=await getTemplateService();if(options.fromBoard){let board=await(await getBoardService()).getBoard(options.fromBoard);if(!board)throw Error(`Board not found: ${options.fromBoard}`);let template2=await tmpl.snapshotFromBoard(board.id,name);console.log(`Created template "${template2.name}" (${template2.id}) from board "${board.name}" with ${template2.columns.length} columns`);return}let columns;if(options.columns)columns=options.columns.split(",").map((colName,i2)=>({id:crypto.randomUUID(),name:colName.trim(),label:colName.trim(),gate:"human",action:null,auto_advance:!1,transitions:[],roles:["*"],color:"#94a3b8",parallel:!1,on_fail:null,position:i2}));let template=await tmpl.createTemplate({name,description:options.description,columns});console.log(`Created template "${template.name}" (${template.id}) with ${template.columns.length} columns`)}async function handleTemplateEdit(name,options){let tmpl=await getTemplateService(),template=await tmpl.getTemplate(name);if(!template)throw Error(`Template not found: ${name}`);if(!options.column)console.error("Error: --column is required for template edit."),process.exit(1);let updates={};if(options.gate)updates.gate=options.gate;if(options.action)updates.action=options.action;if(options.color)updates.color=options.color;if(options.rename)updates.name=options.rename,updates.label=options.rename;if(!await tmpl.updateTemplateColumn(template.id,options.column,updates))throw Error(`Failed to update template: ${name}`);console.log(`Updated column "${options.column}" on template "${template.name}".`)}async function handleTemplateRename(oldName,newName){let tmpl=await getTemplateService(),template=await tmpl.getTemplate(oldName);if(!template)throw Error(`Template not found: ${oldName}`);let updated=await tmpl.renameTemplate(template.id,newName);if(!updated)throw Error(`Failed to rename template: ${oldName}`);console.log(`Renamed template "${oldName}" to "${updated.name}".`)}async function handleTemplateDelete(name){let tmpl=await getTemplateService(),template=await tmpl.getTemplate(name);if(!template)throw Error(`Template not found: ${name}`);if(!await tmpl.deleteTemplate(template.id))throw Error(`Failed to delete template: ${name}`);console.log(`Deleted template "${template.name}" (${template.id}).`)}function registerBoardCommands(program2){let board=program2.command("board").description("Board and pipeline management");board.command("create <name>").description("Create a new board").option("--project <project>","Project name").option("--from <template>","Create from template name").option("--columns <columns>","Comma-separated column names").option("--description <text>","Board description").action(async(name,options)=>{try{await handleBoardCreate(name,options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),board.command("list").description("List all boards").option("--project <project>","Filter by project").option("--all","Include archived boards").option("--json","Output as JSON").action(async(options)=>{try{await handleBoardList(options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),board.command("show <name...>").description("Show board detail").option("--project <project>","Disambiguate by project").option("--json","Output as JSON").action(async(nameParts,options)=>{try{await handleBoardShow(nameParts.join(" "),options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),board.command("edit <name...>").description("Edit board or column properties").option("--project <project>","Disambiguate by project").option("--column <col>","Column name to edit").option("--gate <gate>","New gate value (human|agent|human+agent)").option("--action <action>","New action skill").option("--color <color>","New color hex").option("--rename <new>","Rename the column").option("--name <new>","Rename the board itself").option("--description <text>","Update description").action(async(nameParts,options)=>{try{await handleBoardEdit(nameParts.join(" "),options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),board.command("delete <name...>").description("Delete a board").option("--project <project>","Disambiguate by project").option("--force","Skip confirmation").action(async(nameParts,options)=>{try{await handleBoardDelete(nameParts.join(" "),options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),board.command("columns <name...>").description("Show board column pipeline").option("--project <project>","Disambiguate by project").option("--json","Output as JSON").action(async(nameParts,options)=>{try{await handleBoardColumns(nameParts.join(" "),options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),board.command("use <name...>").description("Set active board for current repo").option("--project <project>","Disambiguate by project").action(async(nameParts,options)=>{try{await handleBoardUse(nameParts.join(" "),options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),board.command("export <name...>").description("Export board as JSON").option("--project <project>","Disambiguate by project").option("--output <file>","Write to file instead of stdout").option("--json","Output as JSON (default, accepted for consistency)").action(async(nameParts,options)=>{try{await handleBoardExport(nameParts.join(" "),options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),board.command("reconcile <name...>").description("Fix orphaned column_ids by matching task stage to board columns").option("--project <project>","Disambiguate by project").option("--json","Output as JSON").action(async(nameParts,options)=>{try{await handleBoardReconcile(nameParts.join(" "),options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),board.command("archive <name...>").description("Archive a board and its unfinished tasks").option("--project <project>","Disambiguate by project").action(async(nameParts,options)=>{try{let ts3=await getTaskService3(),board2=await resolveBoard(nameParts.join(" "),options.project);await ts3.archiveBoard(board2.id),console.log(`Archived board "${board2.name}" and its unfinished tasks.`)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),board.command("import").description("Import board from JSON file").requiredOption("--json <file>","JSON file to import").requiredOption("--project <project>","Target project").action(async(options)=>{try{await handleBoardImport(options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}});let template=board.command("template").description("Board template management");template.command("list").description("List all board templates").option("--json","Output as JSON").action(async(options)=>{try{await handleTemplateList(options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),template.command("show <name>").description("Show template detail with pipeline view").option("--json","Output as JSON").action(async(name,options)=>{try{await handleTemplateShow(name,options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),template.command("create <name>").description("Create a board template").option("--from-board <board>","Create from existing board").option("--columns <columns>","Comma-separated column names").option("--description <text>","Template description").action(async(name,options)=>{try{await handleTemplateCreate(name,options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),template.command("edit <name>").description("Edit a template column").option("--column <col>","Column name to edit").option("--gate <gate>","New gate value (human|agent|human+agent)").option("--action <action>","New action skill").option("--rename <new>","Rename the column").option("--color <color>","New color hex").action(async(name,options)=>{try{await handleTemplateEdit(name,options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),template.command("rename <old> <new>").description("Rename a template").action(async(oldName,newName)=>{try{await handleTemplateRename(oldName,newName)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),template.command("delete <name>").description("Delete a template").action(async(name)=>{try{await handleTemplateDelete(name)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}})}import{execSync as execSync11}from"child_process";import{existsSync as existsSync29,mkdirSync as mkdirSync13,readFileSync as readFileSync17,realpathSync as realpathSync4,writeFileSync as writeFileSync14}from"fs";import{homedir as homedir25}from"os";import{dirname as dirname7,join as join38,resolve as resolve6}from"path";var BRAIN_PKG2="@automagik/genie-brain",BRAIN_REPO="github:automagik-dev/genie-brain";function resolveGenieRoot(){try{let scriptDir=dirname7(realpathSync4(process.argv[1])),candidates=[resolve6(scriptDir,".."),resolve6(scriptDir,"..","..")];for(let c of candidates)if(existsSync29(join38(c,"package.json")))return c}catch{}return resolve6(import.meta.dir,"..","..")}var BRAIN_DIR2=join38(resolveGenieRoot(),"node_modules","@automagik","genie-brain"),CACHE_PATH=join38(homedir25(),".genie","brain-version-check.json");function compareVersions(a,b2){let partsA=a.split(".").map(Number),partsB=b2.split(".").map(Number);for(let i2=0;i2<Math.max(partsA.length,partsB.length);i2++){let diff=(partsA[i2]??0)-(partsB[i2]??0);if(diff!==0)return diff}return 0}function readLocalBrainVersion(){try{return JSON.parse(readFileSync17(join38(BRAIN_DIR2,"package.json"),"utf-8")).version??"unknown"}catch{return}}function checkForUpdates(cachePath){try{let p=cachePath??CACHE_PATH;if(!existsSync29(p))return{updateAvailable:!1};let cache=JSON.parse(readFileSync17(p,"utf-8"));if(cache.updateAvailable&&cache.latestVersion)return{updateAvailable:!0,latestVersion:cache.latestVersion};return{updateAvailable:!1}}catch{return{updateAvailable:!1}}}function refreshVersionCache(localVersion){try{if(!existsSync29(join38(BRAIN_DIR2,".git")))return;let version=localVersion??readLocalBrainVersion();if(!version)return;execSync11(`git -C "${BRAIN_DIR2}" fetch origin --tags`,{stdio:"pipe"});let latestTag=execSync11(`git -C "${BRAIN_DIR2}" tag -l "v0.*" --sort=-version:refname`,{encoding:"utf-8"}).trim().split(`
2931
- `)[0]??"",latestVersion=latestTag.replace(/^v/,""),localCore=version.replace(/^\d+\./,""),latestCore=latestVersion.replace(/^\d+\./,""),updateAvailable=compareVersions(latestCore,localCore)>0,cacheDir=join38(homedir25(),".genie");mkdirSync13(cacheDir,{recursive:!0}),writeFileSync14(CACHE_PATH,JSON.stringify({checkedAt:new Date().toISOString(),localVersion:version,latestTag,latestVersion,updateAvailable},null,2))}catch{}}async function updateBrain(){if(!existsSync29(join38(BRAIN_DIR2,".git")))return console.log(" Brain is not installed. Run: genie brain install"),!1;let oldVersion="unknown";try{oldVersion=(await import(BRAIN_PKG2)).getVersion?.()??"unknown"}catch{}console.log(" Updating brain from GitHub..."),execSync11(`git -C "${BRAIN_DIR2}" checkout main`,{stdio:"pipe"}),execSync11(`git -C "${BRAIN_DIR2}" pull origin main`,{stdio:"inherit"}),execSync11("bun install",{cwd:BRAIN_DIR2,stdio:"inherit"}),execSync11("bun run build",{cwd:BRAIN_DIR2,stdio:"inherit"});let newVersion="unknown";try{newVersion=JSON.parse(readFileSync17(join38(BRAIN_DIR2,"package.json"),"utf-8")).version??"unknown"}catch{}console.log(`
2932
- Updated: ${oldVersion} \u2192 ${newVersion}`);try{let migrateScript=`const b = require('${BRAIN_PKG2}'); if (b.runAllMigrations) b.runAllMigrations().then(() => process.exit(0)).catch((e) => { console.error(e); process.exit(1); }); else process.exit(0);`;execSync11(`bun -e "${migrateScript}"`,{cwd:BRAIN_DIR2,stdio:"inherit"}),console.log(" Migrations applied.")}catch{console.log(" Migration skipped. Run: genie brain migrate")}return refreshVersionCache(newVersion),!0}async function showVersion(){let localVersion="not installed";try{let brain=await import(BRAIN_PKG2);localVersion=brain.getVersion?.()??brain.VERSION??"unknown"}catch(err){let msg=err instanceof Error?err.message:String(err);if(isModuleNotFound(msg)){console.log(" Brain is not installed. Run: genie brain install");return}}console.log(` Local: ${localVersion}`),refreshVersionCache(localVersion);let check=checkForUpdates();if(check.updateAvailable&&check.latestVersion)console.log(` Latest: ${check.latestVersion}`),console.log(""),console.log(" Update available. Run: genie brain upgrade");else console.log(" Status: up to date")}async function installBrain(){console.log(""),console.log(" Installing genie-brain from GitHub (enterprise)..."),console.log(""),console.log(" Source: https://github.com/automagik-dev/genie-brain"),console.log(" Requires: GitHub org membership (automagik-dev)"),console.log("");try{try{execSync11("gh auth token",{stdio:"pipe"})}catch{return console.error(" GitHub CLI not authenticated. Run: gh auth login"),!1}execSync11(`rm -rf "${BRAIN_DIR2}"`,{stdio:"pipe"}),execSync11(`mkdir -p "${dirname7(BRAIN_DIR2)}"`,{stdio:"pipe"}),execSync11(`gh repo clone automagik-dev/genie-brain "${BRAIN_DIR2}" -- --depth 1`,{stdio:"inherit"}),execSync11("bun install",{cwd:BRAIN_DIR2,stdio:"inherit"}),execSync11("bun run build",{cwd:BRAIN_DIR2,stdio:"inherit"}),console.log(""),console.log(" Brain installed from GitHub."),console.log("");try{let brain=await import(BRAIN_PKG2);if(brain.runAllMigrations)console.log(" Running brain migrations..."),await brain.runAllMigrations(),console.log(" Brain tables created in Postgres.")}catch{console.log(" Auto-migration skipped. Run: genie brain migrate")}return console.log(""),console.log(" Get started:"),console.log(" genie brain init --name my-brain --path ./brain"),console.log(""),!0}catch(err){let msg=err instanceof Error?err.message:String(err);if(msg.includes("Authentication")||msg.includes("permission")||msg.includes("404"))console.error(" Access denied. Brain is enterprise-only."),console.log(""),console.log(" You need:"),console.log(" 1. Membership in the automagik-dev GitHub org"),console.log(" 2. SSH key or GH token configured for git"),console.log(""),console.log(" Manual install:"),console.log(` bun add ${BRAIN_REPO}`),console.log("");else console.error(` Install failed: ${msg}`),console.log(""),console.log(" Manual install:"),console.log(` bun add ${BRAIN_REPO}`),console.log("");return!1}}function uninstallBrain(){try{execSync11(`rm -rf "${BRAIN_DIR2}"`,{stdio:"pipe"}),console.log(" Brain uninstalled.")}catch{console.error(" Uninstall failed. Manual: rm -rf node_modules/@automagik/genie-brain")}}function isModuleNotFound(msg){return msg.includes("Cannot find")||msg.includes("not found")||msg.includes("MODULE_NOT_FOUND")}function printNotInstalledMessage(){console.log(""),console.log(" Brain is an enterprise knowledge graph engine."),console.log(" It is not installed."),console.log(""),console.log(" Quick install:"),console.log(""),console.log(" genie brain install"),console.log(""),console.log(" Requires GitHub org membership (automagik-dev)."),console.log("")}async function executeBrainCommand(args){try{let brain=await import(BRAIN_PKG2);if(brain.execute){await brain.execute(args);let check=checkForUpdates();if(check.updateAvailable&&check.latestVersion)console.log(`
2953
+ Template: ${template.name} (${template.id})`),template.description)console.log(`Description: ${template.description}`);if(template.icon)console.log(`Icon: ${template.icon}`);console.log(`Built-in: ${template.isBuiltin?"yes":"no"}`),printColumnPipeline(template.columns,`Pipeline (${template.columns.length} columns)`)}async function handleTemplateCreate(name,options){let tmpl=await getTemplateService();if(options.fromBoard){let board=await(await getBoardService()).getBoard(options.fromBoard);if(!board)throw Error(`Board not found: ${options.fromBoard}`);let template2=await tmpl.snapshotFromBoard(board.id,name);console.log(`Created template "${template2.name}" (${template2.id}) from board "${board.name}" with ${template2.columns.length} columns`);return}let columns;if(options.columns)columns=options.columns.split(",").map((colName,i2)=>({id:crypto.randomUUID(),name:colName.trim(),label:colName.trim(),gate:"human",action:null,auto_advance:!1,transitions:[],roles:["*"],color:"#94a3b8",parallel:!1,on_fail:null,position:i2}));let template=await tmpl.createTemplate({name,description:options.description,columns});console.log(`Created template "${template.name}" (${template.id}) with ${template.columns.length} columns`)}async function handleTemplateEdit(name,options){let tmpl=await getTemplateService(),template=await tmpl.getTemplate(name);if(!template)throw Error(`Template not found: ${name}`);if(!options.column)console.error("Error: --column is required for template edit."),process.exit(1);let updates={};if(options.gate)updates.gate=options.gate;if(options.action)updates.action=options.action;if(options.color)updates.color=options.color;if(options.rename)updates.name=options.rename,updates.label=options.rename;if(!await tmpl.updateTemplateColumn(template.id,options.column,updates))throw Error(`Failed to update template: ${name}`);console.log(`Updated column "${options.column}" on template "${template.name}".`)}async function handleTemplateRename(oldName,newName){let tmpl=await getTemplateService(),template=await tmpl.getTemplate(oldName);if(!template)throw Error(`Template not found: ${oldName}`);let updated=await tmpl.renameTemplate(template.id,newName);if(!updated)throw Error(`Failed to rename template: ${oldName}`);console.log(`Renamed template "${oldName}" to "${updated.name}".`)}async function handleTemplateDelete(name){let tmpl=await getTemplateService(),template=await tmpl.getTemplate(name);if(!template)throw Error(`Template not found: ${name}`);if(!await tmpl.deleteTemplate(template.id))throw Error(`Failed to delete template: ${name}`);console.log(`Deleted template "${template.name}" (${template.id}).`)}function registerBoardCommands(program2){let board=program2.command("board").description("Board and pipeline management");board.command("create <name>").description("Create a new board").option("--project <project>","Project name").option("--from <template>","Create from template name").option("--columns <columns>","Comma-separated column names").option("--description <text>","Board description").action(async(name,options)=>{try{await handleBoardCreate(name,options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),board.command("list").description("List all boards").option("--project <project>","Filter by project").option("--all","Include archived boards").option("--json","Output as JSON").action(async(options)=>{try{await handleBoardList(options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),board.command("show <name...>").description("Show board detail").option("--project <project>","Disambiguate by project").option("--json","Output as JSON").action(async(nameParts,options)=>{try{await handleBoardShow(nameParts.join(" "),options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),board.command("edit <name...>").description("Edit board or column properties").option("--project <project>","Disambiguate by project").option("--column <col>","Column name to edit").option("--gate <gate>","New gate value (human|agent|human+agent)").option("--action <action>","New action skill").option("--color <color>","New color hex").option("--rename <new>","Rename the column").option("--name <new>","Rename the board itself").option("--description <text>","Update description").action(async(nameParts,options)=>{try{await handleBoardEdit(nameParts.join(" "),options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),board.command("delete <name...>").description("Delete a board").option("--project <project>","Disambiguate by project").option("--force","Skip confirmation").action(async(nameParts,options)=>{try{await handleBoardDelete(nameParts.join(" "),options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),board.command("columns <name...>").description("Show board column pipeline").option("--project <project>","Disambiguate by project").option("--json","Output as JSON").action(async(nameParts,options)=>{try{await handleBoardColumns(nameParts.join(" "),options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),board.command("use <name...>").description("Set active board for current repo").option("--project <project>","Disambiguate by project").action(async(nameParts,options)=>{try{await handleBoardUse(nameParts.join(" "),options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),board.command("export <name...>").description("Export board as JSON").option("--project <project>","Disambiguate by project").option("--output <file>","Write to file instead of stdout").option("--json","Output as JSON (default, accepted for consistency)").action(async(nameParts,options)=>{try{await handleBoardExport(nameParts.join(" "),options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),board.command("reconcile <name...>").description("Fix orphaned column_ids by matching task stage to board columns").option("--project <project>","Disambiguate by project").option("--json","Output as JSON").action(async(nameParts,options)=>{try{await handleBoardReconcile(nameParts.join(" "),options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),board.command("archive <name...>").description("Archive a board and its unfinished tasks").option("--project <project>","Disambiguate by project").action(async(nameParts,options)=>{try{let ts3=await getTaskService3(),board2=await resolveBoard(nameParts.join(" "),options.project);await ts3.archiveBoard(board2.id),console.log(`Archived board "${board2.name}" and its unfinished tasks.`)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),board.command("import").description("Import board from JSON file").requiredOption("--json <file>","JSON file to import").requiredOption("--project <project>","Target project").action(async(options)=>{try{await handleBoardImport(options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}});let template=board.command("template").description("Board template management");template.command("list").description("List all board templates").option("--json","Output as JSON").action(async(options)=>{try{await handleTemplateList(options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),template.command("show <name>").description("Show template detail with pipeline view").option("--json","Output as JSON").action(async(name,options)=>{try{await handleTemplateShow(name,options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),template.command("create <name>").description("Create a board template").option("--from-board <board>","Create from existing board").option("--columns <columns>","Comma-separated column names").option("--description <text>","Template description").action(async(name,options)=>{try{await handleTemplateCreate(name,options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),template.command("edit <name>").description("Edit a template column").option("--column <col>","Column name to edit").option("--gate <gate>","New gate value (human|agent|human+agent)").option("--action <action>","New action skill").option("--rename <new>","Rename the column").option("--color <color>","New color hex").action(async(name,options)=>{try{await handleTemplateEdit(name,options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),template.command("rename <old> <new>").description("Rename a template").action(async(oldName,newName)=>{try{await handleTemplateRename(oldName,newName)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),template.command("delete <name>").description("Delete a template").action(async(name)=>{try{await handleTemplateDelete(name)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}})}import{execSync as execSync11}from"child_process";import{existsSync as existsSync29,mkdirSync as mkdirSync13,readFileSync as readFileSync17,realpathSync as realpathSync4,writeFileSync as writeFileSync14}from"fs";import{homedir as homedir25}from"os";import{dirname as dirname7,join as join38,resolve as resolve6}from"path";var BRAIN_PKG2="@khal-os/brain",BRAIN_REPO="khal-os/brain";function resolveGenieRoot(){try{let scriptDir=dirname7(realpathSync4(process.argv[1])),candidates=[resolve6(scriptDir,".."),resolve6(scriptDir,"..","..")];for(let c of candidates)if(existsSync29(join38(c,"package.json")))return c}catch{}return resolve6(import.meta.dir,"..","..")}var BRAIN_DIR2=join38(resolveGenieRoot(),"node_modules","@khal-os","brain"),CACHE_PATH=join38(homedir25(),".genie","brain-version-check.json");function compareVersions(a,b2){let partsA=a.split(".").map(Number),partsB=b2.split(".").map(Number);for(let i2=0;i2<Math.max(partsA.length,partsB.length);i2++){let diff=(partsA[i2]??0)-(partsB[i2]??0);if(diff!==0)return diff}return 0}function readLocalBrainVersion(){try{return JSON.parse(readFileSync17(join38(BRAIN_DIR2,"package.json"),"utf-8")).version??"unknown"}catch{return}}function checkForUpdates(cachePath){try{let p=cachePath??CACHE_PATH;if(!existsSync29(p))return{updateAvailable:!1};let cache=JSON.parse(readFileSync17(p,"utf-8"));if(cache.updateAvailable&&cache.latestVersion)return{updateAvailable:!0,latestVersion:cache.latestVersion};return{updateAvailable:!1}}catch{return{updateAvailable:!1}}}function refreshVersionCache(localVersion){try{let version=localVersion??readLocalBrainVersion();if(!version)return;let latestTag=execSync11(`gh release view --repo ${BRAIN_REPO} --json tagName -q .tagName`,{stdio:"pipe",encoding:"utf-8"}).trim(),latestVersion=latestTag.replace(/^v/,""),localCore=version.replace(/^\d+\./,""),latestCore=latestVersion.replace(/^\d+\./,""),updateAvailable=compareVersions(latestCore,localCore)>0,cacheDir=join38(homedir25(),".genie");mkdirSync13(cacheDir,{recursive:!0}),writeFileSync14(CACHE_PATH,JSON.stringify({checkedAt:new Date().toISOString(),localVersion:version,latestTag,latestVersion,updateAvailable},null,2))}catch{}}async function updateBrain(){if(!existsSync29(join38(BRAIN_DIR2,"package.json")))return console.log(" Brain is not installed. Run: genie brain install"),!1;let oldVersion=readLocalBrainVersion()??"unknown";console.log(" Checking for updates...");let tag;try{tag=execSync11(`gh release view --repo ${BRAIN_REPO} --json tagName -q .tagName`,{stdio:"pipe",encoding:"utf-8"}).trim()}catch{return console.error(" Failed to check latest release. Ensure: gh auth login"),!1}let newVersion=tag.replace(/^v/,"");if(compareVersions(newVersion,oldVersion)<=0)return console.log(` Already at latest version (${oldVersion}).`),!0;console.log(` Upgrading: ${oldVersion} \u2192 ${newVersion}`),console.log("");let tmpDir=join38(homedir25(),".cache","genie-brain");mkdirSync13(tmpDir,{recursive:!0}),execSync11(`gh release download ${tag} --repo ${BRAIN_REPO} --pattern '*.tgz' --dir "${tmpDir}" --clobber`,{stdio:"inherit"}),execSync11(`rm -rf "${BRAIN_DIR2}"`,{stdio:"pipe"}),mkdirSync13(BRAIN_DIR2,{recursive:!0}),execSync11(`tar xzf "${tmpDir}/khal-os-brain-${newVersion}.tgz" -C "${BRAIN_DIR2}" --strip-components=1`,{stdio:"inherit"}),execSync11("bun install",{cwd:BRAIN_DIR2,stdio:"inherit"}),console.log(`
2954
+ Updated: ${oldVersion} \u2192 ${newVersion}`);try{let migrateScript=`const b = require('${BRAIN_PKG2}'); if (b.runAllMigrations) b.runAllMigrations().then(() => process.exit(0)).catch((e) => { console.error(e); process.exit(1); }); else process.exit(0);`;execSync11(`bun -e "${migrateScript}"`,{cwd:BRAIN_DIR2,stdio:"inherit"}),console.log(" Migrations applied.")}catch{console.log(" Migration skipped. Run: genie brain migrate")}return refreshVersionCache(newVersion),!0}async function showVersion(){let localVersion="not installed";try{let brain=await import(BRAIN_PKG2);localVersion=brain.getVersion?.()??brain.VERSION??"unknown"}catch(err){let msg=err instanceof Error?err.message:String(err);if(isModuleNotFound(msg)){console.log(" Brain is not installed. Run: genie brain install");return}}console.log(` Local: ${localVersion}`),refreshVersionCache(localVersion);let check=checkForUpdates();if(check.updateAvailable&&check.latestVersion)console.log(` Latest: ${check.latestVersion}`),console.log(""),console.log(" Update available. Run: genie brain upgrade");else console.log(" Status: up to date")}async function installBrain(){console.log(""),console.log(" Installing brain from GitHub release (enterprise)..."),console.log(""),console.log(" Source: https://github.com/khal-os/brain"),console.log(" Requires: GitHub org membership (khal-os)"),console.log("");try{try{execSync11("gh auth token",{stdio:"pipe"})}catch{return console.error(" GitHub CLI not authenticated. Run: gh auth login"),!1}let tag=execSync11(`gh release view --repo ${BRAIN_REPO} --json tagName -q .tagName`,{stdio:"pipe",encoding:"utf-8"}).trim(),version=tag.replace(/^v/,"");console.log(` Latest release: ${tag}`),console.log("");let root=resolveGenieRoot(),brainDir=join38(root,"node_modules","@khal-os","brain"),tmpDir=join38(homedir25(),".cache","genie-brain");mkdirSync13(tmpDir,{recursive:!0}),execSync11(`gh release download ${tag} --repo ${BRAIN_REPO} --pattern '*.tgz' --dir "${tmpDir}" --clobber`,{stdio:"inherit"}),execSync11(`rm -rf "${brainDir}"`,{stdio:"pipe"}),mkdirSync13(brainDir,{recursive:!0}),execSync11(`tar xzf "${tmpDir}/khal-os-brain-${version}.tgz" -C "${brainDir}" --strip-components=1`,{stdio:"inherit"}),execSync11("bun install",{cwd:brainDir,stdio:"inherit"}),console.log(""),console.log(` Brain ${version} installed from GitHub release.`),console.log("");try{let brain=await import(BRAIN_PKG2);if(brain.runAllMigrations)console.log(" Running brain migrations..."),await brain.runAllMigrations(),console.log(" Brain tables created in Postgres.")}catch{console.log(" Auto-migration skipped. Run: genie brain migrate")}return console.log(""),console.log(" Get started:"),console.log(" genie brain init --name my-brain --path ./brain"),console.log(""),!0}catch(err){let msg=err instanceof Error?err.message:String(err);if(msg.includes("Authentication")||msg.includes("permission")||msg.includes("404"))console.error(" Access denied. Brain is enterprise-only."),console.log(""),console.log(" You need:"),console.log(" 1. Membership in the khal-os GitHub org"),console.log(" 2. GitHub CLI authenticated: gh auth login"),console.log(""),console.log(" Manual install:"),console.log(` gh release download --repo ${BRAIN_REPO} --pattern '*.tgz'`),console.log(" tar xzf khal-os-brain-*.tgz -C node_modules/@khal-os/brain --strip-components=1"),console.log("");else console.error(` Install failed: ${msg}`),console.log(""),console.log(" Manual install:"),console.log(` gh release download --repo ${BRAIN_REPO} --pattern '*.tgz'`),console.log(" tar xzf khal-os-brain-*.tgz -C node_modules/@khal-os/brain --strip-components=1"),console.log("");return!1}}function uninstallBrain(){try{if(!existsSync29(BRAIN_DIR2)){console.log(" Brain is not installed.");return}execSync11(`rm -rf "${BRAIN_DIR2}"`,{stdio:"pipe"}),console.log(" Brain uninstalled.")}catch{console.error(` Uninstall failed. Manual: rm -rf ${BRAIN_DIR2}`)}}function isModuleNotFound(msg){return msg.includes("Cannot find")||msg.includes("not found")||msg.includes("MODULE_NOT_FOUND")}function printNotInstalledMessage(){console.log(""),console.log(" Brain is an enterprise knowledge graph engine."),console.log(" It is not installed."),console.log(""),console.log(" Quick install:"),console.log(""),console.log(" genie brain install"),console.log(""),console.log(" Requires GitHub org membership (khal-os)."),console.log("")}async function executeBrainCommand(args){try{let brain=await import(BRAIN_PKG2);if(brain.execute){await brain.execute(args);let check=checkForUpdates();if(check.updateAvailable&&check.latestVersion)console.log(`
2933
2955
  Update available (${check.latestVersion}). Run: genie brain upgrade`)}else console.error("Brain module loaded but execute() not found."),console.error("Update: genie brain install")}catch(err){let msg=err instanceof Error?err.message:String(err);if(isModuleNotFound(msg))printNotInstalledMessage();else console.error(`Brain error: ${msg}`)}}function registerBrainCommands(program2){let brain=program2.command("brain").description("Knowledge graph engine (enterprise)").allowUnknownOption().allowExcessArguments().action(async(_options,cmd)=>{let args=cmd.args;if(args.length===0){brain.help();return}await executeBrainCommand(args)});brain.command("install").description("Install genie-brain from GitHub").action(async()=>{await installBrain()}),brain.command("uninstall").description("Remove genie-brain installation").action(()=>{uninstallBrain()}),brain.command("upgrade").description("Upgrade genie-brain to latest version").action(async()=>{await updateBrain()}),brain.command("version").description("Show installed brain version").action(async()=>{await showVersion()})}var _brief2;async function getBrief2(){if(!_brief2)_brief2=await Promise.resolve().then(() => (init_brief(),exports_brief));return _brief2}async function handleBrief2(options){let team=options.team??process.env.GENIE_TEAM;if(!team)console.error("Error: --team is required (or set GENIE_TEAM)"),process.exit(1);let agent=options.agent??process.env.GENIE_AGENT_NAME,briefService=await getBrief2(),brief=await briefService.generateBrief({team,agent,since:options.since,repoPath:process.cwd()});console.log(briefService.formatBrief(brief))}function registerBriefCommands(program2){program2.command("brief").description("Show startup brief \u2014 aggregated context since last session").option("--team <name>","Team name (default: GENIE_TEAM)").option("--agent <name>","Agent name (default: GENIE_AGENT_NAME)").option("--since <iso>","Start timestamp (default: last executor end)").action(async(options)=>{try{await handleBrief2(options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}})}import{existsSync as existsSync30,mkdirSync as mkdirSync14,readFileSync as readFileSync18,unlinkSync as unlinkSync7,writeFileSync as writeFileSync15}from"fs";import{homedir as homedir26}from"os";import{join as join39}from"path";function genieHome4(){return process.env.GENIE_HOME??join39(homedir26(),".genie")}function pidFilePath(){return join39(genieHome4(),"scheduler.pid")}function logFilePath(){return join39(genieHome4(),"logs","scheduler.log")}function systemdDir(){return join39(homedir26(),".config","systemd","user")}function systemdUnitPath(){return join39(systemdDir(),"genie-scheduler.service")}function readPid(){let path2=pidFilePath();if(!existsSync30(path2))return null;let raw=readFileSync18(path2,"utf-8").trim(),pid=Number.parseInt(raw,10);if(Number.isNaN(pid)||pid<=0)return null;return pid}function removePid(){let path2=pidFilePath();if(existsSync30(path2))try{unlinkSync7(path2)}catch{}}function isProcessAlive2(pid){try{return process.kill(pid,0),!0}catch{return!1}}function servePidPath2(){return join39(genieHome4(),"serve.pid")}function readServePid2(){let path2=servePidPath2();if(!existsSync30(path2))return null;let raw=readFileSync18(path2,"utf-8").trim(),pid=Number.parseInt(raw,10);if(Number.isNaN(pid)||pid<=0)return null;return pid}function removeServePid2(){let path2=servePidPath2();if(existsSync30(path2))try{unlinkSync7(path2)}catch{}}function generateSystemdUnit(){let genieBin=process.argv[1]??"genie";return`[Unit]
2934
2956
  Description=Genie Serve (headless) \u2014 pgserve + scheduler + services
2935
2957
  Documentation=https://github.com/automagik/genie
@@ -1024,7 +1024,7 @@ Show service health.
1024
1024
 
1025
1025
  ## Brain (Enterprise)
1026
1026
 
1027
- `genie brain` -- Knowledge graph engine (enterprise). Delegates to `@automagik/genie-brain`.
1027
+ `genie brain` -- Knowledge graph engine (enterprise). Delegates to `@khal-os/brain`.
1028
1028
 
1029
1029
  Brain is never a hard dependency. Genie works the same without it.
1030
1030
 
package/knip.json CHANGED
@@ -29,7 +29,7 @@
29
29
  "ignoreBinaries": ["which"],
30
30
  "ignoreExportsUsedInFile": false,
31
31
  "ignoreDependencies": [
32
- "@automagik/genie-brain",
32
+ "@khal-os/brain",
33
33
  "@tauri-apps/cli",
34
34
  "@tauri-apps/api",
35
35
  "react-dom",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automagik/genie",
3
- "version": "4.260406.1",
3
+ "version": "4.260406.2",
4
4
  "description": "Collaborative terminal toolkit for human + AI workflows",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,7 +9,7 @@
9
9
  "scripts": {
10
10
  "prepare": "test \"$CI\" = true || husky",
11
11
  "version": "bun run scripts/version.ts",
12
- "build": "bun build src/genie.ts --outdir dist --target bun --minify-syntax --minify-whitespace --external bun --external pgserve --external @automagik/genie-brain --external @anthropic-ai/claude-agent-sdk && chmod +x dist/*.js",
12
+ "build": "bun build src/genie.ts --outdir dist --target bun --minify-syntax --minify-whitespace --external bun --external pgserve --external @khal-os/brain --external @anthropic-ai/claude-agent-sdk && chmod +x dist/*.js",
13
13
  "build:app": "bun run scripts/build-app.ts",
14
14
  "build:plugin": "node scripts/build.js",
15
15
  "sync": "node scripts/sync.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genie",
3
- "version": "4.260406.1",
3
+ "version": "4.260406.2",
4
4
  "description": "Human-AI partnership for Claude Code. Share a terminal, orchestrate workers, evolve together. Brainstorm ideas, turn them into wishes, execute with /work, validate with /review, and ship as one team.",
5
5
  "author": {
6
6
  "name": "Namastex Labs"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genie-plugin",
3
- "version": "4.260406.1",
3
+ "version": "4.260406.2",
4
4
  "private": true,
5
5
  "description": "Runtime dependencies for genie bundled CLIs",
6
6
  "type": "module",
@@ -16,8 +16,8 @@ import { existsSync } from 'node:fs';
16
16
  import { basename, join } from 'node:path';
17
17
  import type { HandlerResult, HookPayload } from '../types.js';
18
18
 
19
- const BRAIN_PKG = '@automagik/genie-brain';
20
- const BRAIN_DIR = 'node_modules/@automagik/genie-brain';
19
+ const BRAIN_PKG = '@khal-os/brain';
20
+ const BRAIN_DIR = 'node_modules/@khal-os/brain';
21
21
 
22
22
  /** Track which sessions have already been enriched. */
23
23
  const enrichedSessions = new Set<string>();
@@ -299,7 +299,7 @@ export async function spawnWorkerFromTemplate(
299
299
  // Auto-brain: discover brain/ dir → register in knowledge graph
300
300
  try {
301
301
  // @ts-expect-error — brain is enterprise-only, not in genie's deps
302
- const brain = await import('@automagik/genie-brain');
302
+ const brain = await import('@khal-os/brain');
303
303
  if (brain.autoBrain) {
304
304
  await brain.autoBrain({ agentId: workerId, workdir: repoPath });
305
305
  }
@@ -624,7 +624,7 @@ export async function createTask(input: TaskInput, repoPath?: string, projectId?
624
624
  if (input.metadata?.brain) {
625
625
  try {
626
626
  // @ts-expect-error — brain is enterprise-only, not in genie's deps
627
- const brain = await import('@automagik/genie-brain');
627
+ const brain = await import('@khal-os/brain');
628
628
  if (brain.taskBrain) {
629
629
  await brain.taskBrain({ taskId: String(task.id), workdir: repo });
630
630
  }
@@ -444,4 +444,60 @@ describe('ClaudeSdkOmniExecutor', () => {
444
444
  expect(createAndLinkExecutorMock).not.toHaveBeenCalled();
445
445
  });
446
446
  });
447
+
448
+ // ==========================================================================
449
+ // Turn-based prompt injection (Group 4 — omni-turn-based-dx)
450
+ // ==========================================================================
451
+
452
+ describe('turn-based prompt injection', () => {
453
+ beforeEach(() => {
454
+ resetAllMocks();
455
+ });
456
+
457
+ it('includes turn-based instructions in system prompt when OMNI_INSTANCE is set', async () => {
458
+ const env = { OMNI_INSTANCE: 'inst-wb', OMNI_CHAT: 'chat-wb', OMNI_AGENT: 'bot' };
459
+ const session = await executor.spawn('test-agent', 'chat-wb', env);
460
+
461
+ await executor.deliver(session, {
462
+ content: 'Hello from WhatsApp',
463
+ sender: 'Alice',
464
+ instanceId: 'inst-wb',
465
+ chatId: 'chat-wb',
466
+ agent: 'test-agent',
467
+ });
468
+ await executor.waitForDeliveries(session.id);
469
+
470
+ expect(queryMock).toHaveBeenCalledTimes(1);
471
+ const callArgs = (queryMock.mock.calls[0] as unknown as [{ options?: { systemPrompt?: string } }])[0];
472
+ const systemPrompt = callArgs.options?.systemPrompt ?? '';
473
+
474
+ // Verify turn-based prompt content
475
+ expect(systemPrompt).toContain('WhatsApp');
476
+ expect(systemPrompt).toContain('Alice');
477
+ expect(systemPrompt).toContain('omni say');
478
+ expect(systemPrompt).toContain('omni done');
479
+ expect(systemPrompt).toContain('inst-wb');
480
+ });
481
+
482
+ it('does NOT include turn-based instructions when OMNI_INSTANCE is absent', async () => {
483
+ const session = await executor.spawn('test-agent', 'chat-plain', {});
484
+
485
+ await executor.deliver(session, {
486
+ content: 'Hello from CLI',
487
+ sender: 'bob',
488
+ instanceId: 'inst-1',
489
+ chatId: 'chat-plain',
490
+ agent: 'test-agent',
491
+ });
492
+ await executor.waitForDeliveries(session.id);
493
+
494
+ expect(queryMock).toHaveBeenCalledTimes(1);
495
+ const callArgs = (queryMock.mock.calls[0] as unknown as [{ options?: { systemPrompt?: string } }])[0];
496
+ const systemPrompt = callArgs.options?.systemPrompt ?? '';
497
+
498
+ expect(systemPrompt).not.toContain('WhatsApp');
499
+ expect(systemPrompt).not.toContain('omni say');
500
+ expect(systemPrompt).not.toContain('omni done');
501
+ });
502
+ });
447
503
  });
@@ -9,6 +9,7 @@ import { resolvePermissionConfig } from '../../lib/providers/claude-sdk-permissi
9
9
  import { ClaudeSdkProvider } from '../../lib/providers/claude-sdk.js';
10
10
  import type { IExecutor, NatsPublishFn, OmniMessage, OmniSession, SafePgCallFn } from '../executor.js';
11
11
  import { endSession, recordTurn, startSession, updateTurnCount } from './sdk-session-capture.js';
12
+ import { buildTurnBasedPrompt } from './turn-based-prompt.js';
12
13
 
13
14
  // ============================================================================
14
15
  // Types
@@ -40,6 +41,28 @@ async function loadSystemPrompt(entry: directory.DirectoryEntry): Promise<string
40
41
  }
41
42
  }
42
43
 
44
+ /**
45
+ * Resolve the system prompt for a delivery, optionally prepending turn-based
46
+ * WhatsApp instructions when the session is running via the Omni bridge.
47
+ *
48
+ * Returns `{ prompt, isTurnBased }` so the caller can decide whether to
49
+ * send the prompt on resume deliveries.
50
+ */
51
+ async function resolveSystemPrompt(
52
+ entry: directory.DirectoryEntry,
53
+ state: SdkSessionState,
54
+ message: OmniMessage,
55
+ chatId: string,
56
+ ): Promise<{ prompt: string | undefined; isTurnBased: boolean }> {
57
+ let prompt = await loadSystemPrompt(entry);
58
+ const isTurnBased = Boolean(state.env.OMNI_INSTANCE);
59
+ if (isTurnBased) {
60
+ const turnPrompt = buildTurnBasedPrompt(message.sender, state.env.OMNI_INSTANCE, state.env.OMNI_CHAT ?? chatId);
61
+ prompt = prompt ? `${turnPrompt}\n\n${prompt}` : turnPrompt;
62
+ }
63
+ return { prompt, isTurnBased };
64
+ }
65
+
43
66
  interface QueryResult {
44
67
  text: string;
45
68
  sessionId?: string;
@@ -313,7 +336,7 @@ export class ClaudeSdkOmniExecutor implements IExecutor {
313
336
 
314
337
  const entry = resolved.entry;
315
338
  const permissionConfig = resolvePermissionConfig(entry.permissions);
316
- const systemPrompt = await loadSystemPrompt(entry);
339
+ const { prompt: systemPrompt, isTurnBased } = await resolveSystemPrompt(entry, state, message, session.chatId);
317
340
 
318
341
  if (state.executorId) await this.updateState(state.executorId, 'working', session.chatId);
319
342
 
@@ -350,7 +373,7 @@ export class ClaudeSdkOmniExecutor implements IExecutor {
350
373
  role: session.agentName,
351
374
  cwd: entry.dir || process.cwd(),
352
375
  model: entry.model,
353
- systemPrompt: state.claudeSessionId ? undefined : systemPrompt,
376
+ systemPrompt: state.claudeSessionId && !isTurnBased ? undefined : systemPrompt,
354
377
  },
355
378
  queryContent,
356
379
  permissionConfig,
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Turn-based WhatsApp system prompt for agents spawned via the Omni bridge.
3
+ *
4
+ * Injected on every delivery when OMNI_INSTANCE is present in the executor env.
5
+ * Teaches the agent how to reply via omni CLI verbs and close the turn.
6
+ */
7
+
8
+ export function buildTurnBasedPrompt(senderName: string, instanceId: string, chatId: string): string {
9
+ return `
10
+ # WhatsApp Turn-Based Conversation
11
+
12
+ You are responding to a WhatsApp message from ${senderName}.
13
+ Your context is pre-set (instance: ${instanceId}, chat: ${chatId}) — do NOT use \`omni use\` or \`omni open\`.
14
+
15
+ ## Available Commands
16
+
17
+ - omni say 'text' — reply with a text message
18
+ - omni speak 'text' — reply with a voice note
19
+ - omni imagine 'prompt' — generate and send an image
20
+ - omni react 'emoji' --message <id> — react to a message
21
+ - omni history — see recent messages for context
22
+ - omni done — end your turn (REQUIRED as the last action)
23
+
24
+ ## Rules
25
+
26
+ 1. Use \`omni say\` to send your response. You can send multiple messages.
27
+ 2. Use \`omni history\` to see recent messages if you need context.
28
+ 3. ALWAYS call \`omni done\` as your LAST action to close the turn.
29
+ 4. Do NOT generate bare text as your reply — it will go nowhere. Use omni say or omni done.
30
+ `.trim();
31
+ }
@@ -1,9 +1,9 @@
1
1
  /**
2
- * genie brain — delegate to @automagik/genie-brain (enterprise).
2
+ * genie brain — delegate to @khal-os/brain (enterprise).
3
3
  *
4
- * Brain installs directly from the private GitHub repo.
4
+ * Brain installs from a release tarball on the private GitHub repo.
5
5
  * Only people with repo access can install = enterprise license.
6
- * Source code stays in git, never published to npm.
6
+ * Published to GitHub Packages + release tarballs, never to npmjs.
7
7
  *
8
8
  * Brain is NEVER a hard dependency. genie works exactly the same
9
9
  * without it. Zero behavior change for OSS users.
@@ -15,8 +15,8 @@ import { homedir } from 'node:os';
15
15
  import { dirname, join, resolve } from 'node:path';
16
16
  import type { Command } from 'commander';
17
17
 
18
- const BRAIN_PKG = '@automagik/genie-brain';
19
- const BRAIN_REPO = 'github:automagik-dev/genie-brain';
18
+ const BRAIN_PKG = '@khal-os/brain';
19
+ const BRAIN_REPO = 'khal-os/brain';
20
20
 
21
21
  /** Resolve genie's package root — works from both src/ (dev) and dist/ (compiled). */
22
22
  function resolveGenieRoot(): string {
@@ -36,7 +36,7 @@ function resolveGenieRoot(): string {
36
36
  return resolve(import.meta.dir, '..', '..');
37
37
  }
38
38
 
39
- const BRAIN_DIR = join(resolveGenieRoot(), 'node_modules', '@automagik', 'genie-brain');
39
+ const BRAIN_DIR = join(resolveGenieRoot(), 'node_modules', '@khal-os', 'brain');
40
40
  const CACHE_PATH = join(homedir(), '.genie', 'brain-version-check.json');
41
41
 
42
42
  /** Compare dot-separated version strings numerically (e.g., "260403.9" vs "260403.10"). */
@@ -87,20 +87,15 @@ export function checkForUpdates(cachePath?: string): UpdateCheck {
87
87
 
88
88
  function refreshVersionCache(localVersion?: string): void {
89
89
  try {
90
- if (!existsSync(join(BRAIN_DIR, '.git'))) return;
91
-
92
90
  // Resolve local version from param or package.json
93
91
  const version = localVersion ?? readLocalBrainVersion();
94
92
  if (!version) return;
95
93
 
96
- // Fetch latest tags from remote
97
- execSync(`git -C "${BRAIN_DIR}" fetch origin --tags`, { stdio: 'pipe' });
98
-
99
- // Find latest v0.* tag via version sort
100
- const tagsOutput = execSync(`git -C "${BRAIN_DIR}" tag -l "v0.*" --sort=-version:refname`, {
94
+ // Query latest release from GitHub via gh CLI (works for private repos)
95
+ const latestTag = execSync(`gh release view --repo ${BRAIN_REPO} --json tagName -q .tagName`, {
96
+ stdio: 'pipe',
101
97
  encoding: 'utf-8',
102
- });
103
- const latestTag = tagsOutput.trim().split('\n')[0] ?? '';
98
+ }).trim();
104
99
  const latestVersion = latestTag.replace(/^v/, '');
105
100
 
106
101
  // Compare: strip prefix digit for comparison (dev uses 1.x, main uses 0.x)
@@ -133,40 +128,55 @@ function refreshVersionCache(localVersion?: string): void {
133
128
  // ── Update brain from GitHub ───────────────────────────────────────────────
134
129
 
135
130
  async function updateBrain(): Promise<boolean> {
136
- // Check brain is installed (has .git dir from clone)
137
- if (!existsSync(join(BRAIN_DIR, '.git'))) {
131
+ // Check brain is installed (has package.json from tarball extract)
132
+ if (!existsSync(join(BRAIN_DIR, 'package.json'))) {
138
133
  console.log(' Brain is not installed. Run: genie brain install');
139
134
  return false;
140
135
  }
141
136
 
142
- // Get old version before pull
143
- let oldVersion = 'unknown';
137
+ // Get old version before update
138
+ const oldVersion = readLocalBrainVersion() ?? 'unknown';
139
+
140
+ console.log(' Checking for updates...');
141
+
142
+ // Query latest release from GitHub
143
+ let tag: string;
144
144
  try {
145
- const brain = await import(BRAIN_PKG);
146
- oldVersion = brain.getVersion?.() ?? 'unknown';
145
+ tag = execSync(`gh release view --repo ${BRAIN_REPO} --json tagName -q .tagName`, {
146
+ stdio: 'pipe',
147
+ encoding: 'utf-8',
148
+ }).trim();
147
149
  } catch {
148
- /* ok */
150
+ console.error(' Failed to check latest release. Ensure: gh auth login');
151
+ return false;
149
152
  }
150
153
 
151
- console.log(' Updating brain from GitHub...');
154
+ const newVersion = tag.replace(/^v/, '');
155
+ if (compareVersions(newVersion, oldVersion) <= 0) {
156
+ console.log(` Already at latest version (${oldVersion}).`);
157
+ return true;
158
+ }
152
159
 
153
- // Ensure we're on main before pulling — if the clone is on dev,
154
- // `git pull origin main` merges main INTO dev, keeping the dev version.
155
- execSync(`git -C "${BRAIN_DIR}" checkout main`, { stdio: 'pipe' });
156
- execSync(`git -C "${BRAIN_DIR}" pull origin main`, { stdio: 'inherit' });
160
+ console.log(` Upgrading: ${oldVersion} ${newVersion}`);
161
+ console.log('');
157
162
 
158
- // Rebuild
159
- execSync('bun install', { cwd: BRAIN_DIR, stdio: 'inherit' });
160
- execSync('bun run build', { cwd: BRAIN_DIR, stdio: 'inherit' });
163
+ // Download and extract new tarball (same flow as install)
164
+ const tmpDir = join(homedir(), '.cache', 'genie-brain');
165
+ mkdirSync(tmpDir, { recursive: true });
161
166
 
162
- // Get new version (read from package.json since module cache won't refresh)
163
- let newVersion = 'unknown';
164
- try {
165
- const pkg = JSON.parse(readFileSync(join(BRAIN_DIR, 'package.json'), 'utf-8'));
166
- newVersion = pkg.version ?? 'unknown';
167
- } catch {
168
- /* ok */
169
- }
167
+ execSync(`gh release download ${tag} --repo ${BRAIN_REPO} --pattern '*.tgz' --dir "${tmpDir}" --clobber`, {
168
+ stdio: 'inherit',
169
+ });
170
+
171
+ // Replace existing install
172
+ execSync(`rm -rf "${BRAIN_DIR}"`, { stdio: 'pipe' });
173
+ mkdirSync(BRAIN_DIR, { recursive: true });
174
+ execSync(`tar xzf "${tmpDir}/khal-os-brain-${newVersion}.tgz" -C "${BRAIN_DIR}" --strip-components=1`, {
175
+ stdio: 'inherit',
176
+ });
177
+
178
+ // Install runtime deps
179
+ execSync('bun install', { cwd: BRAIN_DIR, stdio: 'inherit' });
170
180
 
171
181
  console.log(`\n Updated: ${oldVersion} → ${newVersion}`);
172
182
 
@@ -219,13 +229,13 @@ async function showVersion(): Promise<void> {
219
229
 
220
230
  // ── Install brain ──────────────────────────────────────────────────────────
221
231
 
222
- /** Install brain package directly from GitHub repo */
232
+ /** Install brain package from GitHub release tarball */
223
233
  async function installBrain(): Promise<boolean> {
224
234
  console.log('');
225
- console.log(' Installing genie-brain from GitHub (enterprise)...');
235
+ console.log(' Installing brain from GitHub release (enterprise)...');
226
236
  console.log('');
227
- console.log(' Source: https://github.com/automagik-dev/genie-brain');
228
- console.log(' Requires: GitHub org membership (automagik-dev)');
237
+ console.log(' Source: https://github.com/khal-os/brain');
238
+ console.log(' Requires: GitHub org membership (khal-os)');
229
239
  console.log('');
230
240
 
231
241
  try {
@@ -237,19 +247,41 @@ async function installBrain(): Promise<boolean> {
237
247
  return false;
238
248
  }
239
249
 
240
- // Clone brain repo using gh CLI (handles private repos without exposing tokens in process list)
241
- execSync(`rm -rf "${BRAIN_DIR}"`, { stdio: 'pipe' });
242
- execSync(`mkdir -p "${dirname(BRAIN_DIR)}"`, { stdio: 'pipe' });
243
- execSync(`gh repo clone automagik-dev/genie-brain "${BRAIN_DIR}" -- --depth 1`, {
250
+ // Resolve latest release tag and download tarball via gh (handles private repo auth)
251
+ const tag = execSync(`gh release view --repo ${BRAIN_REPO} --json tagName -q .tagName`, {
252
+ stdio: 'pipe',
253
+ encoding: 'utf-8',
254
+ }).trim();
255
+ const version = tag.replace(/^v/, '');
256
+
257
+ console.log(` Latest release: ${tag}`);
258
+ console.log('');
259
+
260
+ // Download tarball via gh (handles private repo auth) and extract to node_modules.
261
+ // We bypass `bun add` because .npmrc scope config causes bun to verify against
262
+ // GitHub Packages registry even for local tarballs, triggering 401 on machines
263
+ // without registry tokens.
264
+ const root = resolveGenieRoot();
265
+ const brainDir = join(root, 'node_modules', '@khal-os', 'brain');
266
+ const tmpDir = join(homedir(), '.cache', 'genie-brain');
267
+ mkdirSync(tmpDir, { recursive: true });
268
+
269
+ execSync(`gh release download ${tag} --repo ${BRAIN_REPO} --pattern '*.tgz' --dir "${tmpDir}" --clobber`, {
244
270
  stdio: 'inherit',
245
271
  });
246
272
 
247
- // Install brain's deps + build
248
- execSync('bun install', { cwd: BRAIN_DIR, stdio: 'inherit' });
249
- execSync('bun run build', { cwd: BRAIN_DIR, stdio: 'inherit' });
273
+ // Extract tarball npm tarballs contain a `package/` prefix
274
+ execSync(`rm -rf "${brainDir}"`, { stdio: 'pipe' });
275
+ mkdirSync(brainDir, { recursive: true });
276
+ execSync(`tar xzf "${tmpDir}/khal-os-brain-${version}.tgz" -C "${brainDir}" --strip-components=1`, {
277
+ stdio: 'inherit',
278
+ });
279
+
280
+ // Install brain's runtime deps (postgres, pgserve, etc.)
281
+ execSync('bun install', { cwd: brainDir, stdio: 'inherit' });
250
282
 
251
283
  console.log('');
252
- console.log(' Brain installed from GitHub.');
284
+ console.log(` Brain ${version} installed from GitHub release.`);
253
285
  console.log('');
254
286
 
255
287
  // Auto-run migrations
@@ -276,17 +308,19 @@ async function installBrain(): Promise<boolean> {
276
308
  console.error(' Access denied. Brain is enterprise-only.');
277
309
  console.log('');
278
310
  console.log(' You need:');
279
- console.log(' 1. Membership in the automagik-dev GitHub org');
280
- console.log(' 2. SSH key or GH token configured for git');
311
+ console.log(' 1. Membership in the khal-os GitHub org');
312
+ console.log(' 2. GitHub CLI authenticated: gh auth login');
281
313
  console.log('');
282
314
  console.log(' Manual install:');
283
- console.log(` bun add ${BRAIN_REPO}`);
315
+ console.log(` gh release download --repo ${BRAIN_REPO} --pattern '*.tgz'`);
316
+ console.log(' tar xzf khal-os-brain-*.tgz -C node_modules/@khal-os/brain --strip-components=1');
284
317
  console.log('');
285
318
  } else {
286
319
  console.error(` Install failed: ${msg}`);
287
320
  console.log('');
288
321
  console.log(' Manual install:');
289
- console.log(` bun add ${BRAIN_REPO}`);
322
+ console.log(` gh release download --repo ${BRAIN_REPO} --pattern '*.tgz'`);
323
+ console.log(' tar xzf khal-os-brain-*.tgz -C node_modules/@khal-os/brain --strip-components=1');
290
324
  console.log('');
291
325
  }
292
326
  return false;
@@ -295,10 +329,14 @@ async function installBrain(): Promise<boolean> {
295
329
 
296
330
  function uninstallBrain(): void {
297
331
  try {
332
+ if (!existsSync(BRAIN_DIR)) {
333
+ console.log(' Brain is not installed.');
334
+ return;
335
+ }
298
336
  execSync(`rm -rf "${BRAIN_DIR}"`, { stdio: 'pipe' });
299
337
  console.log(' Brain uninstalled.');
300
338
  } catch {
301
- console.error(' Uninstall failed. Manual: rm -rf node_modules/@automagik/genie-brain');
339
+ console.error(` Uninstall failed. Manual: rm -rf ${BRAIN_DIR}`);
302
340
  }
303
341
  }
304
342
 
@@ -315,7 +353,7 @@ function printNotInstalledMessage(): void {
315
353
  console.log('');
316
354
  console.log(' genie brain install');
317
355
  console.log('');
318
- console.log(' Requires GitHub org membership (automagik-dev).');
356
+ console.log(' Requires GitHub org membership (khal-os).');
319
357
  console.log('');
320
358
  }
321
359