@hera-al/server 1.6.12 → 1.6.13

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.
Files changed (61) hide show
  1. package/bundled/a2ui/SKILL.md +339 -0
  2. package/bundled/buongiorno/SKILL.md +151 -0
  3. package/bundled/council/SKILL.md +168 -0
  4. package/bundled/council/scripts/council.mjs +202 -0
  5. package/bundled/dreaming/SKILL.md +177 -0
  6. package/bundled/google-workspace/SKILL.md +229 -0
  7. package/bundled/google-workspace/scripts/auth.sh +87 -0
  8. package/bundled/google-workspace/scripts/calendar.sh +508 -0
  9. package/bundled/google-workspace/scripts/drive.sh +459 -0
  10. package/bundled/google-workspace/scripts/gmail.sh +452 -0
  11. package/bundled/humanizer/SKILL.md +488 -0
  12. package/bundled/librarian/SKILL.md +155 -0
  13. package/bundled/plasma/SKILL.md +1417 -0
  14. package/bundled/sera/SKILL.md +143 -0
  15. package/bundled/the-skill-guardian/SKILL.md +103 -0
  16. package/bundled/the-skill-guardian/scripts/scan.sh +314 -0
  17. package/bundled/unix-time/SKILL.md +58 -0
  18. package/bundled/wandering/SKILL.md +174 -0
  19. package/bundled/xai-search/SKILL.md +91 -0
  20. package/bundled/xai-search/scripts/search.sh +197 -0
  21. package/dist/a2ui/parser.d.ts +76 -0
  22. package/dist/a2ui/parser.js +1 -0
  23. package/dist/a2ui/types.d.ts +147 -0
  24. package/dist/a2ui/types.js +1 -0
  25. package/dist/a2ui/validator.d.ts +32 -0
  26. package/dist/a2ui/validator.js +1 -0
  27. package/dist/agent/agent-service.d.ts +17 -11
  28. package/dist/agent/agent-service.js +1 -1
  29. package/dist/agent/session-agent.d.ts +1 -1
  30. package/dist/agent/session-agent.js +1 -1
  31. package/dist/agent/session-error-handler.js +1 -1
  32. package/dist/commands/debuga2ui.d.ts +13 -0
  33. package/dist/commands/debuga2ui.js +1 -0
  34. package/dist/commands/debugdynamic.d.ts +13 -0
  35. package/dist/commands/debugdynamic.js +1 -0
  36. package/dist/commands/mcp.d.ts +6 -3
  37. package/dist/commands/mcp.js +1 -1
  38. package/dist/gateway/node-registry.d.ts +29 -1
  39. package/dist/gateway/node-registry.js +1 -1
  40. package/dist/installer/hera.js +1 -1
  41. package/dist/memory/concept-store.d.ts +109 -0
  42. package/dist/memory/concept-store.js +1 -0
  43. package/dist/nostromo/nostromo.js +1 -1
  44. package/dist/server.d.ts +3 -2
  45. package/dist/server.js +1 -1
  46. package/dist/tools/a2ui-tools.d.ts +23 -0
  47. package/dist/tools/a2ui-tools.js +1 -0
  48. package/dist/tools/concept-tools.d.ts +3 -0
  49. package/dist/tools/concept-tools.js +1 -0
  50. package/dist/tools/dynamic-ui-tools.d.ts +25 -0
  51. package/dist/tools/dynamic-ui-tools.js +1 -0
  52. package/dist/tools/node-tools.js +1 -1
  53. package/dist/tools/plasma-client-tools.d.ts +28 -0
  54. package/dist/tools/plasma-client-tools.js +1 -0
  55. package/installationPkg/AGENTS.md +168 -22
  56. package/installationPkg/SOUL.md +56 -0
  57. package/installationPkg/TOOLS.md +126 -0
  58. package/installationPkg/USER.md +54 -1
  59. package/installationPkg/config.example.yaml +145 -34
  60. package/installationPkg/default-jobs.json +77 -0
  61. package/package.json +3 -2
@@ -0,0 +1 @@
1
+ export const A2UI_ACTION_KEYS=["beginRendering","surfaceUpdate","dataModelUpdate","deleteSurface"];export const A2UI_COMPONENT_TYPES=["Column","Row","Card","Divider","Tabs","Modal","List","Text","Image","Icon","AudioPlayer","Video","Button","TextField","CheckBox","MultipleChoice","Slider","DateTimeInput"];export function parseA2UIJsonl(e){const n=e.split(/\r?\n/),t=[];for(let e=0;e<n.length;e++){const o=n[e].trim();if(!o)continue;let r;try{r=JSON.parse(o)}catch(n){const t=n instanceof Error?n.message:String(n);throw new Error(`A2UI JSONL line ${e+1}: invalid JSON — ${t}`)}if(!r||"object"!=typeof r||Array.isArray(r))throw new Error(`A2UI JSONL line ${e+1}: expected JSON object, got ${typeof r}`);const i=r,a=A2UI_ACTION_KEYS.filter(e=>e in i);if(0===a.length)throw new Error(`A2UI JSONL line ${e+1}: no action key found. Expected one of: ${A2UI_ACTION_KEYS.join(", ")}`);if(a.length>1)throw new Error(`A2UI JSONL line ${e+1}: multiple action keys found (${a.join(", ")}). Expected exactly one.`);t.push(r)}if(0===t.length)throw new Error("A2UI JSONL: no valid messages found (empty or whitespace only)");return t}export function buildTextSurfaceJsonl(e,n="main"){const t="root",o="text";return[{surfaceUpdate:{surfaceId:n,components:[{id:t,component:{Column:{children:{explicitList:[o]}}}},{id:o,component:{Text:{text:{literalString:e},usageHint:"body"}}}]}},{beginRendering:{surfaceId:n,root:t}}].map(e=>JSON.stringify(e)).join("\n")}export function formatUserActionForAgent(e){const n=e.context&&Object.keys(e.context).length>0?` ${JSON.stringify(e.context)}`:"";return`[A2UI Action] ${e.name} on surface ${e.surfaceId} (component: ${e.sourceComponentId})${n}`}export function validateA2UIMessages(e){const n=new Set;let t=!1,o=!1;for(const r of e)"surfaceUpdate"in r?(n.add(r.surfaceUpdate.surfaceId),t=!0):"beginRendering"in r?(n.add(r.beginRendering.surfaceId),o=!0):"dataModelUpdate"in r?n.add(r.dataModelUpdate.surfaceId):"deleteSurface"in r&&n.add(r.deleteSurface.surfaceId);return t&&!o?{valid:!0,surfaceIds:n,hasUpdates:t,hasRendering:o,error:"Warning: surfaceUpdate found but no beginRendering. The surface may not be visible."}:{valid:!0,surfaceIds:n,hasUpdates:t,hasRendering:o}}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * A2UI JSONL Validator
3
+ *
4
+ * Uses the SAME @a2ui/lit processor used by the client-side renderer
5
+ * to validate JSONL. If the server-side processor accepts it, the
6
+ * client-side renderer will also accept it.
7
+ *
8
+ * This is the most reliable validation approach because:
9
+ * 1. No schema drift between validator and renderer
10
+ * 2. Same validation logic as production renderer
11
+ * 3. Catches all renderer-specific requirements
12
+ */
13
+ export interface ValidationResult {
14
+ valid: boolean;
15
+ errors?: string[];
16
+ messageCount?: number;
17
+ }
18
+ /**
19
+ * Validate A2UI JSONL using the same @a2ui/lit processor as the renderer.
20
+ *
21
+ * @param jsonl - JSONL string (one JSON message per line)
22
+ * @returns ValidationResult with detailed errors if invalid
23
+ */
24
+ export declare function validateA2UIJsonl(jsonl: string): ValidationResult;
25
+ /**
26
+ * Format validation errors for display to agent.
27
+ *
28
+ * @param result - Validation result
29
+ * @returns Human-readable error report
30
+ */
31
+ export declare function formatValidationErrors(result: ValidationResult): string;
32
+ //# sourceMappingURL=validator.d.ts.map
@@ -0,0 +1 @@
1
+ import{v0_8 as e}from"@a2ui/lit";import{parseA2UIJsonl as t}from"./types.js";import{createLogger as n}from"../utils/logger.js";const a=n("A2UIValidator");export function validateA2UIJsonl(n){try{const o=t(n);if(0===o.length)return{valid:!1,errors:["No A2UI messages found in JSONL"]};const r=[];for(const e of o){if("dataModelUpdate"in e){const t=e.dataModelUpdate;"/"!==t.path||"object"!=typeof t.value||null===t.value||Array.isArray(t.value)||r.push("dataModelUpdate with path '/' cannot have object value - use individual paths like '/fieldname' or omit dataModelUpdate entirely")}if("surfaceUpdate"in e){const t=e.surfaceUpdate.components||[];for(const e of t){const t=e.component;if(t){if("Button"in t){const n=t.Button;n.action&&!n.action.event&&r.push(`Button component "${e.id}" has invalid action structure - must be {"action":{"event":{"name":"..."}}} not {"action":{"name":"..."}}`),n.child||r.push(`Button component "${e.id}" missing required 'child' property (must reference a Text component ID)`)}if("TextField"in t){const n=t.TextField;n.text&&n.text.path||r.push(`TextField component "${e.id}" missing required 'text.path' property - must use {"text":{"path":"/fieldname"}} not "dataRef"`)}}}}}if(r.length>0)return a.warn(`A2UI pre-validation failed: ${r.length} error(s)`),{valid:!1,errors:r,messageCount:o.length};const i=e.Data.createSignalA2uiMessageProcessor();try{return i.processMessages(o),a.info(`A2UI validation passed: ${o.length} message(s)`),{valid:!0,messageCount:o.length}}catch(e){const t=e instanceof Error?e.message:String(e);a.warn(`A2UI validation failed: ${t}`);const n=[t];return(t.includes("Button")||t.includes("expected Button"))&&n.push("Button components require:"," - 'child' property (Text component ID)"," - 'action.event' wrapper (not just 'action.name')",' Example: {"Button":{"child":"text_id","action":{"event":{"name":"submit"}}}}'),(t.includes("TextField")||t.includes("path"))&&n.push("TextField components require:"," - 'text.path' property starting with '/'",' Example: {"TextField":{"label":{"literalString":"Name"},"text":{"path":"/name"}}}'),{valid:!1,errors:n,messageCount:o.length}}}catch(e){const t=e instanceof Error?e.message:String(e);return a.error(`A2UI validation error: ${t}`),{valid:!1,errors:[`Parsing error: ${t}`]}}}export function formatValidationErrors(e){if(e.valid)return`✅ Valid A2UI JSONL (${e.messageCount} message${1===e.messageCount?"":"s"})`;return`❌ Invalid A2UI JSONL - Renderer will reject this\n\n${e.errors?.map((e,t)=>`${t+1}. ${e}`).join("\n")||"Unknown error"}\n\nThe A2UI renderer (@a2ui/lit) rejected your JSONL. Common fixes:\n\nButton components:\n ✅ CORRECT: {"Button":{"child":"text_id","action":{"event":{"name":"submit","context":[]}}}}\n ❌ WRONG: {"Button":{"label":"Submit","action":{"name":"submit"}}}\n\nTextField components:\n ✅ CORRECT: {"TextField":{"label":{"literalString":"Name"},"text":{"path":"/name"}}}\n ❌ WRONG: {"TextField":{"label":{"literalString":"Name"},"dataRef":"name"}}\n\nFix the errors above and call a2ui_render again.`}
@@ -11,18 +11,24 @@ export declare class AgentService {
11
11
  private config;
12
12
  private agents;
13
13
  private usageBySession;
14
- private nodeToolsServer;
15
- private messageToolsServer;
16
- private serverToolsServer;
17
- private cronToolsServer;
18
- private ttsToolsServer;
19
- private memoryToolsServer;
20
- private browserToolsServer;
21
- private picoToolsServer;
22
- private telegramToolsServer;
14
+ private nodeToolsFactory;
15
+ private messageToolsFactory;
16
+ private serverToolsFactory;
17
+ private cronToolsFactory;
18
+ private ttsToolsFactory;
19
+ private memoryToolsFactory;
20
+ private browserToolsFactory;
21
+ private picoToolsFactory;
22
+ private telegramToolsFactory;
23
+ private a2uiToolsFactory;
24
+ private dynamicUIToolsFactory;
25
+ private plasmaClientToolsFactory;
26
+ private conceptToolsFactory;
27
+ private refToolServers;
23
28
  private channelManager;
29
+ private nodeRegistry;
24
30
  private showToolUseGetter;
25
- constructor(config: AppConfig, nodeRegistry?: NodeRegistry, channelManager?: ChannelManager, serverToolsServer?: ReturnType<typeof createServerToolsServer>, cronToolsServer?: unknown, sessionDb?: import("./session-db.js").SessionDB, ttsToolsServer?: ReturnType<typeof createTTSToolsServer>, memoryToolsServer?: unknown, showToolUseGetter?: (sessionKey: string) => boolean, browserToolsServer?: unknown, picoToolsServer?: unknown);
31
+ constructor(config: AppConfig, nodeRegistry?: NodeRegistry, channelManager?: ChannelManager, serverToolsFactory?: () => ReturnType<typeof createServerToolsServer>, cronToolsFactory?: () => unknown, sessionDb?: import("./session-db.js").SessionDB, ttsToolsFactory?: () => ReturnType<typeof createTTSToolsServer>, memoryToolsFactory?: () => unknown, showToolUseGetter?: (sessionKey: string) => boolean, browserToolsFactory?: () => unknown, picoToolsFactory?: () => unknown, plasmaClientToolsFactory?: (channel?: string, chatId?: string) => unknown, conceptToolsFactory?: () => unknown);
26
32
  /**
27
33
  * Send a message to the agent for a given session.
28
34
  * Creates a long-lived SessionAgent on first call; subsequent messages for
@@ -31,7 +37,7 @@ export declare class AgentService {
31
37
  sendMessage(sessionKey: string, prompt: BuiltPrompt, sessionId: string | undefined, systemPrompt: string, subagentSystemPrompt: string, modelOverride?: string, coderSkill?: boolean, subagentsEnabled?: boolean, customSubAgentsEnabled?: boolean, sandboxEnabled?: boolean): Promise<QueryResult>;
32
38
  hasNodeTools(): boolean;
33
39
  hasMessageTools(): boolean;
34
- /** Return all active MCP tool server instances for introspection. */
40
+ /** Return MCP tool server instances for introspection (prompt building, /mcp command, etc.). */
35
41
  getToolServers(): Array<{
36
42
  name: string;
37
43
  instance: unknown;
@@ -1 +1 @@
1
- import{query as e}from"@anthropic-ai/claude-agent-sdk";import{SessionAgent as s}from"./session-agent.js";import{createNodeToolsServer as t}from"../tools/node-tools.js";import{createMessageToolsServer as o}from"../tools/message-tools.js";import{createTelegramActionsToolsServer as r}from"../tools/telegram-actions-tools.js";import{createLogger as n}from"../utils/logger.js";const i=n("AgentService");export class AgentService{config;agents=new Map;usageBySession=new Map;nodeToolsServer=null;messageToolsServer=null;serverToolsServer=null;cronToolsServer=null;ttsToolsServer=null;memoryToolsServer=null;browserToolsServer=null;picoToolsServer=null;telegramToolsServer=null;channelManager=null;showToolUseGetter=null;constructor(e,s,n,i,l,a,h,g,c,d,u){this.config=e,n&&(this.channelManager=n,this.telegramToolsServer=r(n,()=>this.config)),s&&(this.nodeToolsServer=t(s)),n&&a&&(this.messageToolsServer=o(n,()=>this.config,a)),i&&(this.serverToolsServer=i),l&&(this.cronToolsServer=l),h&&(this.ttsToolsServer=h),g&&(this.memoryToolsServer=g),d&&(this.browserToolsServer=d),u&&(this.picoToolsServer=u),c&&(this.showToolUseGetter=c)}async sendMessage(e,s,t,o,r,n,l,a,h,g){const c=this.getOrCreateAgent(e,t,o,r,n,l,a,h,g);if(n&&n!==c.getModel()){const t=e=>{const s=this.config.models.find(s=>s.id===e);return!(!s?.proxy||"not-used"===s.proxy)},d=t(c.getModel()),u=t(n);if(d||u){i.info(`[${e}] Proxy config change detected (old=${d}, new=${u}), restarting session`),this.destroySession(e);const t=this.getOrCreateAgent(e,void 0,o,r,n,l,a,h,g);return await t.send(s)}await c.setModel(n)}try{return await c.send(s)}catch(d){const u=d instanceof Error?d.message:String(d);if(u.includes("INVALID_ARGUMENT")||/API Error: 400/.test(u)){i.warn(`[${e}] Transient API 400 error, retrying once: ${u.slice(0,120)}`),this.agents.delete(e),c.close();const d=this.getOrCreateAgent(e,t,o,r,n,l,a,h,g);try{return await d.send(s)}catch(s){i.error(`[${e}] Retry also failed: ${s}`),this.agents.delete(e),d.close();const o=s instanceof Error?s.message:String(s);if(t)return{response:o.includes("SessionAgent closed")?"[AGENT_CLOSED]":"",sessionId:"",sessionReset:!0};throw s}}if(this.agents.delete(e),c.close(),t)return i.warn(`Session agent failed for ${e}: ${d}`),{response:u.includes("SessionAgent closed")?"[AGENT_CLOSED]":"",sessionId:"",sessionReset:!0};throw d}}hasNodeTools(){return null!==this.nodeToolsServer}hasMessageTools(){return null!==this.messageToolsServer}getToolServers(){const e=[];return this.nodeToolsServer&&e.push(this.nodeToolsServer),this.messageToolsServer&&e.push(this.messageToolsServer),this.serverToolsServer&&e.push(this.serverToolsServer),this.cronToolsServer&&e.push(this.cronToolsServer),this.ttsToolsServer&&e.push(this.ttsToolsServer),this.memoryToolsServer&&e.push(this.memoryToolsServer),this.browserToolsServer&&e.push(this.browserToolsServer),this.picoToolsServer&&e.push(this.picoToolsServer),this.telegramToolsServer&&e.push(this.telegramToolsServer),e}getOrCreateAgent(e,t,o,r,n,i,l,a,h){const g=this.agents.get(e);if(g&&g.isActive())return g;g&&(g.close(),this.agents.delete(e));const c=new s(e,this.config,o,r,t,n,this.nodeToolsServer??void 0,this.messageToolsServer??void 0,this.serverToolsServer??void 0,this.cronToolsServer??void 0,i,l,a,this.ttsToolsServer??void 0,this.memoryToolsServer??void 0,this.browserToolsServer??void 0,h,this.picoToolsServer??void 0,this.telegramToolsServer??void 0);if(this.channelManager){const e=this.channelManager;if(c.setChannelSender(async(s,t,o,r)=>{r&&r.length>0?await e.sendButtons(s,t,o,[r]):await e.sendToChannel(s,t,o)}),this.showToolUseGetter){const s=this.showToolUseGetter;c.setToolUseNotifier(async(t,o,r)=>{if(!s(`${t}:${o}`))return;const n=`⚙️ Using ${r.replace(/^mcp__[^_]+__/,"")}`;await e.sendToChannel(t,o,n),await e.setTyping(t,o)})}c.setTypingSetter(async(s,t)=>{await e.setTyping(s,t)}),c.setTypingClearer(async(s,t)=>{await e.clearTyping(s,t)}),c.setTextBlockStreamer(async(s,t,o)=>{await e.sendResponse(s,t,o)})}return c.setUsageRecorder((e,s,t,o,r)=>{this.usageBySession.set(e,{totalCostUsd:s,durationMs:t,numTurns:o,modelUsage:r,recordedAt:Date.now()})}),this.agents.set(e,c),c}async interrupt(e){const s=this.agents.get(e);return!!s&&s.interrupt()}isBusy(e){const s=this.agents.get(e);return!!s&&s.isBusy()}hasPendingPermission(e){const s=this.agents.get(e);return!!s&&s.hasPendingPermission()}resolvePermission(e,s){const t=this.agents.get(e);t&&t.resolvePermission(s)}hasPendingQuestion(e){const s=this.agents.get(e);return!!s&&s.hasPendingQuestion()}resolveQuestion(e,s){const t=this.agents.get(e);t&&t.resolveQuestion(s)}destroySession(e){const s=this.agents.get(e);s&&(s.close(),this.agents.delete(e),i.info(`Session agent destroyed: ${e}`))}destroyAll(){for(const[e,s]of this.agents)s.close(),i.info(`Session agent destroyed (reconfigure): ${e}`);this.agents.clear()}getActiveSessions(){return Array.from(this.agents.keys()).filter(e=>{const s=this.agents.get(e);return s&&s.isActive()})}getActiveSessionCount(){return this.getActiveSessions().length}getUsage(e){return this.usageBySession.get(e)}getSdkSlashCommands(){for(const e of this.agents.values()){const s=e.getSdkSlashCommands();if(s.length>0)return s}return[]}async listModels(){try{const s=e({prompt:"list models",options:{maxTurns:0}}),t=await s.supportedModels();for await(const e of s)break;return t.map(e=>({id:e.id??e.name??String(e),name:e.name??e.id??String(e)}))}catch(e){return i.error(`Failed to list models: ${e}`),[{id:"claude-sonnet-4-6",name:"Claude Sonnet 4.6"},{id:"claude-opus-4-6",name:"Claude Opus 4.6"},{id:"claude-haiku-3-5-20241022",name:"Claude Haiku 3.5"}]}}}
1
+ import{query as s}from"@anthropic-ai/claude-agent-sdk";import{SessionAgent as o}from"./session-agent.js";import{createNodeToolsServer as t}from"../tools/node-tools.js";import{createMessageToolsServer as e}from"../tools/message-tools.js";import{createTelegramActionsToolsServer as r}from"../tools/telegram-actions-tools.js";import{createA2UIToolsServer as n}from"../tools/a2ui-tools.js";import{createDynamicUIToolsServer as i}from"../tools/dynamic-ui-tools.js";import{createLogger as a}from"../utils/logger.js";const l=a("AgentService");export class AgentService{config;agents=new Map;usageBySession=new Map;nodeToolsFactory=null;messageToolsFactory=null;serverToolsFactory=null;cronToolsFactory=null;ttsToolsFactory=null;memoryToolsFactory=null;browserToolsFactory=null;picoToolsFactory=null;telegramToolsFactory=null;a2uiToolsFactory=null;dynamicUIToolsFactory=null;plasmaClientToolsFactory=null;conceptToolsFactory=null;refToolServers=null;channelManager=null;nodeRegistry=null;showToolUseGetter=null;constructor(s,o,a,l,c,h,g,y,d,u,T,m,F){this.config=s,a&&(this.channelManager=a,this.telegramToolsFactory=()=>r(a,()=>this.config)),o&&(this.nodeRegistry=o,this.nodeToolsFactory=()=>t(o),this.a2uiToolsFactory=()=>n({nodeRegistry:o}),this.dynamicUIToolsFactory=()=>i({nodeRegistry:o})),a&&h&&(this.messageToolsFactory=()=>e(a,()=>this.config,h)),l&&(this.serverToolsFactory=l),c&&(this.cronToolsFactory=c),g&&(this.ttsToolsFactory=g),y&&(this.memoryToolsFactory=y),u&&(this.browserToolsFactory=u),T&&(this.picoToolsFactory=T),m&&(this.plasmaClientToolsFactory=m),F&&(this.conceptToolsFactory=F),d&&(this.showToolUseGetter=d)}async sendMessage(s,o,t,e,r,n,i,a,c,h){const g=this.getOrCreateAgent(s,t,e,r,n,i,a,c,h);if(n&&n!==g.getModel()){const t=s=>{const o=this.config.models.find(o=>o.id===s);return!(!o?.proxy||"not-used"===o.proxy)},y=t(g.getModel()),d=t(n);if(y||d){l.info(`[${s}] Proxy config change detected (old=${y}, new=${d}), restarting session`),this.destroySession(s);const t=this.getOrCreateAgent(s,void 0,e,r,n,i,a,c,h);return await t.send(o)}await g.setModel(n)}try{return await g.send(o)}catch(y){const d=y instanceof Error?y.message:String(y);if(d.includes("INVALID_ARGUMENT")||/API Error: 400/.test(d)){l.warn(`[${s}] Transient API 400 error, retrying once: ${d.slice(0,120)}`),this.agents.delete(s),g.close();const y=this.getOrCreateAgent(s,t,e,r,n,i,a,c,h);try{return await y.send(o)}catch(o){l.error(`[${s}] Retry also failed: ${o}`),this.agents.delete(s),y.close();const e=o instanceof Error?o.message:String(o);if(t)return{response:e.includes("SessionAgent closed")?"[AGENT_CLOSED]":"",sessionId:"",sessionReset:!0};throw o}}if(this.agents.delete(s),g.close(),t)return l.warn(`Session agent failed for ${s}: ${y}`),{response:d.includes("SessionAgent closed")?"[AGENT_CLOSED]":"",sessionId:"",sessionReset:!0};throw y}}hasNodeTools(){return null!==this.nodeToolsFactory}hasMessageTools(){return null!==this.messageToolsFactory}getToolServers(){if(!this.refToolServers){const s=[];this.nodeToolsFactory&&s.push(this.nodeToolsFactory()),this.messageToolsFactory&&s.push(this.messageToolsFactory()),this.serverToolsFactory&&s.push(this.serverToolsFactory()),this.cronToolsFactory&&s.push(this.cronToolsFactory()),this.ttsToolsFactory&&s.push(this.ttsToolsFactory()),this.memoryToolsFactory&&s.push(this.memoryToolsFactory()),this.browserToolsFactory&&s.push(this.browserToolsFactory()),this.picoToolsFactory&&s.push(this.picoToolsFactory()),this.telegramToolsFactory&&s.push(this.telegramToolsFactory()),this.a2uiToolsFactory&&s.push(this.a2uiToolsFactory()),this.dynamicUIToolsFactory&&s.push(this.dynamicUIToolsFactory()),this.plasmaClientToolsFactory&&s.push(this.plasmaClientToolsFactory()),this.conceptToolsFactory&&s.push(this.conceptToolsFactory()),this.refToolServers=s}return this.refToolServers}getOrCreateAgent(s,t,e,r,n,a,l,c,h){const g=this.agents.get(s);if(g&&g.isActive())return g;let y,d;g&&(g.close(),this.agents.delete(s));const u=s.indexOf(":"),T=u>0?s.substring(0,u):void 0,m=u>0?s.substring(u+1):void 0;this.nodeRegistry&&(T&&m?y=i({nodeRegistry:this.nodeRegistry,channel:T,chatId:m}):this.dynamicUIToolsFactory&&(y=this.dynamicUIToolsFactory())),this.plasmaClientToolsFactory&&(d=this.plasmaClientToolsFactory(T,m));const F=new o(s,this.config,e,r,t,n,this.nodeToolsFactory?.()??void 0,this.messageToolsFactory?.()??void 0,this.serverToolsFactory?.()??void 0,this.cronToolsFactory?.()??void 0,a,l,c,this.ttsToolsFactory?.()??void 0,this.memoryToolsFactory?.()??void 0,this.browserToolsFactory?.()??void 0,h,this.picoToolsFactory?.()??void 0,this.telegramToolsFactory?.()??void 0,this.a2uiToolsFactory?.()??void 0,y??void 0,d??void 0,this.conceptToolsFactory?.()??void 0);if(this.channelManager){const s=this.channelManager;if(F.setChannelSender(async(o,t,e,r)=>{r&&r.length>0?await s.sendButtons(o,t,e,[r]):await s.sendToChannel(o,t,e)}),this.showToolUseGetter){const o=this.showToolUseGetter;F.setToolUseNotifier(async(t,e,r)=>{if(!o(`${t}:${e}`))return;const n=`⚙️ Using ${r.replace(/^mcp__[^_]+__/,"")}`;await s.sendToChannel(t,e,n),await s.setTyping(t,e)})}F.setTypingSetter(async(o,t)=>{await s.setTyping(o,t)}),F.setTypingClearer(async(o,t)=>{await s.clearTyping(o,t)}),F.setTextBlockStreamer(async(o,t,e)=>{await s.sendResponse(o,t,e)})}return F.setUsageRecorder((s,o,t,e,r)=>{this.usageBySession.set(s,{totalCostUsd:o,durationMs:t,numTurns:e,modelUsage:r,recordedAt:Date.now()})}),this.agents.set(s,F),F}async interrupt(s){const o=this.agents.get(s);return!!o&&o.interrupt()}isBusy(s){const o=this.agents.get(s);return!!o&&o.isBusy()}hasPendingPermission(s){const o=this.agents.get(s);return!!o&&o.hasPendingPermission()}resolvePermission(s,o){const t=this.agents.get(s);t&&t.resolvePermission(o)}hasPendingQuestion(s){const o=this.agents.get(s);return!!o&&o.hasPendingQuestion()}resolveQuestion(s,o){const t=this.agents.get(s);t&&t.resolveQuestion(o)}destroySession(s){const o=this.agents.get(s);o&&(o.close(),this.agents.delete(s),l.info(`Session agent destroyed: ${s}`))}destroyAll(){for(const[s,o]of this.agents)o.close(),l.info(`Session agent destroyed (reconfigure): ${s}`);this.agents.clear()}getActiveSessions(){return Array.from(this.agents.keys()).filter(s=>{const o=this.agents.get(s);return o&&o.isActive()})}getActiveSessionCount(){return this.getActiveSessions().length}getUsage(s){return this.usageBySession.get(s)}getSdkSlashCommands(){for(const s of this.agents.values()){const o=s.getSdkSlashCommands();if(o.length>0)return o}return[]}async listModels(){try{const o=s({prompt:"list models",options:{maxTurns:0}}),t=await o.supportedModels();for await(const s of o)break;return t.map(s=>({id:s.id??s.name??String(s),name:s.name??s.id??String(s)}))}catch(s){return l.error(`Failed to list models: ${s}`),[{id:"claude-sonnet-4-6",name:"Claude Sonnet 4.6"},{id:"claude-opus-4-6",name:"Claude Opus 4.6"},{id:"claude-haiku-3-5-20241022",name:"Claude Haiku 3.5"}]}}}
@@ -99,7 +99,7 @@ export declare class SessionAgent {
99
99
  private autoApproveTools;
100
100
  private pendingPermission;
101
101
  private pendingQuestion;
102
- constructor(sessionKey: string, config: AppConfig, systemPrompt: string, subagentSystemPrompt: string, sessionId?: string, modelOverride?: string, nodeToolsServer?: unknown, messageToolsServer?: unknown, serverToolsServer?: unknown, cronToolsServer?: unknown, coderSkill?: boolean, subagentsEnabled?: boolean, customSubAgentsEnabled?: boolean, ttsToolsServer?: unknown, memoryToolsServer?: unknown, browserToolsServer?: unknown, sandboxEnabled?: boolean, picoToolsServer?: unknown, telegramToolsServer?: unknown);
102
+ constructor(sessionKey: string, config: AppConfig, systemPrompt: string, subagentSystemPrompt: string, sessionId?: string, modelOverride?: string, nodeToolsServer?: unknown, messageToolsServer?: unknown, serverToolsServer?: unknown, cronToolsServer?: unknown, coderSkill?: boolean, subagentsEnabled?: boolean, customSubAgentsEnabled?: boolean, ttsToolsServer?: unknown, memoryToolsServer?: unknown, browserToolsServer?: unknown, sandboxEnabled?: boolean, picoToolsServer?: unknown, telegramToolsServer?: unknown, a2uiToolsServer?: unknown, dynamicUIToolsServer?: unknown, plasmaClientToolsServer?: unknown, conceptToolsServer?: unknown);
103
103
  /**
104
104
  * Resolve Pi provider configuration based on the active model.
105
105
  *
@@ -1 +1 @@
1
- import{query as e}from"@anthropic-ai/claude-agent-sdk";import{MessageQueue as s}from"./message-queue.js";import{resolveModelId as t}from"../config.js";import{createLogger as i}from"../utils/logger.js";const o=i("SessionAgent");export class SessionAgent{sessionKey;config;queue;queryHandle=null;pendingResponses=[];currentResponse="";currentSessionId;model;queueMode;closed=!1;piProviderConfig=null;outputDone=!1;initialized=!1;opts;collectBuffer=[];lastCollectAt=0;debounceMs;debounceTimer=null;debounceResolve=null;queueCap;dropPolicy;droppedResolvers=[];droppedSummaries=[];sdkSlashCommands=[];channelSender=null;toolUseNotifier=null;typingSetter=null;typingClearer=null;textBlockStreamer=null;pendingTextBlock="";streamedAny=!1;streamedText="";usageRecorder=null;autoApproveTools;pendingPermission=null;pendingQuestion=null;constructor(e,i,n,r,l,a,h,u,c,d,p,g,m,f,y,$,b,v,w){this.sessionKey=e,this.config=i,this.currentSessionId=l??"";const T=a??i.agent.model;this.model=T?t(i,T):"",this.queueMode=i.agent.queueMode,this.debounceMs=Math.max(0,i.agent.queueDebounceMs),this.queueCap=Math.max(0,i.agent.queueCap),this.dropPolicy=i.agent.queueDropPolicy,this.autoApproveTools=i.agent.autoApproveTools,this.queue=new s,this.opts={...this.model?{model:this.model}:{},systemPrompt:p?{type:"preset",preset:"claude_code",append:n}:n,...i.agent.maxTurns>0?{maxTurns:i.agent.maxTurns}:{},cwd:i.agent.workspacePath,env:process.env,permissionMode:i.agent.permissionMode,allowDangerouslySkipPermissions:!1,...b?{sandbox:{enabled:!0,autoAllowBashIfSandboxed:!0,network:{allowLocalBinding:!0}}}:{},canUseTool:async(e,s)=>this.handleCanUseTool(e,s),hooks:{PreCompact:[{hooks:[async e=>{const s=e?.trigger??"auto";if(o.info(`[${this.sessionKey}] PreCompact hook fired (trigger=${s})`),this.channelSender){const e=this.sessionKey.indexOf(":");if(e>0){const t=this.sessionKey.substring(0,e),i=this.sessionKey.substring(e+1);if("cron"!==t){const e="auto"===s?"The conversation context is getting large — compacting memory to keep things running smoothly.":"Compacting conversation memory...";this.channelSender(t,i,e).catch(()=>{})}}}return{}}]}]},stderr:s=>{o.error(`[${e}] SDK stderr: ${s.trimEnd()}`)}};const S=i.agent.settingSources;"user"===S?this.opts.settingSources=["user"]:"project"===S?this.opts.settingSources=["project"]:"both"===S&&(this.opts.settingSources=["user","project"]);const K=i.agent.mainFallback;K&&(this.opts.fallbackModel=t(i,K)),i.agent.allowedTools.length>0&&(this.opts.allowedTools=i.agent.allowedTools),i.agent.disallowedTools.length>0&&(this.opts.disallowedTools=i.agent.disallowedTools);const x={};if(Object.keys(i.agent.mcpServers).length>0&&Object.assign(x,i.agent.mcpServers),h&&(x["node-tools"]=h),u&&(x["message-tools"]=u),c&&(x["server-tools"]=c),d&&(x["cron-tools"]=d),f&&(x["tts-tools"]=f),y&&(x["memory-tools"]=y),$&&(x["browser-tools"]=$),v&&(x["pico-tools"]=v),w&&(x["telegram-actions"]=w),Object.keys(x).length>0&&(this.opts.mcpServers=x,this.opts.allowedTools&&this.opts.allowedTools.length>0))for(const e of Object.keys(x)){const s=`mcp__${e}__*`;this.opts.allowedTools.includes(s)||this.opts.allowedTools.push(s)}if(l&&(this.opts.resume=l),!1===g&&(this.opts.allowedTools&&this.opts.allowedTools.length>0?this.opts.allowedTools=this.opts.allowedTools.filter(e=>"Task"!==e):(this.opts.disallowedTools||(this.opts.disallowedTools=[]),this.opts.disallowedTools.includes("Task")||this.opts.disallowedTools.push("Task"))),m){const e={};for(const s of i.agent.customSubAgents){if(!s.enabled)continue;const t=s.expandContext?r+"\n\n"+s.prompt:s.prompt;e[s.name]={description:s.description,prompt:t,tools:s.tools,..."inherit"!==s.model?{model:s.model}:{}}}Object.keys(e).length>0&&(this.opts.agents=e)}const P=i.agent.plugins.filter(e=>e.enabled);P.length>0&&(this.opts.options={...this.opts.options,plugins:P.map(e=>({type:"local",path:e.path}))});const R=this.buildEnvForModel(this.model);this.opts.env=R.env,R.disableThinking&&(this.opts.maxThinkingTokens=0),this.piProviderConfig=this.resolvePiConfig(),this.piProviderConfig&&o.info(`[${e}] Pi engine: ${this.piProviderConfig.provider}/${this.piProviderConfig.modelId}`)}resolvePiConfig(){const e=this.model,s=this.config.models?.find(s=>s.id===e),t=s?.name??"";let i;const n=this.config.agent.picoAgent;if(n?.enabled&&Array.isArray(n.modelRefs)&&(i=n.modelRefs.find(e=>e.split(":")[0]===t)),!i){const e=this.config.agent.engine;if(!e||"pi"!==e.type||!e.piModelRef)return null;i=e.piModelRef}const r=i.split(":");if(r.length<2)return o.warn(`[${this.currentSessionId}] Invalid piModelRef (missing ':'): ${i}`),null;const l=r[0].trim();let a,h;if(r.length>=3)a=r[1].trim(),h=r.slice(2).join(":").trim(),h.startsWith(a+":")&&(h=h.substring(a.length+1));else{const e=r[1].trim(),s=e.indexOf("/");s>0?(a=e.substring(0,s),h=e.substring(s+1)):(a="openrouter",h=e)}const u=this.config.models?.find(e=>e.name===l);let c,d;u?.baseURL&&u.baseURL.includes("openrouter.ai")&&"openrouter"!==a&&(o.info(`[${this.currentSessionId}] piModelRef auto-correction: baseURL is openrouter.ai, switching provider from "${a}" to "openrouter" (modelId: "${a}/${h}")`),h=`${a}/${h}`,a="openrouter"),o.info(`[${this.currentSessionId}] piModelRef resolved: provider="${a}", modelId="${h}", contextWindow=${u?.contextWindow??128e3}`);const p=n?.rollingMemoryModel;if(p){const e=p.split(":");if(e.length>=3)c=e[1].trim(),d=e.slice(2).join(":").trim();else if(2===e.length){const s=e[1].indexOf("/");s>0?(c=e[1].substring(0,s).trim(),d=e[1].substring(s+1).trim()):d=e[1].trim()}d&&o.info(`[${this.currentSessionId}] Summarization model resolved: ${c}/${d}`)}return{provider:a,modelId:h,apiKey:u?.apiKey||void 0,baseUrl:u?.baseURL||void 0,contextWindowTokens:u?.contextWindow||void 0,costInput:u?.costInput||void 0,costOutput:u?.costOutput||void 0,costCacheRead:u?.costCacheRead||void 0,costCacheWrite:u?.costCacheWrite||void 0,summarizationProvider:c,summarizationModelId:d}}async send(e,s){const t=s??0;if(this.closed||this.outputDone)throw new Error("SessionAgent is closed");let i;switch(this.ensureInitialized(),this.queueMode){case"collect":i=await this.sendCollect(e);break;case"steer":i=await this.sendSteer(e);break;default:i=await this.sendDirect(e)}const n=i.response.includes("API Error: 500")||i.fullResponse&&i.fullResponse.includes("API Error: 500");if(n&&t<3){const s=t+1;if(o.warn(`[${this.sessionKey}] API Error 500 detected, retrying (${s}/3)...`),this.channelSender){const e=this.sessionKey.indexOf(":");if(e>0){const t=this.sessionKey.substring(0,e),i=this.sessionKey.substring(e+1);if("cron"!==t){const e=`API temporarily unavailable, retrying (attempt ${s}/3)...`;this.channelSender(t,i,e).catch(()=>{})}}}return await new Promise(e=>setTimeout(e,2e3)),this.send(e,s)}if(n&&t>=3&&(o.error(`[${this.sessionKey}] API Error 500 persists after 3 retries`),this.channelSender)){const e=this.sessionKey.indexOf(":");if(e>0){const s=this.sessionKey.substring(0,e),t=this.sessionKey.substring(e+1);if("cron"!==s){const e="API did not respond after 3 attempts. Please try again later.";this.channelSender(s,t,e).catch(()=>{})}}}return i}async interrupt(){if(this.closed||!this.queryHandle)return!1;try{return await this.queryHandle.interrupt(),o.info(`[${this.sessionKey}] Interrupted`),!0}catch{return!1}}async setModel(e){if(this.queryHandle)try{await this.queryHandle.setModel(e),this.model=e,o.info(`[${this.sessionKey}] Model changed to ${e}`)}catch(e){o.error(`[${this.sessionKey}] Failed to set model: ${e}`)}}close(){if(this.closed)return;this.closed=!0,this.debounceTimer&&(clearTimeout(this.debounceTimer),this.debounceTimer=null),this.debounceResolve&&(this.debounceResolve(),this.debounceResolve=null),this.queue.close(),this.queryHandle&&this.queryHandle.close();const e=new Error("SessionAgent closed");for(const s of this.pendingResponses)s.reject(e);for(const s of this.collectBuffer)s.reject(e);for(const s of this.droppedResolvers)s.reject(e);this.pendingResponses=[],this.collectBuffer=[],this.droppedResolvers=[],this.droppedSummaries=[],o.info(`[${this.sessionKey}] Closed`)}isActive(){return!this.closed&&!this.outputDone}getSessionId(){return this.currentSessionId}getModel(){return this.model}getSdkSlashCommands(){return this.sdkSlashCommands}setChannelSender(e){this.channelSender=e}setToolUseNotifier(e){this.toolUseNotifier=e}setTypingSetter(e){this.typingSetter=e}setTypingClearer(e){this.typingClearer=e}setTextBlockStreamer(e){this.textBlockStreamer=e}setUsageRecorder(e){this.usageRecorder=e}buildEnvForModel(e){const s=this.config.models.find(s=>s.id===e);if(!s?.proxy||"not-used"===s.proxy)return{env:{...process.env},proxied:!1,disableThinking:!1};const t={...process.env};return"direct"===s.proxy?(t.ANTHROPIC_BASE_URL=s.baseURL,t.ANTHROPIC_AUTH_TOKEN=s.apiKey,t.ANTHROPIC_API_KEY="",o.info(`[${this.sessionKey}] Direct env applied for model ${e} (url=${t.ANTHROPIC_BASE_URL})`),{env:t,proxied:!0,disableThinking:!1}):(t.ANTHROPIC_BASE_URL=s.fastUrl||this.config.fastProxyUrl,t.ANTHROPIC_AUTH_TOKEN=s.fastProxyApiKey,t.ANTHROPIC_API_KEY="",delete t.ANTHROPIC_BETAS,delete t.CLAUDE_CODE_EXTRA_BODY,o.info(`[${this.sessionKey}] Proxy env applied for model ${e} (url=${t.ANTHROPIC_BASE_URL})`),{env:t,proxied:!0,disableThinking:!0})}hasPendingPermission(){return null!==this.pendingPermission}resolvePermission(e){if(!this.pendingPermission)return;const s=this.pendingPermission;this.pendingPermission=null,e?(o.info(`[${this.sessionKey}] Permission approved: ${s.toolName}`),s.resolve({behavior:"allow",updatedInput:s.input})):(o.info(`[${this.sessionKey}] Permission denied: ${s.toolName}`),s.resolve({behavior:"deny",message:"User denied this action"}))}isBusy(){return this.pendingResponses.length>0}hasPendingQuestion(){return null!==this.pendingQuestion}resolveQuestion(e){if(!this.pendingQuestion)return;const s=this.pendingQuestion;this.pendingQuestion=null,o.info(`[${this.sessionKey}] Question answered: "${e}" for "${s.questionText}"`),s.resolve(e)}async handleCanUseTool(e,s){if("AskUserQuestion"===e){if(!this.channelSender)return o.warn(`[${this.sessionKey}] No channel sender for AskUserQuestion, auto-approving`),{behavior:"allow",updatedInput:s};const e=this.sessionKey.indexOf(":");if(e<0)return{behavior:"allow",updatedInput:s};const t=this.sessionKey.substring(0,e),i=this.sessionKey.substring(e+1);if(!t||!i||"cron"===t)return{behavior:"allow",updatedInput:s};const n=s?.questions;if(!Array.isArray(n)||0===n.length)return{behavior:"allow",updatedInput:s};const r={};for(const e of n){const n=e.question||"?",l=Array.isArray(e.options)?e.options:[],a=[];if(e.header&&a.push(`*${e.header}*`),a.push(n),l.some(e=>e.description)){a.push("");for(const e of l){const s=e.description?`: ${e.description}`:"";a.push(`• ${e.label}${s}`)}}const h=a.join("\n");if(this.typingClearer)try{await this.typingClearer(t,i)}catch{}try{if(l.length>0){const e=l.map(e=>({text:e.label||String(e),callbackData:`__ask:${e.label||String(e)}`}));await this.channelSender(t,i,h,e)}else await this.channelSender(t,i,h)}catch(e){return o.error(`[${this.sessionKey}] Failed to send AskUserQuestion: ${e}`),{behavior:"allow",updatedInput:s}}const u=55e3,c=await new Promise(e=>{const s=setTimeout(()=>{if(this.pendingQuestion){this.pendingQuestion=null;const s=l.length>0?l[0].label||String(l[0]):"No answer";o.warn(`[${this.sessionKey}] Question timeout, defaulting to "${s}"`),this.channelSender&&this.channelSender(t,i,`[Timeout] Auto-selected: ${s}`).catch(()=>{}),e(s)}},u);this.pendingQuestion={resolve:t=>{clearTimeout(s),e(t)},questionText:n}});if(r[n]=c,this.typingSetter)try{await this.typingSetter(t,i)}catch{}}return o.info(`[${this.sessionKey}] AskUserQuestion answered: ${JSON.stringify(r)}`),{behavior:"allow",updatedInput:{questions:s.questions,answers:r}}}if(this.autoApproveTools)return o.debug(`[${this.sessionKey}] Auto-approving tool: ${e}`),{behavior:"allow",updatedInput:s};if(!this.channelSender)return o.warn(`[${this.sessionKey}] No channel sender for interactive permission, auto-approving: ${e}`),{behavior:"allow",updatedInput:s};const t=this.sessionKey.indexOf(":");if(t<0)return{behavior:"allow",updatedInput:s};const i=this.sessionKey.substring(0,t),n=this.sessionKey.substring(t+1);if(!i||!n||"cron"===i)return{behavior:"allow",updatedInput:s};const r=[`[Permission Request] Tool: ${e}`];if("Bash"===e&&s?.command)r.push(`Command: ${s.command}`),s.description&&r.push(`Description: ${s.description}`);else if("Write"===e&&s?.file_path)r.push(`File: ${s.file_path}`);else if("Edit"===e&&s?.file_path)r.push(`File: ${s.file_path}`);else if("ExitPlanMode"===e&&s?.plan){if(r.push(""),r.push(s.plan),Array.isArray(s.allowedPrompts)&&s.allowedPrompts.length>0){r.push(""),r.push("Requested permissions:");for(const e of s.allowedPrompts)r.push(` - [${e.tool}] ${e.prompt}`)}}else{const e=JSON.stringify(s);e.length<=300?r.push(`Input: ${e}`):r.push(`Input: ${e.slice(0,297)}...`)}r.push(""),r.push("Reply: approve to allow, deny to reject");const l=r.join("\n"),a=[{text:"Approve",callbackData:"__tool_perm:approve"},{text:"Deny",callbackData:"__tool_perm:deny"}];try{await this.channelSender(i,n,l,a)}catch(e){return o.error(`[${this.sessionKey}] Failed to send permission request: ${e}`),{behavior:"allow",updatedInput:s}}if(this.typingClearer)try{await this.typingClearer(i,n)}catch{}const h=12e4;return new Promise(t=>{const r=setTimeout(()=>{this.pendingPermission?.resolve===t&&(this.pendingPermission=null,o.warn(`[${this.sessionKey}] Permission timeout for ${e}, auto-denying`),this.channelSender&&this.channelSender(i,n,`[Permission timeout] Tool ${e} denied after 120s`).catch(()=>{}),t({behavior:"deny",message:"Permission request timed out"}))},h);this.pendingPermission={resolve:t,toolName:e,input:s};const l=t;this.pendingPermission.resolve=e=>{clearTimeout(r),l(e)}})}async forwardAskUserQuestion(e){if(!this.channelSender)return;const s=this.sessionKey.indexOf(":");if(s<0)return;const t=this.sessionKey.substring(0,s),i=this.sessionKey.substring(s+1);if(!t||!i||"cron"===t)return;const n=e?.questions;if(Array.isArray(n)){for(const e of n){const s=e.question||"?",n=Array.isArray(e.options)?e.options:[],r=[];if(e.header&&r.push(`*${e.header}*`),r.push(s),n.some(e=>e.description)){r.push("");for(const e of n){const s=e.description?`: ${e.description}`:"";r.push(`• ${e.label}${s}`)}}const l=r.join("\n");try{if(n.length>0){const e=n.map(e=>({text:e.label||String(e),callbackData:e.label||String(e)}));await this.channelSender(t,i,l,e)}else await this.channelSender(t,i,l)}catch(e){o.error(`[${this.sessionKey}] Failed to forward AskUserQuestion: ${e}`)}}if(this.typingClearer)try{await this.typingClearer(t,i)}catch(e){o.error(`[${this.sessionKey}] Failed to clear typing: ${e}`)}}}async notifyToolUse(e){if(!this.toolUseNotifier)return;const s=this.sessionKey.indexOf(":");if(s<0)return;const t=this.sessionKey.substring(0,s),i=this.sessionKey.substring(s+1);if(t&&i&&"cron"!==t)try{await this.toolUseNotifier(t,i,e)}catch(e){o.error(`[${this.sessionKey}] Failed to notify tool use: ${e}`)}}async flushPendingTextBlock(){if(!this.textBlockStreamer||!this.pendingTextBlock)return;const e=this.sessionKey.indexOf(":");if(e<0)return;const s=this.sessionKey.substring(0,e),t=this.sessionKey.substring(e+1);if(!s||!t||"cron"===s)return;const i=this.pendingTextBlock;this.pendingTextBlock="",this.streamedAny=!0,this.streamedText+=i;try{await this.textBlockStreamer(s,t,i)}catch(e){o.error(`[${this.sessionKey}] Text block stream error: ${e}`)}}sendDirect(e){if(this.queueCap>0&&this.pendingResponses.length>=this.queueCap)return o.warn(`[${this.sessionKey}] Queue cap reached (${this.queueCap}), rejecting message`),Promise.resolve({response:"Queue is full. Please wait for the current processing to complete.",sessionId:this.currentSessionId,sessionReset:!1});const s=this.buildQueueMessage(e);return new Promise((e,t)=>{this.pendingResponses.push({resolve:e,reject:t}),this.queue.push(s),o.info(`[${this.sessionKey}] Message queued (pending=${this.pendingResponses.length})`)})}sendCollect(e){return this.pendingResponses.length>0?this.queueCap>0&&this.collectBuffer.length>=this.queueCap?this.applyDropPolicy(e):(this.lastCollectAt=Date.now(),o.info(`[${this.sessionKey}] Collecting message (buffer=${this.collectBuffer.length+1})`),new Promise((s,t)=>{this.collectBuffer.push({prompt:e,resolve:s,reject:t})})):this.sendDirect(e)}async sendSteer(e){return this.pendingResponses.length>0&&(o.info(`[${this.sessionKey}] Steer: interrupting current processing`),await this.interrupt()),this.sendDirect(e)}applyDropPolicy(e){if("new"===this.dropPolicy)return o.warn(`[${this.sessionKey}] Queue cap reached, rejecting new message`),Promise.resolve({response:"Queue is full. Please wait for the current processing to complete.",sessionId:this.currentSessionId,sessionReset:!1});const s=this.collectBuffer.shift();return"summarize"===this.dropPolicy&&this.droppedSummaries.push(function(e,s){const t=e.replace(/\s+/g," ").trim();return t.length<=s?t:`${t.slice(0,s-1).trimEnd()}…`}(s.prompt.text,140)),this.droppedResolvers.push({resolve:s.resolve,reject:s.reject}),o.warn(`[${this.sessionKey}] Queue cap reached, dropped oldest message (policy=${this.dropPolicy}, dropped=${this.droppedResolvers.length})`),this.lastCollectAt=Date.now(),new Promise((s,t)=>{this.collectBuffer.push({prompt:e,resolve:s,reject:t})})}async debounceThenFlush(){if(this.debounceMs<=0||this.closed)this.flushCollectBuffer();else{for(;!this.closed&&this.collectBuffer.length>0;){const e=Date.now()-this.lastCollectAt;if(e>=this.debounceMs)break;await new Promise(s=>{this.debounceResolve=s,this.debounceTimer=setTimeout(s,this.debounceMs-e)}),this.debounceTimer=null,this.debounceResolve=null}this.closed||this.flushCollectBuffer()}}flushCollectBuffer(){if(0===this.collectBuffer.length&&0===this.droppedResolvers.length)return;const e=this.collectBuffer.splice(0),s=e.map(e=>e.prompt),t=this.mergePrompts(s),i=this.buildQueueMessage(t),n=[...this.droppedResolvers.splice(0),...e.map(e=>({resolve:e.resolve,reject:e.reject}))];this.droppedSummaries=[],this.pendingResponses.push({resolve:e=>{for(const s of n)s.resolve(e)},reject:e=>{for(const s of n)s.reject(e)}}),this.queue.push(i),o.info(`[${this.sessionKey}] Flushed ${e.length} collected message(s) as one prompt`)}mergePrompts(e){const s=[],t=[];if(this.droppedSummaries.length>0){s.push(`[${this.droppedSummaries.length} earlier message(s) dropped due to queue cap]`);for(const e of this.droppedSummaries)s.push(`- ${e}`);s.push("")}if(1===e.length&&0===this.droppedSummaries.length)return e[0];if(e.length>0){s.push("[Queued messages while agent was busy]");for(let i=0;i<e.length;i++)e[i].text&&s.push(`${i+1}. ${e[i].text}`),t.push(...e[i].images)}return{text:s.join("\n"),images:t}}ensureInitialized(){if(this.initialized)return;this.initialized=!0;const s=this.piProviderConfig?"pi":"claudecode";o.info(`[${this.sessionKey}] Starting agent: engine=${s}, model=${this.model}, mode=${this.queueMode}, debounce=${this.debounceMs}ms, cap=${this.queueCap||"unlimited"}, drop=${this.dropPolicy}, session=${this.currentSessionId||"new"}`),this.piProviderConfig?this.initPiEngine():(this.queryHandle=e({prompt:this.queue,options:this.opts}),this.processOutput())}async initPiEngine(){try{const e=await import("../pi-agent-provider/index.js"),s=await e.createToolRegistryFromOptions(this.opts);this.queryHandle=e.piQuery({prompt:this.queue,options:this.opts},this.piProviderConfig,s),this.processOutput(),o.info(`[${this.sessionKey}] Pi engine initialized: ${this.piProviderConfig.provider}/${this.piProviderConfig.modelId}`)}catch(s){o.error(`[${this.sessionKey}] Failed to initialize Pi engine: ${s}`),o.warn(`[${this.sessionKey}] Falling back to Claude SDK (claudecode engine)`),this.queryHandle=e({prompt:this.queue,options:this.opts}),this.processOutput()}}buildQueueMessage(e){if(0===e.images.length)return o.debug(`[${this.sessionKey}] SDK request: text-only (${e.text.length} chars): ${this.config.verboseDebugLogs?e.text:e.text.slice(0,15)+"..."}`),{type:"user",message:{role:"user",content:e.text}};const s=[];for(const t of e.images)s.push({type:"image",source:{type:"base64",media_type:t.mimeType,data:t.base64}});return e.text&&s.push({type:"text",text:e.text}),o.debug(`[${this.sessionKey}] SDK request: ${s.length} block(s) [${s.map(e=>"image"===e.type?`image/${e.source.media_type}`:`text(${e.text?.length??0})`).join(", ")}]`),{type:"user",message:{role:"user",content:s}}}async processOutput(){if(this.queryHandle)try{for await(const e of this.queryHandle){if(this.closed)break;if(o.debug(`[${this.sessionKey}] SDK message: type=${e.type}, subtype=${e.subtype??"-"}, keys=${Object.keys(e).join(",")}`),"system"===e.type){const s=e,t=s.subtype;if("init"===t){const e=s.slash_commands;Array.isArray(e)&&(this.sdkSlashCommands=e.map(e=>e.replace(/^\//,"")))}if("compact_boundary"===t){const e=s.compact_metadata,t=["Context compacted."];e?.pre_tokens&&t.push(`Pre-compaction tokens: ${e.pre_tokens}.`),e?.trigger&&t.push(`Trigger: ${e.trigger}.`),this.currentResponse=t.join(" "),o.info(`[${this.sessionKey}] Compact: ${this.currentResponse}`)}else if("init"!==t&&"status"!==t){const{type:e,...i}=s;this.currentResponse=JSON.stringify(i,null,2),o.info(`[${this.sessionKey}] System message (${t??"unknown"}): ${this.currentResponse.slice(0,200)}`)}}if("assistant"===e.type){const s=e.message.content,t=s.filter(e=>"text"===e.type).map(e=>e.text).join("");t&&(this.currentResponse=t,this.pendingTextBlock=t);const i=s.map(e=>e.type).join(", ");o.debug(`[${this.sessionKey}] SDK assistant message: blocks=[${i}], text length=${t.length}: ${this.config.verboseDebugLogs?t:t.slice(0,15)+"..."}`);s.some(e=>"tool_use"===e.type)&&this.pendingTextBlock&&this.textBlockStreamer&&await this.flushPendingTextBlock();for(const e of s)if("tool_use"===e.type){const s=JSON.stringify(e.input);o.debug(`[${this.sessionKey}] Tool call: ${e.name} ${this.config.verboseDebugLogs?s:s.slice(0,100)+(s.length>100?"...":"")}`),this.toolUseNotifier&&"AskUserQuestion"!==e.name&&await this.notifyToolUse(e.name)}}if("tool_progress"===e.type){const s=e;o.debug(`[${this.sessionKey}] Tool progress: ${s.tool_name} (${s.elapsed_time_seconds}s)`)}if("result"===e.type){const s=e;let t;o.debug(`[${this.sessionKey}] SDK result: subtype=${s.subtype}, stop_reason=${s.stop_reason??"null"}, session=${s.session_id??"n/a"}, result length=${s.result?.length??0}`),"session_id"in s&&(this.currentSessionId=s.session_id),this.usageRecorder&&(void 0!==s.total_cost_usd||s.modelUsage)&&this.usageRecorder(this.sessionKey,s.total_cost_usd,s.duration_ms,s.num_turns,s.modelUsage);const i=s.stop_reason??null;if("success"===s.subtype){if(s.result)this.currentResponse=s.result;else if(!this.currentResponse&&(void 0!==s.total_cost_usd||s.usage)){const e=[];if(void 0!==s.total_cost_usd&&e.push(`Total cost: $${Number(s.total_cost_usd).toFixed(4)}`),void 0!==s.duration_ms&&e.push(`Duration: ${(s.duration_ms/1e3).toFixed(1)}s`),void 0!==s.num_turns&&e.push(`Turns: ${s.num_turns}`),s.modelUsage)for(const[t,i]of Object.entries(s.modelUsage)){const s=i,o=[` ${t}:`];s.inputTokens&&o.push(`input=${s.inputTokens}`),s.outputTokens&&o.push(`output=${s.outputTokens}`),s.cacheReadInputTokens&&o.push(`cache_read=${s.cacheReadInputTokens}`),s.cacheCreationInputTokens&&o.push(`cache_create=${s.cacheCreationInputTokens}`),void 0!==s.costUSD&&o.push(`cost=$${Number(s.costUSD).toFixed(4)}`),e.push(o.join(" "))}e.length>0&&(this.currentResponse=e.join("\n"))}if(!s.result&&!this.currentResponse){const e=this.piProviderConfig;o.warn(`[${this.sessionKey}] Empty response on success: provider=${e?.provider??"sdk"}, modelId=${e?.modelId??"n/a"}, stop_reason=${i}. Check provider routing and API key.`)}"refusal"===i?(o.warn(`[${this.sessionKey}] Model refused the request`),this.currentResponse||(this.currentResponse="I'm unable to fulfill this request.")):"max_tokens"===i&&o.warn(`[${this.sessionKey}] Response truncated: output token limit reached`)}else if("error_max_turns"===s.subtype)t="max_turns",o.warn(`[${this.sessionKey}] Max turns reached`);else if("error_max_budget_usd"===s.subtype)t="max_budget",o.warn(`[${this.sessionKey}] Max budget reached`);else{const e=s.errors??[];e.some(e=>e.includes("aborted"))?o.info(`[${this.sessionKey}] Request aborted (steer interrupt)`):o.error(`[${this.sessionKey}] SDK error: ${JSON.stringify(s)}`)}const n=this.pendingResponses.shift();if(n){const e=this.currentResponse||"";let s=e;this.streamedAny&&(s=this.pendingTextBlock||"",!s&&e&&e.length>this.streamedText.length&&(s=e.startsWith(this.streamedText)?e.slice(this.streamedText.length).replace(/^\n+/,""):e)),o.info(`[${this.sessionKey}] Response ready: session=${this.currentSessionId}, length=${s.length}${this.streamedAny?` (streamed, full=${e.length})`:""}`),n.resolve({response:s,fullResponse:this.streamedAny?e:void 0,sessionId:this.currentSessionId,sessionReset:!1,errorType:t,stopReason:i})}this.currentResponse="",this.pendingTextBlock="",this.streamedAny=!1,this.streamedText="","collect"===this.queueMode&&(this.collectBuffer.length>0||this.droppedResolvers.length>0)&&await this.debounceThenFlush()}}}catch(e){o.error(`[${this.sessionKey}] Output stream error: ${e}`);const s=this.pendingResponses.shift();s&&(this.currentSessionId?(o.warn(`[${this.sessionKey}] Session corrupted: ${this.currentSessionId}`),s.resolve({response:"",sessionId:"",sessionReset:!0})):s.reject(e instanceof Error?e:new Error(String(e))));const t=new Error("SessionAgent terminated");for(const e of this.pendingResponses)e.reject(t);for(const e of this.collectBuffer)e.reject(t);for(const e of this.droppedResolvers)e.reject(t);this.pendingResponses=[],this.collectBuffer=[],this.droppedResolvers=[],this.droppedSummaries=[]}finally{this.outputDone=!0}}}
1
+ import{query as e}from"@anthropic-ai/claude-agent-sdk";import{MessageQueue as s}from"./message-queue.js";import{resolveModelId as t}from"../config.js";import{createLogger as i}from"../utils/logger.js";const o=i("SessionAgent");export class SessionAgent{sessionKey;config;queue;queryHandle=null;pendingResponses=[];currentResponse="";currentSessionId;model;queueMode;closed=!1;piProviderConfig=null;outputDone=!1;initialized=!1;opts;collectBuffer=[];lastCollectAt=0;debounceMs;debounceTimer=null;debounceResolve=null;queueCap;dropPolicy;droppedResolvers=[];droppedSummaries=[];sdkSlashCommands=[];channelSender=null;toolUseNotifier=null;typingSetter=null;typingClearer=null;textBlockStreamer=null;pendingTextBlock="";streamedAny=!1;streamedText="";usageRecorder=null;autoApproveTools;pendingPermission=null;pendingQuestion=null;constructor(e,i,n,r,l,a,h,u,c,d,p,g,m,f,y,$,b,v,w,T,S,K,R){this.sessionKey=e,this.config=i,this.currentSessionId=l??"";const x=a??i.agent.model;this.model=x?t(i,x):"",this.queueMode=i.agent.queueMode,this.debounceMs=Math.max(0,i.agent.queueDebounceMs),this.queueCap=Math.max(0,i.agent.queueCap),this.dropPolicy=i.agent.queueDropPolicy,this.autoApproveTools=i.agent.autoApproveTools,this.queue=new s,this.opts={...this.model?{model:this.model}:{},systemPrompt:p?{type:"preset",preset:"claude_code",append:n}:n,...i.agent.maxTurns>0?{maxTurns:i.agent.maxTurns}:{},cwd:i.agent.workspacePath,env:process.env,permissionMode:i.agent.permissionMode,allowDangerouslySkipPermissions:!1,...b?{sandbox:{enabled:!0,autoAllowBashIfSandboxed:!0,network:{allowLocalBinding:!0}}}:{},canUseTool:async(e,s)=>this.handleCanUseTool(e,s),hooks:{PreCompact:[{hooks:[async e=>{const s=e?.trigger??"auto";if(o.info(`[${this.sessionKey}] PreCompact hook fired (trigger=${s})`),this.channelSender){const e=this.sessionKey.indexOf(":");if(e>0){const t=this.sessionKey.substring(0,e),i=this.sessionKey.substring(e+1);if("cron"!==t){const e="auto"===s?"The conversation context is getting large — compacting memory to keep things running smoothly.":"Compacting conversation memory...";this.channelSender(t,i,e).catch(()=>{})}}}return{}}]}]},stderr:s=>{o.error(`[${e}] SDK stderr: ${s.trimEnd()}`)}};const P=i.agent.settingSources;"user"===P?this.opts.settingSources=["user"]:"project"===P?this.opts.settingSources=["project"]:"both"===P&&(this.opts.settingSources=["user","project"]);const k=i.agent.mainFallback;k&&(this.opts.fallbackModel=t(i,k)),i.agent.allowedTools.length>0&&(this.opts.allowedTools=i.agent.allowedTools),i.agent.disallowedTools.length>0&&(this.opts.disallowedTools=i.agent.disallowedTools);const _={};if(Object.keys(i.agent.mcpServers).length>0&&Object.assign(_,i.agent.mcpServers),h&&(_["node-tools"]=h),u&&(_["message-tools"]=u),c&&(_["server-tools"]=c),d&&(_["cron-tools"]=d),f&&(_["tts-tools"]=f),y&&(_["memory-tools"]=y),$&&(_["browser-tools"]=$),v&&(_["pico-tools"]=v),w&&(_["telegram-actions"]=w),T&&(_["a2ui-tools"]=T),S&&(_["dynamic-ui-tools"]=S),K&&(_["plasma-client-tools"]=K),R&&(_["concept-tools"]=R),Object.keys(_).length>0&&(this.opts.mcpServers=_,this.opts.allowedTools&&this.opts.allowedTools.length>0))for(const e of Object.keys(_)){const s=`mcp__${e}__*`;this.opts.allowedTools.includes(s)||this.opts.allowedTools.push(s)}if(l&&(this.opts.resume=l),!1===g&&(this.opts.allowedTools&&this.opts.allowedTools.length>0?this.opts.allowedTools=this.opts.allowedTools.filter(e=>"Task"!==e):(this.opts.disallowedTools||(this.opts.disallowedTools=[]),this.opts.disallowedTools.includes("Task")||this.opts.disallowedTools.push("Task"))),m){const e={};for(const s of i.agent.customSubAgents){if(!s.enabled)continue;const t=s.expandContext?r+"\n\n"+s.prompt:s.prompt;e[s.name]={description:s.description,prompt:t,tools:s.tools,..."inherit"!==s.model?{model:s.model}:{}}}Object.keys(e).length>0&&(this.opts.agents=e)}const A=i.agent.plugins.filter(e=>e.enabled);A.length>0&&(this.opts.options={...this.opts.options,plugins:A.map(e=>({type:"local",path:e.path}))});const C=this.buildEnvForModel(this.model);this.opts.env=C.env,C.disableThinking&&(this.opts.maxThinkingTokens=0),this.piProviderConfig=this.resolvePiConfig(),this.piProviderConfig&&o.info(`[${e}] Pi engine: ${this.piProviderConfig.provider}/${this.piProviderConfig.modelId}`)}resolvePiConfig(){const e=this.model,s=this.config.models?.find(s=>s.id===e),t=s?.name??"";let i;const n=this.config.agent.picoAgent;if(n?.enabled&&Array.isArray(n.modelRefs)&&(i=n.modelRefs.find(e=>e.split(":")[0]===t)),!i){const e=this.config.agent.engine;if(!e||"pi"!==e.type||!e.piModelRef)return null;i=e.piModelRef}const r=i.split(":");if(r.length<2)return o.warn(`[${this.currentSessionId}] Invalid piModelRef (missing ':'): ${i}`),null;const l=r[0].trim();let a,h;if(r.length>=3)a=r[1].trim(),h=r.slice(2).join(":").trim(),h.startsWith(a+":")&&(h=h.substring(a.length+1));else{const e=r[1].trim(),s=e.indexOf("/");s>0?(a=e.substring(0,s),h=e.substring(s+1)):(a="openrouter",h=e)}const u=this.config.models?.find(e=>e.name===l);let c,d;u?.baseURL&&u.baseURL.includes("openrouter.ai")&&"openrouter"!==a&&(o.info(`[${this.currentSessionId}] piModelRef auto-correction: baseURL is openrouter.ai, switching provider from "${a}" to "openrouter" (modelId: "${a}/${h}")`),h=`${a}/${h}`,a="openrouter"),o.info(`[${this.currentSessionId}] piModelRef resolved: provider="${a}", modelId="${h}", contextWindow=${u?.contextWindow??128e3}`);const p=n?.rollingMemoryModel;if(p){const e=p.split(":");if(e.length>=3)c=e[1].trim(),d=e.slice(2).join(":").trim();else if(2===e.length){const s=e[1].indexOf("/");s>0?(c=e[1].substring(0,s).trim(),d=e[1].substring(s+1).trim()):d=e[1].trim()}d&&o.info(`[${this.currentSessionId}] Summarization model resolved: ${c}/${d}`)}return{provider:a,modelId:h,apiKey:u?.apiKey||void 0,baseUrl:u?.baseURL||void 0,contextWindowTokens:u?.contextWindow||void 0,costInput:u?.costInput||void 0,costOutput:u?.costOutput||void 0,costCacheRead:u?.costCacheRead||void 0,costCacheWrite:u?.costCacheWrite||void 0,summarizationProvider:c,summarizationModelId:d}}async send(e,s){const t=s??0;if(this.closed||this.outputDone)throw new Error("SessionAgent is closed");let i;switch(this.ensureInitialized(),this.queueMode){case"collect":i=await this.sendCollect(e);break;case"steer":i=await this.sendSteer(e);break;default:i=await this.sendDirect(e)}const n=i.response.includes("API Error: 500")||i.fullResponse&&i.fullResponse.includes("API Error: 500");if(n&&t<3){const s=t+1;if(o.warn(`[${this.sessionKey}] API Error 500 detected, retrying (${s}/3)...`),this.channelSender){const e=this.sessionKey.indexOf(":");if(e>0){const t=this.sessionKey.substring(0,e),i=this.sessionKey.substring(e+1);if("cron"!==t){const e=`API temporarily unavailable, retrying (attempt ${s}/3)...`;this.channelSender(t,i,e).catch(()=>{})}}}return await new Promise(e=>setTimeout(e,2e3)),this.send(e,s)}if(n&&t>=3&&(o.error(`[${this.sessionKey}] API Error 500 persists after 3 retries`),this.channelSender)){const e=this.sessionKey.indexOf(":");if(e>0){const s=this.sessionKey.substring(0,e),t=this.sessionKey.substring(e+1);if("cron"!==s){const e="API did not respond after 3 attempts. Please try again later.";this.channelSender(s,t,e).catch(()=>{})}}}return i}async interrupt(){if(this.closed||!this.queryHandle)return!1;try{return await this.queryHandle.interrupt(),o.info(`[${this.sessionKey}] Interrupted`),!0}catch{return!1}}async setModel(e){if(this.queryHandle)try{await this.queryHandle.setModel(e),this.model=e,o.info(`[${this.sessionKey}] Model changed to ${e}`)}catch(e){o.error(`[${this.sessionKey}] Failed to set model: ${e}`)}}close(){if(this.closed)return;this.closed=!0,this.debounceTimer&&(clearTimeout(this.debounceTimer),this.debounceTimer=null),this.debounceResolve&&(this.debounceResolve(),this.debounceResolve=null),this.queue.close(),this.queryHandle&&this.queryHandle.close();const e=new Error("SessionAgent closed");for(const s of this.pendingResponses)s.reject(e);for(const s of this.collectBuffer)s.reject(e);for(const s of this.droppedResolvers)s.reject(e);this.pendingResponses=[],this.collectBuffer=[],this.droppedResolvers=[],this.droppedSummaries=[],o.info(`[${this.sessionKey}] Closed`)}isActive(){return!this.closed&&!this.outputDone}getSessionId(){return this.currentSessionId}getModel(){return this.model}getSdkSlashCommands(){return this.sdkSlashCommands}setChannelSender(e){this.channelSender=e}setToolUseNotifier(e){this.toolUseNotifier=e}setTypingSetter(e){this.typingSetter=e}setTypingClearer(e){this.typingClearer=e}setTextBlockStreamer(e){this.textBlockStreamer=e}setUsageRecorder(e){this.usageRecorder=e}buildEnvForModel(e){const s=this.config.models.find(s=>s.id===e);if(!s?.proxy||"not-used"===s.proxy)return{env:{...process.env},proxied:!1,disableThinking:!1};const t={...process.env};return"direct"===s.proxy?(t.ANTHROPIC_BASE_URL=s.baseURL,t.ANTHROPIC_AUTH_TOKEN=s.apiKey,t.ANTHROPIC_API_KEY="",o.info(`[${this.sessionKey}] Direct env applied for model ${e} (url=${t.ANTHROPIC_BASE_URL})`),{env:t,proxied:!0,disableThinking:!1}):(t.ANTHROPIC_BASE_URL=s.fastUrl||this.config.fastProxyUrl,t.ANTHROPIC_AUTH_TOKEN=s.fastProxyApiKey,t.ANTHROPIC_API_KEY="",delete t.ANTHROPIC_BETAS,delete t.CLAUDE_CODE_EXTRA_BODY,o.info(`[${this.sessionKey}] Proxy env applied for model ${e} (url=${t.ANTHROPIC_BASE_URL})`),{env:t,proxied:!0,disableThinking:!0})}hasPendingPermission(){return null!==this.pendingPermission}resolvePermission(e){if(!this.pendingPermission)return;const s=this.pendingPermission;this.pendingPermission=null,e?(o.info(`[${this.sessionKey}] Permission approved: ${s.toolName}`),s.resolve({behavior:"allow",updatedInput:s.input})):(o.info(`[${this.sessionKey}] Permission denied: ${s.toolName}`),s.resolve({behavior:"deny",message:"User denied this action"}))}isBusy(){return this.pendingResponses.length>0}hasPendingQuestion(){return null!==this.pendingQuestion}resolveQuestion(e){if(!this.pendingQuestion)return;const s=this.pendingQuestion;this.pendingQuestion=null,o.info(`[${this.sessionKey}] Question answered: "${e}" for "${s.questionText}"`),s.resolve(e)}async handleCanUseTool(e,s){if("AskUserQuestion"===e){if(!this.channelSender)return o.warn(`[${this.sessionKey}] No channel sender for AskUserQuestion, auto-approving`),{behavior:"allow",updatedInput:s};const e=this.sessionKey.indexOf(":");if(e<0)return{behavior:"allow",updatedInput:s};const t=this.sessionKey.substring(0,e),i=this.sessionKey.substring(e+1);if(!t||!i||"cron"===t)return{behavior:"allow",updatedInput:s};const n=s?.questions;if(!Array.isArray(n)||0===n.length)return{behavior:"allow",updatedInput:s};const r={};for(const e of n){const n=e.question||"?",l=Array.isArray(e.options)?e.options:[],a=[];if(e.header&&a.push(`*${e.header}*`),a.push(n),l.some(e=>e.description)){a.push("");for(const e of l){const s=e.description?`: ${e.description}`:"";a.push(`• ${e.label}${s}`)}}const h=a.join("\n");if(this.typingClearer)try{await this.typingClearer(t,i)}catch{}try{if(l.length>0){const e=l.map(e=>({text:e.label||String(e),callbackData:`__ask:${e.label||String(e)}`}));await this.channelSender(t,i,h,e)}else await this.channelSender(t,i,h)}catch(e){return o.error(`[${this.sessionKey}] Failed to send AskUserQuestion: ${e}`),{behavior:"allow",updatedInput:s}}const u=55e3,c=await new Promise(e=>{const s=setTimeout(()=>{if(this.pendingQuestion){this.pendingQuestion=null;const s=l.length>0?l[0].label||String(l[0]):"No answer";o.warn(`[${this.sessionKey}] Question timeout, defaulting to "${s}"`),this.channelSender&&this.channelSender(t,i,`[Timeout] Auto-selected: ${s}`).catch(()=>{}),e(s)}},u);this.pendingQuestion={resolve:t=>{clearTimeout(s),e(t)},questionText:n}});if(r[n]=c,this.typingSetter)try{await this.typingSetter(t,i)}catch{}}return o.info(`[${this.sessionKey}] AskUserQuestion answered: ${JSON.stringify(r)}`),{behavior:"allow",updatedInput:{questions:s.questions,answers:r}}}if(this.autoApproveTools)return o.debug(`[${this.sessionKey}] Auto-approving tool: ${e}`),{behavior:"allow",updatedInput:s};if(!this.channelSender)return o.warn(`[${this.sessionKey}] No channel sender for interactive permission, auto-approving: ${e}`),{behavior:"allow",updatedInput:s};const t=this.sessionKey.indexOf(":");if(t<0)return{behavior:"allow",updatedInput:s};const i=this.sessionKey.substring(0,t),n=this.sessionKey.substring(t+1);if(!i||!n||"cron"===i)return{behavior:"allow",updatedInput:s};const r=[`[Permission Request] Tool: ${e}`];if("Bash"===e&&s?.command)r.push(`Command: ${s.command}`),s.description&&r.push(`Description: ${s.description}`);else if("Write"===e&&s?.file_path)r.push(`File: ${s.file_path}`);else if("Edit"===e&&s?.file_path)r.push(`File: ${s.file_path}`);else if("ExitPlanMode"===e&&s?.plan){if(r.push(""),r.push(s.plan),Array.isArray(s.allowedPrompts)&&s.allowedPrompts.length>0){r.push(""),r.push("Requested permissions:");for(const e of s.allowedPrompts)r.push(` - [${e.tool}] ${e.prompt}`)}}else{const e=JSON.stringify(s);e.length<=300?r.push(`Input: ${e}`):r.push(`Input: ${e.slice(0,297)}...`)}r.push(""),r.push("Reply: approve to allow, deny to reject");const l=r.join("\n"),a=[{text:"Approve",callbackData:"__tool_perm:approve"},{text:"Deny",callbackData:"__tool_perm:deny"}];try{await this.channelSender(i,n,l,a)}catch(e){return o.error(`[${this.sessionKey}] Failed to send permission request: ${e}`),{behavior:"allow",updatedInput:s}}if(this.typingClearer)try{await this.typingClearer(i,n)}catch{}const h=12e4;return new Promise(t=>{const r=setTimeout(()=>{this.pendingPermission?.resolve===t&&(this.pendingPermission=null,o.warn(`[${this.sessionKey}] Permission timeout for ${e}, auto-denying`),this.channelSender&&this.channelSender(i,n,`[Permission timeout] Tool ${e} denied after 120s`).catch(()=>{}),t({behavior:"deny",message:"Permission request timed out"}))},h);this.pendingPermission={resolve:t,toolName:e,input:s};const l=t;this.pendingPermission.resolve=e=>{clearTimeout(r),l(e)}})}async forwardAskUserQuestion(e){if(!this.channelSender)return;const s=this.sessionKey.indexOf(":");if(s<0)return;const t=this.sessionKey.substring(0,s),i=this.sessionKey.substring(s+1);if(!t||!i||"cron"===t)return;const n=e?.questions;if(Array.isArray(n)){for(const e of n){const s=e.question||"?",n=Array.isArray(e.options)?e.options:[],r=[];if(e.header&&r.push(`*${e.header}*`),r.push(s),n.some(e=>e.description)){r.push("");for(const e of n){const s=e.description?`: ${e.description}`:"";r.push(`• ${e.label}${s}`)}}const l=r.join("\n");try{if(n.length>0){const e=n.map(e=>({text:e.label||String(e),callbackData:e.label||String(e)}));await this.channelSender(t,i,l,e)}else await this.channelSender(t,i,l)}catch(e){o.error(`[${this.sessionKey}] Failed to forward AskUserQuestion: ${e}`)}}if(this.typingClearer)try{await this.typingClearer(t,i)}catch(e){o.error(`[${this.sessionKey}] Failed to clear typing: ${e}`)}}}async notifyToolUse(e){if(!this.toolUseNotifier)return;const s=this.sessionKey.indexOf(":");if(s<0)return;const t=this.sessionKey.substring(0,s),i=this.sessionKey.substring(s+1);if(t&&i&&"cron"!==t)try{await this.toolUseNotifier(t,i,e)}catch(e){o.error(`[${this.sessionKey}] Failed to notify tool use: ${e}`)}}async flushPendingTextBlock(){if(!this.textBlockStreamer||!this.pendingTextBlock)return;const e=this.sessionKey.indexOf(":");if(e<0)return;const s=this.sessionKey.substring(0,e),t=this.sessionKey.substring(e+1);if(!s||!t||"cron"===s)return;const i=this.pendingTextBlock;this.pendingTextBlock="",this.streamedAny=!0,this.streamedText+=i;try{await this.textBlockStreamer(s,t,i)}catch(e){o.error(`[${this.sessionKey}] Text block stream error: ${e}`)}}sendDirect(e){if(this.queueCap>0&&this.pendingResponses.length>=this.queueCap)return o.warn(`[${this.sessionKey}] Queue cap reached (${this.queueCap}), rejecting message`),Promise.resolve({response:"Queue is full. Please wait for the current processing to complete.",sessionId:this.currentSessionId,sessionReset:!1});const s=this.buildQueueMessage(e);return new Promise((e,t)=>{this.pendingResponses.push({resolve:e,reject:t}),this.queue.push(s),o.info(`[${this.sessionKey}] Message queued (pending=${this.pendingResponses.length})`)})}sendCollect(e){return this.pendingResponses.length>0?this.queueCap>0&&this.collectBuffer.length>=this.queueCap?this.applyDropPolicy(e):(this.lastCollectAt=Date.now(),o.info(`[${this.sessionKey}] Collecting message (buffer=${this.collectBuffer.length+1})`),new Promise((s,t)=>{this.collectBuffer.push({prompt:e,resolve:s,reject:t})})):this.sendDirect(e)}async sendSteer(e){return this.pendingResponses.length>0&&(o.info(`[${this.sessionKey}] Steer: interrupting current processing`),await this.interrupt()),this.sendDirect(e)}applyDropPolicy(e){if("new"===this.dropPolicy)return o.warn(`[${this.sessionKey}] Queue cap reached, rejecting new message`),Promise.resolve({response:"Queue is full. Please wait for the current processing to complete.",sessionId:this.currentSessionId,sessionReset:!1});const s=this.collectBuffer.shift();return"summarize"===this.dropPolicy&&this.droppedSummaries.push(function(e,s){const t=e.replace(/\s+/g," ").trim();return t.length<=s?t:`${t.slice(0,s-1).trimEnd()}…`}(s.prompt.text,140)),this.droppedResolvers.push({resolve:s.resolve,reject:s.reject}),o.warn(`[${this.sessionKey}] Queue cap reached, dropped oldest message (policy=${this.dropPolicy}, dropped=${this.droppedResolvers.length})`),this.lastCollectAt=Date.now(),new Promise((s,t)=>{this.collectBuffer.push({prompt:e,resolve:s,reject:t})})}async debounceThenFlush(){if(this.debounceMs<=0||this.closed)this.flushCollectBuffer();else{for(;!this.closed&&this.collectBuffer.length>0;){const e=Date.now()-this.lastCollectAt;if(e>=this.debounceMs)break;await new Promise(s=>{this.debounceResolve=s,this.debounceTimer=setTimeout(s,this.debounceMs-e)}),this.debounceTimer=null,this.debounceResolve=null}this.closed||this.flushCollectBuffer()}}flushCollectBuffer(){if(0===this.collectBuffer.length&&0===this.droppedResolvers.length)return;const e=this.collectBuffer.splice(0),s=e.map(e=>e.prompt),t=this.mergePrompts(s),i=this.buildQueueMessage(t),n=[...this.droppedResolvers.splice(0),...e.map(e=>({resolve:e.resolve,reject:e.reject}))];this.droppedSummaries=[],this.pendingResponses.push({resolve:e=>{for(const s of n)s.resolve(e)},reject:e=>{for(const s of n)s.reject(e)}}),this.queue.push(i),o.info(`[${this.sessionKey}] Flushed ${e.length} collected message(s) as one prompt`)}mergePrompts(e){const s=[],t=[];if(this.droppedSummaries.length>0){s.push(`[${this.droppedSummaries.length} earlier message(s) dropped due to queue cap]`);for(const e of this.droppedSummaries)s.push(`- ${e}`);s.push("")}if(1===e.length&&0===this.droppedSummaries.length)return e[0];if(e.length>0){s.push("[Queued messages while agent was busy]");for(let i=0;i<e.length;i++)e[i].text&&s.push(`${i+1}. ${e[i].text}`),t.push(...e[i].images)}return{text:s.join("\n"),images:t}}ensureInitialized(){if(this.initialized)return;this.initialized=!0;const s=this.piProviderConfig?"pi":"claudecode";o.info(`[${this.sessionKey}] Starting agent: engine=${s}, model=${this.model}, mode=${this.queueMode}, debounce=${this.debounceMs}ms, cap=${this.queueCap||"unlimited"}, drop=${this.dropPolicy}, session=${this.currentSessionId||"new"}`),this.piProviderConfig?this.initPiEngine():(this.queryHandle=e({prompt:this.queue,options:this.opts}),this.processOutput())}async initPiEngine(){try{const e=await import("../pi-agent-provider/index.js"),s=await e.createToolRegistryFromOptions(this.opts);this.queryHandle=e.piQuery({prompt:this.queue,options:this.opts},this.piProviderConfig,s),this.processOutput(),o.info(`[${this.sessionKey}] Pi engine initialized: ${this.piProviderConfig.provider}/${this.piProviderConfig.modelId}`)}catch(s){o.error(`[${this.sessionKey}] Failed to initialize Pi engine: ${s}`),o.warn(`[${this.sessionKey}] Falling back to Claude SDK (claudecode engine)`),this.queryHandle=e({prompt:this.queue,options:this.opts}),this.processOutput()}}buildQueueMessage(e){if(0===e.images.length)return o.debug(`[${this.sessionKey}] SDK request: text-only (${e.text.length} chars): ${this.config.verboseDebugLogs?e.text:e.text.slice(0,15)+"..."}`),{type:"user",message:{role:"user",content:e.text}};const s=[];for(const t of e.images)s.push({type:"image",source:{type:"base64",media_type:t.mimeType,data:t.base64}});return e.text&&s.push({type:"text",text:e.text}),o.debug(`[${this.sessionKey}] SDK request: ${s.length} block(s) [${s.map(e=>"image"===e.type?`image/${e.source.media_type}`:`text(${e.text?.length??0})`).join(", ")}]`),{type:"user",message:{role:"user",content:s}}}async processOutput(){if(this.queryHandle)try{for await(const e of this.queryHandle){if(this.closed)break;if(o.debug(`[${this.sessionKey}] SDK message: type=${e.type}, subtype=${e.subtype??"-"}, keys=${Object.keys(e).join(",")}`),"system"===e.type){const s=e,t=s.subtype;if("init"===t){const e=s.slash_commands;Array.isArray(e)&&(this.sdkSlashCommands=e.map(e=>e.replace(/^\//,"")))}if("compact_boundary"===t){const e=s.compact_metadata,t=["Context compacted."];e?.pre_tokens&&t.push(`Pre-compaction tokens: ${e.pre_tokens}.`),e?.trigger&&t.push(`Trigger: ${e.trigger}.`),this.currentResponse=t.join(" "),o.info(`[${this.sessionKey}] Compact: ${this.currentResponse}`)}else if("init"!==t&&"status"!==t){const{type:e,...i}=s;this.currentResponse=JSON.stringify(i,null,2),o.info(`[${this.sessionKey}] System message (${t??"unknown"}): ${this.currentResponse.slice(0,200)}`)}}if("assistant"===e.type){const s=e.message.content,t=s.filter(e=>"text"===e.type).map(e=>e.text).join("");t&&(this.currentResponse=t,this.pendingTextBlock=t);const i=s.map(e=>e.type).join(", ");o.debug(`[${this.sessionKey}] SDK assistant message: blocks=[${i}], text length=${t.length}: ${this.config.verboseDebugLogs?t:t.slice(0,15)+"..."}`);s.some(e=>"tool_use"===e.type)&&this.pendingTextBlock&&this.textBlockStreamer&&await this.flushPendingTextBlock();for(const e of s)if("tool_use"===e.type){const s=JSON.stringify(e.input);o.debug(`[${this.sessionKey}] Tool call: ${e.name} ${this.config.verboseDebugLogs?s:s.slice(0,100)+(s.length>100?"...":"")}`),this.toolUseNotifier&&"AskUserQuestion"!==e.name&&await this.notifyToolUse(e.name)}}if("tool_progress"===e.type){const s=e;o.debug(`[${this.sessionKey}] Tool progress: ${s.tool_name} (${s.elapsed_time_seconds}s)`)}if("result"===e.type){const s=e;let t;o.debug(`[${this.sessionKey}] SDK result: subtype=${s.subtype}, stop_reason=${s.stop_reason??"null"}, session=${s.session_id??"n/a"}, result length=${s.result?.length??0}`),"session_id"in s&&(this.currentSessionId=s.session_id),this.usageRecorder&&(void 0!==s.total_cost_usd||s.modelUsage)&&this.usageRecorder(this.sessionKey,s.total_cost_usd,s.duration_ms,s.num_turns,s.modelUsage);const i=s.stop_reason??null;if("success"===s.subtype){if(s.result)this.currentResponse=s.result;else if(!this.currentResponse&&this.pendingResponses.length<=1&&(void 0!==s.total_cost_usd||s.usage)){const e=[];if(void 0!==s.total_cost_usd&&e.push(`Total cost: $${Number(s.total_cost_usd).toFixed(4)}`),void 0!==s.duration_ms&&e.push(`Duration: ${(s.duration_ms/1e3).toFixed(1)}s`),void 0!==s.num_turns&&e.push(`Turns: ${s.num_turns}`),s.modelUsage)for(const[t,i]of Object.entries(s.modelUsage)){const s=i,o=[` ${t}:`];s.inputTokens&&o.push(`input=${s.inputTokens}`),s.outputTokens&&o.push(`output=${s.outputTokens}`),s.cacheReadInputTokens&&o.push(`cache_read=${s.cacheReadInputTokens}`),s.cacheCreationInputTokens&&o.push(`cache_create=${s.cacheCreationInputTokens}`),void 0!==s.costUSD&&o.push(`cost=$${Number(s.costUSD).toFixed(4)}`),e.push(o.join(" "))}e.length>0&&(this.currentResponse=e.join("\n"))}if(!s.result&&!this.currentResponse&&this.pendingResponses.length<=1){const e=this.piProviderConfig;o.warn(`[${this.sessionKey}] Empty response on success: provider=${e?.provider??"sdk"}, modelId=${e?.modelId??"n/a"}, stop_reason=${i}. Check provider routing and API key.`)}"refusal"===i?(o.warn(`[${this.sessionKey}] Model refused the request`),this.currentResponse||(this.currentResponse="I'm unable to fulfill this request.")):"max_tokens"===i&&o.warn(`[${this.sessionKey}] Response truncated: output token limit reached`)}else if("error_max_turns"===s.subtype)t="max_turns",o.warn(`[${this.sessionKey}] Max turns reached`);else if("error_max_budget_usd"===s.subtype)t="max_budget",o.warn(`[${this.sessionKey}] Max budget reached`);else{const e=s.errors??[];e.some(e=>e.includes("aborted"))?o.info(`[${this.sessionKey}] Request aborted (steer interrupt)`):o.error(`[${this.sessionKey}] SDK error: ${JSON.stringify(s)}`)}const n=this.pendingResponses.shift();if(n){const e=this.currentResponse||"";let s=e;this.streamedAny&&(s=this.pendingTextBlock||"",!s&&e&&e.length>this.streamedText.length&&(s=e.startsWith(this.streamedText)?e.slice(this.streamedText.length).replace(/^\n+/,""):e)),o.info(`[${this.sessionKey}] Response ready: session=${this.currentSessionId}, length=${s.length}${this.streamedAny?` (streamed, full=${e.length})`:""}`),n.resolve({response:s,fullResponse:this.streamedAny?e:void 0,sessionId:this.currentSessionId,sessionReset:!1,errorType:t,stopReason:i})}this.currentResponse="",this.pendingTextBlock="",this.streamedAny=!1,this.streamedText="","collect"===this.queueMode&&(this.collectBuffer.length>0||this.droppedResolvers.length>0)&&await this.debounceThenFlush()}}}catch(e){o.error(`[${this.sessionKey}] Output stream error: ${e}`);const s=this.pendingResponses.shift();if(s)if(this.currentSessionId){const t=e instanceof Error?e.message:String(e);o.warn(`[${this.sessionKey}] Session error (${this.currentSessionId}): ${t}`),s.resolve({response:t,sessionId:"",sessionReset:!0})}else s.reject(e instanceof Error?e:new Error(String(e)));const t=new Error("SessionAgent terminated");for(const e of this.pendingResponses)e.reject(t);for(const e of this.collectBuffer)e.reject(t);for(const e of this.droppedResolvers)e.reject(t);this.pendingResponses=[],this.collectBuffer=[],this.droppedResolvers=[],this.droppedSummaries=[]}finally{this.outputDone=!0}}}
@@ -1 +1 @@
1
- import{createLogger as e}from"../utils/logger.js";const r=e("SessionErrorHandler");export var SessionErrorType;!function(e){e.AGENT_CLOSED="AGENT_CLOSED",e.SESSION_CORRUPTED="SESSION_CORRUPTED",e.SESSION_NOT_FOUND="SESSION_NOT_FOUND",e.API_ERROR="API_ERROR",e.NETWORK_ERROR="NETWORK_ERROR",e.UNKNOWN="UNKNOWN"}(SessionErrorType||(SessionErrorType={}));export class SessionErrorHandler{static analyzeError(e,s){const o=e.message.toLowerCase();return o.includes("sessionagent closed")||o.includes("agent closed")||o.includes("closed")?(r.debug(`[${s.sessionKey}] Error type: AGENT_CLOSED`),SessionErrorType.AGENT_CLOSED):o.includes("session not found")||o.includes("invalid session")||o.includes("session does not exist")?(r.debug(`[${s.sessionKey}] Error type: SESSION_NOT_FOUND`),SessionErrorType.SESSION_NOT_FOUND):o.includes("api error")||o.includes("unauthorized")||o.includes("rate limit")?(r.debug(`[${s.sessionKey}] Error type: API_ERROR`),SessionErrorType.API_ERROR):o.includes("econnrefused")||o.includes("timeout")||o.includes("network")?(r.debug(`[${s.sessionKey}] Error type: NETWORK_ERROR`),SessionErrorType.NETWORK_ERROR):o.includes("corrupted")||o.includes("invalid state")||o.includes("resume failed")?(r.debug(`[${s.sessionKey}] Error type: SESSION_CORRUPTED`),SessionErrorType.SESSION_CORRUPTED):(r.debug(`[${s.sessionKey}] Error type: UNKNOWN - ${o}`),SessionErrorType.UNKNOWN)}static getRecoveryStrategy(e){switch(e){case SessionErrorType.AGENT_CLOSED:return{action:"retry_with_new_agent",clearSession:!1,notifyUser:!1,logLevel:"info",message:"Agent was closed (likely due to restart), retrying with new agent"};case SessionErrorType.SESSION_NOT_FOUND:case SessionErrorType.SESSION_CORRUPTED:return{action:"clear_and_retry",clearSession:!0,notifyUser:!1,logLevel:"warn",message:"Session invalid, starting fresh"};case SessionErrorType.API_ERROR:return{action:"retry_with_backoff",clearSession:!1,notifyUser:!0,logLevel:"error",message:"API error, retrying with backoff"};case SessionErrorType.NETWORK_ERROR:return{action:"retry_immediate",clearSession:!1,notifyUser:!1,logLevel:"warn",message:"Network error, retrying immediately"};default:return{action:"fallback",clearSession:!0,notifyUser:!0,logLevel:"error",message:"Unknown error, falling back to session reset"}}}}
1
+ import{createLogger as e}from"../utils/logger.js";const r=e("SessionErrorHandler");export var SessionErrorType;!function(e){e.AGENT_CLOSED="AGENT_CLOSED",e.SESSION_CORRUPTED="SESSION_CORRUPTED",e.SESSION_NOT_FOUND="SESSION_NOT_FOUND",e.API_ERROR="API_ERROR",e.NETWORK_ERROR="NETWORK_ERROR",e.UNKNOWN="UNKNOWN"}(SessionErrorType||(SessionErrorType={}));export class SessionErrorHandler{static analyzeError(e,s){const o=e.message.toLowerCase();return o.includes("sessionagent closed")||o.includes("agent closed")||o.includes("closed")?(r.debug(`[${s.sessionKey}] Error type: AGENT_CLOSED`),SessionErrorType.AGENT_CLOSED):o.includes("session not found")||o.includes("invalid session")||o.includes("session does not exist")||o.includes("no conversation found")?(r.debug(`[${s.sessionKey}] Error type: SESSION_NOT_FOUND`),SessionErrorType.SESSION_NOT_FOUND):o.includes("api error")||o.includes("unauthorized")||o.includes("rate limit")?(r.debug(`[${s.sessionKey}] Error type: API_ERROR`),SessionErrorType.API_ERROR):o.includes("econnrefused")||o.includes("timeout")||o.includes("network")?(r.debug(`[${s.sessionKey}] Error type: NETWORK_ERROR`),SessionErrorType.NETWORK_ERROR):o.includes("corrupted")||o.includes("invalid state")||o.includes("resume failed")?(r.debug(`[${s.sessionKey}] Error type: SESSION_CORRUPTED`),SessionErrorType.SESSION_CORRUPTED):(r.debug(`[${s.sessionKey}] Error type: UNKNOWN - ${o}`),SessionErrorType.UNKNOWN)}static getRecoveryStrategy(e){switch(e){case SessionErrorType.AGENT_CLOSED:return{action:"retry_with_new_agent",clearSession:!1,notifyUser:!1,logLevel:"info",message:"Agent was closed (likely due to restart), retrying with new agent"};case SessionErrorType.SESSION_NOT_FOUND:case SessionErrorType.SESSION_CORRUPTED:return{action:"clear_and_retry",clearSession:!0,notifyUser:!1,logLevel:"warn",message:"Session invalid, starting fresh"};case SessionErrorType.API_ERROR:return{action:"retry_with_backoff",clearSession:!1,notifyUser:!0,logLevel:"error",message:"API error, retrying with backoff"};case SessionErrorType.NETWORK_ERROR:return{action:"retry_immediate",clearSession:!1,notifyUser:!1,logLevel:"warn",message:"Network error, retrying immediately"};default:return{action:"fallback",clearSession:!0,notifyUser:!0,logLevel:"error",message:"Unknown error, falling back to session reset"}}}}
@@ -0,0 +1,13 @@
1
+ import type { Command, CommandContext, CommandResult } from "./command.js";
2
+ import type { NodeRegistry } from "../gateway/node-registry.js";
3
+ /**
4
+ * /debuga2ui - Send hardcoded A2UI JSONL to first available node for testing
5
+ */
6
+ export declare class DebugA2UICommand implements Command {
7
+ private nodeRegistry;
8
+ name: string;
9
+ description: string;
10
+ constructor(nodeRegistry: NodeRegistry);
11
+ execute(ctx: CommandContext): Promise<CommandResult>;
12
+ }
13
+ //# sourceMappingURL=debuga2ui.d.ts.map
@@ -0,0 +1 @@
1
+ export class DebugA2UICommand{nodeRegistry;name="debuga2ui";description="Send test A2UI surface to ElectroNode (debug rendering)";constructor(e){this.nodeRegistry=e}async execute(e){const t=this.nodeRegistry.findNodesWithCapability("a2ui");if(0===t.length)return{text:"❌ No A2UI-capable nodes connected. Connect ElectroNode first."};const i=t[0],n='{"dataModelUpdate":{"surfaceId":"debug","path":"/","contents":[{"key":"nome","valueString":""},{"key":"email","valueString":""},{"key":"note","valueString":""},{"key":"data","valueString":"2024-01-15"},{"key":"ora","valueString":"14:30"},{"key":"newsletter","valueBoolean":false},{"key":"termini","valueBoolean":true},{"key":"fattura","valueBoolean":false},{"key":"rating","valueNumber":5},{"key":"budget","valueNumber":1000}]}}\n{"surfaceUpdate":{"surfaceId":"debug","components":[{"id":"root","component":{"Column":{"children":{"explicitList":["card1","divider1","card2","divider2","button_row"]}}}},{"id":"card1","component":{"Card":{"children":{"explicitList":["title","subtitle","text_inputs"]}}}},{"id":"title","component":{"Text":{"text":{"literalString":"Form Completa — Tutti i Componenti"},"usageHint":"h1"}}},{"id":"subtitle","component":{"Text":{"text":{"literalString":"Test di rendering per A2UI v0.8"},"usageHint":"caption"}}},{"id":"text_inputs","component":{"Column":{"children":{"explicitList":["field_nome","field_email","field_note"]}}}},{"id":"field_nome","component":{"TextField":{"label":{"literalString":"Nome completo"},"text":{"path":"/nome"},"textFieldType":"shortText"}}},{"id":"field_email","component":{"TextField":{"label":{"literalString":"Email"},"text":{"path":"/email"},"textFieldType":"shortText"}}},{"id":"field_note","component":{"TextField":{"label":{"literalString":"Note"},"text":{"path":"/note"},"textFieldType":"longText"}}},{"id":"divider1","component":{"Divider":{}}},{"id":"card2","component":{"Card":{"children":{"explicitList":["datetime_section","checkbox_section","slider_section"]}}}},{"id":"datetime_section","component":{"Column":{"children":{"explicitList":["field_data","field_ora"]}}}},{"id":"field_data","component":{"DateTimeInput":{"value":{"path":"/data"},"enableDate":true,"enableTime":false}}},{"id":"field_ora","component":{"DateTimeInput":{"value":{"path":"/ora"},"enableDate":false,"enableTime":true}}},{"id":"checkbox_section","component":{"Column":{"children":{"explicitList":["check_newsletter","check_termini","check_fattura"]}}}},{"id":"check_newsletter","component":{"CheckBox":{"label":{"literalString":"Iscriviti alla newsletter"},"value":{"path":"/newsletter"}}}},{"id":"check_termini","component":{"CheckBox":{"label":{"literalString":"Accetto i termini e condizioni"},"value":{"path":"/termini"}}}},{"id":"check_fattura","component":{"CheckBox":{"label":{"literalString":"Richiedi fattura"},"value":{"path":"/fattura"}}}},{"id":"slider_section","component":{"Column":{"children":{"explicitList":["slider_rating","slider_budget"]}}}},{"id":"slider_rating","component":{"Slider":{"value":{"path":"/rating"},"minValue":1,"maxValue":10}}},{"id":"slider_budget","component":{"Slider":{"value":{"path":"/budget"},"minValue":0,"maxValue":10000}}},{"id":"divider2","component":{"Divider":{}}},{"id":"button_row","component":{"Row":{"children":{"explicitList":["button_submit","button_reset"]}}}},{"id":"button_submit","component":{"Button":{"child":"button_submit_text","action":{"name":"submit_form","context":[{"key":"nome","value":{"path":"/nome"}},{"key":"email","value":{"path":"/email"}},{"key":"note","value":{"path":"/note"}},{"key":"data","value":{"path":"/data"}},{"key":"ora","value":{"path":"/ora"}},{"key":"newsletter","value":{"path":"/newsletter"}},{"key":"termini","value":{"path":"/termini"}},{"key":"fattura","value":{"path":"/fattura"}},{"key":"rating","value":{"path":"/rating"}},{"key":"budget","value":{"path":"/budget"}}]}}}},{"id":"button_submit_text","component":{"Text":{"text":{"literalString":"Invia Tutto"}}}},{"id":"button_reset","component":{"Button":{"child":"button_reset_text","action":{"name":"reset_form","context":[]}}}},{"id":"button_reset_text","component":{"Text":{"text":{"literalString":"Reset"}}}}]}}\n{"beginRendering":{"surfaceId":"debug","root":"root"}}',a=n.split("\n").filter(e=>e.trim()).map(e=>JSON.parse(e)),o={type:"a2ui_surface",messages:a,jsonl:n.trim()};try{return i.ws.send(JSON.stringify(o)),{text:`✅ A2UI form completa inviata a ${i.displayName??i.nodeId}\n\nJSONL (${a.length} messaggi)\n\nComponenti testati:\n- Card (2 container)\n- Column & Row (layout)\n- Text (h1, caption, body)\n- TextField (3 campi: nome, email, note)\n- DateTimeInput (2 campi: data, ora)\n- Checkbox (3 opzioni: newsletter, termini, fattura)\n- Slider (2 slider: rating, budget)\n- Divider (2 separatori)\n- Button (2 azioni: submit, reset)\n\nSe vedi schermo nero o crash, controlla DevTools console.`}}catch(e){return{text:`❌ Failed to send A2UI debug surface: ${e instanceof Error?e.message:String(e)}`}}}}
@@ -0,0 +1,13 @@
1
+ import type { Command, CommandContext, CommandResult } from "./command.js";
2
+ import type { NodeRegistry } from "../gateway/node-registry.js";
3
+ /**
4
+ * /debugdynamic - Send test Dynamic UI (Three.js spinning cube) to ElectroNode
5
+ */
6
+ export declare class DebugDynamicCommand implements Command {
7
+ private nodeRegistry;
8
+ name: string;
9
+ description: string;
10
+ constructor(nodeRegistry: NodeRegistry);
11
+ execute(ctx: CommandContext): Promise<CommandResult>;
12
+ }
13
+ //# sourceMappingURL=debugdynamic.d.ts.map
@@ -0,0 +1 @@
1
+ export class DebugDynamicCommand{nodeRegistry;name="debugdynamic";description="Send test Dynamic UI with Three.js to ElectroNode";constructor(e){this.nodeRegistry=e}async execute(e){const n=this.nodeRegistry.findNodesWithCapability("plasma");if(0===n.length)return{text:"❌ No capable nodes connected. Connect ElectroNode first."};const t=n[0],o={type:"dynamic_ui",html:'\n <div style="display: flex; flex-direction: column; height: 100%; width: 100%;">\n <div id="container" style="flex: 1; width: 100%; min-height: 400px;"></div>\n <div style="padding: 16px 0; display: flex; gap: 12px; flex-wrap: wrap;">\n <button id="btn-reset" style="padding: 8px 16px; background: #e91e8c; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 14px;">Reset Rotation</button>\n <button id="btn-faster" style="padding: 8px 16px; background: #22c55e; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 14px;">Speed Up</button>\n <button id="btn-slower" style="padding: 8px 16px; background: #f59e0b; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 14px;">Slow Down</button>\n <input type="color" id="color-picker" value="#e91e8c" style="width: 60px; height: 38px; border: none; border-radius: 6px; cursor: pointer;">\n </div>\n <div style="font-size: 13px; color: #a0a0a0;">\n <p>Speed: <span id="speed-display">1.0</span>x</p>\n </div>\n </div>\n ',css:"\n html, body {\n height: 100%;\n margin: 0;\n padding: 0;\n }\n #container {\n background: linear-gradient(135deg, #1a1a1a 0%, #0f0f0f 100%);\n border-radius: 12px;\n border: 1px solid #333;\n }\n button:hover {\n opacity: 0.9;\n transform: translateY(-1px);\n }\n button:active {\n transform: translateY(0);\n }\n ",js:"\n // Import Three.js from CDN\n const script = document.createElement('script');\n script.src = 'https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js';\n script.onload = function() {\n const scene = new THREE.Scene();\n const camera = new THREE.PerspectiveCamera(75, container.offsetWidth / container.offsetHeight, 0.1, 1000);\n const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });\n renderer.setSize(container.offsetWidth, container.offsetHeight);\n container.appendChild(renderer.domElement);\n\n const geometry = new THREE.BoxGeometry(2, 2, 2);\n const material = new THREE.MeshPhongMaterial({ color: 0xe91e8c });\n const cube = new THREE.Mesh(geometry, material);\n scene.add(cube);\n\n const light = new THREE.DirectionalLight(0xffffff, 1);\n light.position.set(5, 5, 5);\n scene.add(light);\n scene.add(new THREE.AmbientLight(0x404040));\n\n camera.position.z = 5;\n\n let speed = 1.0;\n\n function animate() {\n requestAnimationFrame(animate);\n cube.rotation.x += 0.01 * speed;\n cube.rotation.y += 0.01 * speed;\n renderer.render(scene, camera);\n }\n animate();\n\n // Handle resize (fullscreen toggle)\n window.__handleResize = function() {\n const width = container.offsetWidth;\n const height = container.offsetHeight;\n camera.aspect = width / height;\n camera.updateProjectionMatrix();\n renderer.setSize(width, height);\n console.log('[Demo] Resized to', width, 'x', height);\n };\n\n // CUSTOM handlers for activities (called by auto-injected listeners)\n window.addEventListener('DOMContentLoaded', function() {\n // btn-reset activity handler\n const resetBtn = document.getElementById('btn-reset');\n if (resetBtn) {\n resetBtn.addEventListener('click', function() {\n cube.rotation.x = 0;\n cube.rotation.y = 0;\n console.log('[Demo] Reset rotation');\n });\n }\n\n // btn-faster activity handler\n const fasterBtn = document.getElementById('btn-faster');\n if (fasterBtn) {\n fasterBtn.addEventListener('click', function() {\n speed = Math.min(speed + 0.5, 5.0);\n document.getElementById('speed-display').textContent = speed.toFixed(1);\n console.log('[Demo] Speed up:', speed);\n });\n }\n\n // btn-slower activity handler\n const slowerBtn = document.getElementById('btn-slower');\n if (slowerBtn) {\n slowerBtn.addEventListener('click', function() {\n speed = Math.max(speed - 0.5, 0.1);\n document.getElementById('speed-display').textContent = speed.toFixed(1);\n console.log('[Demo] Slow down:', speed);\n });\n }\n\n // color-picker activity handler\n const colorPicker = document.getElementById('color-picker');\n if (colorPicker) {\n colorPicker.addEventListener('input', function(e) {\n material.color.set(e.target.value);\n console.log('[Demo] Color changed:', e.target.value);\n });\n }\n });\n };\n document.head.appendChild(script);\n ",activities:[{id:"btn-reset",type:"button",context:{action:"reset"}},{id:"btn-faster",type:"button",context:{action:"faster"}},{id:"btn-slower",type:"button",context:{action:"slower"}},{id:"color-picker",type:"input",context:{action:"color"}}]};try{return t.ws.send(JSON.stringify(o)),{text:`✅ Dynamic UI (Three.js cube) inviata a ${t.displayName??t.nodeId}\n\nHTML/CSS/JS renderizzato in iframe sandbox\n\nInterazioni:\n- Reset Rotation: resetta la rotazione del cubo\n- Speed Up: aumenta velocità rotazione\n- Slow Down: diminuisce velocità rotazione\n- Color Picker: cambia colore del cubo\n\nLe azioni verranno inviate al server come dynamic_ui_action quando clicchi.`}}catch(e){return{text:`❌ Failed to send Dynamic UI: ${e instanceof Error?e.message:String(e)}`}}}}
@@ -1,9 +1,12 @@
1
1
  import type { Command, CommandContext, CommandResult } from "./command.js";
2
2
  export declare class McpCommand implements Command {
3
- private getSdkCommands;
3
+ private getToolServers;
4
4
  name: string;
5
5
  description: string;
6
- constructor(getSdkCommands: () => string[]);
7
- execute(_ctx: CommandContext): Promise<CommandResult>;
6
+ constructor(getToolServers: () => Array<{
7
+ name: string;
8
+ instance: unknown;
9
+ }>);
10
+ execute(ctx: CommandContext): Promise<CommandResult>;
8
11
  }
9
12
  //# sourceMappingURL=mcp.d.ts.map
@@ -1 +1 @@
1
- export class McpCommand{getSdkCommands;name="mcp";description="List available MCP skills";constructor(s){this.getSdkCommands=s}async execute(s){const t=this.getSdkCommands().filter(s=>s.startsWith("mcp__"));if(0===t.length)return{text:"No MCP skills available. Start a conversation first or check your MCP server configuration."};const o=["MCP Skills:",""],n=new Map;for(const s of t){const t=s.split("__"),o=t.slice(1,-1).join(" / ")||"unknown",e=t[t.length-1]||s;n.has(o)||n.set(o,[]),n.get(o).push(e)}for(const[s,t]of n){o.push(`${s}:`);for(const s of t)o.push(`- ${s}`);o.push("")}return o.push("Invoke via: /mcp__<server>__<skill>"),{text:o.join("\n")}}}
1
+ export class McpCommand{getToolServers;name="mcp";description="List registered MCP tool servers and their tools (usage: /mcp [page])";constructor(e){this.getToolServers=e}async execute(e){const t=this.getToolServers();if(0===t.length)return{text:"No MCP tool servers registered."};const s=t.map(e=>{const t=e,s=t.name||"unknown",o=t.instance?._registeredTools;return{serverName:s,toolNames:o?Object.keys(o).sort():[]}});s.sort((e,t)=>e.serverName.localeCompare(t.serverName));const o=s.reduce((e,t)=>e+t.toolNames.length,0),a=e.args.trim(),r=a?Math.max(1,parseInt(a,10)):1,n=Math.ceil(s.length/4),l=Math.min(r,n),c=4*(l-1),h=Math.min(c+4,s.length),m=s.slice(c,h),p=[],u="telegram"===e.channelName,g="whatsapp"===e.channelName,i=e=>g?`*${e}*`:`**${e}**`;p.push(`${i("MCP Tool Servers")} (${l}/${n})`),p.push(`${t.length} servers, ${o} tools total`),p.push("");for(const e of m){if(0===e.toolNames.length)p.push(`${i(e.serverName)}: (no tools)`);else{p.push(`${i(e.serverName)} (${e.toolNames.length} tools):`);for(const t of e.toolNames)p.push(` - ${t}`)}p.push("")}let $;if(n>1&&p.push(`Use /mcp <page> to see other pages (1-${n})`),(u||"webchat"===e.channelName)&&n>1){const e=[];l>1&&e.push({text:"◀️ Prev",callbackData:"/mcp "+(l-1)}),e.push({text:`${l}/${n}`,callbackData:`/mcp ${l}`}),l<n&&e.push({text:"Next ▶️",callbackData:`/mcp ${l+1}`}),$=[e]}return{text:p.join("\n"),buttons:$}}}
@@ -9,6 +9,10 @@ export interface NodeSession {
9
9
  commands: string[];
10
10
  connectedAt: number;
11
11
  ws: WebSocket;
12
+ /** Timestamp of the last pong received (ms since epoch) */
13
+ lastPongAt: number;
14
+ /** Number of consecutive pings without a pong reply */
15
+ missedPongs: number;
12
16
  }
13
17
  export interface CommandResult {
14
18
  ok: boolean;
@@ -18,11 +22,29 @@ export interface CommandResult {
18
22
  export declare class NodeRegistry {
19
23
  private nodes;
20
24
  private pendingCommands;
21
- register(nodeId: string, ws: WebSocket, info: Omit<NodeSession, "nodeId" | "connectedAt" | "ws">): void;
25
+ private pingTimer;
26
+ /**
27
+ * Start the periodic WS ping loop.
28
+ * Call once at startup (e.g. from Server.start()).
29
+ */
30
+ startPingLoop(): void;
31
+ /**
32
+ * Stop the periodic ping loop.
33
+ */
34
+ stopPingLoop(): void;
35
+ register(nodeId: string, ws: WebSocket, info: Omit<NodeSession, "nodeId" | "connectedAt" | "ws" | "lastPongAt" | "missedPongs">): void;
22
36
  unregister(nodeId: string): void;
23
37
  getNode(nodeId: string): NodeSession | undefined;
24
38
  listNodes(): Array<Omit<NodeSession, "ws">>;
25
39
  getCount(): number;
40
+ /**
41
+ * Find nodes that have a specific capability.
42
+ */
43
+ findNodesWithCapability(capability: string): NodeSession[];
44
+ /**
45
+ * Check if a node appears healthy (received a pong recently).
46
+ */
47
+ isNodeHealthy(nodeId: string): boolean;
26
48
  /**
27
49
  * Send a command to a node via WebSocket and wait for the result.
28
50
  * Returns a Promise that resolves when the node sends back a command_result
@@ -34,5 +56,11 @@ export declare class NodeRegistry {
34
56
  * message arrives from a node.
35
57
  */
36
58
  handleCommandResult(id: string, result: CommandResult): void;
59
+ /**
60
+ * Also treat application-level pong messages (type: "pong") as proof of life.
61
+ * Called from Nostromo when a { type: "pong" } message is received.
62
+ */
63
+ handleApplicationPong(nodeId: string): void;
64
+ private pingAllNodes;
37
65
  }
38
66
  //# sourceMappingURL=node-registry.d.ts.map
@@ -1 +1 @@
1
- import{randomUUID as e}from"node:crypto";import{createLogger as o}from"../utils/logger.js";const t=o("NodeRegistry");export class NodeRegistry{nodes=new Map;pendingCommands=new Map;register(e,o,s){this.nodes.set(e,{nodeId:e,...s,connectedAt:Date.now(),ws:o}),t.info(`Node registered: ${e} (${s.displayName??"unnamed"})`)}unregister(e){this.nodes.delete(e)&&t.info(`Node unregistered: ${e}`)}getNode(e){return this.nodes.get(e)}listNodes(){return Array.from(this.nodes.values()).map(({ws:e,...o})=>o)}getCount(){return this.nodes.size}executeCommand(o,t,s,n){const r=this.nodes.get(o);if(!r)return Promise.resolve({ok:!1,error:`Node "${o}" not found`});if(r.ws.readyState!==r.ws.OPEN)return Promise.resolve({ok:!1,error:`Node "${o}" is not connected`});const d=e(),i=n??3e4;return new Promise(e=>{const o=setTimeout(()=>{this.pendingCommands.delete(d),e({ok:!1,error:`Command timed out after ${i}ms`})},i);this.pendingCommands.set(d,{resolve:e,timer:o}),r.ws.send(JSON.stringify({type:"command",id:d,command:t,params:s}))})}handleCommandResult(e,o){const t=this.pendingCommands.get(e);t&&(clearTimeout(t.timer),this.pendingCommands.delete(e),t.resolve(o))}}
1
+ import{randomUUID as e}from"node:crypto";import{createLogger as s}from"../utils/logger.js";const n=s("NodeRegistry");export class NodeRegistry{nodes=new Map;pendingCommands=new Map;pingTimer=null;startPingLoop(){this.pingTimer||(this.pingTimer=setInterval(()=>this.pingAllNodes(),3e4),this.pingTimer.unref(),n.info("Node ping loop started (every 30s, timeout 10s)"))}stopPingLoop(){this.pingTimer&&(clearInterval(this.pingTimer),this.pingTimer=null)}register(e,s,o){const t=Date.now();this.nodes.set(e,{nodeId:e,...o,connectedAt:t,lastPongAt:t,missedPongs:0,ws:s}),s.on("pong",()=>{const s=this.nodes.get(e);s&&(s.lastPongAt=Date.now(),s.missedPongs=0)}),n.info(`Node registered: ${e} (${o.displayName??"unnamed"})`)}unregister(e){this.nodes.delete(e)&&n.info(`Node unregistered: ${e}`)}getNode(e){return this.nodes.get(e)}listNodes(){return Array.from(this.nodes.values()).map(({ws:e,...s})=>s)}getCount(){return this.nodes.size}findNodesWithCapability(e){return Array.from(this.nodes.values()).filter(s=>s.capabilities.includes(e))}isNodeHealthy(e){const s=this.nodes.get(e);return!!s&&(s.ws.readyState===s.ws.OPEN&&s.missedPongs<2)}executeCommand(s,n,o,t){const i=this.nodes.get(s);if(!i)return Promise.resolve({ok:!1,error:`Node "${s}" not found`});if(i.ws.readyState!==i.ws.OPEN)return Promise.resolve({ok:!1,error:`Node "${s}" is not connected (WebSocket closed)`});if(i.missedPongs>=2)return Promise.resolve({ok:!1,error:`Node "${s}" is unresponsive (${i.missedPongs} missed pongs). It may be offline or sleeping.`});const r=e(),d=t??3e4;return new Promise(e=>{const s=setTimeout(()=>{this.pendingCommands.delete(r),e({ok:!1,error:`Command timed out after ${d}ms`})},d);this.pendingCommands.set(r,{resolve:e,timer:s});try{i.ws.send(JSON.stringify({type:"command",id:r,command:n,params:o}))}catch(n){clearTimeout(s),this.pendingCommands.delete(r),e({ok:!1,error:`Failed to send command: ${n instanceof Error?n.message:String(n)}`})}})}handleCommandResult(e,s){const n=this.pendingCommands.get(e);n&&(clearTimeout(n.timer),this.pendingCommands.delete(e),n.resolve(s))}handleApplicationPong(e){const s=this.nodes.get(e);s&&(s.lastPongAt=Date.now(),s.missedPongs=0)}pingAllNodes(){for(const[e,s]of this.nodes)if(s.ws.readyState===s.ws.OPEN){if(s.missedPongs++,s.missedPongs>2){const o=Math.round((Date.now()-s.lastPongAt)/1e3);n.warn(`Node ${e} (${s.displayName??"unnamed"}) unresponsive (${s.missedPongs} missed pongs, last seen ${o}s ago) — unregistering`);try{s.ws.close(1001,"Unresponsive")}catch{}this.nodes.delete(e);continue}try{s.ws.ping()}catch{n.warn(`Failed to ping node ${e} — unregistering`),this.nodes.delete(e)}}else n.warn(`Node ${e} (${s.displayName??"unnamed"}) has closed WebSocket — unregistering`),this.nodes.delete(e)}}
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import{execSync as e}from"node:child_process";import{homedir as t}from"node:os";import{resolve as o,join as s}from"node:path";import{existsSync as n,mkdirSync as a,renameSync as r,readdirSync as l,copyFileSync as i,readFileSync as c,writeFileSync as $}from"node:fs";import{createServer as u}from"node:net";import{parse as p,stringify as d}from"yaml";import*as m from"@clack/prompts";import{resolveInstallPkgDir as g,resolvePackageAsset as f}from"../utils/package-paths.js";const h="",w="",v="",b=e=>`[38;5;${e}m`,S=e=>`[48;5;${e}m`,y=218,P=205,T=199,C=200,x=197,N=196,k=161,I=204;function A(e,t){const o=e*e+t*t-1;return o*o*o-e*e*t*t*t<=0}function H(e,t){const o=Math.abs(e),s=e<0;return t>.8?o>.65?y:t>1.6*o+.4?P:s?T:I:t>.3?o>.9?P:t>1.1*o+.05?s?T:C:x:t>-.1?t>.6*o?x:N:t>-.55&&t>.9*o-.55?N:k}function W(){const e=process.cwd();console.log("");for(const e of function(){const e=[];for(let t=0;t<28;t++){const o=[];for(let e=0;e<52;e++){const s=e/51*2.6-1.3,n=1.3-t/27*2.3;o.push(A(s,n)?H(s,n):0)}e.push(o)}const t=[];for(let o=0;o<28;o+=2){let s="",n=!1;for(let t=0;t<52;t++){const a=e[o]?.[t]??0,r=e[o+1]?.[t]??0;a&&r&&a!==r?(s+=b(a)+S(r)+"▀",n=!0):(n&&(s+=h,n=!1),s+=a&&r?b(a)+"█":a?b(a)+"▀":r?b(r)+"▄":" ")}t.push(" "+s+h)}for(;t.length>0&&""===t[t.length-1].replace(/\x1b\[[0-9;]*m/g,"").trim();)t.pop();return t}())console.log(e);console.log("");for(const e of[`${b(T)}${w} ██╗ ██╗ ███████╗ ██████╗ █████╗ ${h}`,`${b(T)}${w} ██║ ██║ ██╔════╝ ██╔══██╗ ██╔══██╗${h}`,`${b(C)}${w} ███████║ █████╗ ██████╔╝ ███████║${h}`,`${b(x)}${w} ██╔══██║ ██╔══╝ ██╔══██╗ ██╔══██║${h}`,`${b(N)}${w} ██║ ██║ ███████╗ ██║ ██║ ██║ ██║${h}`,`${b(k)}${w} ╚═╝ ╚═╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝${h}`])console.log(" "+e);console.log("");let t="0.9.0",o="";try{const e=JSON.parse(c(f("package.json"),"utf-8"));t=e.version??t,o=e.codename?` [${e.codename}]`:""}catch{}console.log(` ${b(P)}${w}Welcome to Hera${h} ${v}v${t}${o} — Setup Wizard${h}`),console.log(` ${v}${"─".repeat(46)}${h}`),console.log(` ${v}📁 ${e}${h}`),console.log("")}function R(t,o){try{return e(`"${t}" serve --bg --https ${o} http://127.0.0.1:${o}`,{stdio:"inherit"}),!0}catch{return!1}}function D(e){return new Promise(t=>{const o=u();o.once("error",()=>t(!1)),o.listen(e,"127.0.0.1",()=>o.close(()=>t(!0)))})}const U=g();async function M(e){const t=s(e,"data");if(n(t)){const o=`data_backup_${function(){const e=new Date;return[e.getFullYear(),String(e.getMonth()+1).padStart(2,"0"),String(e.getDate()).padStart(2,"0"),"_",String(e.getHours()).padStart(2,"0"),String(e.getMinutes()).padStart(2,"0"),String(e.getSeconds()).padStart(2,"0")].join("")}()}`,n=s(e,o);m.log.warn(`Existing ${w}data/${h} folder found in ${w}${e}${h}\n It will be backed up to ${w}${o}${h}`);const a=await m.confirm({message:"Continue and backup existing data?"});!m.isCancel(a)&&a||(m.cancel("Setup cancelled. Your data folder was not modified."),process.exit(0)),r(t,n),m.log.success(`Backup saved to ${w}${n}${h}`)}a(t,{recursive:!0});const o=l(U).filter(e=>e.endsWith(".md")&&!e.startsWith(".")&&!e.startsWith("SYSTEM_PROMPT"));for(const e of o)i(s(U,e),s(t,e));const c=s(t,".templates");a(c,{recursive:!0});const $=l(U).filter(e=>e.startsWith("SYSTEM_PROMPT")||e.endsWith(".json"));for(const e of $)i(s(U,e),s(c,e));m.log.success(`Data provisioned: ${w}${t}${h}\n ${v}${o.length} prompt files + .templates/ (${$.length} files)${h}`)}(async function(){W(),m.intro(`${b(P)}Checking your environment...${h}`),n(U)||(m.log.error(`${w}Missing required files.${h}\n\n The ${w}./installationPkg${h} folder was not found.\n Please re-download the full Hera package and try again.`),m.outro("Setup aborted."),process.exit(1));let r="";try{r=e("claude --version",{encoding:"utf-8"}).trim()}catch{}if(r)m.log.success(`Claude Code ${v}${r}${h}`);else{m.log.error(`${w}Claude Code does not seem to be installed.${h}\n\n Hera requires Claude Code to be installed, configured\n and working before you can continue.\n\n ${b(P)}Install: ${w}https://docs.anthropic.com/en/docs/claude-code${h}`);const e=await m.confirm({message:"Continue anyway?"});!m.isCancel(e)&&e||(m.cancel("Setup cancelled. Install Claude Code and try again."),process.exit(0))}let l="basic",u=null;const g=function(){const t="/Applications/Tailscale.app/Contents/MacOS/Tailscale";try{return e(`"${t}" version`,{stdio:"ignore"}),t}catch{}try{const t=e("which tailscale",{encoding:"utf-8"}).trim();if(t)return t}catch{}return null}();if(g)if(u=function(t){try{const o=e(`"${t}" status --json`,{encoding:"utf-8"}),s=JSON.parse(o),n=s.Self??{};return{hostname:n.HostName??"unknown",dnsName:(n.DNSName??"").replace(/\.$/,""),ip:(n.TailscaleIPs??[])[0]??"",tailnet:s.MagicDNSSuffix??""}}catch{return null}}(g),u){m.log.success("Tailscale detected!"),m.note([` Hostname ${w}${u.hostname}${h}`,` Network ${w}${u.dnsName}${h}`,` IP ${w}${u.ip}${h}`,` Tailnet ${v}${u.tailnet}${h}`].join("\n"),"Your Tailscale identity");const e=await m.select({message:"Do you want to configure Tailscale Serve now?",options:[{value:"tailscale",label:"Yes, configure Tailscale now (recommended)",hint:"auto-configures Tailscale Serve for secure HTTPS access"},{value:"basic",label:"No, I'll do it later",hint:"manual setup instructions will be shown at the end"}]});m.isCancel(e)&&(m.cancel("Setup cancelled."),process.exit(0)),l=e}else{m.log.warn("Tailscale is installed but not running or not logged in.");const e=await m.confirm({message:"Continue with a basic setup?"});!m.isCancel(e)&&e||(m.cancel("Setup cancelled. Start Tailscale and try again."),process.exit(0)),m.log.info("Proceeding with basic setup.")}else{m.log.warn(`${w}Tailscale is not installed.${h}\n\n We recommend Tailscale to securely expose Hera\n to your devices without port forwarding or DNS setup.\n\n ${v}Tailscale Serve lets you access your bot from any device\n on your private network with a simple HTTPS address.${h}\n\n ${b(P)}Install: ${w}https://tailscale.com/download${h}`);const e=await m.confirm({message:"Continue with a basic setup (no Tailscale)?"});!m.isCancel(e)&&e||(m.cancel("Setup cancelled. Install Tailscale and try again."),process.exit(0)),m.log.info("Proceeding with basic setup.")}const f=process.env.GMAB_PATH,S=process.env.WORKSPACE_PATH,y=!(!f||!S),T=o(t(),"gmab"),C=o(process.cwd(),"workspace"),x=f||"/app/hera/gmab",N=S||"/app/hera/workspace",k=[{value:"basic",label:"Basic configuration",hint:`data: ${T}, workspace: ${C}`}];y&&k.push({value:"docker",label:"Docker configuration (recommended)",hint:`data: ${x}, workspace: ${N}`}),k.push({value:"custom",label:"Custom configuration",hint:"choose paths and ports manually"});const I=await m.select({message:"Which configuration profile do you want to use?",options:k});let A,H,j,O,B,E;if(m.isCancel(I)&&(m.cancel("Setup cancelled."),process.exit(0)),"docker"===I)A=x,H=N,j="0.0.0.0",O=3001,B="/nostromo",E=3002,m.note([` Data path ${w}${A}${h}`,` Workspace ${w}${H}${h}`,` Host ${w}${j}${h}`,` Nostromo port ${w}${O}${h}`,` Base path ${w}${B}${h}`,` Responses port ${w}${E}${h}`].join("\n"),"Docker configuration");else if("basic"===I){A=T,H=C,j="127.0.0.1",O=3001,B="/nostromo",E=3002;const e=await D(3001),t=await D(3002);m.note([` Data path ${w}${A}${h}`,` Workspace ${w}${H}${h}`,` Host ${w}${j}${h}`,` Nostromo port ${w}${O}${h}${e?"":` ${v}(appears in use)${h}`}`,` Base path ${w}${B}${h}`,` Responses port ${w}${E}${h}${t?"":` ${v}(appears in use)${h}`}`].join("\n"),"Basic configuration")}else{const e=y?x:T,s=await m.text({message:"Where should Hera store its data?",placeholder:e,defaultValue:e,validate(e=""){const s=o(e.replace(/^~/,t())),a=o(s,"..");if(!n(a))return`Parent folder does not exist: ${a}`}});m.isCancel(s)&&(m.cancel("Setup cancelled."),process.exit(0)),A=o(s.replace(/^~/,t())),n(A)?m.log.info(`Using existing folder: ${w}${A}${h}`):m.log.info(`Folder will be created: ${w}${A}${h}`);const a=y?N:C,r=await m.text({message:"Where should the agent workspace live?",placeholder:a,defaultValue:a,validate(e=""){const s=o(e.replace(/^~/,t())),a=o(s,"..");if(!n(a))return`Parent folder does not exist: ${a}`}});m.isCancel(r)&&(m.cancel("Setup cancelled."),process.exit(0)),H=o(r.replace(/^~/,t())),n(H)?m.log.info(`Using existing folder: ${w}${H}${h}`):m.log.info(`Folder will be created: ${w}${H}${h}`),m.log.info(`${w}Nostromo Admin UI${h} is your main entry point to Hera.\n Use it to configure channels, manage tokens, and monitor sessions.`);const l=await m.text({message:"Host for Nostromo Admin UI?",placeholder:y?"0.0.0.0":"127.0.0.1",defaultValue:y?"0.0.0.0":"127.0.0.1"});m.isCancel(l)&&(m.cancel("Setup cancelled."),process.exit(0)),j=l;const i=await D(3001),c=await m.select({message:"Port for Nostromo admin UI?",options:[{value:"3001",label:"3001"+(i?"":" (appears in use)"),hint:"default Nostromo port"},{value:"custom",label:"Custom",hint:"enter a different port"}]});if(m.isCancel(c)&&(m.cancel("Setup cancelled."),process.exit(0)),"custom"===c){const e=await m.text({message:"Enter port for Nostromo admin UI:",placeholder:"3001",validate(e=""){if(!e)return"Port is required";const t=Number(e);return!Number.isInteger(t)||t<3e3||t>65535?"Enter a valid port number (3000-65535)":void 0}});m.isCancel(e)&&(m.cancel("Setup cancelled."),process.exit(0)),O=Number(e)}else O=3001;const $=await m.text({message:"Base path for Nostromo Admin UI?",placeholder:"/nostromo",defaultValue:"/nostromo"});m.isCancel($)&&(m.cancel("Setup cancelled."),process.exit(0)),B=$.trim().replace(/\/+$/,"")||"/","/"===B||B.startsWith("/")||(B="/"+B),m.log.info(`Hera exposes an ${w}OpenAI-compatible Responses API${h}\n so chat clients can connect to your bot as if it were an OpenAI endpoint.`);const u=O+1,p=await D(u),d=await m.select({message:"Port for the Responses API?",options:[{value:"default",label:`${u}${p?"":" (appears in use)"}`,hint:"Nostromo port + 1"},{value:"custom",label:"Custom",hint:"enter a different port"}]});if(m.isCancel(d)&&(m.cancel("Setup cancelled."),process.exit(0)),"custom"===d){const e=await m.text({message:"Enter port for the Responses API:",placeholder:String(u),validate(e=""){if(!e)return"Port is required";const t=Number(e);return!Number.isInteger(t)||t<3e3||t>65535?"Enter a valid port number (3000-65535)":t===O?`Already used by Nostromo Admin UI (:${O})`:void 0}});m.isCancel(e)&&(m.cancel("Setup cancelled."),process.exit(0)),E=Number(e)}else E=u}a(A,{recursive:!0}),a(H,{recursive:!0}),await M(A);const _=m.spinner();_.start("Provisioning files..."),await async function(){const e=s(U,"config.example.yaml");n(e)&&(i(e,o("./config.yaml")),m.log.success(`Created ${w}./config.yaml${h}`));const t=s(U,".env.example");if(n(t)){const e=o("./.env");if(n(e)){const o=await m.confirm({message:".env already exists. Overwrite it?"});m.isCancel(o)||!o?m.log.info(`Kept existing ${w}./.env${h}`):(i(t,e),m.log.success(`Overwritten ${w}./.env${h}`))}else i(t,e),m.log.success(`Created ${w}./.env${h}`)}}(),function(e){const t=o("./config.yaml");if(!n(t))return;const s=c(t,"utf-8"),a=p(s);a.gmabPath=e.gmabPath,a.host=e.host,a.channels||(a.channels={}),a.channels.responses||(a.channels.responses={}),a.channels.responses.port=e.responsesPort,a.agent||(a.agent={}),a.agent.workspacePath=e.workspacePath,a.nostromo||(a.nostromo={}),a.nostromo.port=e.nostromoPort,a.nostromo.basePath=e.nostromoBasePath,$(t,d(a,{lineWidth:120}),"utf-8"),m.log.success(`Updated ${w}./config.yaml${h} with your settings`)}({gmabPath:A,host:j,workspacePath:H,nostromoPort:O,nostromoBasePath:B,responsesPort:E}),_.stop("Files provisioned.");const Y="tailscale"===l&&u?`https://${u.dnsName}:${O}${B}`:null,q=`http://${j}:${O}${B}`,F=`http://${j}:${E}/v1/responses`,V="tailscale"===l&&u?`https://${u.dnsName}:${E}/v1/responses`:null,J=`ws://${j}:${O}${B}/ws/nodes`,K="tailscale"===l&&u?`wss://${u.dnsName}:${O}${B}/ws/nodes`:null,z=Y??q;m.note(` ${w}${z}${h}`,`${b(P)} Nostromo Admin UI`);const G=[` Mode ${w}${"tailscale"===l?"Tailscale":"Basic"}${h}`,` Data path ${w}${A}${h}`,` Workspace ${w}${H}${h}`,"",` Admin UI ${w}${q}${h}`];Y&&G.push(` ${w}${Y}${h} ${v}(Tailscale)${h}`),G.push("",` Nodes WS ${w}${J}${h}`),K&&G.push(` ${w}${K}${h} ${v}(Tailscale)${h}`),G.push("",` Responses API ${w}${F}${h}`),V&&G.push(` ${w}${V}${h} ${v}(Tailscale)${h}`),m.note(G.join("\n"),"Setup summary");const L=m.spinner();L.start("Starting Hera...");try{e("npm run restart",{cwd:process.cwd(),stdio:"ignore"}),L.stop("Hera is running in background."),m.log.info(` ${v}npm run logs${h} ${v}— view logs${h}\n ${v}npm run down${h} ${v}— stop${h}\n ${v}npm run restart${h} ${v}— restart${h}\n ${v}npm run status${h} ${v}— check status${h}`)}catch{L.stop("Could not start Hera automatically."),m.log.warn(`Run ${w}npm run up${h} manually to start the service.`)}if(g&&u){const e=`tailscale serve --bg --https ${O} http://127.0.0.1:${O}`,t=`tailscale serve --bg --https ${E} http://127.0.0.1:${E}`;if("tailscale"===l){m.note([` ${w}${e}${h}`,` ${w}${t}${h}`].join("\n"),"Tailscale Serve — run manually if needed");const o=await m.confirm({message:"Do you want me to try configuring Tailscale Serve automatically?"});if(!m.isCancel(o)&&o){const e=m.spinner();e.start("Configuring Tailscale Serve...");const o=R(g,O),s=R(g,E);o&&s?(e.stop("Tailscale Serve configured."),m.log.success(`Nostromo at ${w}${Y}${h}`),m.log.success(`Responses API at ${w}https://${u.dnsName}:${E}${h}`)):o?(e.stop("Tailscale Serve partially configured."),m.log.success(`Nostromo at ${w}${Y}${h}`),m.log.warn(`Responses API failed. Run manually:\n ${w}${t}${h}`)):(e.stop("Could not configure Tailscale Serve."),m.log.warn("Run the commands shown above manually."))}else m.log.info(`Skipped automatic configuration.\n Please remember to run the commands above when ready to expose\n Nostromo (port ${w}${O}${h}) and the Responses API (port ${w}${E}${h}) through Tailscale.\n Without Tailscale Serve, these services will only be reachable on localhost.`)}else m.note([" When you're ready to expose Hera via Tailscale, run:","",` ${w}${e}${h}`,` ${w}${t}${h}`,""," This will make Hera available at:",` ${w}https://${u.dnsName}:${O}${B}${h} (Admin UI)`,` ${w}https://${u.dnsName}:${E}/v1/responses${h} (API)`].join("\n"),"Tailscale Serve — manual setup")}m.outro(`${b(P)}${w}All set!${h} Open ${w}${z}${h} to get started.`)})().catch(e=>{m.cancel(`Error: ${e.message}`),process.exit(1)});
2
+ import{execSync as e}from"node:child_process";import{homedir as t}from"node:os";import{resolve as o,join as s}from"node:path";import{existsSync as n,mkdirSync as a,cpSync as r,renameSync as i,readdirSync as l,copyFileSync as c,readFileSync as $,writeFileSync as u}from"node:fs";import{createServer as p}from"node:net";import{parse as d,stringify as m}from"yaml";import*as g from"@clack/prompts";import{resolveInstallPkgDir as f,resolvePackageAsset as h,resolveBundledDir as w}from"../utils/package-paths.js";const v="",b="",S="",y=e=>`[38;5;${e}m`,P=e=>`[48;5;${e}m`,T=218,k=205,x=199,C=200,N=197,I=196,A=161,H=204;function W(e,t){const o=e*e+t*t-1;return o*o*o-e*e*t*t*t<=0}function j(e,t){const o=Math.abs(e),s=e<0;return t>.8?o>.65?T:t>1.6*o+.4?k:s?x:H:t>.3?o>.9?k:t>1.1*o+.05?s?x:C:N:t>-.1?t>.6*o?N:I:t>-.55&&t>.9*o-.55?I:A}function R(){const e=process.cwd();console.log("");for(const e of function(){const e=[];for(let t=0;t<28;t++){const o=[];for(let e=0;e<52;e++){const s=e/51*2.6-1.3,n=1.3-t/27*2.3;o.push(W(s,n)?j(s,n):0)}e.push(o)}const t=[];for(let o=0;o<28;o+=2){let s="",n=!1;for(let t=0;t<52;t++){const a=e[o]?.[t]??0,r=e[o+1]?.[t]??0;a&&r&&a!==r?(s+=y(a)+P(r)+"▀",n=!0):(n&&(s+=v,n=!1),s+=a&&r?y(a)+"█":a?y(a)+"▀":r?y(r)+"▄":" ")}t.push(" "+s+v)}for(;t.length>0&&""===t[t.length-1].replace(/\x1b\[[0-9;]*m/g,"").trim();)t.pop();return t}())console.log(e);console.log("");for(const e of[`${y(x)}${b} ██╗ ██╗ ███████╗ ██████╗ █████╗ ${v}`,`${y(x)}${b} ██║ ██║ ██╔════╝ ██╔══██╗ ██╔══██╗${v}`,`${y(C)}${b} ███████║ █████╗ ██████╔╝ ███████║${v}`,`${y(N)}${b} ██╔══██║ ██╔══╝ ██╔══██╗ ██╔══██║${v}`,`${y(I)}${b} ██║ ██║ ███████╗ ██║ ██║ ██║ ██║${v}`,`${y(A)}${b} ╚═╝ ╚═╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝${v}`])console.log(" "+e);console.log("");let t="0.9.0",o="";try{const e=JSON.parse($(h("package.json"),"utf-8"));t=e.version??t,o=e.codename?` [${e.codename}]`:""}catch{}console.log(` ${y(k)}${b}Welcome to Hera${v} ${S}v${t}${o} — Setup Wizard${v}`),console.log(` ${S}${"─".repeat(46)}${v}`),console.log(` ${S}📁 ${e}${v}`),console.log("")}function D(t,o){try{return e(`"${t}" serve --bg --https ${o} http://127.0.0.1:${o}`,{stdio:"inherit"}),!0}catch{return!1}}function U(e){return new Promise(t=>{const o=p();o.once("error",()=>t(!1)),o.listen(e,"127.0.0.1",()=>o.close(()=>t(!0)))})}const M=f();async function O(e){const t=s(e,"data");if(n(t)){const o=`data_backup_${function(){const e=new Date;return[e.getFullYear(),String(e.getMonth()+1).padStart(2,"0"),String(e.getDate()).padStart(2,"0"),"_",String(e.getHours()).padStart(2,"0"),String(e.getMinutes()).padStart(2,"0"),String(e.getSeconds()).padStart(2,"0")].join("")}()}`,n=s(e,o);g.log.warn(`Existing ${b}data/${v} folder found in ${b}${e}${v}\n It will be backed up to ${b}${o}${v}`);const a=await g.confirm({message:"Continue and backup existing data?"});!g.isCancel(a)&&a||(g.cancel("Setup cancelled. Your data folder was not modified."),process.exit(0)),i(t,n),g.log.success(`Backup saved to ${b}${n}${v}`)}a(t,{recursive:!0});const o=l(M).filter(e=>e.endsWith(".md")&&!e.startsWith(".")&&!e.startsWith("SYSTEM_PROMPT"));for(const e of o)c(s(M,e),s(t,e));const r=s(t,".templates");a(r,{recursive:!0});const $=l(M).filter(e=>e.startsWith("SYSTEM_PROMPT")||e.endsWith(".json"));for(const e of $)c(s(M,e),s(r,e));const u=s(M,"default-jobs.json");if(n(u)){const e=s(t,"cron");a(e,{recursive:!0});const o=s(e,"jobs.json");n(o)||c(u,o)}g.log.success(`Data provisioned: ${b}${t}${v}\n ${S}${o.length} prompt files + .templates/ (${$.length} files)${v}`)}const B=["a2ui","blogwatcher","buongiorno","dreaming","humanizer","librarian","markitdown","plasma","sera","skill-creator","ssh","the-skill-guardian","unix-time","wandering","weather"];(async function(){R(),g.intro(`${y(k)}Checking your environment...${v}`),n(M)||(g.log.error(`${b}Missing required files.${v}\n\n The ${b}./installationPkg${v} folder was not found.\n Please re-download the full Hera package and try again.`),g.outro("Setup aborted."),process.exit(1));let i="";try{i=e("claude --version",{encoding:"utf-8"}).trim()}catch{}if(i)g.log.success(`Claude Code ${S}${i}${v}`);else{g.log.error(`${b}Claude Code does not seem to be installed.${v}\n\n Hera requires Claude Code to be installed, configured\n and working before you can continue.\n\n ${y(k)}Install: ${b}https://docs.anthropic.com/en/docs/claude-code${v}`);const e=await g.confirm({message:"Continue anyway?"});!g.isCancel(e)&&e||(g.cancel("Setup cancelled. Install Claude Code and try again."),process.exit(0))}let l="basic",p=null;const f=function(){const t="/Applications/Tailscale.app/Contents/MacOS/Tailscale";try{return e(`"${t}" version`,{stdio:"ignore"}),t}catch{}try{const t=e("which tailscale",{encoding:"utf-8"}).trim();if(t)return t}catch{}return null}();if(f)if(p=function(t){try{const o=e(`"${t}" status --json`,{encoding:"utf-8"}),s=JSON.parse(o),n=s.Self??{};return{hostname:n.HostName??"unknown",dnsName:(n.DNSName??"").replace(/\.$/,""),ip:(n.TailscaleIPs??[])[0]??"",tailnet:s.MagicDNSSuffix??""}}catch{return null}}(f),p){g.log.success("Tailscale detected!"),g.note([` Hostname ${b}${p.hostname}${v}`,` Network ${b}${p.dnsName}${v}`,` IP ${b}${p.ip}${v}`,` Tailnet ${S}${p.tailnet}${v}`].join("\n"),"Your Tailscale identity");const e=await g.select({message:"Do you want to configure Tailscale Serve now?",options:[{value:"tailscale",label:"Yes, configure Tailscale now (recommended)",hint:"auto-configures Tailscale Serve for secure HTTPS access"},{value:"basic",label:"No, I'll do it later",hint:"manual setup instructions will be shown at the end"}]});g.isCancel(e)&&(g.cancel("Setup cancelled."),process.exit(0)),l=e}else{g.log.warn("Tailscale is installed but not running or not logged in.");const e=await g.confirm({message:"Continue with a basic setup?"});!g.isCancel(e)&&e||(g.cancel("Setup cancelled. Start Tailscale and try again."),process.exit(0)),g.log.info("Proceeding with basic setup.")}else{g.log.warn(`${b}Tailscale is not installed.${v}\n\n We recommend Tailscale to securely expose Hera\n to your devices without port forwarding or DNS setup.\n\n ${S}Tailscale Serve lets you access your bot from any device\n on your private network with a simple HTTPS address.${v}\n\n ${y(k)}Install: ${b}https://tailscale.com/download${v}`);const e=await g.confirm({message:"Continue with a basic setup (no Tailscale)?"});!g.isCancel(e)&&e||(g.cancel("Setup cancelled. Install Tailscale and try again."),process.exit(0)),g.log.info("Proceeding with basic setup.")}const h=process.env.GMAB_PATH,P=process.env.WORKSPACE_PATH,T=!(!h||!P),x=o(t(),"gmab"),C=o(process.cwd(),"workspace"),N=h||"/app/hera/gmab",I=P||"/app/hera/workspace",A=[{value:"basic",label:"Basic configuration",hint:`data: ${x}, workspace: ${C}`}];T&&A.push({value:"docker",label:"Docker configuration (recommended)",hint:`data: ${N}, workspace: ${I}`}),A.push({value:"custom",label:"Custom configuration",hint:"choose paths and ports manually"});const H=await g.select({message:"Which configuration profile do you want to use?",options:A});let W,j,E,_,Y,q;if(g.isCancel(H)&&(g.cancel("Setup cancelled."),process.exit(0)),"docker"===H)W=N,j=I,E="0.0.0.0",_=3001,Y="/nostromo",q=3002,g.note([` Data path ${b}${W}${v}`,` Workspace ${b}${j}${v}`,` Host ${b}${E}${v}`,` Nostromo port ${b}${_}${v}`,` Base path ${b}${Y}${v}`,` Responses port ${b}${q}${v}`].join("\n"),"Docker configuration");else if("basic"===H){W=x,j=C,E="127.0.0.1",_=3001,Y="/nostromo",q=3002;const e=await U(3001),t=await U(3002);g.note([` Data path ${b}${W}${v}`,` Workspace ${b}${j}${v}`,` Host ${b}${E}${v}`,` Nostromo port ${b}${_}${v}${e?"":` ${S}(appears in use)${v}`}`,` Base path ${b}${Y}${v}`,` Responses port ${b}${q}${v}${t?"":` ${S}(appears in use)${v}`}`].join("\n"),"Basic configuration")}else{const e=T?N:x,s=await g.text({message:"Where should Hera store its data?",placeholder:e,defaultValue:e,validate(e=""){const s=o(e.replace(/^~/,t())),a=o(s,"..");if(!n(a))return`Parent folder does not exist: ${a}`}});g.isCancel(s)&&(g.cancel("Setup cancelled."),process.exit(0)),W=o(s.replace(/^~/,t())),n(W)?g.log.info(`Using existing folder: ${b}${W}${v}`):g.log.info(`Folder will be created: ${b}${W}${v}`);const a=T?I:C,r=await g.text({message:"Where should the agent workspace live?",placeholder:a,defaultValue:a,validate(e=""){const s=o(e.replace(/^~/,t())),a=o(s,"..");if(!n(a))return`Parent folder does not exist: ${a}`}});g.isCancel(r)&&(g.cancel("Setup cancelled."),process.exit(0)),j=o(r.replace(/^~/,t())),n(j)?g.log.info(`Using existing folder: ${b}${j}${v}`):g.log.info(`Folder will be created: ${b}${j}${v}`),g.log.info(`${b}Nostromo Admin UI${v} is your main entry point to Hera.\n Use it to configure channels, manage tokens, and monitor sessions.`);const i=await g.text({message:"Host for Nostromo Admin UI?",placeholder:T?"0.0.0.0":"127.0.0.1",defaultValue:T?"0.0.0.0":"127.0.0.1"});g.isCancel(i)&&(g.cancel("Setup cancelled."),process.exit(0)),E=i;const l=await U(3001),c=await g.select({message:"Port for Nostromo admin UI?",options:[{value:"3001",label:"3001"+(l?"":" (appears in use)"),hint:"default Nostromo port"},{value:"custom",label:"Custom",hint:"enter a different port"}]});if(g.isCancel(c)&&(g.cancel("Setup cancelled."),process.exit(0)),"custom"===c){const e=await g.text({message:"Enter port for Nostromo admin UI:",placeholder:"3001",validate(e=""){if(!e)return"Port is required";const t=Number(e);return!Number.isInteger(t)||t<3e3||t>65535?"Enter a valid port number (3000-65535)":void 0}});g.isCancel(e)&&(g.cancel("Setup cancelled."),process.exit(0)),_=Number(e)}else _=3001;const $=await g.text({message:"Base path for Nostromo Admin UI?",placeholder:"/nostromo",defaultValue:"/nostromo"});g.isCancel($)&&(g.cancel("Setup cancelled."),process.exit(0)),Y=$.trim().replace(/\/+$/,"")||"/","/"===Y||Y.startsWith("/")||(Y="/"+Y),g.log.info(`Hera exposes an ${b}OpenAI-compatible Responses API${v}\n so chat clients can connect to your bot as if it were an OpenAI endpoint.`);const u=_+1,p=await U(u),d=await g.select({message:"Port for the Responses API?",options:[{value:"default",label:`${u}${p?"":" (appears in use)"}`,hint:"Nostromo port + 1"},{value:"custom",label:"Custom",hint:"enter a different port"}]});if(g.isCancel(d)&&(g.cancel("Setup cancelled."),process.exit(0)),"custom"===d){const e=await g.text({message:"Enter port for the Responses API:",placeholder:String(u),validate(e=""){if(!e)return"Port is required";const t=Number(e);return!Number.isInteger(t)||t<3e3||t>65535?"Enter a valid port number (3000-65535)":t===_?`Already used by Nostromo Admin UI (:${_})`:void 0}});g.isCancel(e)&&(g.cancel("Setup cancelled."),process.exit(0)),q=Number(e)}else q=u}a(W,{recursive:!0}),a(j,{recursive:!0}),await O(W);const F=g.spinner();F.start("Provisioning files..."),await async function(){const e=s(M,"config.example.yaml");n(e)&&(c(e,o("./config.yaml")),g.log.success(`Created ${b}./config.yaml${v}`));const t=s(M,".env.example");if(n(t)){const e=o("./.env");if(n(e)){const o=await g.confirm({message:".env already exists. Overwrite it?"});g.isCancel(o)||!o?g.log.info(`Kept existing ${b}./.env${v}`):(c(t,e),g.log.success(`Overwritten ${b}./.env${v}`))}else c(t,e),g.log.success(`Created ${b}./.env${v}`)}}(),function(e){const t=w();if(!t)return;const o=s(e,".claude","skills");a(o,{recursive:!0});let i=0;for(const e of B){const a=s(t,e),l=s(o,e);n(a)&&!n(l)&&(r(a,l,{recursive:!0}),i++)}i>0&&g.log.success(`Installed ${b}${i}${v} default skill${i>1?"s":""} into ${b}${o}${v}`)}(j),function(e){const t=o("./config.yaml");if(!n(t))return;const s=$(t,"utf-8"),a=d(s);a.gmabPath=e.gmabPath,a.host=e.host,a.channels||(a.channels={}),a.channels.responses||(a.channels.responses={}),a.channels.responses.port=e.responsesPort,a.agent||(a.agent={}),a.agent.workspacePath=e.workspacePath,a.nostromo||(a.nostromo={}),a.nostromo.port=e.nostromoPort,a.nostromo.basePath=e.nostromoBasePath,u(t,m(a,{lineWidth:120}),"utf-8"),g.log.success(`Updated ${b}./config.yaml${v} with your settings`)}({gmabPath:W,host:E,workspacePath:j,nostromoPort:_,nostromoBasePath:Y,responsesPort:q}),F.stop("Files provisioned.");const V="tailscale"===l&&p?`https://${p.dnsName}:${_}${Y}`:null,z=`http://${E}:${_}${Y}`,J=`http://${E}:${q}/v1/responses`,K="tailscale"===l&&p?`https://${p.dnsName}:${q}/v1/responses`:null,G=`ws://${E}:${_}${Y}/ws/nodes`,L="tailscale"===l&&p?`wss://${p.dnsName}:${_}${Y}/ws/nodes`:null,Q=V??z;g.note(` ${b}${Q}${v}`,`${y(k)} Nostromo Admin UI`);const X=[` Mode ${b}${"tailscale"===l?"Tailscale":"Basic"}${v}`,` Data path ${b}${W}${v}`,` Workspace ${b}${j}${v}`,"",` Admin UI ${b}${z}${v}`];V&&X.push(` ${b}${V}${v} ${S}(Tailscale)${v}`),X.push("",` Nodes WS ${b}${G}${v}`),L&&X.push(` ${b}${L}${v} ${S}(Tailscale)${v}`),X.push("",` Responses API ${b}${J}${v}`),K&&X.push(` ${b}${K}${v} ${S}(Tailscale)${v}`),g.note(X.join("\n"),"Setup summary");const Z=g.spinner();Z.start("Starting Hera...");try{e("npm run restart",{cwd:process.cwd(),stdio:"ignore"}),Z.stop("Hera is running in background."),g.log.info(` ${S}npm run logs${v} ${S}— view logs${v}\n ${S}npm run down${v} ${S}— stop${v}\n ${S}npm run restart${v} ${S}— restart${v}\n ${S}npm run status${v} ${S}— check status${v}`)}catch{Z.stop("Could not start Hera automatically."),g.log.warn(`Run ${b}npm run up${v} manually to start the service.`)}if(f&&p){const e=`tailscale serve --bg --https ${_} http://127.0.0.1:${_}`,t=`tailscale serve --bg --https ${q} http://127.0.0.1:${q}`;if("tailscale"===l){g.note([` ${b}${e}${v}`,` ${b}${t}${v}`].join("\n"),"Tailscale Serve — run manually if needed");const o=await g.confirm({message:"Do you want me to try configuring Tailscale Serve automatically?"});if(!g.isCancel(o)&&o){const e=g.spinner();e.start("Configuring Tailscale Serve...");const o=D(f,_),s=D(f,q);o&&s?(e.stop("Tailscale Serve configured."),g.log.success(`Nostromo at ${b}${V}${v}`),g.log.success(`Responses API at ${b}https://${p.dnsName}:${q}${v}`)):o?(e.stop("Tailscale Serve partially configured."),g.log.success(`Nostromo at ${b}${V}${v}`),g.log.warn(`Responses API failed. Run manually:\n ${b}${t}${v}`)):(e.stop("Could not configure Tailscale Serve."),g.log.warn("Run the commands shown above manually."))}else g.log.info(`Skipped automatic configuration.\n Please remember to run the commands above when ready to expose\n Nostromo (port ${b}${_}${v}) and the Responses API (port ${b}${q}${v}) through Tailscale.\n Without Tailscale Serve, these services will only be reachable on localhost.`)}else g.note([" When you're ready to expose Hera via Tailscale, run:","",` ${b}${e}${v}`,` ${b}${t}${v}`,""," This will make Hera available at:",` ${b}https://${p.dnsName}:${_}${Y}${v} (Admin UI)`,` ${b}https://${p.dnsName}:${q}/v1/responses${v} (API)`].join("\n"),"Tailscale Serve — manual setup")}g.outro(`${y(k)}${b}All set!${v} Open ${b}${Q}${v} to get started.`)})().catch(e=>{g.cancel(`Error: ${e.message}`),process.exit(1)});