agentix-cli 0.13.0 → 0.15.0

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 (82) hide show
  1. package/README.md +65 -478
  2. package/dist/agent-LRSSVFDR.js +2 -0
  3. package/dist/board-dashboard-DAPHL6W2.js +4508 -0
  4. package/dist/board-dashboard-DAPHL6W2.js.map +1 -0
  5. package/dist/chunk-3XULOWYB.js +199 -0
  6. package/dist/chunk-3XULOWYB.js.map +1 -0
  7. package/dist/chunk-6U2XOQHU.js +7 -0
  8. package/dist/chunk-6U2XOQHU.js.map +1 -0
  9. package/dist/{chunk-ZRYBSI5G.js → chunk-7MBTMCKY.js} +2 -2
  10. package/dist/{chunk-D6EDMQRV.js → chunk-AU37OREL.js} +5 -5
  11. package/dist/chunk-B4Q4X7D5.js +6 -0
  12. package/dist/chunk-B4Q4X7D5.js.map +1 -0
  13. package/dist/chunk-CXY2RYNI.js +11 -0
  14. package/dist/chunk-CXY2RYNI.js.map +1 -0
  15. package/dist/chunk-FIOB3MEX.js +2 -0
  16. package/dist/chunk-HLRVDCNH.js +5 -0
  17. package/dist/chunk-HLRVDCNH.js.map +1 -0
  18. package/dist/chunk-IEVEMKAI.js +168 -0
  19. package/dist/chunk-IEVEMKAI.js.map +1 -0
  20. package/dist/chunk-RB7265LO.js +12 -0
  21. package/dist/chunk-RB7265LO.js.map +1 -0
  22. package/dist/chunk-RGUYBH77.js +7 -0
  23. package/dist/chunk-RGUYBH77.js.map +1 -0
  24. package/dist/chunk-WVNSWBCU.js +138 -0
  25. package/dist/chunk-WVNSWBCU.js.map +1 -0
  26. package/dist/cli.js +64 -48
  27. package/dist/cli.js.map +1 -1
  28. package/dist/{compaction-7I3E5QUO.js → compaction-JBZZ7I7Y.js} +4 -4
  29. package/dist/config-DCJYECL4.js +2 -0
  30. package/dist/config-mutator-6EJHSP3Y.js +2 -0
  31. package/dist/debug-3MEUO2P2.js +2 -0
  32. package/dist/{heal-OTGT5HHJ.js → heal-WKY5I4KB.js} +2 -2
  33. package/dist/index.d.ts +894 -70
  34. package/dist/index.js +11 -11
  35. package/dist/index.js.map +1 -1
  36. package/dist/loader-PTCXLJEX.js +2 -0
  37. package/dist/memory-extract-AD7KMRPC.js +2 -0
  38. package/dist/{providers-G3WZ4RVJ.js → providers-VUIWO3ZK.js} +2 -2
  39. package/dist/registry-5ND7GLFJ.js +2 -0
  40. package/dist/scheduler-FCXOXCN5.js +2 -0
  41. package/dist/scheduler-FCXOXCN5.js.map +1 -0
  42. package/dist/{subagent-WV7QKQ3L.js → subagent-2SJHSSJS.js} +2 -2
  43. package/dist/{usage-dashboard-2TJQBFSH.js → usage-dashboard-QX3CF6KK.js} +8 -8
  44. package/dist/usage-dashboard-QX3CF6KK.js.map +1 -0
  45. package/dist/whatsapp-state-6D6FHHD2.js +2 -0
  46. package/dist/whatsapp-state-6D6FHHD2.js.map +1 -0
  47. package/dist/workspace-setup-ZWTT42XT.js +2 -0
  48. package/dist/workspace-setup-ZWTT42XT.js.map +1 -0
  49. package/package.json +10 -1
  50. package/dist/agent-5KIMJI3F.js +0 -2
  51. package/dist/chunk-34UAFPK4.js +0 -10
  52. package/dist/chunk-34UAFPK4.js.map +0 -1
  53. package/dist/chunk-4YCH6IZV.js +0 -2
  54. package/dist/chunk-CJ45Y2IR.js +0 -12
  55. package/dist/chunk-CJ45Y2IR.js.map +0 -1
  56. package/dist/chunk-T25UEXVK.js +0 -126
  57. package/dist/chunk-T25UEXVK.js.map +0 -1
  58. package/dist/chunk-VH23CDHL.js +0 -157
  59. package/dist/chunk-VH23CDHL.js.map +0 -1
  60. package/dist/chunk-WJ6ZF6EZ.js +0 -6
  61. package/dist/chunk-WJ6ZF6EZ.js.map +0 -1
  62. package/dist/config-HAD5E4YX.js +0 -2
  63. package/dist/debug-N3B5NVJU.js +0 -2
  64. package/dist/loader-YQE6XJBT.js +0 -2
  65. package/dist/memory-extract-RGPUPMKU.js +0 -2
  66. package/dist/registry-CDWY7CLL.js +0 -2
  67. package/dist/usage-dashboard-2TJQBFSH.js.map +0 -1
  68. package/dist/workspace-setup-QQTCJY4B.js +0 -2
  69. /package/dist/{agent-5KIMJI3F.js.map → agent-LRSSVFDR.js.map} +0 -0
  70. /package/dist/{chunk-ZRYBSI5G.js.map → chunk-7MBTMCKY.js.map} +0 -0
  71. /package/dist/{chunk-D6EDMQRV.js.map → chunk-AU37OREL.js.map} +0 -0
  72. /package/dist/{chunk-4YCH6IZV.js.map → chunk-FIOB3MEX.js.map} +0 -0
  73. /package/dist/{compaction-7I3E5QUO.js.map → compaction-JBZZ7I7Y.js.map} +0 -0
  74. /package/dist/{config-HAD5E4YX.js.map → config-DCJYECL4.js.map} +0 -0
  75. /package/dist/{debug-N3B5NVJU.js.map → config-mutator-6EJHSP3Y.js.map} +0 -0
  76. /package/dist/{heal-OTGT5HHJ.js.map → debug-3MEUO2P2.js.map} +0 -0
  77. /package/dist/{loader-YQE6XJBT.js.map → heal-WKY5I4KB.js.map} +0 -0
  78. /package/dist/{memory-extract-RGPUPMKU.js.map → loader-PTCXLJEX.js.map} +0 -0
  79. /package/dist/{providers-G3WZ4RVJ.js.map → memory-extract-AD7KMRPC.js.map} +0 -0
  80. /package/dist/{registry-CDWY7CLL.js.map → providers-VUIWO3ZK.js.map} +0 -0
  81. /package/dist/{workspace-setup-QQTCJY4B.js.map → registry-5ND7GLFJ.js.map} +0 -0
  82. /package/dist/{subagent-WV7QKQ3L.js.map → subagent-2SJHSSJS.js.map} +0 -0
@@ -0,0 +1,12 @@
1
+ import{a as c,b,d as w}from"./chunk-M7HKBG3V.js";import{e as m}from"./chunk-DSYKYZMT.js";import{z as l}from"zod";var d=["pre:generate","post:generate","pre:file-write","post:file-write","pre:prompt","post:response","pre:command","on:error","pre:tool-call","post:tool-call","pre:channel-message","post:channel-message","pre:a2a-task","pre:cron-run","post:cron-run","on:gitlab-pipeline","on:gitlab-issue"],H=["pre:generate","pre:file-write","pre:prompt","post:response","pre:command","pre:tool-call","pre:channel-message","post:channel-message","pre:a2a-task","pre:cron-run"],v=l.object({name:l.string(),type:l.enum(["command","prompt","script"]),command:l.string().optional(),prompt:l.string().optional(),provider:l.enum(["claude-code","claude","openai","ollama","custom"]).optional(),model:l.string().optional(),script:l.string().optional(),priority:l.number().default(100),enabled:l.boolean().default(!0)});var x={"claude-sonnet":{input:3,output:15},"claude-opus":{input:15,output:75},"claude-haiku":{input:.25,output:1.25}};function T(a){let o=a.toLowerCase();return o.includes("opus")?"claude-opus":o.includes("haiku")?"claude-haiku":"claude-sonnet"}var h=class{steps=[];models=new Map;recordStep(o,e,t,r){let n=T(e),s=x[n]||x["claude-sonnet"],p=t/1e6*s.input+r/1e6*s.output;this.steps.push({step:o,model:e,inputTokens:t,outputTokens:r,cost:p,timestamp:Date.now()});let i=this.models.get(e)||{inputTokens:0,outputTokens:0,cost:0,steps:0};i.inputTokens+=t,i.outputTokens+=r,i.cost+=p,i.steps++,this.models.set(e,i)}getSummary(){let o=0,e=0,t=0;for(let n of this.steps)o+=n.inputTokens,e+=n.outputTokens,t+=n.cost;let r={};for(let[n,s]of this.models)r[n]={...s};return{totalInputTokens:o,totalOutputTokens:e,totalTokens:o+e,totalCost:t,steps:[...this.steps],models:r}}reset(){this.steps=[],this.models.clear()}},E=new h;function C(a,o){let e=[];if(e.push(`# Session: ${a.id}`),e.push(""),e.push(`- **Created**: ${a.createdAt}`),e.push(`- **Updated**: ${a.updatedAt}`),e.push(`- **CWD**: ${a.cwd}`),e.push(`- **Messages**: ${a.messages.length}`),e.push(`- **Tokens**: ${a.tokensUsed.toLocaleString()}`),e.push(""),a.filesGenerated.length){e.push("## Files Generated"),e.push("");for(let t of a.filesGenerated)e.push(`- \`${t}\``);e.push("")}if(a.messages.length){e.push("## Conversation"),e.push("");for(let t of a.messages){if(t.role==="system")continue;let r=t.role==="user"?"**User**":"**Assistant**";e.push(`### ${r}`),e.push(""),t.role,e.push(t.content),e.push("")}}if(o){if(e.push("## Cost Summary"),e.push(""),e.push(`- **Total tokens**: ${o.totalTokens.toLocaleString()}`),e.push(` - Input: ${o.totalInputTokens.toLocaleString()}`),e.push(` - Output: ${o.totalOutputTokens.toLocaleString()}`),e.push(`- **Total cost**: $${o.totalCost.toFixed(4)}`),e.push(""),Object.keys(o.models).length){e.push("### Per Model"),e.push(""),e.push("| Model | Input | Output | Cost | Steps |"),e.push("|-------|-------|--------|------|-------|");for(let[t,r]of Object.entries(o.models))e.push(`| ${t} | ${r.inputTokens.toLocaleString()} | ${r.outputTokens.toLocaleString()} | $${r.cost.toFixed(4)} | ${r.steps} |`);e.push("")}if(o.steps.length){e.push("### Per Step"),e.push(""),e.push("| Step | Model | Input | Output | Cost |"),e.push("|------|-------|-------|--------|------|");for(let t of o.steps)e.push(`| ${t.step} | ${t.model} | ${t.inputTokens.toLocaleString()} | ${t.outputTokens.toLocaleString()} | $${t.cost.toFixed(4)} |`);e.push("")}}return e.join(`
2
+ `)}import{execa as O}from"execa";var k=class{hooks=new Map;register(o,e){if(!e.enabled)return;let t=this.createHandler(e),r={event:o,definition:e,handler:t},n=this.hooks.get(o)||[];n.push(r),n.sort((s,p)=>s.definition.priority-p.definition.priority),this.hooks.set(o,n)}async execute(o,e){let t=this.hooks.get(o);if(!t?.length)return{};let r=H.includes(o),n={};for(let s of t){let p=Date.now();try{let i=await s.handler({...e,...n}),u=Date.now()-p;if(i.modified&&(n={...n,...i.modified}),r&&i.blocked)return m.hook(s.definition.name,u,"blocked"),{blocked:!0,message:i.message||`Blocked by hook: ${s.definition.name}`,modified:n};m.hook(s.definition.name,u,"ok")}catch(i){let u=Date.now()-p;if(m.hook(s.definition.name,u,`error: ${i.message}`),c.warn(`Hook "${s.definition.name}" failed: ${i.message}`),r)return{blocked:!0,message:`Hook "${s.definition.name}" errored: ${i.message}`}}}return{modified:Object.keys(n).length?n:void 0}}has(o){return!!this.hooks.get(o)?.length}clear(){this.hooks.clear()}createHandler(o){switch(o.type){case"command":return this.createCommandHandler(o);case"script":return this.createScriptHandler(o);case"prompt":return this.createPromptHandler(o);default:throw new Error(`Unknown hook type: ${o.type}`)}}createCommandHandler(o){return async e=>{if(!o.command)return{};let t=o.command;t=t.replace(/\{\{(\w+)\}\}/g,(n,s)=>String(e[s]??""));let r=await O("sh",["-c",t],{cwd:e.cwd||process.cwd(),reject:!1,timeout:6e4,env:process.env});return r.exitCode!==0?{blocked:!0,message:r.stderr||r.stdout||`Command hook "${o.name}" failed`}:{}}}createScriptHandler(o){return async e=>{if(!o.script)return{};let r=await import(o.script.startsWith("/")?o.script:`${e.cwd||process.cwd()}/${o.script}`),n=r.default||r.handler||r;return typeof n=="function"?await n(e):{}}}createPromptHandler(o){return async e=>{if(!o.prompt)return{};let t=o.provider||b()?.provider||"claude-code",r=w();if(!r&&t!=="claude-code")return c.warn(`Prompt hook "${o.name}" skipped: no credentials available for provider "${t}"`),{};try{let{createProvider:n}=await import("./providers-VUIWO3ZK.js"),s=n(t,r?.token),p=o.prompt;p=p.replace(/\{\{(\w+)\}\}/g,(M,$)=>String(e[$]??""));let i=`You are a hook validator for agentx. Analyze the following context and respond with ONLY a JSON object (no markdown, no code fences).
3
+
4
+ Response format:
5
+ {"action": "allow", "message": "optional explanation"}
6
+ OR
7
+ {"action": "block", "message": "reason for blocking"}
8
+ OR
9
+ {"action": "allow", "message": "optional", "modified": {"key": "new value"}}
10
+
11
+ The "modified" field is optional and can contain updated values to pass forward.`,y=(await s.generate([{role:"system",content:i},{role:"user",content:p}],{model:o.model,maxTokens:1024})).content.trim().match(/\{[\s\S]*\}/);if(!y)return c.warn(`Prompt hook "${o.name}": LLM response was not valid JSON, allowing`),{};let f=JSON.parse(y[0]);return f.action==="block"?{blocked:!0,message:f.message||`Blocked by prompt hook: ${o.name}`}:f.modified?{modified:f.modified}:{}}catch(n){return c.warn(`Prompt hook "${o.name}" failed: ${n.message}`),{}}}}};import{existsSync as S,readFileSync as R,readdirSync as D}from"fs";import g from"path";function U(a,o){j(a,o),N(a,o)}function j(a,o){let e=g.join(a,"agentx.config.json");if(S(e))try{let t=JSON.parse(R(e,"utf8"));if(!t.hooks)return;for(let[r,n]of Object.entries(t.hooks)){if(!d.includes(r)){c.warn(`Unknown hook event in config: ${r}`);continue}let s=r;if(Array.isArray(n))for(let p of n)try{let i=v.parse(p);o.register(s,i)}catch(i){c.warn(`Invalid hook definition for ${s}: ${i.message}`)}}}catch(t){c.warn(`Failed to load hooks from config: ${t.message}`)}}function N(a,o){let e=g.join(a,".agentx","hooks");if(S(e))try{let t=D(e).filter(r=>r.endsWith(".ts")||r.endsWith(".js")||r.endsWith(".mjs"));for(let r of t){let n=r.replace(/\.(ts|js|mjs)$/,"").split(".");if(n.length<2){c.warn(`Hook file "${r}" should be named <event>.<name>.<ext>`);continue}let s=n[0],p=n.slice(1).join("."),i=s.replace(/-/g,":");if(!d.includes(i)){let u=s.replace("-",":");if(!d.includes(u)){c.warn(`Unknown hook event in filename: ${r}`);continue}o.register(u,{name:p,type:"script",script:g.join(e,r),priority:100,enabled:!0});continue}o.register(i,{name:p,type:"script",script:g.join(e,r),priority:100,enabled:!0})}}catch(t){c.warn(`Failed to load hooks from directory: ${t.message}`)}}var se=new k;export{d as a,H as b,h as c,E as d,C as e,k as f,U as g,se as h};
12
+ //# sourceMappingURL=chunk-RB7265LO.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/hooks/types.ts","../src/observability/tracker.ts","../src/observability/export.ts","../src/hooks/registry.ts","../src/hooks/loader.ts","../src/hooks/index.ts"],"sourcesContent":["import { z } from \"zod\"\n\n// --- Hook system types ---\n\nexport const HOOK_EVENTS = [\n \"pre:generate\",\n \"post:generate\",\n \"pre:file-write\",\n \"post:file-write\",\n \"pre:prompt\",\n \"post:response\",\n \"pre:command\",\n \"on:error\",\n \"pre:tool-call\",\n \"post:tool-call\",\n // Daemon events\n \"pre:channel-message\",\n \"post:channel-message\",\n \"pre:a2a-task\",\n \"pre:cron-run\",\n \"post:cron-run\",\n // GitLab-specific events (fired without agent routing — side-effect hooks\n // OR routing-modifying hooks that return `modified.dispatch` to pick the\n // agent(s) to run for this event).\n \"on:gitlab-pipeline\",\n \"on:gitlab-issue\",\n] as const\n\nexport type HookEvent = (typeof HOOK_EVENTS)[number]\n\n// Which hooks can block (return { blocked: true } to cancel the operation)\nexport const BLOCKING_EVENTS: HookEvent[] = [\n \"pre:generate\",\n \"pre:file-write\",\n \"pre:prompt\",\n \"post:response\",\n \"pre:command\",\n \"pre:tool-call\",\n // Daemon blocking events\n \"pre:channel-message\",\n \"post:channel-message\",\n \"pre:a2a-task\",\n \"pre:cron-run\",\n]\n\nexport type HookType = \"command\" | \"prompt\" | \"script\"\n\nexport const hookDefinitionSchema = z.object({\n name: z.string(),\n type: z.enum([\"command\", \"prompt\", \"script\"]),\n // For \"command\" type: shell command with {{variable}} interpolation\n command: z.string().optional(),\n // For \"prompt\" type: LLM prompt template\n prompt: z.string().optional(),\n // For \"prompt\" type: override provider/model for this hook\n provider: z.enum([\"claude-code\", \"claude\", \"openai\", \"ollama\", \"custom\"]).optional(),\n model: z.string().optional(),\n // For \"script\" type: path to JS/TS file exporting a handler\n script: z.string().optional(),\n // Hook priority (lower runs first, default 100)\n priority: z.number().default(100),\n // Whether this hook is enabled\n enabled: z.boolean().default(true),\n})\n\nexport type HookDefinition = z.infer<typeof hookDefinitionSchema>\n\nexport interface HookContext {\n event: HookEvent\n // Data varies by event\n task?: string\n file?: string\n fileContent?: string\n content?: string\n command?: string\n error?: Error\n cwd?: string\n [key: string]: unknown\n}\n\nexport interface HookResult {\n // If true, the operation is blocked/cancelled\n blocked?: boolean\n // Optional message explaining why it was blocked\n message?: string\n // Modified data to pass forward (e.g., modified file content)\n modified?: Record<string, unknown>\n}\n\nexport type HookHandler = (context: HookContext) => Promise<HookResult>\n","// --- Usage tracking: tokens, cost, per-step and per-model breakdown ---\n\nexport interface StepUsage {\n step: number\n model: string\n inputTokens: number\n outputTokens: number\n cost: number\n timestamp: number\n}\n\nexport interface ModelUsage {\n inputTokens: number\n outputTokens: number\n cost: number\n steps: number\n}\n\nexport interface UsageSummary {\n totalInputTokens: number\n totalOutputTokens: number\n totalTokens: number\n totalCost: number\n steps: StepUsage[]\n models: Record<string, ModelUsage>\n}\n\n// Cost per million tokens (input / output)\nconst MODEL_PRICING: Record<string, { input: number; output: number }> = {\n \"claude-sonnet\": { input: 3, output: 15 },\n \"claude-opus\": { input: 15, output: 75 },\n \"claude-haiku\": { input: 0.25, output: 1.25 },\n}\n\nfunction getModelFamily(model: string): string {\n const lower = model.toLowerCase()\n if (lower.includes(\"opus\")) return \"claude-opus\"\n if (lower.includes(\"haiku\")) return \"claude-haiku\"\n return \"claude-sonnet\" // default\n}\n\nexport class UsageTracker {\n private steps: StepUsage[] = []\n private models: Map<string, ModelUsage> = new Map()\n\n recordStep(\n step: number,\n model: string,\n inputTokens: number,\n outputTokens: number\n ): void {\n const family = getModelFamily(model)\n const pricing = MODEL_PRICING[family] || MODEL_PRICING[\"claude-sonnet\"]\n const cost =\n (inputTokens / 1_000_000) * pricing.input +\n (outputTokens / 1_000_000) * pricing.output\n\n this.steps.push({\n step,\n model,\n inputTokens,\n outputTokens,\n cost,\n timestamp: Date.now(),\n })\n\n const existing = this.models.get(model) || {\n inputTokens: 0,\n outputTokens: 0,\n cost: 0,\n steps: 0,\n }\n existing.inputTokens += inputTokens\n existing.outputTokens += outputTokens\n existing.cost += cost\n existing.steps++\n this.models.set(model, existing)\n }\n\n getSummary(): UsageSummary {\n let totalInputTokens = 0\n let totalOutputTokens = 0\n let totalCost = 0\n\n for (const step of this.steps) {\n totalInputTokens += step.inputTokens\n totalOutputTokens += step.outputTokens\n totalCost += step.cost\n }\n\n const models: Record<string, ModelUsage> = {}\n for (const [name, usage] of this.models) {\n models[name] = { ...usage }\n }\n\n return {\n totalInputTokens,\n totalOutputTokens,\n totalTokens: totalInputTokens + totalOutputTokens,\n totalCost,\n steps: [...this.steps],\n models,\n }\n }\n\n reset(): void {\n this.steps = []\n this.models.clear()\n }\n}\n\nexport const globalTracker = new UsageTracker()\n","// --- Session export: convert a session to readable markdown ---\n\nimport type { Session } from \"@/repl/session\"\nimport type { UsageSummary } from \"./tracker\"\n\nexport function exportSession(session: Session, usage?: UsageSummary): string {\n const lines: string[] = []\n\n // Header\n lines.push(`# Session: ${session.id}`)\n lines.push(\"\")\n lines.push(`- **Created**: ${session.createdAt}`)\n lines.push(`- **Updated**: ${session.updatedAt}`)\n lines.push(`- **CWD**: ${session.cwd}`)\n lines.push(`- **Messages**: ${session.messages.length}`)\n lines.push(`- **Tokens**: ${session.tokensUsed.toLocaleString()}`)\n lines.push(\"\")\n\n // Files generated\n if (session.filesGenerated.length) {\n lines.push(\"## Files Generated\")\n lines.push(\"\")\n for (const file of session.filesGenerated) {\n lines.push(`- \\`${file}\\``)\n }\n lines.push(\"\")\n }\n\n // Conversation\n if (session.messages.length) {\n lines.push(\"## Conversation\")\n lines.push(\"\")\n for (const msg of session.messages) {\n if (msg.role === \"system\") continue\n const label = msg.role === \"user\" ? \"**User**\" : \"**Assistant**\"\n lines.push(`### ${label}`)\n lines.push(\"\")\n if (msg.role === \"assistant\") {\n lines.push(msg.content)\n } else {\n lines.push(msg.content)\n }\n lines.push(\"\")\n }\n }\n\n // Cost summary\n if (usage) {\n lines.push(\"## Cost Summary\")\n lines.push(\"\")\n lines.push(`- **Total tokens**: ${usage.totalTokens.toLocaleString()}`)\n lines.push(` - Input: ${usage.totalInputTokens.toLocaleString()}`)\n lines.push(` - Output: ${usage.totalOutputTokens.toLocaleString()}`)\n lines.push(`- **Total cost**: $${usage.totalCost.toFixed(4)}`)\n lines.push(\"\")\n\n if (Object.keys(usage.models).length) {\n lines.push(\"### Per Model\")\n lines.push(\"\")\n lines.push(\"| Model | Input | Output | Cost | Steps |\")\n lines.push(\"|-------|-------|--------|------|-------|\")\n for (const [model, mu] of Object.entries(usage.models)) {\n lines.push(\n `| ${model} | ${mu.inputTokens.toLocaleString()} | ${mu.outputTokens.toLocaleString()} | $${mu.cost.toFixed(4)} | ${mu.steps} |`\n )\n }\n lines.push(\"\")\n }\n\n if (usage.steps.length) {\n lines.push(\"### Per Step\")\n lines.push(\"\")\n lines.push(\"| Step | Model | Input | Output | Cost |\")\n lines.push(\"|------|-------|-------|--------|------|\")\n for (const step of usage.steps) {\n lines.push(\n `| ${step.step} | ${step.model} | ${step.inputTokens.toLocaleString()} | ${step.outputTokens.toLocaleString()} | $${step.cost.toFixed(4)} |`\n )\n }\n lines.push(\"\")\n }\n }\n\n return lines.join(\"\\n\")\n}\n","import { execa } from \"execa\"\nimport type { HookEvent, HookDefinition, HookContext, HookResult, HookHandler } from \"./types\"\nimport { BLOCKING_EVENTS } from \"./types\"\nimport { logger } from \"@/utils/logger\"\nimport { debug } from \"@/observability\"\nimport { loadAuthConfig, resolveToken } from \"@/utils/auth-store\"\n\n// --- Hook Registry: registration, priority ordering, execution ---\n\ninterface RegisteredHook {\n event: HookEvent\n definition: HookDefinition\n handler: HookHandler\n}\n\nexport class HookRegistry {\n private hooks: Map<HookEvent, RegisteredHook[]> = new Map()\n\n /**\n * Register a hook for an event.\n */\n register(event: HookEvent, definition: HookDefinition): void {\n if (!definition.enabled) return\n\n const handler = this.createHandler(definition)\n const entry: RegisteredHook = { event, definition, handler }\n\n const existing = this.hooks.get(event) || []\n existing.push(entry)\n // Sort by priority (lower number = runs first)\n existing.sort((a, b) => a.definition.priority - b.definition.priority)\n this.hooks.set(event, existing)\n }\n\n /**\n * Execute all hooks for an event. Returns combined result.\n * For blocking events, stops at the first hook that returns { blocked: true }.\n */\n async execute(event: HookEvent, context: HookContext): Promise<HookResult> {\n const registered = this.hooks.get(event)\n if (!registered?.length) {\n return {}\n }\n\n const canBlock = BLOCKING_EVENTS.includes(event)\n let combinedModified: Record<string, unknown> = {}\n\n for (const hook of registered) {\n const start = Date.now()\n try {\n const result = await hook.handler({ ...context, ...combinedModified })\n const duration = Date.now() - start\n\n if (result.modified) {\n combinedModified = { ...combinedModified, ...result.modified }\n }\n\n if (canBlock && result.blocked) {\n debug.hook(hook.definition.name, duration, \"blocked\")\n return {\n blocked: true,\n message: result.message || `Blocked by hook: ${hook.definition.name}`,\n modified: combinedModified,\n }\n }\n\n debug.hook(hook.definition.name, duration, \"ok\")\n } catch (error: any) {\n const duration = Date.now() - start\n debug.hook(hook.definition.name, duration, `error: ${error.message}`)\n logger.warn(`Hook \"${hook.definition.name}\" failed: ${error.message}`)\n // Non-blocking hooks swallow errors; blocking hooks propagate\n if (canBlock) {\n return {\n blocked: true,\n message: `Hook \"${hook.definition.name}\" errored: ${error.message}`,\n }\n }\n }\n }\n\n return { modified: Object.keys(combinedModified).length ? combinedModified : undefined }\n }\n\n /**\n * Check if any hooks are registered for an event.\n */\n has(event: HookEvent): boolean {\n const registered = this.hooks.get(event)\n return !!registered?.length\n }\n\n /**\n * Clear all hooks (useful for testing).\n */\n clear(): void {\n this.hooks.clear()\n }\n\n /**\n * Create a HookHandler from a HookDefinition.\n */\n private createHandler(definition: HookDefinition): HookHandler {\n switch (definition.type) {\n case \"command\":\n return this.createCommandHandler(definition)\n case \"script\":\n return this.createScriptHandler(definition)\n case \"prompt\":\n return this.createPromptHandler(definition)\n default:\n throw new Error(`Unknown hook type: ${definition.type}`)\n }\n }\n\n /**\n * Command hook: runs a shell command with {{variable}} interpolation.\n * Exit code 0 = success, non-zero = blocked (for blocking events).\n */\n private createCommandHandler(definition: HookDefinition): HookHandler {\n return async (context: HookContext): Promise<HookResult> => {\n if (!definition.command) {\n return {}\n }\n\n // Interpolate {{variable}} placeholders\n let cmd = definition.command\n cmd = cmd.replace(/\\{\\{(\\w+)\\}\\}/g, (_match, key) => {\n return String(context[key] ?? \"\")\n })\n\n const result = await execa(\"sh\", [\"-c\", cmd], {\n cwd: context.cwd || process.cwd(),\n reject: false,\n timeout: 60_000,\n env: process.env,\n })\n\n if (result.exitCode !== 0) {\n return {\n blocked: true,\n message: result.stderr || result.stdout || `Command hook \"${definition.name}\" failed`,\n }\n }\n\n return {}\n }\n }\n\n /**\n * Script hook: imports and runs a JS/TS module that exports a handler function.\n */\n private createScriptHandler(definition: HookDefinition): HookHandler {\n return async (context: HookContext): Promise<HookResult> => {\n if (!definition.script) {\n return {}\n }\n\n const scriptPath = definition.script.startsWith(\"/\")\n ? definition.script\n : `${context.cwd || process.cwd()}/${definition.script}`\n\n const mod = await import(scriptPath)\n const handler = mod.default || mod.handler || mod\n\n if (typeof handler === \"function\") {\n return await handler(context)\n }\n\n return {}\n }\n }\n\n /**\n * Prompt hook: single-turn LLM call that can block or modify the operation.\n * The prompt template supports {{variable}} interpolation from hook context.\n * The LLM must respond with JSON: { \"action\": \"allow\"|\"block\", \"message\": \"...\", \"modified\": {...} }\n */\n private createPromptHandler(definition: HookDefinition): HookHandler {\n return async (context: HookContext): Promise<HookResult> => {\n if (!definition.prompt) {\n return {}\n }\n\n // Check credentials are available (don't prompt interactively)\n const providerName = definition.provider || loadAuthConfig()?.provider || \"claude-code\"\n const resolved = resolveToken()\n if (!resolved && providerName !== \"claude-code\") {\n logger.warn(\n `Prompt hook \"${definition.name}\" skipped: no credentials available for provider \"${providerName}\"`\n )\n return {}\n }\n\n try {\n // Lazy import to avoid circular deps\n const { createProvider } = await import(\"@/agent/providers\")\n const provider = createProvider(providerName as any, resolved?.token)\n\n // Interpolate {{variable}} placeholders in the prompt template\n let prompt = definition.prompt\n prompt = prompt.replace(/\\{\\{(\\w+)\\}\\}/g, (_match, key) => {\n return String(context[key] ?? \"\")\n })\n\n const systemPrompt = `You are a hook validator for agentx. Analyze the following context and respond with ONLY a JSON object (no markdown, no code fences).\n\nResponse format:\n{\"action\": \"allow\", \"message\": \"optional explanation\"}\nOR\n{\"action\": \"block\", \"message\": \"reason for blocking\"}\nOR\n{\"action\": \"allow\", \"message\": \"optional\", \"modified\": {\"key\": \"new value\"}}\n\nThe \"modified\" field is optional and can contain updated values to pass forward.`\n\n const result = await provider.generate(\n [\n { role: \"system\", content: systemPrompt },\n { role: \"user\", content: prompt },\n ],\n {\n model: definition.model,\n maxTokens: 1024,\n }\n )\n\n // Parse the LLM response as JSON\n const content = result.content.trim()\n // Try to extract JSON from the response (handle markdown fences)\n const jsonMatch = content.match(/\\{[\\s\\S]*\\}/)\n if (!jsonMatch) {\n logger.warn(`Prompt hook \"${definition.name}\": LLM response was not valid JSON, allowing`)\n return {}\n }\n\n const parsed = JSON.parse(jsonMatch[0])\n\n if (parsed.action === \"block\") {\n return {\n blocked: true,\n message: parsed.message || `Blocked by prompt hook: ${definition.name}`,\n }\n }\n\n if (parsed.modified) {\n return { modified: parsed.modified }\n }\n\n return {}\n } catch (error: any) {\n // Don't block on hook failures — log and continue\n logger.warn(`Prompt hook \"${definition.name}\" failed: ${error.message}`)\n return {}\n }\n }\n }\n}\n","import { existsSync, readFileSync, readdirSync } from \"fs\"\nimport path from \"path\"\nimport type { HookEvent, HookDefinition } from \"./types\"\nimport { HOOK_EVENTS, hookDefinitionSchema } from \"./types\"\nimport { HookRegistry } from \"./registry\"\nimport { logger } from \"@/utils/logger\"\n\n// --- Hook Loader: load hooks from config and .agentx/hooks/ ---\n\ninterface HooksConfig {\n hooks?: Partial<Record<HookEvent, HookDefinition[]>>\n}\n\n/**\n * Load hooks from agentx.config.json and .agentx/hooks/ directory.\n */\nexport function loadHooks(cwd: string, registry: HookRegistry): void {\n loadHooksFromConfig(cwd, registry)\n loadHooksFromDirectory(cwd, registry)\n}\n\n/**\n * Load hook definitions from agentx.config.json.\n */\nfunction loadHooksFromConfig(cwd: string, registry: HookRegistry): void {\n const configPath = path.join(cwd, \"agentx.config.json\")\n if (!existsSync(configPath)) return\n\n try {\n const raw = JSON.parse(readFileSync(configPath, \"utf8\")) as HooksConfig\n if (!raw.hooks) return\n\n for (const [eventName, definitions] of Object.entries(raw.hooks)) {\n if (!HOOK_EVENTS.includes(eventName as HookEvent)) {\n logger.warn(`Unknown hook event in config: ${eventName}`)\n continue\n }\n\n const event = eventName as HookEvent\n if (!Array.isArray(definitions)) continue\n\n for (const def of definitions) {\n try {\n const parsed = hookDefinitionSchema.parse(def)\n registry.register(event, parsed)\n } catch (error: any) {\n logger.warn(`Invalid hook definition for ${event}: ${error.message}`)\n }\n }\n }\n } catch (error: any) {\n logger.warn(`Failed to load hooks from config: ${error.message}`)\n }\n}\n\n/**\n * Load hook scripts from .agentx/hooks/ directory.\n * Files are named <event>.<name>.ts (e.g., pre-file-write.format.ts).\n */\nfunction loadHooksFromDirectory(cwd: string, registry: HookRegistry): void {\n const hooksDir = path.join(cwd, \".agentx\", \"hooks\")\n if (!existsSync(hooksDir)) return\n\n try {\n const files = readdirSync(hooksDir).filter(\n (f) => f.endsWith(\".ts\") || f.endsWith(\".js\") || f.endsWith(\".mjs\")\n )\n\n for (const file of files) {\n // Parse filename: <event>.<name>.<ext>\n // Event uses dashes in filename: pre-file-write → pre:file-write\n const parts = file.replace(/\\.(ts|js|mjs)$/, \"\").split(\".\")\n if (parts.length < 2) {\n logger.warn(`Hook file \"${file}\" should be named <event>.<name>.<ext>`)\n continue\n }\n\n const eventSlug = parts[0]\n const name = parts.slice(1).join(\".\")\n\n // Convert slug to event name: pre-file-write → pre:file-write\n const eventName = eventSlug.replace(/-/g, \":\") as HookEvent\n if (!HOOK_EVENTS.includes(eventName)) {\n // Try replacing first dash only: pre-generate → pre:generate\n const altEvent = eventSlug.replace(\"-\", \":\") as HookEvent\n if (!HOOK_EVENTS.includes(altEvent)) {\n logger.warn(`Unknown hook event in filename: ${file}`)\n continue\n }\n registry.register(altEvent, {\n name,\n type: \"script\",\n script: path.join(hooksDir, file),\n priority: 100,\n enabled: true,\n })\n continue\n }\n\n registry.register(eventName, {\n name,\n type: \"script\",\n script: path.join(hooksDir, file),\n priority: 100,\n enabled: true,\n })\n }\n } catch (error: any) {\n logger.warn(`Failed to load hooks from directory: ${error.message}`)\n }\n}\n","export { HookRegistry } from \"./registry\"\nexport { loadHooks } from \"./loader\"\nexport type { HookEvent, HookDefinition, HookContext, HookResult, HookHandler } from \"./types\"\nexport { HOOK_EVENTS, BLOCKING_EVENTS } from \"./types\"\n\nimport { HookRegistry } from \"./registry\"\n\n// Singleton hook registry for the CLI process\nexport const globalHooks = new HookRegistry()\n"],"mappings":"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","names":["z","HOOK_EVENTS","BLOCKING_EVENTS","hookDefinitionSchema","MODEL_PRICING","getModelFamily","model","lower","UsageTracker","step","inputTokens","outputTokens","family","pricing","cost","existing","totalInputTokens","totalOutputTokens","totalCost","models","name","usage","globalTracker","exportSession","session","usage","lines","file","msg","label","model","mu","step","execa","HookRegistry","event","definition","handler","entry","existing","a","b","context","registered","canBlock","BLOCKING_EVENTS","combinedModified","hook","start","result","duration","debug","error","logger","cmd","_match","key","execa","mod","providerName","loadAuthConfig","resolved","resolveToken","createProvider","provider","prompt","systemPrompt","jsonMatch","parsed","existsSync","readFileSync","readdirSync","path","loadHooks","cwd","registry","loadHooksFromConfig","loadHooksFromDirectory","configPath","path","existsSync","raw","readFileSync","eventName","definitions","HOOK_EVENTS","logger","event","def","parsed","hookDefinitionSchema","error","hooksDir","files","readdirSync","f","file","parts","eventSlug","name","altEvent","globalHooks","HookRegistry"]}
@@ -0,0 +1,7 @@
1
+ import{a as v,b}from"./chunk-HLRVDCNH.js";import{a as M}from"./chunk-FIOB3MEX.js";import{writeFileSync as R,mkdirSync as m,existsSync as f}from"fs";import{resolve as h}from"path";function $(a,e){if(!e)return a;let s=e*4;return`${a}
2
+
3
+ [Response budget]
4
+ Keep your response under ~${e} tokens (~${s} chars). Be concise \u2014 this is automated batch work, not a conversation.`}function d(a,e,s){let t=[];for(let r of a.split(","))if(r==="*")for(let i=e;i<=s;i++)t.push(i);else if(r.includes("/")){let[i,n]=r.split("/"),o=parseInt(n,10),c=i==="*"?e:parseInt(i,10);for(let l=c;l<=s;l+=o)t.push(l)}else if(r.includes("-")){let[i,n]=r.split("-").map(Number);for(let o=i;o<=n;o++)t.push(o)}else t.push(parseInt(r,10));return[...new Set(t)].sort((r,i)=>r-i)}function w(a,e,s){let t=a.trim().split(/\s+/);if(t.length!==5)throw new Error(`Invalid cron: ${a}`);let r=d(t[0],0,59),i=d(t[1],0,23),n=d(t[2],1,31),o=d(t[3],1,12),c=d(t[4],0,6),l=new Intl.DateTimeFormat("en-US",{timeZone:s,year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",hour12:!1}),u=new Date(e.getTime()+6e4);u.setSeconds(0,0);let D=new Date(u.getTime()+366*24*60*60*1e3);for(;u<D;){let S=l.formatToParts(u),g=T=>parseInt(S.find(F=>F.type===T)?.value||"0",10),k=g("minute"),x=g("hour"),C=g("day"),A=g("month"),J=u.getDay();if(r.includes(k)&&i.includes(x)&&n.includes(C)&&o.includes(A)&&c.includes(J))return u;u.setTime(u.getTime()+6e4)}throw new Error(`No next run found for cron "${a}" within 1 year`)}var p=5,y=[3e4,6e4,3e5,9e5,36e5],E=class{jobs=new Map;timers=new Map;registry;hooks;runsDir;lastRunFile;running=!1;notifyCallback;log;constructor(e,s,t,r=console.error.bind(console,"[cron]")){this.registry=s,this.hooks=t,this.log=r,this.runsDir=h(process.cwd(),".agentx/cron/runs"),this.lastRunFile=h(process.cwd(),".agentx/cron/last-runs.json");for(let[i,n]of Object.entries(e.crons))this.jobs.set(i,{id:i,enabled:n.enabled,schedule:n.schedule,timezone:n.timezone,agent:n.agent,prompt:n.prompt,timeout:n.timeout,model:n.model,maxOutputTokens:n.maxOutputTokens,onError:n.onError,consecutiveErrors:0,totalRuns:0,totalFailures:0})}setNotifyCallback(e){this.notifyCallback=e}async start(){this.running=!0,f(this.runsDir)||m(this.runsDir,{recursive:!0});let e=this.detectMissedRuns();if(e.length>0){this.log(`Detected ${e.length} missed cron run(s) while daemon was down:`);for(let{jobId:s,missedAt:t}of e)this.log(` "${s}" should have run at ${t.toISOString()}`);this.executeMissedRuns(e).catch(s=>{this.log(`Error executing missed runs: ${s.message}`)})}for(let[s,t]of this.jobs){if(!t.enabled){this.log(`Job "${s}" is disabled, skipping`);continue}this.scheduleNext(s)}this.log(`${this.jobs.size} cron job(s) loaded, ${Array.from(this.jobs.values()).filter(s=>s.enabled).length} enabled`)}async stop(){this.running=!1;for(let e of this.timers.values())clearTimeout(e);this.timers.clear(),this.saveLastRuns()}scheduleNext(e){let s=this.jobs.get(e);if(!s||!s.enabled||!this.running)return;let t=this.timers.get(e);t&&clearTimeout(t);try{let r=w(s.schedule,new Date,s.timezone);s.nextRun=r,s.retryPending=!1;let i=r.getTime()-Date.now();this.log(`Job "${e}" next run: ${r.toISOString()} (in ${Math.round(i/1e3)}s)`);let n=setTimeout(()=>this.executeJob(e),i);this.timers.set(e,n)}catch(r){this.log(`Failed to schedule "${e}": ${r.message}`)}}scheduleRetry(e,s){let t=this.jobs.get(e);if(!t||!t.enabled||!this.running)return;if(s>=p){this.log(`Job "${e}" exhausted all ${p} retries, scheduling next regular run`),this.scheduleNext(e);return}let r=y[s]||y[y.length-1];t.retryPending=!0,this.log(`Job "${e}" retry ${s+1}/${p} in ${Math.round(r/1e3)}s`);let i=setTimeout(()=>this.executeJob(e,s+1),r);this.timers.set(e,i)}async executeJob(e,s=0){let t=this.jobs.get(e);if(!t||!this.running)return;let r=s>0;if(this.hooks?.has("pre:cron-run")){let n=await this.hooks.execute("pre:cron-run",{event:"pre:cron-run",jobId:e,agent:t.agent,prompt:t.prompt});if(n.blocked){this.log(`Job "${e}" blocked by hook: ${n.message}`),this.scheduleNext(e);return}}this.log(`${r?`[retry ${s}] `:""}Executing job "${e}" -> agent "${t.agent}"`);let i=new Date;t.lastRun=i,t.totalRuns++;try{let n=await this.registry.execute({message:$(t.prompt,t.maxOutputTokens),agentId:t.agent,model:t.model,context:{channel:"cron"}}),o={jobId:e,startedAt:i,completedAt:new Date,success:!n.error,response:n.content,error:n.error,duration:n.duration||Date.now()-i.getTime(),isRetry:r,retryAttempt:s};if(n.error){if(t.consecutiveErrors++,t.totalFailures++,t.lastError=n.error,this.log(`Job "${e}" failed (${t.consecutiveErrors} consecutive): ${n.error}`),await this.notifyFailure(t,n.error),t.onError.includes("disable")&&t.consecutiveErrors>=3){t.enabled=!1,this.log(`Job "${e}" disabled after ${t.consecutiveErrors} consecutive errors`);try{let c=await v(l=>b(l,`crons.${e}.enabled`,!1),{reload:!1});c.success||this.log(`Failed to persist disable for "${e}": ${c.error}`)}catch(c){this.log(`Failed to persist disable for "${e}": ${c.message}`)}await this.notifyDisabled(t)}this.logRun(o),this.scheduleRetry(e,s);return}else t.consecutiveErrors>0&&this.log(`Job "${e}" recovered after ${t.consecutiveErrors} failure(s)`),t.consecutiveErrors=0,t.lastSuccess=new Date,t.lastError=void 0,t.retryPending=!1,this.log(`Job "${e}" completed in ${o.duration}ms`);this.logRun(o),this.hooks?.has("post:cron-run")&&await this.hooks.execute("post:cron-run",{event:"post:cron-run",jobId:e,success:o.success,duration:o.duration,error:o.error?new Error(o.error):void 0})}catch(n){t.consecutiveErrors++,t.totalFailures++,t.lastError=n.message,this.log(`Job "${e}" threw: ${n.message}`),await this.notifyFailure(t,n.message),this.scheduleRetry(e,s);return}this.saveLastRuns(),this.scheduleNext(e)}detectMissedRuns(){let e=[],s=this.loadLastRuns();if(!s)return e;for(let[t,r]of this.jobs){if(!r.enabled)continue;let i=s[t];if(!i)continue;let n=new Date(i);try{let o=w(r.schedule,n,r.timezone);o.getTime()<Date.now()&&e.push({jobId:t,missedAt:o})}catch{}}return e}async executeMissedRuns(e){for(let{jobId:s,missedAt:t}of e){let r=this.jobs.get(s);if(!(!r||!r.enabled)){this.log(`Running missed job "${s}" (was due at ${t.toISOString()})`);try{let i=await this.registry.execute({message:$(`[MISSED RUN \u2014 was scheduled for ${t.toISOString()}]
5
+
6
+ ${r.prompt}`,r.maxOutputTokens),agentId:r.agent,context:{channel:"cron"}}),n={jobId:s,startedAt:new Date,completedAt:new Date,success:!i.error,response:i.content,error:i.error,duration:i.duration||0,isRetry:!1};this.logRun(n),i.error?this.log(`Missed job "${s}" failed: ${i.error}`):(this.log(`Missed job "${s}" completed`),r.lastSuccess=new Date),r.lastRun=new Date,r.totalRuns++}catch(i){this.log(`Missed job "${s}" threw: ${i.message}`)}}}this.saveLastRuns()}async notifyFailure(e,s){if(!(!e.onError.includes("notify")&&e.consecutiveErrors<2)){if(!this.notifyCallback){this.log(`[ALERT] Cron "${e.id}" failed (${e.consecutiveErrors}x): ${s.slice(0,200)}`);return}try{await this.notifyCallback(e.id,e.agent,s,e.consecutiveErrors)}catch{}}}async notifyDisabled(e){if(!this.notifyCallback){this.log(`[ALERT] Cron "${e.id}" has been DISABLED after ${e.consecutiveErrors} consecutive failures`);return}try{await this.notifyCallback(e.id,e.agent,`AUTO-DISABLED after ${e.consecutiveErrors} consecutive failures. Last error: ${e.lastError||"unknown"}`,e.consecutiveErrors)}catch{}}saveLastRuns(){try{let e={};for(let[t,r]of this.jobs)r.lastRun&&(e[t]=r.lastRun.toISOString());let s=h(process.cwd(),".agentx/cron");f(s)||m(s,{recursive:!0}),R(this.lastRunFile,JSON.stringify(e,null,2))}catch{}}loadLastRuns(){try{if(!f(this.lastRunFile))return null;let{readFileSync:e}=M("fs");return JSON.parse(e(this.lastRunFile,"utf-8"))}catch{return null}}logRun(e){try{let s=h(this.runsDir,e.jobId);f(s)||m(s,{recursive:!0});let t=`${e.startedAt.toISOString().replace(/[:.]/g,"-")}.json`;R(h(s,t),JSON.stringify(e,null,2))}catch{}}list(){return Array.from(this.jobs.values())}health(){let e=Array.from(this.jobs.values()),s=this.detectMissedRuns();return{healthy:e.filter(t=>t.enabled&&t.consecutiveErrors===0).length,failing:e.filter(t=>t.enabled&&t.consecutiveErrors>0).length,disabled:e.filter(t=>!t.enabled).length,missed:s.length,jobs:e.map(t=>({id:t.id,status:t.enabled?t.retryPending?"retrying":t.consecutiveErrors>0?"failing":"healthy":"disabled",consecutiveErrors:t.consecutiveErrors,lastError:t.lastError}))}}};export{w as a,E as b};
7
+ //# sourceMappingURL=chunk-RGUYBH77.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/crons/scheduler.ts"],"sourcesContent":["import type { DaemonConfig, CronJobDef } from \"@/daemon/config\"\nimport type { AgentRegistry } from \"@/agents/registry\"\nimport type { HookRegistry } from \"@/hooks\"\nimport type { CronJobState, CronRunResult } from \"./types\"\nimport { writeFileSync, mkdirSync, existsSync } from \"fs\"\nimport { resolve } from \"path\"\nimport { applyConfigMutation, setAtPath } from \"@/daemon/config-mutator\"\n\n// --- Cron Scheduler: lightweight cron engine with timezone support ---\n// No external dependencies — uses setTimeout-based scheduling.\n\n/**\n * Append a soft output-length cap to a cron prompt when the job configured\n * `maxOutputTokens`. Claude Code CLI has no hard flag for output length,\n * but a clear instruction at the end of the prompt is reliably honored by\n * the model. Keeps a cheap knob available for operators without us needing\n * to invent our own token counter or hack a post-response truncate.\n */\nfunction withOutputCap(prompt: string, maxOutputTokens?: number): string {\n if (!maxOutputTokens) return prompt\n const approxChars = maxOutputTokens * 4\n return `${prompt}\\n\\n[Response budget]\\nKeep your response under ~${maxOutputTokens} tokens (~${approxChars} chars). Be concise — this is automated batch work, not a conversation.`\n}\n\n/**\n * Parse a cron expression into next-fire timestamp.\n * Supports standard 5-field format: minute hour day-of-month month day-of-week\n */\nfunction parseCronField(field: string, min: number, max: number): number[] {\n const values: number[] = []\n\n for (const part of field.split(\",\")) {\n if (part === \"*\") {\n for (let i = min; i <= max; i++) values.push(i)\n } else if (part.includes(\"/\")) {\n const [range, stepStr] = part.split(\"/\")\n const step = parseInt(stepStr, 10)\n const start = range === \"*\" ? min : parseInt(range, 10)\n for (let i = start; i <= max; i += step) values.push(i)\n } else if (part.includes(\"-\")) {\n const [a, b] = part.split(\"-\").map(Number)\n for (let i = a; i <= b; i++) values.push(i)\n } else {\n values.push(parseInt(part, 10))\n }\n }\n\n return [...new Set(values)].sort((a, b) => a - b)\n}\n\nexport function getNextCronDate(expression: string, after: Date, timezone: string): Date {\n const fields = expression.trim().split(/\\s+/)\n if (fields.length !== 5) throw new Error(`Invalid cron: ${expression}`)\n\n const minutes = parseCronField(fields[0], 0, 59)\n const hours = parseCronField(fields[1], 0, 23)\n const daysOfMonth = parseCronField(fields[2], 1, 31)\n const months = parseCronField(fields[3], 1, 12)\n const daysOfWeek = parseCronField(fields[4], 0, 6)\n\n // Convert to timezone-aware date\n const fmt = new Intl.DateTimeFormat(\"en-US\", {\n timeZone: timezone,\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n hour12: false,\n })\n\n // Start searching from next minute\n const candidate = new Date(after.getTime() + 60_000)\n candidate.setSeconds(0, 0)\n\n // Search up to 1 year ahead\n const limit = new Date(candidate.getTime() + 366 * 24 * 60 * 60 * 1000)\n\n while (candidate < limit) {\n const parts = fmt.formatToParts(candidate)\n const get = (type: string) => parseInt(parts.find((p) => p.type === type)?.value || \"0\", 10)\n\n const m = get(\"minute\")\n const h = get(\"hour\")\n const dom = get(\"day\")\n const mon = get(\"month\")\n const dow = candidate.getDay()\n\n if (\n minutes.includes(m) &&\n hours.includes(h) &&\n daysOfMonth.includes(dom) &&\n months.includes(mon) &&\n daysOfWeek.includes(dow)\n ) {\n return candidate\n }\n\n // Advance by 1 minute\n candidate.setTime(candidate.getTime() + 60_000)\n }\n\n throw new Error(`No next run found for cron \"${expression}\" within 1 year`)\n}\n\n// Retry config (inspired by OpenClaw's exponential backoff)\nconst MAX_RETRIES = 5\nconst RETRY_DELAYS = [30_000, 60_000, 300_000, 900_000, 3_600_000] // 30s, 1m, 5m, 15m, 60m\n\n/** Notification callback — injected by daemon to send alerts via channels */\nexport type CronNotifyCallback = (jobId: string, agent: string, error: string, consecutiveErrors: number) => Promise<void>\n\nexport class CronScheduler {\n private jobs: Map<string, CronJobState> = new Map()\n private timers: Map<string, ReturnType<typeof setTimeout>> = new Map()\n private registry: AgentRegistry\n private hooks?: HookRegistry\n private runsDir: string\n private lastRunFile: string\n private running = false\n private notifyCallback?: CronNotifyCallback\n private log: (...args: unknown[]) => void\n\n constructor(\n config: DaemonConfig,\n registry: AgentRegistry,\n hooks?: HookRegistry,\n log: (...args: unknown[]) => void = console.error.bind(console, \"[cron]\"),\n ) {\n this.registry = registry\n this.hooks = hooks\n this.log = log\n this.runsDir = resolve(process.cwd(), \".agentx/cron/runs\")\n this.lastRunFile = resolve(process.cwd(), \".agentx/cron/last-runs.json\")\n\n for (const [id, def] of Object.entries(config.crons)) {\n this.jobs.set(id, {\n id,\n enabled: def.enabled,\n schedule: def.schedule,\n timezone: def.timezone,\n agent: def.agent,\n prompt: def.prompt,\n timeout: def.timeout,\n model: def.model,\n maxOutputTokens: def.maxOutputTokens,\n onError: def.onError,\n consecutiveErrors: 0,\n totalRuns: 0,\n totalFailures: 0,\n })\n }\n }\n\n /**\n * Set a notification callback for failed crons.\n * Called when onError is \"notify\" or after multiple consecutive failures.\n */\n setNotifyCallback(cb: CronNotifyCallback): void {\n this.notifyCallback = cb\n }\n\n async start(): Promise<void> {\n this.running = true\n\n if (!existsSync(this.runsDir)) {\n mkdirSync(this.runsDir, { recursive: true })\n }\n\n // Detect missed runs before scheduling\n const missedRuns = this.detectMissedRuns()\n if (missedRuns.length > 0) {\n this.log(`Detected ${missedRuns.length} missed cron run(s) while daemon was down:`)\n for (const { jobId, missedAt } of missedRuns) {\n this.log(` \"${jobId}\" should have run at ${missedAt.toISOString()}`)\n }\n // Execute missed runs (fire-and-forget, don't block startup)\n this.executeMissedRuns(missedRuns).catch((e) => {\n this.log(`Error executing missed runs: ${e.message}`)\n })\n }\n\n for (const [id, job] of this.jobs) {\n if (!job.enabled) {\n this.log(`Job \"${id}\" is disabled, skipping`)\n continue\n }\n this.scheduleNext(id)\n }\n\n this.log(`${this.jobs.size} cron job(s) loaded, ${Array.from(this.jobs.values()).filter((j) => j.enabled).length} enabled`)\n }\n\n async stop(): Promise<void> {\n this.running = false\n for (const timer of this.timers.values()) {\n clearTimeout(timer)\n }\n this.timers.clear()\n\n // Persist last run times for missed run detection on next start\n this.saveLastRuns()\n }\n\n private scheduleNext(jobId: string): void {\n const job = this.jobs.get(jobId)\n if (!job || !job.enabled || !this.running) return\n\n // Clear any existing timer\n const existing = this.timers.get(jobId)\n if (existing) clearTimeout(existing)\n\n try {\n const nextRun = getNextCronDate(job.schedule, new Date(), job.timezone)\n job.nextRun = nextRun\n job.retryPending = false\n const delay = nextRun.getTime() - Date.now()\n\n this.log(`Job \"${jobId}\" next run: ${nextRun.toISOString()} (in ${Math.round(delay / 1000)}s)`)\n\n const timer = setTimeout(() => this.executeJob(jobId), delay)\n this.timers.set(jobId, timer)\n } catch (e: any) {\n this.log(`Failed to schedule \"${jobId}\": ${e.message}`)\n }\n }\n\n private scheduleRetry(jobId: string, attempt: number): void {\n const job = this.jobs.get(jobId)\n if (!job || !job.enabled || !this.running) return\n if (attempt >= MAX_RETRIES) {\n this.log(`Job \"${jobId}\" exhausted all ${MAX_RETRIES} retries, scheduling next regular run`)\n this.scheduleNext(jobId)\n return\n }\n\n const delay = RETRY_DELAYS[attempt] || RETRY_DELAYS[RETRY_DELAYS.length - 1]\n job.retryPending = true\n\n this.log(`Job \"${jobId}\" retry ${attempt + 1}/${MAX_RETRIES} in ${Math.round(delay / 1000)}s`)\n\n const timer = setTimeout(() => this.executeJob(jobId, attempt + 1), delay)\n this.timers.set(jobId, timer)\n }\n\n private async executeJob(jobId: string, retryAttempt: number = 0): Promise<void> {\n const job = this.jobs.get(jobId)\n if (!job || !this.running) return\n\n const isRetry = retryAttempt > 0\n\n // Pre-hook\n if (this.hooks?.has(\"pre:cron-run\" as any)) {\n const hookResult = await this.hooks.execute(\"pre:cron-run\" as any, {\n event: \"pre:cron-run\" as any,\n jobId,\n agent: job.agent,\n prompt: job.prompt,\n })\n if (hookResult.blocked) {\n this.log(`Job \"${jobId}\" blocked by hook: ${hookResult.message}`)\n this.scheduleNext(jobId)\n return\n }\n }\n\n this.log(`${isRetry ? `[retry ${retryAttempt}] ` : \"\"}Executing job \"${jobId}\" -> agent \"${job.agent}\"`)\n const startedAt = new Date()\n job.lastRun = startedAt\n job.totalRuns++\n\n try {\n const response = await this.registry.execute({\n message: withOutputCap(job.prompt, job.maxOutputTokens),\n agentId: job.agent,\n model: job.model,\n context: { channel: \"cron\" },\n })\n\n const result: CronRunResult = {\n jobId,\n startedAt,\n completedAt: new Date(),\n success: !response.error,\n response: response.content,\n error: response.error,\n duration: response.duration || Date.now() - startedAt.getTime(),\n isRetry,\n retryAttempt,\n }\n\n if (response.error) {\n job.consecutiveErrors++\n job.totalFailures++\n job.lastError = response.error\n this.log(`Job \"${jobId}\" failed (${job.consecutiveErrors} consecutive): ${response.error}`)\n\n // Notify on failure\n await this.notifyFailure(job, response.error)\n\n if (job.onError.includes(\"disable\") && job.consecutiveErrors >= 3) {\n job.enabled = false\n this.log(`Job \"${jobId}\" disabled after ${job.consecutiveErrors} consecutive errors`)\n // Persist to agentx.json so a daemon restart doesn't resurrect the\n // broken cron (previously this was in-memory only — a repeatedly\n // failing cron would keep burning money across restarts).\n try {\n const res = await applyConfigMutation(\n (cfg) => setAtPath(cfg, `crons.${jobId}.enabled`, false),\n { reload: false },\n )\n if (!res.success) {\n this.log(`Failed to persist disable for \"${jobId}\": ${res.error}`)\n }\n } catch (e: any) {\n this.log(`Failed to persist disable for \"${jobId}\": ${e.message}`)\n }\n await this.notifyDisabled(job)\n }\n\n // Retry\n this.logRun(result)\n this.scheduleRetry(jobId, retryAttempt)\n return\n } else {\n // Success — reset error state\n if (job.consecutiveErrors > 0) {\n this.log(`Job \"${jobId}\" recovered after ${job.consecutiveErrors} failure(s)`)\n }\n job.consecutiveErrors = 0\n job.lastSuccess = new Date()\n job.lastError = undefined\n job.retryPending = false\n this.log(`Job \"${jobId}\" completed in ${result.duration}ms`)\n }\n\n // Log run\n this.logRun(result)\n\n // Post-hook\n if (this.hooks?.has(\"post:cron-run\" as any)) {\n await this.hooks.execute(\"post:cron-run\" as any, {\n event: \"post:cron-run\" as any,\n jobId,\n success: result.success,\n duration: result.duration,\n error: result.error ? new Error(result.error) : undefined,\n })\n }\n } catch (e: any) {\n job.consecutiveErrors++\n job.totalFailures++\n job.lastError = e.message\n this.log(`Job \"${jobId}\" threw: ${e.message}`)\n\n await this.notifyFailure(job, e.message)\n this.scheduleRetry(jobId, retryAttempt)\n return\n }\n\n // Persist last run time\n this.saveLastRuns()\n\n // Schedule next regular run\n this.scheduleNext(jobId)\n }\n\n // --- Missed run detection ---\n\n /**\n * Detect cron jobs that should have fired while the daemon was down.\n * Compares saved last-run times against the cron schedule.\n */\n private detectMissedRuns(): Array<{ jobId: string; missedAt: Date }> {\n const missed: Array<{ jobId: string; missedAt: Date }> = []\n\n const lastRuns = this.loadLastRuns()\n if (!lastRuns) return missed // First boot — no history\n\n for (const [jobId, job] of this.jobs) {\n if (!job.enabled) continue\n\n const lastRunStr = lastRuns[jobId]\n if (!lastRunStr) continue // Never ran before\n\n const lastRun = new Date(lastRunStr)\n try {\n // What SHOULD the next run have been after the last recorded run?\n const shouldHaveRun = getNextCronDate(job.schedule, lastRun, job.timezone)\n if (shouldHaveRun.getTime() < Date.now()) {\n missed.push({ jobId, missedAt: shouldHaveRun })\n }\n } catch {\n // Invalid schedule — skip\n }\n }\n\n return missed\n }\n\n /**\n * Execute missed runs (catch-up). Runs sequentially to avoid overwhelming agents.\n */\n private async executeMissedRuns(missed: Array<{ jobId: string; missedAt: Date }>): Promise<void> {\n for (const { jobId, missedAt } of missed) {\n const job = this.jobs.get(jobId)\n if (!job || !job.enabled) continue\n\n this.log(`Running missed job \"${jobId}\" (was due at ${missedAt.toISOString()})`)\n\n try {\n const response = await this.registry.execute({\n message: withOutputCap(\n `[MISSED RUN — was scheduled for ${missedAt.toISOString()}]\\n\\n${job.prompt}`,\n job.maxOutputTokens,\n ),\n agentId: job.agent,\n context: { channel: \"cron\" },\n })\n\n const result: CronRunResult = {\n jobId,\n startedAt: new Date(),\n completedAt: new Date(),\n success: !response.error,\n response: response.content,\n error: response.error,\n duration: response.duration || 0,\n isRetry: false,\n }\n\n this.logRun(result)\n\n if (response.error) {\n this.log(`Missed job \"${jobId}\" failed: ${response.error}`)\n } else {\n this.log(`Missed job \"${jobId}\" completed`)\n job.lastSuccess = new Date()\n }\n job.lastRun = new Date()\n job.totalRuns++\n } catch (e: any) {\n this.log(`Missed job \"${jobId}\" threw: ${e.message}`)\n }\n }\n\n this.saveLastRuns()\n }\n\n // --- Notification ---\n\n private async notifyFailure(job: CronJobState, error: string): Promise<void> {\n if (!job.onError.includes(\"notify\") && job.consecutiveErrors < 2) return\n if (!this.notifyCallback) {\n this.log(`[ALERT] Cron \"${job.id}\" failed (${job.consecutiveErrors}x): ${error.slice(0, 200)}`)\n return\n }\n\n try {\n await this.notifyCallback(job.id, job.agent, error, job.consecutiveErrors)\n } catch {\n // Don't fail on notification errors\n }\n }\n\n private async notifyDisabled(job: CronJobState): Promise<void> {\n if (!this.notifyCallback) {\n this.log(`[ALERT] Cron \"${job.id}\" has been DISABLED after ${job.consecutiveErrors} consecutive failures`)\n return\n }\n\n try {\n await this.notifyCallback(\n job.id,\n job.agent,\n `AUTO-DISABLED after ${job.consecutiveErrors} consecutive failures. Last error: ${job.lastError || \"unknown\"}`,\n job.consecutiveErrors,\n )\n } catch {\n // Don't fail on notification errors\n }\n }\n\n // --- Persistence ---\n\n private saveLastRuns(): void {\n try {\n const data: Record<string, string> = {}\n for (const [id, job] of this.jobs) {\n if (job.lastRun) data[id] = job.lastRun.toISOString()\n }\n const dir = resolve(process.cwd(), \".agentx/cron\")\n if (!existsSync(dir)) mkdirSync(dir, { recursive: true })\n writeFileSync(this.lastRunFile, JSON.stringify(data, null, 2))\n } catch {\n // Best-effort\n }\n }\n\n private loadLastRuns(): Record<string, string> | null {\n try {\n if (!existsSync(this.lastRunFile)) return null\n const { readFileSync } = require(\"fs\")\n return JSON.parse(readFileSync(this.lastRunFile, \"utf-8\"))\n } catch {\n return null\n }\n }\n\n private logRun(result: CronRunResult): void {\n try {\n const runDir = resolve(this.runsDir, result.jobId)\n if (!existsSync(runDir)) {\n mkdirSync(runDir, { recursive: true })\n }\n\n const filename = `${result.startedAt.toISOString().replace(/[:.]/g, \"-\")}.json`\n writeFileSync(\n resolve(runDir, filename),\n JSON.stringify(result, null, 2),\n )\n } catch {\n // Don't fail on logging errors\n }\n }\n\n /**\n * List all jobs and their status (including failure info).\n */\n list(): CronJobState[] {\n return Array.from(this.jobs.values())\n }\n\n /**\n * Get a health summary of cron jobs.\n */\n health(): { healthy: number; failing: number; disabled: number; missed: number; jobs: Array<{ id: string; status: string; consecutiveErrors: number; lastError?: string }> } {\n const jobs = Array.from(this.jobs.values())\n const missed = this.detectMissedRuns()\n\n return {\n healthy: jobs.filter(j => j.enabled && j.consecutiveErrors === 0).length,\n failing: jobs.filter(j => j.enabled && j.consecutiveErrors > 0).length,\n disabled: jobs.filter(j => !j.enabled).length,\n missed: missed.length,\n jobs: jobs.map(j => ({\n id: j.id,\n status: !j.enabled ? \"disabled\" : j.retryPending ? \"retrying\" : j.consecutiveErrors > 0 ? \"failing\" : \"healthy\",\n consecutiveErrors: j.consecutiveErrors,\n lastError: j.lastError,\n })),\n }\n }\n}\n"],"mappings":"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","names":["writeFileSync","mkdirSync","existsSync","resolve","withOutputCap","prompt","maxOutputTokens","approxChars","parseCronField","field","min","max","values","part","range","stepStr","step","start","i","a","b","getNextCronDate","expression","after","timezone","fields","minutes","hours","daysOfMonth","months","daysOfWeek","fmt","candidate","limit","parts","get","type","p","m","h","dom","mon","dow","MAX_RETRIES","RETRY_DELAYS","CronScheduler","config","registry","hooks","log","resolve","id","def","cb","existsSync","mkdirSync","missedRuns","jobId","missedAt","e","job","j","timer","existing","nextRun","delay","attempt","retryAttempt","isRetry","hookResult","startedAt","response","result","res","applyConfigMutation","cfg","setAtPath","missed","lastRuns","lastRunStr","lastRun","shouldHaveRun","error","data","dir","writeFileSync","readFileSync","runDir","filename","jobs"]}