daimon 0.4.3 → 0.6.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 (55) hide show
  1. package/CHANGELOG.md +231 -0
  2. package/dist/cli.js +71 -58
  3. package/dist/dashboard/3rdpartylicenses.txt +461 -0
  4. package/dist/dashboard/browser/chunk-4VNZXWQZ.js +3 -0
  5. package/dist/dashboard/browser/chunk-4ZR4NTJW.js +1 -0
  6. package/dist/dashboard/browser/chunk-7L4DWDGL.js +4 -0
  7. package/dist/dashboard/browser/chunk-BLNPJ4WD.js +4 -0
  8. package/dist/dashboard/browser/chunk-E3HUMJ6S.js +6 -0
  9. package/dist/dashboard/browser/chunk-EBOHC6KX.js +1 -0
  10. package/dist/dashboard/browser/chunk-ESDYJ3BP.js +1 -0
  11. package/dist/dashboard/browser/chunk-JUZHAE4L.js +2 -0
  12. package/dist/dashboard/browser/chunk-K43KW4K2.js +4 -0
  13. package/dist/dashboard/browser/chunk-K46KCL6L.js +1 -0
  14. package/dist/dashboard/browser/chunk-KDCKLTCZ.js +1 -0
  15. package/dist/dashboard/browser/chunk-L63FHXGH.js +4 -0
  16. package/dist/dashboard/browser/chunk-LBRCWYRN.js +3 -0
  17. package/dist/dashboard/browser/chunk-MHOSWLRR.js +3 -0
  18. package/dist/dashboard/browser/chunk-N5GRSTMJ.js +6 -0
  19. package/dist/dashboard/browser/chunk-NDSAQ2HK.js +1 -0
  20. package/dist/dashboard/browser/chunk-NN2YNLGP.js +3 -0
  21. package/dist/dashboard/browser/chunk-PJPGLT4T.js +1 -0
  22. package/dist/dashboard/browser/chunk-R6J2WYUD.js +1 -0
  23. package/dist/dashboard/browser/chunk-T2YKGOEM.js +3 -0
  24. package/dist/dashboard/browser/chunk-U6AY7XZK.js +5 -0
  25. package/dist/dashboard/browser/chunk-UC3XMN2Y.js +1 -0
  26. package/dist/dashboard/browser/chunk-UNT27XFJ.js +2 -0
  27. package/dist/dashboard/browser/chunk-V2CQL6W5.js +1 -0
  28. package/dist/dashboard/browser/chunk-VZ6E4VQY.js +2 -0
  29. package/dist/dashboard/browser/chunk-WAN7TQQW.js +1 -0
  30. package/dist/dashboard/browser/chunk-WJUGRIIZ.js +1 -0
  31. package/dist/dashboard/browser/chunk-WRBP4DQN.js +4 -0
  32. package/dist/dashboard/browser/chunk-XUU4AY4M.js +2 -0
  33. package/dist/dashboard/browser/chunk-XYVF5I5T.js +1 -0
  34. package/dist/dashboard/browser/chunk-Y6B6X4Y6.js +2 -0
  35. package/dist/dashboard/browser/chunk-YNQPX5G6.js +1 -0
  36. package/dist/dashboard/browser/chunk-YYAZGY5M.js +1 -0
  37. package/dist/dashboard/browser/index.html +15 -0
  38. package/dist/dashboard/browser/main-ZADV4SOC.js +1 -0
  39. package/dist/dashboard/browser/styles-SIPYJLMG.css +1 -0
  40. package/dist/dashboard/prerendered-routes.json +3 -0
  41. package/dist/main.js +52 -43
  42. package/dist/mcp.js +3 -2
  43. package/package.json +6 -5
  44. package/src/templates/claude/skill.md.tmpl +23 -31
  45. package/src/dashboard.html +0 -451
  46. package/src/templates/claude/commands/doctor.md.tmpl +0 -10
  47. package/src/templates/claude/commands/errors.md.tmpl +0 -10
  48. package/src/templates/claude/commands/logs.md.tmpl +0 -10
  49. package/src/templates/claude/commands/restart.md.tmpl +0 -10
  50. package/src/templates/claude/commands/start.md.tmpl +0 -12
  51. package/src/templates/claude/commands/status.md.tmpl +0 -10
  52. package/src/templates/claude/commands/stop.md.tmpl +0 -10
  53. package/src/templates/claude/commands/up.md.tmpl +0 -10
  54. package/src/templates/claude/commands/wait.md.tmpl +0 -10
  55. package/src/templates/claude/commands/why.md.tmpl +0 -10
package/dist/mcp.js CHANGED
@@ -1,3 +1,4 @@
1
- import{McpServer as V}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as W}from"@modelcontextprotocol/sdk/server/stdio.js";import{z as a}from"zod";import f from"node:fs";import c from"node:path";import m from"node:os";import{fileURLToPath as I}from"node:url";var $=I(import.meta.url),A=c.dirname($);function j(){return{searchRoots:[],portRange:[4200,4299],apiPort:4999,overrides:{},autoStart:[],profiles:{},tags:{},autoRestart:{enabled:!1,maxAttempts:5,windowMs:3e5},healthProbe:{enabled:!0,intervalMs:3e4,timeoutMs:2e3,path:"/",host:null,scheme:null,rejectUnauthorized:!1,fallbackHosts:["127.0.0.1","::1"]},logs:{enabled:!1,dir:c.join(m.homedir(),".daimon","logs"),maxFiles:5,maxBytesPerFile:1e7},depends:{},cascadeRestart:!1,history:{enabled:!0,path:c.join(m.homedir(),".daimon","history.db"),retentionDays:30},notifications:{enabled:!0,onError:!0,onUnhealthy:!0,tray:!1},staleDetect:{enabled:!0,silentMs:3e4},headless:!1,envFiles:{},requestLog:{enabled:!1,portOffset:1e3},metrics:{enabled:!1},editor:{scheme:"vscode"},apiToken:null}}function P(r){return r.startsWith("~/")||r.startsWith("~\\")?c.join(m.homedir(),r.slice(2)):r==="~"?m.homedir():r}function x(r,n){if(!r||typeof r!="object")throw new Error(`Config at ${n} is not a JSON object`);let t=r,e=j();if(t.searchRoots!==void 0){if(!Array.isArray(t.searchRoots)||!t.searchRoots.every(o=>typeof o=="string"||o&&typeof o=="object"&&typeof o.path=="string"))throw new Error(`Config "searchRoots" must be an array of strings or { path, viteSubfolders? } objects (${n})`);e.searchRoots=t.searchRoots}if(t.portRange!==void 0){if(!Array.isArray(t.portRange)||t.portRange.length!==2||typeof t.portRange[0]!="number"||typeof t.portRange[1]!="number"||t.portRange[0]>t.portRange[1])throw new Error(`Config "portRange" must be [min, max] numbers (${n})`);e.portRange=[t.portRange[0],t.portRange[1]]}if(t.apiPort!==void 0){if(typeof t.apiPort!="number")throw new Error(`Config "apiPort" must be a number (${n})`);e.apiPort=t.apiPort}if(t.overrides!==void 0){if(typeof t.overrides!="object"||t.overrides===null||Array.isArray(t.overrides))throw new Error(`Config "overrides" must be an object (${n})`);e.overrides=t.overrides}if(t.autoStart!==void 0){if(!Array.isArray(t.autoStart)||!t.autoStart.every(o=>typeof o=="string"))throw new Error(`Config "autoStart" must be an array of strings (${n})`);e.autoStart=t.autoStart}if(t.profiles!==void 0){if(typeof t.profiles!="object"||t.profiles===null||Array.isArray(t.profiles))throw new Error(`Config "profiles" must be an object (${n})`);for(let[o,i]of Object.entries(t.profiles))if(!Array.isArray(i)||!i.every(s=>typeof s=="string"))throw new Error(`Config "profiles.${o}" must be an array of strings (${n})`);e.profiles=t.profiles}if(t.tags!==void 0){if(typeof t.tags!="object"||t.tags===null||Array.isArray(t.tags))throw new Error(`Config "tags" must be an object (${n})`);e.tags=t.tags}if(t.autoRestart&&typeof t.autoRestart=="object"&&(e.autoRestart={...e.autoRestart,...t.autoRestart}),t.healthProbe&&typeof t.healthProbe=="object"&&(e.healthProbe={...e.healthProbe,...t.healthProbe}),t.logs&&typeof t.logs=="object"&&(e.logs={...e.logs,...t.logs},e.logs.dir=P(e.logs.dir)),t.depends&&typeof t.depends=="object"&&!Array.isArray(t.depends)){for(let[o,i]of Object.entries(t.depends))if(!Array.isArray(i)||!i.every(s=>typeof s=="string"))throw new Error(`Config "depends.${o}" must be an array of strings (${n})`);e.depends=t.depends}if(typeof t.cascadeRestart=="boolean"&&(e.cascadeRestart=t.cascadeRestart),t.history&&typeof t.history=="object"&&(e.history={...e.history,...t.history},e.history.path=P(e.history.path)),t.notifications&&typeof t.notifications=="object"&&(e.notifications={...e.notifications,...t.notifications}),t.staleDetect&&typeof t.staleDetect=="object"&&(e.staleDetect={...e.staleDetect,...t.staleDetect}),typeof t.headless=="boolean"&&(e.headless=t.headless),t.envFiles&&typeof t.envFiles=="object"&&!Array.isArray(t.envFiles)&&(e.envFiles=t.envFiles),t.requestLog&&typeof t.requestLog=="object"&&(e.requestLog={...e.requestLog,...t.requestLog}),t.metrics&&typeof t.metrics=="object"&&(e.metrics={...e.metrics,...t.metrics}),t.editor&&typeof t.editor=="object"){let o=t.editor.scheme;typeof o=="string"&&o.trim()&&(e.editor={scheme:o.trim()})}return(typeof t.apiToken=="string"||t.apiToken===null)&&(e.apiToken=t.apiToken),e}function E(){return{local:c.join(process.cwd(),"daimon.config.json"),user:c.join(m.homedir(),".daimon","config.json")}}function C(){let{local:r,user:n}=E();if(f.existsSync(r)){let o=JSON.parse(f.readFileSync(r,"utf8"));return{kind:"loaded",config:x(o,r),path:r}}if(f.existsSync(n)){let o=JSON.parse(f.readFileSync(n,"utf8"));return{kind:"loaded",config:x(o,n),path:n}}let e=[c.resolve(A,"..","daimon.config.example.json"),c.resolve(A,"..","..","daimon.config.example.json")].find(o=>f.existsSync(o));return f.mkdirSync(c.dirname(n),{recursive:!0}),e?f.copyFileSync(e,n):f.writeFileSync(n,JSON.stringify(j(),null,2)+`
2
- `,"utf8"),{kind:"stub-created",path:n}}import L from"node:fs";import h from"node:path";import U from"node:os";import{spawn as q}from"node:child_process";import{fileURLToPath as J}from"node:url";import _ from"node:fs";import R from"node:path";import{fileURLToPath as F}from"node:url";var O=R.dirname(F(import.meta.url));function M(){let r=[R.resolve(O,"..","package.json"),R.resolve(O,"..","..","package.json")];for(let n of r)try{return JSON.parse(_.readFileSync(n,"utf8"))}catch{}return{}}var S=M().version||"0.0.0";var G=h.join(U.homedir(),".daimon"),N=h.join(G,"daemon.lock");function B(r){try{return process.kill(r,0),!0}catch(n){return n&&n.code==="EPERM"}}function b(){try{let r=L.readFileSync(N,"utf8"),n=JSON.parse(r);if(!n||typeof n.pid!="number")return null;if(!B(n.pid)){try{L.unlinkSync(N)}catch{}return null}return n}catch{return null}}function H(){let r=h.dirname(J(import.meta.url));return h.join(r,"main.js")}async function D(r={}){let n={...process.env};r.port&&(n.DAIMON_PORT=String(r.port)),q(process.execPath,[H(),"--headless"],{detached:!0,stdio:"ignore",env:n,windowsHide:!0}).unref();let e=Date.now();for(;Date.now()-e<5e3;){let o=b();if(o&&(!r.port||o.apiPort===r.port))return o;await new Promise(i=>setTimeout(i,100))}throw new Error("daemon failed to start within 5s")}function z(){if(process.env.DAIMON_PORT){let n=Number(process.env.DAIMON_PORT);if(Number.isFinite(n)&&n>0)return n}let r=b();if(r)return r.apiPort;try{let n=C();if(n.kind==="loaded")return n.config.apiPort}catch{}return 4999}var K=()=>`http://127.0.0.1:${z()}`,T=!1;async function Q(){if(!T&&(T=!0,process.env.DAIMON_NO_SPAWN!=="1"&&!b()))try{let r=process.env.DAIMON_PORT?Number(process.env.DAIMON_PORT):void 0;await D({port:Number.isFinite(r)&&r>0?r:void 0})}catch{}}async function l(r,n="GET"){await Q();try{let t=await fetch(K()+r,{method:n}),e=await t.text();try{return{status:t.status,body:JSON.parse(e)}}catch{return{status:t.status,body:e}}}catch{return{status:0,body:{error:"daimon is not running \u2014 start it with: daimon daemon start --detach"}}}}function d(r){return{content:[{type:"text",text:JSON.stringify(r)}]}}function p(r){return{content:[{type:"text",text:JSON.stringify({error:r})}],isError:!0}}async function X(){let r=new V({name:"daimon",version:S});r.registerTool("list_apps",{description:"List all known apps with current status, port, health, etc.",inputSchema:{}},async()=>{let t=await l("/api/apps");return t.status===0?p(t.body?.error||"unknown"):d(t.body)}),r.registerTool("get_status",{description:"Get the current status of one app.",inputSchema:{name:a.string()}},async({name:t})=>{let e=await l(`/api/apps/${encodeURIComponent(t)}`);return e.status===0?p(e.body?.error||"unknown"):e.status===404?p("unknown app"):d(e.body)}),r.registerTool("get_errors",{description:"Get errors for an app. Supports --since duration, --since-last cursor, optional structured form.",inputSchema:{name:a.string(),since:a.string().optional(),sinceLast:a.boolean().optional(),client:a.string().optional(),structured:a.boolean().optional()}},async({name:t,since:e,sinceLast:o,client:i,structured:s})=>{let u=`/api/apps/${encodeURIComponent(t)}/errors`,w=new URLSearchParams;o?(u+="/since-last",i&&w.set("client",i)):e&&w.set("since",e);let v=w.toString(),g=await l(u+(v?"?"+v:""));if(g.status===0)return p(g.body?.error||"unknown");if(g.status===404)return p("unknown app");let y=g.body;return s&&Array.isArray(y)&&(y=y.map(k=>k.parsed??{message:k.message})),d(y)}),r.registerTool("get_logs",{description:"Get recent log lines for an app.",inputSchema:{name:a.string(),tail:a.number().int().positive().optional(),since:a.string().optional()}},async({name:t,tail:e,since:o})=>{let i=new URLSearchParams;e&&i.set("tail",String(e)),o&&i.set("since",o);let s=i.toString(),u=await l(`/api/apps/${encodeURIComponent(t)}/logs${s?"?"+s:""}`);return u.status===0?p(u.body?.error||"unknown"):u.status===404?p("unknown app"):d(u.body)});for(let t of["start","stop","restart"])r.registerTool(`${t}_app`,{description:`${t} an app.`,inputSchema:{name:a.string()}},async({name:e})=>{let o=await l(`/api/apps/${encodeURIComponent(e)}/${t}`,"POST");return o.status===0?p(o.body?.error||"unknown"):d(o.body)});r.registerTool("wait_for_app",{description:"Block until app reaches the given state or timeout (max 600s).",inputSchema:{name:a.string(),until:a.enum(["serving","healthy","stopped","error"]).optional(),timeout:a.number().int().positive().max(600).optional()}},async({name:t,until:e,timeout:o})=>{let i=new URLSearchParams;i.set("until",e||"serving"),i.set("timeout",String(Math.min(o??120,600)));let s=await l(`/api/apps/${encodeURIComponent(t)}/wait?${i.toString()}`);return s.status===0?p(s.body?.error||"unknown"):s.status===404?p("unknown app"):d(s.body)});let n=new W;await r.connect(n)}X().catch(r=>{process.stderr.write(`[daimon-mcp] fatal: ${r?.stack||r}
1
+ import{McpServer as Z}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as tt}from"@modelcontextprotocol/sdk/server/stdio.js";import{z as a}from"zod";import y from"node:fs";import h from"node:path";import w from"node:os";import{fileURLToPath as q}from"node:url";var J=q(import.meta.url),C=h.dirname(J);function O(){return{searchRoots:[],portRange:[4200,4299],apiPort:4999,overrides:{},autoStart:[],profiles:{},tags:{},autoRestart:{enabled:!1,maxAttempts:5,windowMs:3e5},healthProbe:{enabled:!0,intervalMs:3e4,timeoutMs:2e3,path:"/",host:null,scheme:null,rejectUnauthorized:!1,fallbackHosts:["127.0.0.1","::1"]},logs:{enabled:!1,dir:h.join(w.homedir(),".daimon","logs"),maxFiles:5,maxBytesPerFile:1e7},depends:{},cascadeRestart:!1,history:{enabled:!0,path:h.join(w.homedir(),".daimon","history.db"),retentionDays:30},notifications:{enabled:!0,onError:!0,onUnhealthy:!0,tray:!1},staleDetect:{enabled:!0,silentMs:3e4},headless:!1,envFiles:{},requestLog:{enabled:!1,portOffset:1e3},metrics:{enabled:!1},editor:{scheme:"vscode"},apiToken:null,output:{format:"compact",ndjson:!1},doctor:{autoFix:{onInit:!1,permitted:["orphan-daemon","stale-lock","missing-search-root","corrupt-history-db","port-conflict-pred","node-version-mismatch","orphan-node-modules","dead-search-root"]}},dashboard:{theme:"auto",density:"comfortable"}}}function _(o){return o.startsWith("~/")||o.startsWith("~\\")?h.join(w.homedir(),o.slice(2)):o==="~"?w.homedir():o}function j(o,i){if(!o||typeof o!="object")throw new Error(`Config at ${i} is not a JSON object`);let t=o,e=O();if(t.searchRoots!==void 0){if(!Array.isArray(t.searchRoots)||!t.searchRoots.every(r=>typeof r=="string"||r&&typeof r=="object"&&typeof r.path=="string"))throw new Error(`Config "searchRoots" must be an array of strings or { path, viteSubfolders? } objects (${i})`);e.searchRoots=t.searchRoots}if(t.portRange!==void 0){if(!Array.isArray(t.portRange)||t.portRange.length!==2||typeof t.portRange[0]!="number"||typeof t.portRange[1]!="number"||t.portRange[0]>t.portRange[1])throw new Error(`Config "portRange" must be [min, max] numbers (${i})`);e.portRange=[t.portRange[0],t.portRange[1]]}if(t.apiPort!==void 0){if(typeof t.apiPort!="number")throw new Error(`Config "apiPort" must be a number (${i})`);e.apiPort=t.apiPort}if(t.overrides!==void 0){if(typeof t.overrides!="object"||t.overrides===null||Array.isArray(t.overrides))throw new Error(`Config "overrides" must be an object (${i})`);e.overrides=t.overrides}if(t.autoStart!==void 0){if(!Array.isArray(t.autoStart)||!t.autoStart.every(r=>typeof r=="string"))throw new Error(`Config "autoStart" must be an array of strings (${i})`);e.autoStart=t.autoStart}if(t.profiles!==void 0){if(typeof t.profiles!="object"||t.profiles===null||Array.isArray(t.profiles))throw new Error(`Config "profiles" must be an object (${i})`);for(let[r,n]of Object.entries(t.profiles))if(!Array.isArray(n)||!n.every(s=>typeof s=="string"))throw new Error(`Config "profiles.${r}" must be an array of strings (${i})`);e.profiles=t.profiles}if(t.tags!==void 0){if(typeof t.tags!="object"||t.tags===null||Array.isArray(t.tags))throw new Error(`Config "tags" must be an object (${i})`);e.tags=t.tags}if(t.autoRestart&&typeof t.autoRestart=="object"&&(e.autoRestart={...e.autoRestart,...t.autoRestart}),t.healthProbe&&typeof t.healthProbe=="object"&&(e.healthProbe={...e.healthProbe,...t.healthProbe}),t.logs&&typeof t.logs=="object"&&(e.logs={...e.logs,...t.logs},e.logs.dir=_(e.logs.dir)),t.depends&&typeof t.depends=="object"&&!Array.isArray(t.depends)){for(let[r,n]of Object.entries(t.depends))if(!Array.isArray(n)||!n.every(s=>typeof s=="string"))throw new Error(`Config "depends.${r}" must be an array of strings (${i})`);e.depends=t.depends}if(typeof t.cascadeRestart=="boolean"&&(e.cascadeRestart=t.cascadeRestart),t.history&&typeof t.history=="object"&&(e.history={...e.history,...t.history},e.history.path=_(e.history.path)),t.notifications&&typeof t.notifications=="object"&&(e.notifications={...e.notifications,...t.notifications}),t.staleDetect&&typeof t.staleDetect=="object"&&(e.staleDetect={...e.staleDetect,...t.staleDetect}),typeof t.headless=="boolean"&&(e.headless=t.headless),t.envFiles&&typeof t.envFiles=="object"&&!Array.isArray(t.envFiles)&&(e.envFiles=t.envFiles),t.requestLog&&typeof t.requestLog=="object"&&(e.requestLog={...e.requestLog,...t.requestLog}),t.metrics&&typeof t.metrics=="object"&&(e.metrics={...e.metrics,...t.metrics}),t.editor&&typeof t.editor=="object"){let r=t.editor.scheme;typeof r=="string"&&r.trim()&&(e.editor={scheme:r.trim()})}if((typeof t.apiToken=="string"||t.apiToken===null)&&(e.apiToken=t.apiToken),t.output&&typeof t.output=="object"){let r=t.output;(r.format==="compact"||r.format==="full")&&(e.output.format=r.format),typeof r.ndjson=="boolean"&&(e.output.ndjson=r.ndjson)}if(t.doctor&&typeof t.doctor=="object"){let r=t.doctor.autoFix;r&&typeof r=="object"&&(typeof r.onInit=="boolean"&&(e.doctor.autoFix.onInit=r.onInit),Array.isArray(r.permitted)&&(e.doctor.autoFix.permitted=r.permitted.filter(n=>typeof n=="string")))}if(t.dashboard&&typeof t.dashboard=="object"){let r=t.dashboard;(r.theme==="auto"||r.theme==="light"||r.theme==="dark")&&(e.dashboard.theme=r.theme),(r.density==="comfortable"||r.density==="compact")&&(e.dashboard.density=r.density)}return e}function G(){return{local:h.join(process.cwd(),"daimon.config.json"),user:h.join(w.homedir(),".daimon","config.json")}}function T(){let{local:o,user:i}=G();if(y.existsSync(o)){let r=JSON.parse(y.readFileSync(o,"utf8"));return{kind:"loaded",config:j(r,o),path:o}}if(y.existsSync(i)){let r=JSON.parse(y.readFileSync(i,"utf8"));return{kind:"loaded",config:j(r,i),path:i}}let e=[h.resolve(C,"..","daimon.config.example.json"),h.resolve(C,"..","..","daimon.config.example.json")].find(r=>y.existsSync(r));return y.mkdirSync(h.dirname(i),{recursive:!0}),e?y.copyFileSync(e,i):y.writeFileSync(i,JSON.stringify(O(),null,2)+`
2
+ `,"utf8"),{kind:"stub-created",path:i}}import L from"node:fs";import S from"node:path";import B from"node:os";import{spawn as W}from"node:child_process";import{fileURLToPath as K}from"node:url";import H from"node:fs";import R from"node:path";import{fileURLToPath as V}from"node:url";var I=R.dirname(V(import.meta.url));function z(){let o=[R.resolve(I,"..","package.json"),R.resolve(I,"..","..","package.json")];for(let i of o)try{return JSON.parse(H.readFileSync(i,"utf8"))}catch{}return{}}var P=z().version||"0.0.0";var Q=S.join(B.homedir(),".daimon"),M=S.join(Q,"daemon.lock");function X(o){try{return process.kill(o,0),!0}catch(i){return i&&i.code==="EPERM"}}function v(){try{let o=L.readFileSync(M,"utf8"),i=JSON.parse(o);if(!i||typeof i.pid!="number")return null;if(!X(i.pid)){try{L.unlinkSync(M)}catch{}return null}return i}catch{return null}}function Y(){let o=S.dirname(K(import.meta.url));return S.join(o,"main.js")}async function $(o={}){let i={...process.env};o.port&&(i.DAIMON_PORT=String(o.port)),W(process.execPath,[Y(),"--headless"],{detached:!0,stdio:"ignore",env:i,windowsHide:!0}).unref();let e=Date.now();for(;Date.now()-e<5e3;){let r=v();if(r&&(!o.port||r.apiPort===o.port))return r;await new Promise(n=>setTimeout(n,100))}throw new Error("daemon failed to start within 5s")}function et(){if(process.env.DAIMON_PORT){let i=Number(process.env.DAIMON_PORT);if(Number.isFinite(i)&&i>0)return i}let o=v();if(o)return o.apiPort;try{let i=T();if(i.kind==="loaded")return i.config.apiPort}catch{}return 4999}var D=()=>`http://127.0.0.1:${et()}`,N=!1;async function U(){if(!N&&(N=!0,process.env.DAIMON_NO_SPAWN!=="1"&&!v()))try{let o=process.env.DAIMON_PORT?Number(process.env.DAIMON_PORT):void 0;await $({port:Number.isFinite(o)&&o>0?o:void 0})}catch{}}async function f(o,i="GET"){await U();try{let t=await fetch(D()+o,{method:i}),e=await t.text();try{return{status:t.status,body:JSON.parse(e)}}catch{return{status:t.status,body:e}}}catch{return{status:0,body:{error:"daimon is not running \u2014 start it with: daimon daemon start --detach"}}}}function u(o){return{content:[{type:"text",text:JSON.stringify(o)}]}}function p(o){return{content:[{type:"text",text:JSON.stringify({error:o})}],isError:!0}}async function rt(){let o=new Z({name:"daimon",version:P});o.registerTool("list_apps",{description:"List apps in compact form: name, status, port, health, errCount, lastChangeMs. Use list_apps_full for the verbose v0.4 shape.",inputSchema:{}},async()=>{let t=await f("/api/apps?format=compact");return t.status===0?p(t.body?.error||"unknown"):u(t.body)}),o.registerTool("list_apps_full",{description:"List apps in the verbose v0.4 form (uptimeMs, lastCompileMs, metrics, etc.). Heavier \u2014 prefer list_apps unless you need extra fields.",inputSchema:{}},async()=>{let t=await f("/api/apps?format=full");return t.status===0?p(t.body?.error||"unknown"):u(t.body)}),o.registerTool("get_status",{description:"Compact status: name, status, port, url, health, errCount, lastChangeMs, uptime. Use get_status_full for the verbose v0.4 shape.",inputSchema:{name:a.string()}},async({name:t})=>{let e=await f(`/api/apps/${encodeURIComponent(t)}?format=compact`);return e.status===0?p(e.body?.error||"unknown"):e.status===404?p("unknown app"):u(e.body)}),o.registerTool("get_status_full",{description:"Verbose v0.4 status form including events, compile history, metrics. Prefer get_status unless you need extra fields.",inputSchema:{name:a.string()}},async({name:t})=>{let e=await f(`/api/apps/${encodeURIComponent(t)}?format=full`);return e.status===0?p(e.body?.error||"unknown"):e.status===404?p("unknown app"):u(e.body)}),o.registerTool("get_errors",{description:"Get errors for an app. Supports --since duration, --since-last cursor, optional structured form.",inputSchema:{name:a.string(),since:a.string().optional(),sinceLast:a.boolean().optional(),client:a.string().optional(),structured:a.boolean().optional()}},async({name:t,since:e,sinceLast:r,client:n,structured:s})=>{let c=`/api/apps/${encodeURIComponent(t)}/errors`,g=new URLSearchParams;r?(c+="/since-last",n&&g.set("client",n)):e&&g.set("since",e);let b=g.toString(),m=await f(c+(b?"?"+b:""));if(m.status===0)return p(m.body?.error||"unknown");if(m.status===404)return p("unknown app");let l=m.body;return s&&Array.isArray(l)&&(l=l.map(d=>d.parsed??{message:d.message})),u(l)}),o.registerTool("get_logs",{description:"Get recent log lines for an app.",inputSchema:{name:a.string(),tail:a.number().int().positive().optional(),since:a.string().optional()}},async({name:t,tail:e,since:r})=>{let n=new URLSearchParams;e&&n.set("tail",String(e)),r&&n.set("since",r);let s=n.toString(),c=await f(`/api/apps/${encodeURIComponent(t)}/logs${s?"?"+s:""}`);return c.status===0?p(c.body?.error||"unknown"):c.status===404?p("unknown app"):u(c.body)});for(let t of["start","stop","restart"])o.registerTool(`${t}_app`,{description:`${t} an app.`,inputSchema:{name:a.string()}},async({name:e})=>{let r=await f(`/api/apps/${encodeURIComponent(e)}/${t}`,"POST");return r.status===0?p(r.body?.error||"unknown"):u(r.body)});o.registerTool("overview",{description:'Decision-ready snapshot of the workspace: totals, byStatus, needsAttention (with first parsed error per failing app), recentlyChanged. The recommended first call in a session \u2014 answers "what is going on right now?" in one round-trip. Pass `budget` (tokens) to cap response size; overflow collapses to _meta.omitted.',inputSchema:{workspace:a.string().optional(),profile:a.string().optional(),budget:a.number().int().positive().max(5e4).optional()}},async({workspace:t,profile:e,budget:r})=>{let n=new URLSearchParams;t&&n.set("workspace",t),e&&n.set("profile",e),r&&n.set("budget",String(r));let s=n.toString(),c=await f("/api/overview"+(s?"?"+s:""));return c.status===0?p(c.body?.error||"unknown"):u(c.body)}),o.registerTool("diff_errors",{description:'Errors that appeared since the last call by this client. Compact list of {file,line,col,code,message,tool}; bounded by `budget` tokens \u2014 overflow collapses to {omitted:N}. Use this as the "did my last change introduce new errors?" round-trip.',inputSchema:{name:a.string(),client:a.string().optional(),budget:a.number().int().positive().max(5e4).optional()}},async({name:t,client:e,budget:r})=>{let n=new URLSearchParams;n.set("client",e||"mcp-default");let s=await f(`/api/apps/${encodeURIComponent(t)}/errors/since-last?${n.toString()}`);if(s.status===0)return p(s.body?.error||"unknown");if(s.status===404)return p("unknown app");let g=(Array.isArray(s.body)?s.body:[]).map(d=>({file:d.file??null,line:d.line??null,col:d.col??null,code:d.code??null,tool:d.tool??null,message:d.message??""})),b=(r??800)*4,m=0,l=g;for(;JSON.stringify(l).length>b&&l.length>0;)l.pop(),m++;return u({errors:l,_meta:{omitted:m,total:g.length,budget:r??800}})}),o.registerTool("try_fix",{description:"Composite remediation: run doctor --auto-fix for permitted rules, restart the named app, wait for the target state, return {before, after, fixed:[ruleName], stillFailing:[\u2026parsed first 5]}. Never edits user source code, only daemon state. Pair with `focus` for narration; use this for the action.",inputSchema:{name:a.string(),until:a.enum(["serving","healthy"]).optional(),timeoutMs:a.number().int().positive().max(6e5).optional()}},async({name:t,until:e,timeoutMs:r})=>{let n=new URLSearchParams;n.set("until",e||"healthy"),n.set("timeoutMs",String(Math.min(r??18e4,6e5)));let s=await f(`/api/apps/${encodeURIComponent(t)}/try-fix?${n.toString()}`,"POST");return s.status===0?p(s.body?.error||"unknown"):s.status===404?p("unknown app"):u(s.body)}),o.registerTool("focus",{description:'Single round-trip "subscribe-then-act" snapshot for one app: starts the app if stopped, then narrates status/error/health events until target state (serving|healthy|stable) or timeout. Returns the captured event list plus final state. Use as a coherent narrative instead of polling.',inputSchema:{name:a.string(),until:a.enum(["serving","healthy","stable"]).optional(),timeoutMs:a.number().int().positive().max(6e5).optional()}},async({name:t,until:e,timeoutMs:r})=>{let n=new URLSearchParams;n.set("until",e||"healthy"),n.set("timeoutMs",String(Math.min(r??18e4,6e5))),await U();try{let s=await fetch(D()+`/api/apps/${encodeURIComponent(t)}/focus?${n.toString()}`,{method:"POST"});if(s.status===404)return p("unknown app");if(!s.body)return p("no response body");let c=s.body.getReader(),g=new TextDecoder,b=[],m=null,l="";for(;;){let{done:d,value:E}=await c.read();if(d)break;l+=g.decode(E,{stream:!0});let A=l.split(`
3
+ `);l=A.pop()??"";for(let F of A){let x=F.trim();if(x)try{let k=JSON.parse(x);k.kind==="done"?m=k:b.push(k)}catch{}}}return u({events:b,final:m})}catch(s){return p(s?.message||String(s))}}),o.registerTool("ensure",{description:'One-call lifecycle: if the app is stopped/crashed/error, start it; then block until it reaches the target state. Idempotent \u2014 returns immediately on already-terminal apps. Replaces the list\u2192status\u2192start\u2192wait\u2192status sequence. Returns compact AppSummary plus _meta.startedFromState / waitedMs. On timeout the body is { error: "timeout", state, _meta: { timedOut: true } } \u2014 treat as exit 2 equivalent.',inputSchema:{name:a.string(),until:a.enum(["serving","healthy"]).optional(),timeoutMs:a.number().int().positive().max(6e5).optional()}},async({name:t,until:e,timeoutMs:r})=>{let n=new URLSearchParams;n.set("until",e||"healthy"),n.set("timeoutMs",String(Math.min(r??18e4,6e5)));let s=await f(`/api/apps/${encodeURIComponent(t)}/ensure?${n.toString()}`,"POST");return s.status===0?p(s.body?.error||"unknown"):s.status===404?p("unknown app"):u(s.body)}),o.registerTool("ensure_up",{description:"One-call profile bring-up: cascade-start every app in the profile (resolving deps) and block until each reaches the target. Returns per-app terminal state plus _meta.totalMs. Use this instead of daimon up + per-app waits.",inputSchema:{profile:a.string(),until:a.enum(["serving","healthy"]).optional(),timeoutMs:a.number().int().positive().max(12e5).optional()}},async({profile:t,until:e,timeoutMs:r})=>{let n=new URLSearchParams;n.set("until",e||"healthy"),n.set("timeoutMs",String(Math.min(r??3e5,12e5)));let s=await f(`/api/profiles/${encodeURIComponent(t)}/ensure-up?${n.toString()}`,"POST");return s.status===0?p(s.body?.error||"unknown"):s.status===404?p("unknown profile"):u(s.body)}),o.registerTool("wait_for_app",{description:"Block until app reaches the given state or timeout (max 600s).",inputSchema:{name:a.string(),until:a.enum(["serving","healthy","stopped","error"]).optional(),timeout:a.number().int().positive().max(600).optional()}},async({name:t,until:e,timeout:r})=>{let n=new URLSearchParams;n.set("until",e||"serving"),n.set("timeout",String(Math.min(r??120,600)));let s=await f(`/api/apps/${encodeURIComponent(t)}/wait?${n.toString()}`);return s.status===0?p(s.body?.error||"unknown"):s.status===404?p("unknown app"):u(s.body)});let i=new tt;await o.connect(i)}rt().catch(o=>{process.stderr.write(`[daimon-mcp] fatal: ${o?.stack||o}
3
4
  `),process.exit(1)});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "daimon",
3
- "version": "0.4.3",
3
+ "version": "0.6.0",
4
4
  "description": "Local dev-server manager for Angular/Nx/Vite/Storybook — TUI, loopback HTTP API, JSON CLI, and MCP server for Claude Code",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "Yosi Azulay (https://flycotech.com)",
@@ -31,20 +31,21 @@
31
31
  },
32
32
  "files": [
33
33
  "dist",
34
- "src/dashboard.html",
35
34
  "src/templates",
36
35
  "README.md",
37
- "LICENSE"
36
+ "LICENSE",
37
+ "CHANGELOG.md"
38
38
  ],
39
39
  "scripts": {
40
40
  "build": "tsc",
41
41
  "start": "npm run build && node dist/main.js",
42
42
  "cli": "node dist/cli.js",
43
43
  "mcp": "node dist/mcp.js",
44
- "test": "node --test test/depends.test.mjs test/bundle.test.mjs test/notifier.test.mjs test/regression.test.mjs",
44
+ "test": "node --test test/depends.test.mjs test/bundle.test.mjs test/notifier.test.mjs test/regression.test.mjs test/parser-corpus.test.mjs test/overview-budget.test.mjs test/autofix-rules.test.mjs",
45
45
  "clean:dist": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
46
46
  "build:bundle": "npm run clean:dist && esbuild src/cli.ts src/main.ts src/mcp.ts --bundle --platform=node --target=node20 --format=esm --minify --legal-comments=none --outdir=dist --packages=external",
47
- "prepublishOnly": "npm run build && npm test && npm run build:bundle"
47
+ "build:dashboard": "node -e \"const fs=require('fs');if(fs.existsSync('dashboard/node_modules')){require('child_process').execSync('npm run build',{cwd:'dashboard',stdio:'inherit'});}else{console.log('[daimon] dashboard/node_modules missing skipping ng build. Run: (cd dashboard && npm install) to enable the Angular SPA bundle.');}\"",
48
+ "prepublishOnly": "npm run build && npm test && npm run build:bundle && npm run build:dashboard"
48
49
  },
49
50
  "engines": {
50
51
  "node": ">=20"
@@ -1,44 +1,36 @@
1
1
  ---
2
2
  name: daimon
3
- description: Manage local Angular/Nx/Vite dev servers via the daimon CLI on the loopback API. Use for starting, stopping, inspecting status, surfacing dedup'd errors, tailing logs, and blocking until a target state is reached. Local-only; no remote access; no source-code mutation.
3
+ description: Local-only manager for Angular/Nx/Vite/Storybook dev servers via the `daimon` CLI on 127.0.0.1. Start/stop/inspect dev servers, surface dedup'd errors, tail logs, and block until a target state is reached. Read-only outside `~/.daimon/*` and daimon's own config. Never bind non-loopback. Never mutate user source.
4
4
  daimon-version: {{daimon_version}}
5
5
  generated-at: {{generated_at}}
6
6
  ---
7
7
 
8
- # daimon skill
8
+ # daimon
9
9
 
10
- You are operating inside a repo managed by **daimon** a local daemon that owns the lifecycle of multiple dev-server processes. Use `daimon` instead of running `npm start`, `nx serve`, or `ng serve` yourself.
10
+ Loopback CLI at `http://127.0.0.1:{{api_port}}`. Compact JSON on stdout, errors as compact JSON on stderr (exit 1; `daimon wait` timeout exits 2). Daemon auto-spawns on first call (set `DAIMON_NO_SPAWN=1` to skip).
11
11
 
12
- ## Available CLI surface
12
+ ## Verbs
13
13
 
14
- The daemon auto-spawns the first time you call any CLI command. Suppress with `DAIMON_NO_SPAWN=1` for read-only scripts.
14
+ - `daimon overview` workspace snapshot (totals, needs-attention, recently-changed). **Start here.**
15
+ - `daimon list` → `{name,status,port,health,errCount,lastChangeMs}` per app. `--full` for v0.4 form.
16
+ - `daimon status <name>` → compact `{name,status,port,url,health,errCount,lastChangeMs,uptime}`. `--full` for events/metrics.
17
+ - `daimon ensure <name> [--until serving|healthy] [--timeout 180]` → one call: starts if stopped, blocks to target, returns terminal state.
18
+ - `daimon ensure-up <profile>` → same for an entire profile; cascades dependencies.
19
+ - `daimon errors <name> [--since 5m|--since-last --client claude] [--structured]` → dedup'd errors, compact `{file,line,col,code,message}`.
20
+ - `daimon logs <name> [--tail N] [--since 30s]` → recent log lines.
21
+ - `daimon start|stop|restart <name>` · `daimon up|down [<profile>]` · `daimon wait <name> --until healthy --timeout 60s`.
22
+ - `daimon why <name>` → last status transition + 5 events. `daimon why-empty` → explain an empty `list`.
23
+ - `daimon doctor [--auto-fix] [--dry-run]` → config + env sanity check; auto-fix repairs orphan daemon, stale lock, missing search root, corrupt history DB.
15
24
 
16
- ```
17
- {{commands_table}}
18
- ```
25
+ ## Patterns
19
26
 
20
- API base: `http://127.0.0.1:{{api_port}}`. All output is compact JSON.
27
+ - **First call in a session:** `daimon overview`. If `totals.apps === 0`, the response includes `_meta.suggestion` — follow it.
28
+ - **Bring up one app idempotently:** `daimon ensure <name>`. Replaces the old `status → start → wait → status` sequence.
29
+ - **Diff-style error checks:** `daimon errors <name> --since-last --client claude` returns only what's new since the last call.
21
30
 
22
- ## When to use what
31
+ ## Do not
23
32
 
24
- - **Find out what's running**: `daimon list`
25
- - **Check one app**: `daimon status <name>`
26
- - **Block until ready** (preferred over polling): `daimon wait <name> --until healthy --timeout 60s`
27
- - **Show errors**: `daimon errors <name> --since 5m` (or `--structured` for parsed TS errors, or `--since-last --client claude` for diff-mode)
28
- - **Tail logs**: `daimon logs <name> --tail 100`
29
- - **Investigate failure**: `daimon why <name>` returns the last status transition plus 5 preceding events
30
- - **Bring up a profile**: `daimon up <profile>` — starts deps in order, blocks until healthy
31
- - **Sanity-check setup**: `daimon doctor` (does not need the daemon)
32
-
33
- ## What not to do
34
-
35
- - Do not start `nx serve` / `ng serve` directly.
36
- - Do not parse log files — `daimon errors` is already dedup'd and structured.
37
- - Do not assume an app is healthy because it's "serving"; use `--until healthy`.
38
- - Do not retry on a timeout from `daimon wait` without first calling `daimon errors` and `daimon why`.
39
-
40
- ## Exit codes
41
-
42
- - `0` success
43
- - `1` generic error (unknown app, daemon down, etc.)
44
- - `2` `daimon wait` timed out
33
+ - Do not run `nx serve`, `ng serve`, or `npm start` directly — go through `daimon ensure`.
34
+ - Do not parse log files; `daimon errors` is already dedup'd and parsed.
35
+ - Do not retry blindly on `daimon wait` timeout (exit 2) read `daimon errors` and `daimon why` first.
36
+ - Do not bind non-loopback ports. Do not modify user source files via daimon.