@automagik/genie 4.260328.4 → 4.260328.6

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.260328.4",
13
+ "version": "4.260328.6",
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
  }
@@ -9,4 +9,4 @@
9
9
 
10
10
  ## Expect
11
11
  - [ ] follow stream contains event kind=message peer=reviewer
12
- - [ ] follow stream contains event source=mailbox text~=review PR
12
+ - [ ] follow stream contains event source=send text~=review PR
package/dist/genie.js CHANGED
@@ -1487,7 +1487,7 @@ ${updated}/${gitItems.length} items updated successfully.`);return}if(!nameOrVer
1487
1487
  SELECT * FROM team_chat
1488
1488
  WHERE team = ${teamName} AND repo_path = ${repoPath}
1489
1489
  ORDER BY created_at ASC
1490
- `).map(rowToMessage2)}function isSystemNoise(text){let trimmed=text.trimStart();return trimmed.startsWith("<command-name>")||trimmed.startsWith("<command-message>")||trimmed.startsWith("Base directory for this skill:")||trimmed.startsWith("<system-reminder>")||trimmed.startsWith("<local-command")}function transcriptToLogEvent(entry,agent,team){let kindMap={user:"user",assistant:"assistant",system:"system",tool_call:"tool_call",tool_result:"tool_result"},text=entry.text.trim();if(isSystemNoise(text))return null;if(!text)return null;return{timestamp:entry.timestamp,kind:kindMap[entry.role]??"assistant",agent,team,text,data:{role:entry.role,...entry.toolCall?{toolCall:entry.toolCall}:{},...entry.model?{model:entry.model}:{},...entry.usage?{usage:entry.usage}:{}},source:"provider"}}function inboxMessageToLogEvent(msg,agent,team){return{timestamp:msg.createdAt,kind:"message",agent,team,direction:"in",peer:msg.from,text:msg.body,data:{messageId:msg.id,from:msg.from,to:msg.to,read:msg.read},source:"mailbox"}}function outboxMessageToLogEvent(msg,agent,team){return{timestamp:msg.createdAt,kind:"message",agent,team,direction:"out",peer:msg.to,text:msg.body,data:{messageId:msg.id,from:msg.from,to:msg.to},source:"mailbox"}}function chatMessageToLogEvent(msg,team){return{timestamp:msg.timestamp,kind:"message",agent:msg.sender,team,text:msg.body,data:{chatId:msg.id,sender:msg.sender},source:"chat"}}function applyLogFilter(events,filter){if(!filter)return events;let result=events;if(filter.since){let sinceMs=new Date(filter.since).getTime();result=result.filter((e)=>new Date(e.timestamp).getTime()>=sinceMs)}if(filter.kinds&&filter.kinds.length>0){let kinds=new Set(filter.kinds);result=result.filter((e)=>kinds.has(e.kind))}if(filter.last&&filter.last>0)result=result.slice(-filter.last);return result}function sortByTimestamp(events){return events.sort((a,b2)=>new Date(a.timestamp).getTime()-new Date(b2.timestamp).getTime())}async function readAgentLog(agent,repoPath,filter){let{id:agentName,team}=agent,[transcriptEntries,inboxMessages,outboxMessages,chatMessages]=await Promise.all([readTranscriptSafe(agent),inbox(repoPath,agentName),readOutbox(repoPath,agentName),team?readMessages(repoPath,team):Promise.resolve([])]),events=[];for(let entry of transcriptEntries){let event=transcriptToLogEvent(entry,agentName,team);if(event)events.push(event)}for(let msg of inboxMessages)events.push(inboxMessageToLogEvent(msg,agentName,team));for(let msg of outboxMessages)events.push(outboxMessageToLogEvent(msg,agentName,team));if(team)for(let msg of chatMessages)events.push(chatMessageToLogEvent(msg,team));let sorted=sortByTimestamp(events);return applyLogFilter(sorted,filter)}async function readTeamLog(agents,repoPath,teamName,filter){let chatEvents=(await readMessages(repoPath,teamName)).map((msg)=>chatMessageToLogEvent(msg,teamName)),perAgentEvents=await Promise.all(agents.map(async(agent)=>{let agentName=agent.id,[transcriptEntries,inboxMessages,outboxMessages]=await Promise.all([readTranscriptSafe(agent),inbox(repoPath,agentName),readOutbox(repoPath,agentName)]),events=[];for(let entry of transcriptEntries){let event=transcriptToLogEvent(entry,agentName,teamName);if(event)events.push(event)}for(let msg of inboxMessages)events.push(inboxMessageToLogEvent(msg,agentName,teamName));for(let msg of outboxMessages)events.push(outboxMessageToLogEvent(msg,agentName,teamName));return events})),allEvents=[...chatEvents,...perAgentEvents.flat()],sorted=sortByTimestamp(allEvents);return applyLogFilter(sorted,filter)}async function followAgentLog(agent,repoPath,filter,onEvent){return startNatsFollow([agent],repoPath,agent.team,filter,onEvent)}async function followTeamLog(agents,repoPath,teamName,filter,onEvent){return startNatsFollow(agents,repoPath,teamName,filter,onEvent)}async function startNatsFollow(_agents,_repoPath,_team,filter,onEvent){let nats=await Promise.resolve().then(() => (init_nats_client(),exports_nats_client));if(!await nats.isAvailable())throw Error("NATS is not available. Install nats package and ensure NATS server is running.");let kindsFilter=filter?.kinds?new Set(filter.kinds):null,subs=[],seenKeys=new Set,eventKey=(e)=>`${e.timestamp}|${e.kind}|${e.agent}|${e.text.slice(0,80)}`,handleNatsEvent=(_subject,data)=>{let event=data;if(!event?.timestamp||!event?.kind)return;if(kindsFilter&&!kindsFilter.has(event.kind))return;let key=eventKey(event);if(seenKeys.has(key))return;seenKeys.add(key),onEvent(event)},allSub=await nats.subscribe("genie.>",handleNatsEvent);return subs.push(allSub),{mode:"nats",stop:async()=>{for(let sub of subs)sub.unsubscribe()}}}async function readTranscriptSafe(agent){try{let{readTranscript:readTranscript2}=await Promise.resolve().then(() => exports_transcript);return await readTranscript2(agent)}catch{return[]}}function kindIcon(kind){switch(kind){case"user":return"U";case"assistant":return"A";case"message":return"M";case"state":return"S";case"tool_call":return"C";case"tool_result":return"R";case"system":return"*";default:return"?"}}function kindColor(kind){switch(kind){case"user":return"\x1B[33m";case"assistant":return"\x1B[36m";case"message":return"\x1B[35m";case"state":return"\x1B[35m";case"tool_call":return"\x1B[32m";case"tool_result":return"\x1B[90m";case"system":return"\x1B[34m";default:return"\x1B[0m"}}var RESET="\x1B[0m",DIM="\x1B[90m",BOLD="\x1B[1m";function summarizeToolCall2(event){let tc=event.data?.toolCall;if(!tc)return event.text;let input=tc.input;switch(tc.name){case"Read":case"Edit":case"Write":return`${tc.name} ${input.file_path??""}`;case"Bash":return`$ ${String(input.command??"").split(`
1490
+ `).map(rowToMessage2)}function isSystemNoise(text){let trimmed=text.trimStart();return trimmed.startsWith("<command-name>")||trimmed.startsWith("<command-message>")||trimmed.startsWith("Base directory for this skill:")||trimmed.startsWith("<system-reminder>")||trimmed.startsWith("<local-command")}function transcriptToLogEvent(entry,agent,team){let kindMap={user:"user",assistant:"assistant",system:"system",tool_call:"tool_call",tool_result:"tool_result"},text=entry.text.trim();if(isSystemNoise(text))return null;if(!text)return null;return{timestamp:entry.timestamp,kind:kindMap[entry.role]??"assistant",agent,team,text,data:{role:entry.role,...entry.toolCall?{toolCall:entry.toolCall}:{},...entry.model?{model:entry.model}:{},...entry.usage?{usage:entry.usage}:{}},source:"provider"}}function inboxMessageToLogEvent(msg,agent,team){return{timestamp:msg.createdAt,kind:"message",agent,team,direction:"in",peer:msg.from,text:msg.body,data:{messageId:msg.id,from:msg.from,to:msg.to,read:msg.read},source:"mailbox"}}function outboxMessageToLogEvent(msg,agent,team){return{timestamp:msg.createdAt,kind:"message",agent,team,direction:"out",peer:msg.to,text:msg.body,data:{messageId:msg.id,from:msg.from,to:msg.to},source:"mailbox"}}function chatMessageToLogEvent(msg,team){return{timestamp:msg.timestamp,kind:"message",agent:msg.sender,team,text:msg.body,data:{chatId:msg.id,sender:msg.sender},source:"chat"}}function applyLogFilter(events,filter){if(!filter)return events;let result=events;if(filter.since){let sinceMs=new Date(filter.since).getTime();result=result.filter((e)=>new Date(e.timestamp).getTime()>=sinceMs)}if(filter.kinds&&filter.kinds.length>0){let kinds=new Set(filter.kinds);result=result.filter((e)=>kinds.has(e.kind))}if(filter.last&&filter.last>0)result=result.slice(-filter.last);return result}function sortByTimestamp(events){return events.sort((a,b2)=>new Date(a.timestamp).getTime()-new Date(b2.timestamp).getTime())}async function readPgMessages(agentName){try{let ts2=await Promise.resolve().then(() => (init_task_service(),exports_task_service)),actor={actorType:"local",actorId:agentName},conversations=await ts2.listConversations(actor),events=[];for(let conv of conversations){let messages2=await ts2.getMessages(conv.id,{limit:100}),peerMember=(await ts2.getMembers(conv.id)).find((m)=>m.actorId!==agentName);for(let msg of messages2){let isOutgoing=msg.senderId===agentName,peer=isOutgoing?peerMember?.actorId:msg.senderId;events.push({timestamp:msg.createdAt,kind:"message",agent:agentName,direction:isOutgoing?"out":"in",peer,text:msg.body,data:{messageId:msg.id,conversationId:msg.conversationId,senderId:msg.senderId},source:"send"})}}return events}catch{return[]}}async function readAgentLog(agent,repoPath,filter){let{id:agentName,team}=agent,[transcriptEntries,inboxMessages,outboxMessages,chatMessages,pgMessages]=await Promise.all([readTranscriptSafe(agent),inbox(repoPath,agentName),readOutbox(repoPath,agentName),team?readMessages(repoPath,team):Promise.resolve([]),readPgMessages(agentName)]),events=[...pgMessages];for(let entry of transcriptEntries){let event=transcriptToLogEvent(entry,agentName,team);if(event)events.push(event)}for(let msg of inboxMessages)events.push(inboxMessageToLogEvent(msg,agentName,team));for(let msg of outboxMessages)events.push(outboxMessageToLogEvent(msg,agentName,team));if(team)for(let msg of chatMessages)events.push(chatMessageToLogEvent(msg,team));let sorted=sortByTimestamp(events);return applyLogFilter(sorted,filter)}async function readTeamLog(agents,repoPath,teamName,filter){let chatEvents=(await readMessages(repoPath,teamName)).map((msg)=>chatMessageToLogEvent(msg,teamName)),perAgentEvents=await Promise.all(agents.map(async(agent)=>{let agentName=agent.id,[transcriptEntries,inboxMessages,outboxMessages,pgMessages]=await Promise.all([readTranscriptSafe(agent),inbox(repoPath,agentName),readOutbox(repoPath,agentName),readPgMessages(agentName)]),events=[...pgMessages];for(let entry of transcriptEntries){let event=transcriptToLogEvent(entry,agentName,teamName);if(event)events.push(event)}for(let msg of inboxMessages)events.push(inboxMessageToLogEvent(msg,agentName,teamName));for(let msg of outboxMessages)events.push(outboxMessageToLogEvent(msg,agentName,teamName));return events})),allEvents=[...chatEvents,...perAgentEvents.flat()],sorted=sortByTimestamp(allEvents);return applyLogFilter(sorted,filter)}async function followAgentLog(agent,repoPath,filter,onEvent){return startNatsFollow([agent],repoPath,agent.team,filter,onEvent)}async function followTeamLog(agents,repoPath,teamName,filter,onEvent){return startNatsFollow(agents,repoPath,teamName,filter,onEvent)}async function startNatsFollow(_agents,_repoPath,_team,filter,onEvent){let nats=await Promise.resolve().then(() => (init_nats_client(),exports_nats_client));if(!await nats.isAvailable())throw Error("NATS is not available. Install nats package and ensure NATS server is running.");let kindsFilter=filter?.kinds?new Set(filter.kinds):null,subs=[],seenKeys=new Set,eventKey=(e)=>`${e.timestamp}|${e.kind}|${e.agent}|${e.text.slice(0,80)}`,handleNatsEvent=(_subject,data)=>{let event=data;if(!event?.timestamp||!event?.kind)return;if(kindsFilter&&!kindsFilter.has(event.kind))return;let key=eventKey(event);if(seenKeys.has(key))return;seenKeys.add(key),onEvent(event)},allSub=await nats.subscribe("genie.>",handleNatsEvent);return subs.push(allSub),{mode:"nats",stop:async()=>{for(let sub of subs)sub.unsubscribe()}}}async function readTranscriptSafe(agent){try{let{readTranscript:readTranscript2}=await Promise.resolve().then(() => exports_transcript);return await readTranscript2(agent)}catch{return[]}}function kindIcon(kind){switch(kind){case"user":return"U";case"assistant":return"A";case"message":return"M";case"state":return"S";case"tool_call":return"C";case"tool_result":return"R";case"system":return"*";default:return"?"}}function kindColor(kind){switch(kind){case"user":return"\x1B[33m";case"assistant":return"\x1B[36m";case"message":return"\x1B[35m";case"state":return"\x1B[35m";case"tool_call":return"\x1B[32m";case"tool_result":return"\x1B[90m";case"system":return"\x1B[34m";default:return"\x1B[0m"}}var RESET="\x1B[0m",DIM="\x1B[90m",BOLD="\x1B[1m";function summarizeToolCall2(event){let tc=event.data?.toolCall;if(!tc)return event.text;let input=tc.input;switch(tc.name){case"Read":case"Edit":case"Write":return`${tc.name} ${input.file_path??""}`;case"Bash":return`$ ${String(input.command??"").split(`
1491
1491
  `)[0]}`;case"Grep":return`Grep "${input.pattern}" ${input.path??""}`;case"Glob":return`Glob ${input.pattern}`;case"Agent":return`Agent: ${input.description??""}`;case"SendMessage":return`SendMessage \u2192 ${input.to}: ${String(input.message??"").slice(0,80)}`;case"shell":case"exec_command":return`$ ${(Array.isArray(input.command)?input.command.join(" "):String(input.command??"")).split(`
1492
1492
  `)[0]}`;case"web_search":return`Search: ${input.query??""}`;default:return`${tc.name}`}}function formatEventBlock(event){let time=formatTime(event.timestamp,{seconds:!0,fallback:"??:??:??"}),icon=kindIcon(event.kind),color=kindColor(event.kind),agent=event.agent;if(event.direction==="in")agent=`${event.peer} \u2192 ${event.agent}`;else if(event.direction==="out")agent=`${event.agent} \u2192 ${event.peer}`;let header=`${DIM}${time}${RESET} ${color}[${icon}]${RESET} ${BOLD}${agent}${RESET}`;if(event.kind==="tool_call"){let summary=summarizeToolCall2(event);return`${header} ${DIM}${summary}${RESET}`}if(event.kind==="tool_result"){let line=event.text.split(`
1493
1493
  `)[0].slice(0,120);return`${header} ${DIM}${line}${RESET}`}let text=event.text.trim();if(text.length<80&&!text.includes(`
@@ -1497,12 +1497,12 @@ ${updated}/${gitItems.length} items updated successfully.`);return}if(!nameOrVer
1497
1497
  `);return`${header}
1498
1498
  ${indented}`}function formatHumanOutput(events,label){let lines=[];if(lines.push(""),lines.push(`${BOLD}Log: ${label}${RESET} (${events.length} events)`),lines.push(""),events.length===0)return lines.push(" No events found."),lines.push(""),lines.join(`
1499
1499
  `);let lastKind=null;for(let event of events){if(lastKind!==null&&!(lastKind==="tool_call"&&event.kind==="tool_call"))lines.push("");lines.push(formatEventBlock(event)),lastKind=event.kind}return lines.push(""),lines.join(`
1500
- `)}async function findAgent(identifier){let agent=await get(identifier);if(agent)return agent;if(agent=await findByTask(identifier),agent)return agent;return(await list()).find((w)=>w.id.includes(identifier)||w.taskId?.includes(identifier)||w.taskTitle?.toLowerCase().includes(identifier.toLowerCase()))??null}async function findTeamAgents(teamName){return(await list()).filter((a)=>a.team===teamName)}function buildFilter2(options){let filter={},hasFilter=!1;if(options.last&&options.last>0)filter.last=options.last,hasFilter=!0;if(options.since)filter.since=options.since,hasFilter=!0;if(options.type)filter.kinds=[options.type],hasFilter=!0;return hasFilter?filter:void 0}function outputNdjson(events){for(let event of events)process.stdout.write(`${JSON.stringify(event)}
1500
+ `)}async function findAgent(identifier,teamName){let agent=await get(identifier);if(agent)return agent;if(agent=await findByTask(identifier),agent)return agent;let all=await list(),teamMatch=(teamName?all.filter((a)=>a.team===teamName):[]).find((w)=>w.id.includes(identifier)||w.taskId?.includes(identifier)||w.taskTitle?.toLowerCase().includes(identifier.toLowerCase()));if(teamMatch)return teamMatch;return all.find((w)=>w.id.includes(identifier)||w.taskId?.includes(identifier)||w.taskTitle?.toLowerCase().includes(identifier.toLowerCase()))??null}async function findTeamAgents(teamName){return(await list()).filter((a)=>a.team===teamName)}function buildFilter2(options){let filter={},hasFilter=!1;if(options.last&&options.last>0)filter.last=options.last,hasFilter=!0;if(options.since)filter.since=options.since,hasFilter=!0;if(options.type)filter.kinds=[options.type],hasFilter=!0;return hasFilter?filter:void 0}function outputNdjson(events){for(let event of events)process.stdout.write(`${JSON.stringify(event)}
1501
1501
  `)}function outputJson(events){process.stdout.write(`${JSON.stringify(events,null,2)}
1502
- `)}async function logCommand(agentName,options){let repoPath=process.cwd(),filter=buildFilter2(options);if(options.follow){await followCommand(agentName,options,repoPath,filter);return}let events,label;if(options.team){let agents=await findTeamAgents(options.team);if(agents.length===0)console.error(`No agents found for team "${options.team}".`),process.exit(1);events=await readTeamLog(agents,repoPath,options.team,filter),label=`team:${options.team} (${agents.length} agents)`}else if(agentName){let agent=await findAgent(agentName);if(!agent)console.error(`Agent "${agentName}" not found. Run \`genie ls\` to see agents.`),process.exit(1);events=await readAgentLog(agent,repoPath,filter),label=agent.id}else{let allAgents=await list();if(allAgents.length===0)console.error("No agents found. Run `genie ls` to see agents."),process.exit(1);events=await readTeamLog(allAgents,repoPath,"all",filter),label=`all agents (${allAgents.length})`}if(options.ndjson){outputNdjson(events);return}if(options.json){outputJson(events);return}console.log(formatHumanOutput(events,label))}async function followCommand(agentName,options,repoPath,filter){let lastFollowKind=null,outputEvent=(event)=>{if(options.ndjson)process.stdout.write(`${JSON.stringify(event)}
1502
+ `)}async function logCommand(agentName,options){let repoPath=process.cwd(),filter=buildFilter2(options);if(options.follow){await followCommand(agentName,options,repoPath,filter);return}let events,label;if(options.team){let agents=await findTeamAgents(options.team);if(agents.length===0)console.error(`No agents found for team "${options.team}".`),process.exit(1);events=await readTeamLog(agents,repoPath,options.team,filter),label=`team:${options.team} (${agents.length} agents)`}else if(agentName){let agent=await findAgent(agentName,options.team);if(!agent)console.error(`Agent "${agentName}" not found. Run \`genie ls\` to see agents.`),process.exit(1);events=await readAgentLog(agent,repoPath,filter),label=agent.id}else{let allAgents=await list();if(allAgents.length===0)console.error("No agents found. Run `genie ls` to see agents."),process.exit(1);events=await readTeamLog(allAgents,repoPath,"all",filter),label=`all agents (${allAgents.length})`}if(options.ndjson){outputNdjson(events);return}if(options.json){outputJson(events);return}console.log(formatHumanOutput(events,label))}async function followCommand(agentName,options,repoPath,filter){let lastFollowKind=null,outputEvent=(event)=>{if(options.ndjson)process.stdout.write(`${JSON.stringify(event)}
1503
1503
  `);else{if(lastFollowKind!==null&&!(lastFollowKind==="tool_call"&&event.kind==="tool_call"))process.stdout.write(`
1504
1504
  `);process.stdout.write(`${formatEventBlock(event)}
1505
- `),lastFollowKind=event.kind}},label;if(options.team){let agents=await findTeamAgents(options.team);if(agents.length===0)console.error(`No agents found for team "${options.team}".`),process.exit(1);label=`team:${options.team} (${agents.length} agents)`;let handle=await followTeamLog(agents,repoPath,options.team,filter,outputEvent);console.error(`Following ${label} via ${handle.mode==="nats"?"NATS":"file polling"} (Ctrl+C to stop)...`),setupShutdown(handle.stop)}else if(agentName){let agent=await findAgent(agentName);if(!agent)console.error(`Agent "${agentName}" not found. Run \`genie ls\` to see agents.`),process.exit(1);label=agent.id;let handle=await followAgentLog(agent,repoPath,filter,outputEvent);console.error(`Following ${label} via ${handle.mode==="nats"?"NATS":"file polling"} (Ctrl+C to stop)...`),setupShutdown(handle.stop)}else{let allAgents=await list();if(allAgents.length===0)console.error("No agents found. Run `genie ls` to see agents."),process.exit(1);label=`all agents (${allAgents.length})`;let handle=await followTeamLog(allAgents,repoPath,"all",filter,outputEvent);console.error(`Following ${label} via ${handle.mode==="nats"?"NATS":"file polling"} (Ctrl+C to stop)...`),setupShutdown(handle.stop)}await new Promise(()=>{})}function setupShutdown(stop){let shutdown2=async()=>{await stop(),process.exit(0)};process.on("SIGINT",shutdown2),process.on("SIGTERM",shutdown2)}init_db();function parseSince2(since){let match=since.match(/^(\d+)([smhd])$/);if(!match)return since;let amount=Number.parseInt(match[1],10),unit=match[2],ms={s:1000,m:60000,h:3600000,d:86400000}[unit]??3600000;return new Date(Date.now()-amount*ms).toISOString()}async function metricsNowCommand(options){if(!await isAvailable())console.error("Database not available."),process.exit(1);let sql=await getConnection(),snapshots=await sql`SELECT * FROM machine_snapshots ORDER BY created_at DESC LIMIT 1`,agentCount=await sql`SELECT count(*)::int as cnt FROM agents WHERE state NOT IN ('done', 'error', 'suspended')`,teamCount=await sql`SELECT count(*)::int as cnt FROM teams WHERE status = 'in_progress'`,snapshot=snapshots[0]??{},data={active_workers:snapshot.active_workers??agentCount[0]?.cnt??0,active_teams:snapshot.active_teams??teamCount[0]?.cnt??0,tmux_sessions:snapshot.tmux_sessions??0,cpu_percent:snapshot.cpu_percent??null,memory_mb:snapshot.memory_mb??null,snapshot_at:snapshot.created_at?new Date(snapshot.created_at).toISOString():null};if(options.json){console.log(JSON.stringify(data,null,2));return}if(console.log("Machine State:"),console.log(` Workers: ${data.active_workers}`),console.log(` Teams: ${data.active_teams}`),console.log(` Tmux: ${data.tmux_sessions} sessions`),data.cpu_percent!==null)console.log(` CPU: ${data.cpu_percent}%`);if(data.memory_mb!==null)console.log(` Memory: ${data.memory_mb} MB`);if(data.snapshot_at)console.log(` As of: ${formatRelativeTimestamp(data.snapshot_at)}`)}async function metricsHistoryCommand(options){if(!await isAvailable())console.error("Database not available."),process.exit(1);let sql=await getConnection(),sinceTs=parseSince2(options.since??"1h"),rows=await sql`
1505
+ `),lastFollowKind=event.kind}},label;if(options.team){let agents=await findTeamAgents(options.team);if(agents.length===0)console.error(`No agents found for team "${options.team}".`),process.exit(1);label=`team:${options.team} (${agents.length} agents)`;let handle=await followTeamLog(agents,repoPath,options.team,filter,outputEvent);console.error(`Following ${label} via ${handle.mode==="nats"?"NATS":"file polling"} (Ctrl+C to stop)...`),setupShutdown(handle.stop)}else if(agentName){let agent=await findAgent(agentName,options.team);if(!agent)console.error(`Agent "${agentName}" not found. Run \`genie ls\` to see agents.`),process.exit(1);label=agent.id;let handle=await followAgentLog(agent,repoPath,filter,outputEvent);console.error(`Following ${label} via ${handle.mode==="nats"?"NATS":"file polling"} (Ctrl+C to stop)...`),setupShutdown(handle.stop)}else{let allAgents=await list();if(allAgents.length===0)console.error("No agents found. Run `genie ls` to see agents."),process.exit(1);label=`all agents (${allAgents.length})`;let handle=await followTeamLog(allAgents,repoPath,"all",filter,outputEvent);console.error(`Following ${label} via ${handle.mode==="nats"?"NATS":"file polling"} (Ctrl+C to stop)...`),setupShutdown(handle.stop)}await new Promise(()=>{})}function setupShutdown(stop){let shutdown2=async()=>{await stop(),process.exit(0)};process.on("SIGINT",shutdown2),process.on("SIGTERM",shutdown2)}init_db();function parseSince2(since){let match=since.match(/^(\d+)([smhd])$/);if(!match)return since;let amount=Number.parseInt(match[1],10),unit=match[2],ms={s:1000,m:60000,h:3600000,d:86400000}[unit]??3600000;return new Date(Date.now()-amount*ms).toISOString()}async function metricsNowCommand(options){if(!await isAvailable())console.error("Database not available."),process.exit(1);let sql=await getConnection(),snapshots=await sql`SELECT * FROM machine_snapshots ORDER BY created_at DESC LIMIT 1`,agentCount=await sql`SELECT count(*)::int as cnt FROM agents WHERE state NOT IN ('done', 'error', 'suspended')`,teamCount=await sql`SELECT count(*)::int as cnt FROM teams WHERE status = 'in_progress'`,snapshot=snapshots[0]??{},data={active_workers:snapshot.active_workers??agentCount[0]?.cnt??0,active_teams:snapshot.active_teams??teamCount[0]?.cnt??0,tmux_sessions:snapshot.tmux_sessions??0,cpu_percent:snapshot.cpu_percent??null,memory_mb:snapshot.memory_mb??null,snapshot_at:snapshot.created_at?new Date(snapshot.created_at).toISOString():null};if(options.json){console.log(JSON.stringify(data,null,2));return}if(console.log("Machine State:"),console.log(` Workers: ${data.active_workers}`),console.log(` Teams: ${data.active_teams}`),console.log(` Tmux: ${data.tmux_sessions} sessions`),data.cpu_percent!==null)console.log(` CPU: ${data.cpu_percent}%`);if(data.memory_mb!==null)console.log(` Memory: ${data.memory_mb} MB`);if(data.snapshot_at)console.log(` As of: ${formatRelativeTimestamp(data.snapshot_at)}`)}async function metricsHistoryCommand(options){if(!await isAvailable())console.error("Database not available."),process.exit(1);let sql=await getConnection(),sinceTs=parseSince2(options.since??"1h"),rows=await sql`
1506
1506
  SELECT active_workers, active_teams, tmux_sessions, cpu_percent, memory_mb, created_at
1507
1507
  FROM machine_snapshots
1508
1508
  WHERE created_at >= ${sinceTs}::timestamptz
@@ -1515,7 +1515,7 @@ ${indented}`}function formatHumanOutput(events,label){let lines=[];if(lines.push
1515
1515
  ORDER BY worker_id, created_at DESC
1516
1516
  `;if(options.json){console.log(JSON.stringify(rows,null,2));return}if(rows.length===0){console.log("No heartbeats found.");return}let headers=["Worker","Status","Last Seen","Team","Wish"],data=rows.map((r)=>{let ctx=typeof r.context==="string"?JSON.parse(r.context):r.context??{};return[String(r.worker_id??"-").slice(0,20),r.status??"-",formatRelativeTimestamp(r.last_seen_at),ctx.team??"-",ctx.wish_slug??"-"]}),widths=headers.map((h,i2)=>{let colVals=data.map((row)=>row[i2]);return Math.min(25,Math.max(h.length,...colVals.map((v)=>v.length)))});console.log(headers.map((h,i2)=>padRight(h,widths[i2])).join(" | ")),console.log(widths.map((w)=>"-".repeat(w)).join("-+-"));for(let row of data)console.log(row.map((v,i2)=>padRight(v.slice(0,widths[i2]),widths[i2])).join(" | "));console.log(`
1517
1517
  (${rows.length} worker${rows.length===1?"":"s"})`)}function registerMetricsCommands(program2){let metrics=program2.command("metrics").description("Machine metrics \u2014 snapshots, heartbeats, agents");metrics.command("now",{isDefault:!0}).description("Current machine state").option("--json","Output as JSON").action(async(options)=>{await metricsNowCommand(options)}),metrics.command("history").description("Machine snapshot history").option("--since <duration>","Time window (e.g., 1h, 6h, 1d)","1h").option("--json","Output as JSON").action(async(options)=>{await metricsHistoryCommand(options)}),metrics.command("agents").description("Per-agent heartbeat summary").option("--json","Output as JSON").action(async(options)=>{await metricsAgentsCommand(options)})}import{readFile as readFile9}from"fs/promises";import{homedir as homedir25}from"os";import{join as join38}from"path";var _registry;async function getRegistry(){if(!_registry)_registry=await Promise.resolve().then(() => (init_agent_registry(),exports_agent_registry));return _registry}var _taskService2;async function getTaskService2(){if(!_taskService2)_taskService2=await Promise.resolve().then(() => (init_task_service(),exports_task_service));return _taskService2}var _teamManager;async function getTeamManager(){if(!_teamManager)_teamManager=await Promise.resolve().then(() => (init_team_manager(),exports_team_manager));return _teamManager}async function detectSenderIdentity(teamName){let envName=process.env.GENIE_AGENT_NAME;if(envName)return envName;let paneId=process.env.TMUX_PANE;if(!paneId)return"cli";let registry=await getRegistry(),worker=typeof registry.findByPane==="function"?await registry.findByPane(paneId):null;if(worker)return worker.role??worker.id;let resolvedTeam=teamName??process.env.GENIE_TEAM;if(resolvedTeam){let memberName=await findMemberByPane(resolvedTeam,paneId);if(memberName)return memberName}return"cli"}async function findMemberByPane(teamName,paneId){let configDir=process.env.CLAUDE_CONFIG_DIR??join38(homedir25(),".claude"),sanitized=teamName.replace(/[^a-zA-Z0-9]/g,"-").toLowerCase(),cfgPath=join38(configDir,"teams",sanitized,"config.json");try{let raw=await readFile9(cfgPath,"utf-8");return(JSON.parse(raw).members??[]).find((m)=>m.tmuxPaneId===paneId)?.name??null}catch{return null}}async function checkSendScope(_repoPath,sender,recipient){if(sender==="cli")return null;let teams=await(await getTeamManager()).listTeams(),senderTeams=resolveSenderTeams(teams,sender);if(senderTeams.length===0)return null;for(let team of senderTeams)if(isRecipientInTeam(team,recipient))return null;let teamNames=senderTeams.map((t)=>t.name).join(", ");return`Scope violation: "${recipient}" is not in sender's team(s): ${teamNames}`}function resolveSenderTeams(teams,sender){let senderTeams=teams.filter((t)=>t.members.includes(sender));if(sender==="team-lead"){let envTeam=process.env.GENIE_TEAM;if(envTeam){let leaderTeam=teams.find((t)=>t.name===envTeam);if(leaderTeam&&!senderTeams.some((t)=>t.name===leaderTeam.name))senderTeams=[...senderTeams,leaderTeam]}}return senderTeams}function isRecipientInTeam(team,recipient){if(team.members.includes(recipient)||recipient==="team-lead")return!0;if(recipient.startsWith(`${team.name}-`)){let roleOnly=recipient.slice(team.name.length+1);if(team.members.includes(roleOnly))return!0}return!1}async function findAgentTeam(_repoPath,agentName){let teams=await(await getTeamManager()).listTeams(),memberTeam=teams.find((t)=>t.members.includes(agentName));if(memberTeam)return memberTeam;if(agentName==="team-lead"){let envTeam=process.env.GENIE_TEAM;if(envTeam)return teams.find((t)=>t.name===envTeam)??null}return null}function localActor(name){return{actorType:"local",actorId:name}}async function resolveTeamName(explicit,repoPath,from){if(explicit)return explicit;let name=(await findAgentTeam(repoPath,from))?.name??process.env.GENIE_TEAM;if(!name)console.error("Error: Could not auto-detect team. Use --team <name>."),process.exit(1);return name}async function handleInbox(agent,options){let ts2=await getTaskService2(),resolvedAgent=agent??await detectSenderIdentity(),actor=localActor(resolvedAgent),conversations=await ts2.listConversations(actor);if(options.json){console.log(JSON.stringify(conversations,null,2));return}if(conversations.length===0){console.log(`No conversations for "${resolvedAgent}".`);return}console.log(""),console.log(`INBOX: ${resolvedAgent}`),console.log("\u2500".repeat(60));for(let conv of conversations)await printConversationSummary(ts2,conv)}async function printConversationSummary(ts2,conv){let messages2=await ts2.getMessages(conv.id,{limit:1}),lastMsg=messages2.length>0?messages2[messages2.length-1]:null,name=conv.name??conv.id,type2=conv.type==="dm"?"DM":"Group",linked=conv.linkedEntity?` [${conv.linkedEntity}:${conv.linkedEntityId}]`:"",preview=lastMsg?truncate(lastMsg.body,50):"(no messages)",time=lastMsg?formatTime(lastMsg.createdAt):"";if(console.log(` ${padRight(name,30)} ${padRight(type2,6)}${linked}`),lastMsg)console.log(` ${time} ${lastMsg.senderId}: ${preview}`);console.log("")}async function handleChatThread(messageId,options){let ts2=await getTaskService2(),from=options.from??await detectSenderIdentity(),actor=localActor(from),parentMsgId=Number(messageId),parentMsg=await ts2.getMessage(parentMsgId);if(!parentMsg)console.error(`Error: Message not found: ${messageId}`),process.exit(1);let conv=await ts2.findOrCreateConversation({type:"group",name:options.name??`Thread on message #${parentMsgId}`,parentMessageId:parentMsgId,createdBy:actor,members:[actor]});console.log(`Thread created: ${conv.id}`),console.log(` Parent message: #${parentMsgId} in ${parentMsg.conversationId}`),console.log(` Name: ${conv.name??"(unnamed)"}`)}function printConversationTable(conversations){console.log(` ${padRight("ID",20)} ${padRight("NAME",25)} ${padRight("TYPE",8)} ${padRight("LINKED",20)} UPDATED`),console.log(` ${"\u2500".repeat(80)}`);for(let c of conversations){let name=truncate(c.name??"(unnamed)",23),linked=c.linkedEntity?`${c.linkedEntity}:${c.linkedEntityId}`:"-",updated=formatTime(c.updatedAt);console.log(` ${padRight(c.id,20)} ${padRight(name,25)} ${padRight(c.type,8)} ${padRight(linked,20)} ${updated}`)}console.log(`
1518
- ${conversations.length} conversation${conversations.length===1?"":"s"}`)}async function handleChatList(options){let ts2=await getTaskService2(),from=options.from??await detectSenderIdentity(),actor=localActor(from),conversations=await ts2.listConversations(actor);if(options.type)conversations=conversations.filter((c)=>c.type===options.type);if(options.linked)conversations=conversations.filter((c)=>c.linkedEntity===options.linked);if(options.json){console.log(JSON.stringify(conversations,null,2));return}if(conversations.length===0){console.log("No conversations found.");return}printConversationTable(conversations)}async function handleChatRead(conversationId,options){let ts2=await getTaskService2(),conv=await ts2.getConversation(conversationId);if(!conv)console.error(`Error: Conversation not found: ${conversationId}`),process.exit(1);let messages2=await ts2.getMessages(conversationId,{since:options.since,limit:Number(options.limit)||50});if(options.json){console.log(JSON.stringify(messages2,null,2));return}let name=conv.name??conversationId;if(messages2.length===0){console.log(`No messages in "${name}".`);return}console.log(""),console.log(`CHAT: ${name}`),console.log("\u2500".repeat(60));for(let msg of messages2){let time=formatTime(msg.createdAt),reply=msg.replyToId?` (reply to #${msg.replyToId})`:"";console.log(` [${time}] ${msg.senderId}: ${msg.body}${reply}`)}console.log("")}async function discoverCurrentTeam(nativeTeams,from){let discovered=await nativeTeams.discoverTeamName().catch(()=>null);if(discovered)return discovered;return(await(await getRegistry()).list()).find((w)=>w.role===from||w.id===from||w.customName===from)?.team??null}async function deliverToTeam(nativeTeams,team,recipient,msg){let nativeName=await nativeTeams.resolveNativeMemberName(team,recipient);if(!nativeName)return!1;return await nativeTeams.writeNativeInbox(team,nativeName,msg),!0}async function bridgeToNativeInbox(from,recipient,body){if(from===recipient)return;let nativeTeams=await Promise.resolve().then(() => (init_claude_native_teams(),exports_claude_native_teams)),nativeMsg={from,text:body,summary:body.length>50?`${body.substring(0,50)}...`:body,timestamp:new Date().toISOString(),color:"blue",read:!1},currentTeam=await discoverCurrentTeam(nativeTeams,from);if(currentTeam&&await deliverToTeam(nativeTeams,currentTeam,recipient,nativeMsg))return;let allTeams=await nativeTeams.listTeams().catch(()=>[]);for(let team of allTeams){if(team===currentTeam)continue;if(await deliverToTeam(nativeTeams,team,recipient,nativeMsg))return}console.warn(`[genie send] Native inbox bridge: could not find native team member for "${recipient}"`)}async function handleSend(body,options){let ts2=await getTaskService2(),repoPath=process.cwd(),from=options.from??await detectSenderIdentity(),scopeError=await checkSendScope(repoPath,from,options.to);if(scopeError)console.error(`Error: ${scopeError}`),process.exit(1);let senderActor=localActor(from),recipientActor=localActor(options.to),conv=await ts2.findOrCreateConversation({type:"dm",members:[senderActor,recipientActor],createdBy:senderActor});await ts2.addMember(conv.id,senderActor),await ts2.addMember(conv.id,recipientActor);let msg=await ts2.sendMessage(conv.id,senderActor,body);await bridgeToNativeInbox(from,options.to,body).catch((err)=>{let reason=err instanceof Error?err.message:String(err);console.warn(`[genie send] Native inbox bridge failed: ${reason}`)}),console.log(`Message sent to "${options.to}".`),console.log(` ID: ${msg.id}`),console.log(` Conversation: ${conv.id}`)}function registerSendInboxCommands(program2){program2.command("send <body>").description("Send a direct message to an agent (PG-backed)").option("--to <agent>","Recipient agent name (default: team-lead)","team-lead").option("--from <sender>","Sender ID (auto-detected from context)").action(async(body,options)=>{try{await handleSend(body,options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),program2.command("broadcast <body>").description("Send a message to your team conversation (PG-backed)").option("--from <sender>","Sender ID (auto-detected from context)").option("--team <name>","Team name (auto-detected from context)").action(async(body,options)=>{try{let ts2=await getTaskService2(),repoPath=process.cwd(),from=options.from??await detectSenderIdentity(),teamName=await resolveTeamName(options.team,repoPath,from),senderActor=localActor(from),conv=await ts2.findOrCreateConversation({type:"group",name:`Team: ${teamName}`,linkedEntity:"team",linkedEntityId:teamName,createdBy:senderActor,members:[senderActor]});await ts2.addMember(conv.id,senderActor);let msg=await ts2.sendMessage(conv.id,senderActor,body);console.log(`Broadcast sent to team "${teamName}".`),console.log(` Message ID: ${msg.id}`),console.log(` Conversation: ${conv.id}`)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}});let inbox2=program2.command("inbox").description("Inbox management \u2014 list messages or watch for new ones");inbox2.command("list [agent]",{isDefault:!0}).description("List conversations with recent messages (PG-backed)").option("--json","Output as JSON").action(async(agent,options)=>{try{await handleInbox(agent,options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),inbox2.command("watch").description("Run inbox watcher in foreground (Ctrl+C to stop)").action(async()=>{let{checkInboxes:checkInboxes2,getInboxPollIntervalMs:getInboxPollIntervalMs2,startInboxWatcher:startInboxWatcher2,stopInboxWatcher:stopInboxWatcher2}=await Promise.resolve().then(() => (init_inbox_watcher(),exports_inbox_watcher)),pollMs=getInboxPollIntervalMs2();if(pollMs===0)console.log("Inbox watcher is disabled (GENIE_INBOX_POLL_MS=0)"),process.exit(0);console.log(`Inbox watcher starting (poll every ${pollMs/1000}s)`),console.log(`Press Ctrl+C to stop.
1518
+ ${conversations.length} conversation${conversations.length===1?"":"s"}`)}async function handleChatList(options){let ts2=await getTaskService2(),from=options.from??await detectSenderIdentity(),actor=localActor(from),conversations=await ts2.listConversations(actor);if(options.type)conversations=conversations.filter((c)=>c.type===options.type);if(options.linked)conversations=conversations.filter((c)=>c.linkedEntity===options.linked);if(options.json){console.log(JSON.stringify(conversations,null,2));return}if(conversations.length===0){console.log("No conversations found.");return}printConversationTable(conversations)}async function handleChatRead(conversationId,options){let ts2=await getTaskService2(),conv=await ts2.getConversation(conversationId);if(!conv)console.error(`Error: Conversation not found: ${conversationId}`),process.exit(1);let messages2=await ts2.getMessages(conversationId,{since:options.since,limit:Number(options.limit)||50});if(options.json){console.log(JSON.stringify(messages2,null,2));return}let name=conv.name??conversationId;if(messages2.length===0){console.log(`No messages in "${name}".`);return}console.log(""),console.log(`CHAT: ${name}`),console.log("\u2500".repeat(60));for(let msg of messages2){let time=formatTime(msg.createdAt),reply=msg.replyToId?` (reply to #${msg.replyToId})`:"";console.log(` [${time}] ${msg.senderId}: ${msg.body}${reply}`)}console.log("")}async function discoverCurrentTeam(nativeTeams,from){let discovered=await nativeTeams.discoverTeamName().catch(()=>null);if(discovered)return discovered;return(await(await getRegistry()).list()).find((w)=>w.role===from||w.id===from||w.customName===from)?.team??null}async function deliverToTeam(nativeTeams,team,recipient,msg){let nativeName=await nativeTeams.resolveNativeMemberName(team,recipient);if(!nativeName)return!1;return await nativeTeams.writeNativeInbox(team,nativeName,msg),!0}async function bridgeToNativeInbox(from,recipient,body){if(from===recipient)return;let nativeTeams=await Promise.resolve().then(() => (init_claude_native_teams(),exports_claude_native_teams)),nativeMsg={from,text:body,summary:body.length>50?`${body.substring(0,50)}...`:body,timestamp:new Date().toISOString(),color:"blue",read:!1},currentTeam=await discoverCurrentTeam(nativeTeams,from);if(currentTeam&&await deliverToTeam(nativeTeams,currentTeam,recipient,nativeMsg))return;let allTeams=await nativeTeams.listTeams().catch(()=>[]);for(let team of allTeams){if(team===currentTeam)continue;if(await deliverToTeam(nativeTeams,team,recipient,nativeMsg))return}console.warn(`[genie send] Native inbox bridge: could not find native team member for "${recipient}"`)}async function handleSend(body,options){let ts2=await getTaskService2(),repoPath=process.cwd(),from=options.from??await detectSenderIdentity(),scopeError=await checkSendScope(repoPath,from,options.to);if(scopeError)console.error(`Error: ${scopeError}`),process.exit(1);let senderActor=localActor(from),recipientActor=localActor(options.to),conv=await ts2.findOrCreateConversation({type:"dm",members:[senderActor,recipientActor],createdBy:senderActor});await ts2.addMember(conv.id,senderActor),await ts2.addMember(conv.id,recipientActor);let msg=await ts2.sendMessage(conv.id,senderActor,body);try{let{publish:publish2}=await Promise.resolve().then(() => (init_nats_client(),exports_nats_client));await publish2(`genie.msg.${options.to}`,{timestamp:new Date().toISOString(),kind:"message",agent:from,direction:"out",peer:options.to,text:body,data:{messageId:msg.id,conversationId:conv.id,from,to:options.to},source:"send"})}catch{}await bridgeToNativeInbox(from,options.to,body).catch((err)=>{let reason=err instanceof Error?err.message:String(err);console.warn(`[genie send] Native inbox bridge failed: ${reason}`)}),console.log(`Message sent to "${options.to}".`),console.log(` ID: ${msg.id}`),console.log(` Conversation: ${conv.id}`)}function registerSendInboxCommands(program2){program2.command("send <body>").description("Send a direct message to an agent (PG-backed)").option("--to <agent>","Recipient agent name (default: team-lead)","team-lead").option("--from <sender>","Sender ID (auto-detected from context)").action(async(body,options)=>{try{await handleSend(body,options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),program2.command("broadcast <body>").description("Send a message to your team conversation (PG-backed)").option("--from <sender>","Sender ID (auto-detected from context)").option("--team <name>","Team name (auto-detected from context)").action(async(body,options)=>{try{let ts2=await getTaskService2(),repoPath=process.cwd(),from=options.from??await detectSenderIdentity(),teamName=await resolveTeamName(options.team,repoPath,from),senderActor=localActor(from),conv=await ts2.findOrCreateConversation({type:"group",name:`Team: ${teamName}`,linkedEntity:"team",linkedEntityId:teamName,createdBy:senderActor,members:[senderActor]});await ts2.addMember(conv.id,senderActor);let msg=await ts2.sendMessage(conv.id,senderActor,body);try{let{publish:publish2}=await Promise.resolve().then(() => (init_nats_client(),exports_nats_client));await publish2("genie.msg.broadcast",{timestamp:new Date().toISOString(),kind:"message",agent:from,direction:"out",peer:teamName,text:body,data:{messageId:msg.id,conversationId:conv.id,from,team:teamName},source:"send"})}catch{}console.log(`Broadcast sent to team "${teamName}".`),console.log(` Message ID: ${msg.id}`),console.log(` Conversation: ${conv.id}`)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}});let inbox2=program2.command("inbox").description("Inbox management \u2014 list messages or watch for new ones");inbox2.command("list [agent]",{isDefault:!0}).description("List conversations with recent messages (PG-backed)").option("--json","Output as JSON").action(async(agent,options)=>{try{await handleInbox(agent,options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),inbox2.command("watch").description("Run inbox watcher in foreground (Ctrl+C to stop)").action(async()=>{let{checkInboxes:checkInboxes2,getInboxPollIntervalMs:getInboxPollIntervalMs2,startInboxWatcher:startInboxWatcher2,stopInboxWatcher:stopInboxWatcher2}=await Promise.resolve().then(() => (init_inbox_watcher(),exports_inbox_watcher)),pollMs=getInboxPollIntervalMs2();if(pollMs===0)console.log("Inbox watcher is disabled (GENIE_INBOX_POLL_MS=0)"),process.exit(0);console.log(`Inbox watcher starting (poll every ${pollMs/1000}s)`),console.log(`Press Ctrl+C to stop.
1519
1519
  `);let initial=await checkInboxes2();if(initial.length>0)console.log(`[inbox-watcher] Spawned team-leads for: ${initial.join(", ")}`);let handle=startInboxWatcher2({listTeamsWithUnreadInbox:(await Promise.resolve().then(() => (init_claude_native_teams(),exports_claude_native_teams))).listTeamsWithUnreadInbox,isTeamActive:async(teamName)=>{let{isTeamActive:isTeamActive2}=await Promise.resolve().then(() => (init_team_auto_spawn(),exports_team_auto_spawn));return isTeamActive2(teamName)},ensureTeamLead:async(teamName,workingDir)=>{let{ensureTeamLead:ensureTeamLead2}=await Promise.resolve().then(() => (init_team_auto_spawn(),exports_team_auto_spawn)),result=await ensureTeamLead2(teamName,workingDir);return console.log(`[inbox-watcher] Spawned team-lead for "${teamName}" in ${workingDir}`),result},warn:(msg)=>console.log(msg)}),shutdown2=()=>{console.log(`
1520
1520
  Stopping inbox watcher...`),stopInboxWatcher2(handle),process.exit(0)};process.on("SIGINT",shutdown2),process.on("SIGTERM",shutdown2),await new Promise(()=>{})});let chat=program2.command("chat").description("Conversation management (PG-backed)");chat.command("send <conversationId> <message>").description("Send a message to a specific conversation").option("--reply-to <msgId>","Reply to a specific message ID").option("--from <sender>","Sender ID (auto-detected)").action(async(conversationId,message,options)=>{try{let ts2=await getTaskService2(),from=options.from??await detectSenderIdentity(),actor=localActor(from),replyTo=options.replyTo?Number(options.replyTo):void 0,msg=await ts2.sendMessage(conversationId,actor,message,replyTo);console.log(`Message #${msg.id} sent to conversation ${conversationId}.`)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),chat.command("thread <messageId>").description("Create a threaded sub-conversation from a message").option("--name <name>","Thread name").option("--from <sender>","Sender ID (auto-detected)").action(async(messageId,options)=>{try{await handleChatThread(messageId,options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),chat.command("list").description("List conversations with filters").option("--type <type>","Filter by type: dm, group").option("--linked <entity>","Filter by linked entity: task, team").option("--json","Output as JSON").option("--from <sender>","Actor ID (auto-detected)").action(async(options)=>{try{await handleChatList(options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),chat.command("read <conversationId>").description("Read messages in a conversation").option("--since <timestamp>","Show messages since timestamp").option("--limit <n>","Limit number of messages","50").option("--json","Output as JSON").action(async(conversationId,options)=>{try{await handleChatRead(conversationId,options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}})}var _taskService3;async function getTaskService3(){if(!_taskService3)_taskService3=await Promise.resolve().then(() => (init_task_service(),exports_task_service));return _taskService3}function currentActor(){return{actorType:"local",actorId:process.env.GENIE_AGENT_NAME??"cli"}}async function handleNotifySet(options){let ts2=await getTaskService3(),actor=currentActor(),pref=await ts2.setPreference(actor,options.channel,{priorityThreshold:options.priority,isDefault:options.default}),defaultLabel=pref.isDefault?", default":"";console.log(`Notification preference set: ${pref.channel} (threshold: ${pref.priorityThreshold}${defaultLabel}).`)}function printPrefsTable(prefs){console.log(` ${padRight("CHANNEL",15)} ${padRight("THRESHOLD",12)} ${padRight("DEFAULT",10)} ENABLED`),console.log(` ${"\u2500".repeat(45)}`);for(let p of prefs){let dflt=p.isDefault?"yes":"no",enabled=p.enabled?"yes":"no";console.log(` ${padRight(p.channel,15)} ${padRight(p.priorityThreshold,12)} ${padRight(dflt,10)} ${enabled}`)}console.log(`
1521
1521
  ${prefs.length} preference${prefs.length===1?"":"s"}`)}async function handleNotifyList(options){let ts2=await getTaskService3(),actor=currentActor(),prefs=await ts2.getPreferences(actor);if(options.json){console.log(JSON.stringify(prefs,null,2));return}if(prefs.length===0){console.log("No notification preferences configured.");return}printPrefsTable(prefs)}async function handleNotifyRemove(options){let ts2=await getTaskService3(),actor=currentActor();if(await ts2.deletePreference(actor,options.channel))console.log(`Removed notification preference for channel: ${options.channel}`);else console.log(`No preference found for channel: ${options.channel}`)}function registerNotifyCommands(program2){let notify=program2.command("notify").description("Notification preference management");notify.command("set").description("Set notification preference for a channel").requiredOption("--channel <channel>","Channel: whatsapp, telegram, email, slack, discord, tmux").option("--priority <priority>","Minimum priority threshold","normal").option("--default","Set as default channel").action(async(options)=>{try{await handleNotifySet(options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),notify.command("list").description("List notification preferences").option("--json","Output as JSON").action(async(options)=>{try{await handleNotifyList(options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}}),notify.command("remove").description("Remove a notification preference").requiredOption("--channel <channel>","Channel to remove").action(async(options)=>{try{await handleNotifyRemove(options)}catch(error2){console.error(`Error: ${error2 instanceof Error?error2.message:String(error2)}`),process.exit(1)}})}init_tmux();function debug(msg){if(process.env.DEBUG)console.error(`[target-resolver] ${msg}`)}async function defaultTmuxLookup(sessionName,windowName){try{let tmux=await Promise.resolve().then(() => (init_tmux(),exports_tmux)),session=await tmux.findSessionByName(sessionName);if(!session)return null;let windows=await tmux.listWindows(session.id);if(!windows||windows.length===0)return null;let targetWindow;if(windowName){if(targetWindow=windows.find((w)=>w.name===windowName),!targetWindow)return null}else targetWindow=windows.find((w)=>w.active)||windows[0];let panes=await tmux.listPanes(targetWindow.id);if(!panes||panes.length===0)return null;return{paneId:(panes.find((p)=>p.active)||panes[0]).id,session:sessionName}}catch{return null}}async function defaultIsPaneLive(paneId){try{return(await(await Promise.resolve().then(() => (init_tmux(),exports_tmux))).executeTmux(`display-message -p -t '${paneId}' '#{pane_id}'`)).trim()===paneId}catch{return!1}}async function defaultCleanupDeadPane(workerId,paneId){try{await(await Promise.resolve().then(() => (init_agent_registry(),exports_agent_registry))).removeSubPane(workerId,paneId)}catch{}}async function defaultDeriveSession(paneId){try{return(await(await Promise.resolve().then(() => (init_tmux(),exports_tmux))).executeTmux(`display-message -p -t '${paneId}' '#{session_name}'`)).trim()||null}catch{return null}}async function assertLive(paneId,isPaneLive,errorMsg,cleanup){if(!await isPaneLive(paneId)){if(cleanup)await cleanup();throw Error(errorMsg)}}async function resolveRawPane(target,opts){if(opts.checkLiveness)await assertLive(target,opts.isPaneLive,`Pane ${target} is dead or does not exist. Check with: tmux list-panes -a`);let session=await opts.deriveSession(target);return{paneId:target,session:session??void 0,resolvedVia:"raw"}}async function resolveWindowId(target,workers,opts){let matchingWorker=Object.values(workers).find((w)=>w.windowId===target);if(!matchingWorker)throw Error(`Window "${target}" not found in worker registry.
@@ -2,7 +2,7 @@
2
2
  "id": "genie",
3
3
  "name": "Genie",
4
4
  "description": "Skills, agents, and hooks for the Genie CLI terminal orchestration toolkit",
5
- "version": "4.260328.4",
5
+ "version": "4.260328.6",
6
6
  "configSchema": {
7
7
  "type": "object",
8
8
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automagik/genie",
3
- "version": "4.260328.4",
3
+ "version": "4.260328.6",
4
4
  "description": "Collaborative terminal toolkit for human + AI workflows",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genie",
3
- "version": "4.260328.4",
3
+ "version": "4.260328.6",
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.260328.4",
3
+ "version": "4.260328.6",
4
4
  "private": true,
5
5
  "description": "Runtime dependencies for genie bundled CLIs",
6
6
  "type": "module",
@@ -1,9 +1,9 @@
1
1
  /**
2
- * Unified Log — Canonical event format and file-based aggregator.
2
+ * Unified Log — Canonical event format and aggregator.
3
3
  *
4
- * Aggregates 5 event sources (transcript, mailbox inbox, mailbox outbox,
5
- * team chat, registry state changes) into a single `LogEvent` stream
6
- * sorted by timestamp.
4
+ * Aggregates 6 event sources (transcript, mailbox inbox, mailbox outbox,
5
+ * team chat, PG messages, registry state changes) into a single `LogEvent`
6
+ * stream sorted by timestamp.
7
7
  *
8
8
  * Usage:
9
9
  * const events = await readAgentLog('engineer', repoPath, { last: 50 });
@@ -20,7 +20,7 @@ import type { TranscriptEntry } from './transcript.js';
20
20
  // ============================================================================
21
21
 
22
22
  export type LogEventKind = 'user' | 'assistant' | 'message' | 'state' | 'tool_call' | 'tool_result' | 'system' | 'qa';
23
- export type LogEventSource = 'provider' | 'mailbox' | 'chat' | 'registry' | 'hook';
23
+ export type LogEventSource = 'provider' | 'mailbox' | 'chat' | 'registry' | 'hook' | 'send';
24
24
 
25
25
  export interface LogEvent {
26
26
  /** ISO timestamp */
@@ -175,6 +175,44 @@ export function sortByTimestamp(events: LogEvent[]): LogEvent[] {
175
175
  return events.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
176
176
  }
177
177
 
178
+ // ============================================================================
179
+ // PG Message Source
180
+ // ============================================================================
181
+
182
+ /** Read messages from PG TaskService for a given agent. Returns [] if PG unavailable. */
183
+ async function readPgMessages(agentName: string): Promise<LogEvent[]> {
184
+ try {
185
+ const ts = await import('./task-service.js');
186
+ const actor = { actorType: 'local' as const, actorId: agentName };
187
+ const conversations = await ts.listConversations(actor);
188
+
189
+ const events: LogEvent[] = [];
190
+ for (const conv of conversations) {
191
+ const messages = await ts.getMessages(conv.id, { limit: 100 });
192
+ const members = await ts.getMembers(conv.id);
193
+ const peerMember = members.find((m) => m.actorId !== agentName);
194
+
195
+ for (const msg of messages) {
196
+ const isOutgoing = msg.senderId === agentName;
197
+ const peer = isOutgoing ? peerMember?.actorId : msg.senderId;
198
+ events.push({
199
+ timestamp: msg.createdAt,
200
+ kind: 'message',
201
+ agent: agentName,
202
+ direction: isOutgoing ? 'out' : 'in',
203
+ peer,
204
+ text: msg.body,
205
+ data: { messageId: msg.id, conversationId: msg.conversationId, senderId: msg.senderId },
206
+ source: 'send',
207
+ });
208
+ }
209
+ }
210
+ return events;
211
+ } catch {
212
+ return [];
213
+ }
214
+ }
215
+
178
216
  // ============================================================================
179
217
  // Aggregators
180
218
  // ============================================================================
@@ -188,15 +226,16 @@ export async function readAgentLog(agent: Agent, repoPath: string, filter?: LogF
188
226
  const team = agent.team;
189
227
 
190
228
  // Read all sources in parallel
191
- const [transcriptEntries, inboxMessages, outboxMessages, chatMessages] = await Promise.all([
229
+ const [transcriptEntries, inboxMessages, outboxMessages, chatMessages, pgMessages] = await Promise.all([
192
230
  readTranscriptSafe(agent),
193
231
  inbox(repoPath, agentName),
194
232
  readOutbox(repoPath, agentName),
195
233
  team ? readMessages(repoPath, team) : Promise.resolve([]),
234
+ readPgMessages(agentName),
196
235
  ]);
197
236
 
198
237
  // Convert to LogEvents
199
- const events: LogEvent[] = [];
238
+ const events: LogEvent[] = [...pgMessages];
200
239
 
201
240
  for (const entry of transcriptEntries) {
202
241
  const event = transcriptToLogEvent(entry, agentName, team);
@@ -241,13 +280,14 @@ export async function readTeamLog(
241
280
  agents.map(async (agent) => {
242
281
  const agentName = agent.id;
243
282
 
244
- const [transcriptEntries, inboxMessages, outboxMessages] = await Promise.all([
283
+ const [transcriptEntries, inboxMessages, outboxMessages, pgMessages] = await Promise.all([
245
284
  readTranscriptSafe(agent),
246
285
  inbox(repoPath, agentName),
247
286
  readOutbox(repoPath, agentName),
287
+ readPgMessages(agentName),
248
288
  ]);
249
289
 
250
- const events: LogEvent[] = [];
290
+ const events: LogEvent[] = [...pgMessages];
251
291
  for (const entry of transcriptEntries) {
252
292
  const event = transcriptToLogEvent(entry, agentName, teamName);
253
293
  if (event) events.push(event);
@@ -196,7 +196,7 @@ function formatHumanOutput(events: LogEvent[], label: string): string {
196
196
  // Agent Resolution
197
197
  // ============================================================================
198
198
 
199
- async function findAgent(identifier: string): Promise<agentRegistry.Agent | null> {
199
+ async function findAgent(identifier: string, teamName?: string): Promise<agentRegistry.Agent | null> {
200
200
  let agent = await agentRegistry.get(identifier);
201
201
  if (agent) return agent;
202
202
 
@@ -204,6 +204,17 @@ async function findAgent(identifier: string): Promise<agentRegistry.Agent | null
204
204
  if (agent) return agent;
205
205
 
206
206
  const all = await agentRegistry.list();
207
+
208
+ // Scope fuzzy match to team first, then fall back to global
209
+ const pool = teamName ? all.filter((a) => a.team === teamName) : [];
210
+ const teamMatch = pool.find(
211
+ (w) =>
212
+ w.id.includes(identifier) ||
213
+ w.taskId?.includes(identifier) ||
214
+ w.taskTitle?.toLowerCase().includes(identifier.toLowerCase()),
215
+ );
216
+ if (teamMatch) return teamMatch;
217
+
207
218
  return (
208
219
  all.find(
209
220
  (w) =>
@@ -286,8 +297,8 @@ export async function logCommand(agentName: string | undefined, options: LogOpti
286
297
  events = await readTeamLog(agents, repoPath, options.team, filter);
287
298
  label = `team:${options.team} (${agents.length} agents)`;
288
299
  } else if (agentName) {
289
- // Single agent mode
290
- const agent = await findAgent(agentName);
300
+ // Single agent mode — scope to team if specified
301
+ const agent = await findAgent(agentName, options.team);
291
302
  if (!agent) {
292
303
  console.error(`Agent "${agentName}" not found. Run \`genie ls\` to see agents.`);
293
304
  process.exit(1);
@@ -357,7 +368,7 @@ async function followCommand(
357
368
  console.error(`Following ${label} via ${handle.mode === 'nats' ? 'NATS' : 'file polling'} (Ctrl+C to stop)...`);
358
369
  setupShutdown(handle.stop);
359
370
  } else if (agentName) {
360
- const agent = await findAgent(agentName);
371
+ const agent = await findAgent(agentName, options.team);
361
372
  if (!agent) {
362
373
  console.error(`Agent "${agentName}" not found. Run \`genie ls\` to see agents.`);
363
374
  process.exit(1);
@@ -435,6 +435,23 @@ async function handleSend(body: string, options: { to: string; from?: string }):
435
435
 
436
436
  const msg = await ts.sendMessage(conv.id, senderActor, body);
437
437
 
438
+ // Emit NATS event for real-time observability (fire-and-forget)
439
+ try {
440
+ const { publish } = await import('../lib/nats-client.js');
441
+ await publish(`genie.msg.${options.to}`, {
442
+ timestamp: new Date().toISOString(),
443
+ kind: 'message',
444
+ agent: from,
445
+ direction: 'out',
446
+ peer: options.to,
447
+ text: body,
448
+ data: { messageId: msg.id, conversationId: conv.id, from, to: options.to },
449
+ source: 'send',
450
+ });
451
+ } catch {
452
+ // NATS unavailable — silent degradation
453
+ }
454
+
438
455
  // Best-effort native inbox bridge
439
456
  await bridgeToNativeInbox(from, options.to, body).catch((err) => {
440
457
  const reason = err instanceof Error ? err.message : String(err);
@@ -495,6 +512,24 @@ export function registerSendInboxCommands(program: Command): void {
495
512
  await ts.addMember(conv.id, senderActor);
496
513
 
497
514
  const msg = await ts.sendMessage(conv.id, senderActor, body);
515
+
516
+ // Emit NATS event for real-time observability (fire-and-forget)
517
+ try {
518
+ const { publish } = await import('../lib/nats-client.js');
519
+ await publish('genie.msg.broadcast', {
520
+ timestamp: new Date().toISOString(),
521
+ kind: 'message',
522
+ agent: from,
523
+ direction: 'out',
524
+ peer: teamName,
525
+ text: body,
526
+ data: { messageId: msg.id, conversationId: conv.id, from, team: teamName },
527
+ source: 'send',
528
+ });
529
+ } catch {
530
+ // NATS unavailable — silent degradation
531
+ }
532
+
498
533
  console.log(`Broadcast sent to team "${teamName}".`);
499
534
  console.log(` Message ID: ${msg.id}`);
500
535
  console.log(` Conversation: ${conv.id}`);