agentix-cli 0.22.0 → 0.25.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.
- package/LICENSE +21 -0
- package/README.md +102 -103
- package/agentx.example.json +9 -3
- package/dist/App-6Y7B3YQH.js +8 -0
- package/dist/App-6Y7B3YQH.js.map +1 -0
- package/dist/ChatApp-KIEUJ2DS.js +7 -0
- package/dist/ChatApp-KIEUJ2DS.js.map +1 -0
- package/dist/{agent-Y4U4R3ZZ.js → agent-QGZIAZMY.js} +2 -2
- package/dist/architect-SEPWMWS4.js +2 -0
- package/dist/{board-dashboard-2CPBWFOB.js → board-dashboard-CQ5F4GPD.js} +3756 -1010
- package/dist/board-dashboard-CQ5F4GPD.js.map +1 -0
- package/dist/builtin-FRKAQMFS.js +2 -0
- package/dist/chunk-2WVX3LEC.js +2 -0
- package/dist/chunk-2WVX3LEC.js.map +1 -0
- package/dist/chunk-3WQFCIB6.js +24 -0
- package/dist/chunk-3WQFCIB6.js.map +1 -0
- package/dist/chunk-53GCFZPR.js +116 -0
- package/dist/chunk-53GCFZPR.js.map +1 -0
- package/dist/{chunk-IKBHHTIT.js → chunk-5U35WAZF.js} +2 -2
- package/dist/chunk-67Q4DCPS.js +54 -0
- package/dist/chunk-67Q4DCPS.js.map +1 -0
- package/dist/chunk-76Z4VHDX.js +8 -0
- package/dist/chunk-76Z4VHDX.js.map +1 -0
- package/dist/{chunk-7P5QRX5W.js → chunk-BFXONUUN.js} +2 -2
- package/dist/{chunk-P3TJHGO5.js → chunk-FDMWOPEL.js} +2 -2
- package/dist/chunk-FDWOECJY.js +54 -0
- package/dist/chunk-FDWOECJY.js.map +1 -0
- package/dist/chunk-G57RZCXM.js +5 -0
- package/dist/chunk-G57RZCXM.js.map +1 -0
- package/dist/{chunk-NOF4HSGK.js → chunk-H5D7ATXS.js} +2 -2
- package/dist/chunk-HA3XI7Z2.js +155 -0
- package/dist/chunk-HA3XI7Z2.js.map +1 -0
- package/dist/chunk-HR4JSRZS.js +3 -0
- package/dist/{chunk-A6BKSZQA.js.map → chunk-HR4JSRZS.js.map} +1 -1
- package/dist/chunk-I3SIXTJO.js +44 -0
- package/dist/chunk-I3SIXTJO.js.map +1 -0
- package/dist/chunk-ISEBLHWX.js +118 -0
- package/dist/chunk-ISEBLHWX.js.map +1 -0
- package/dist/{chunk-4ITQP3XL.js → chunk-JP5IEXR7.js} +2 -2
- package/dist/chunk-JXWAGMI2.js +13 -0
- package/dist/chunk-JXWAGMI2.js.map +1 -0
- package/dist/chunk-KUCCW2VG.js +431 -0
- package/dist/chunk-KUCCW2VG.js.map +1 -0
- package/dist/chunk-LDHPMJ7M.js +257 -0
- package/dist/chunk-LDHPMJ7M.js.map +1 -0
- package/dist/{chunk-WHP4I44J.js → chunk-LNXCJWOU.js} +29 -30
- package/dist/chunk-LNXCJWOU.js.map +1 -0
- package/dist/chunk-M4AMWP3H.js +178 -0
- package/dist/chunk-M4AMWP3H.js.map +1 -0
- package/dist/chunk-NGSIRE4X.js +126 -0
- package/dist/chunk-NGSIRE4X.js.map +1 -0
- package/dist/{chunk-CNNLREHD.js → chunk-P4VWIVKN.js} +268 -410
- package/dist/chunk-P4VWIVKN.js.map +1 -0
- package/dist/chunk-Q6YO7LYE.js +47 -0
- package/dist/chunk-Q6YO7LYE.js.map +1 -0
- package/dist/chunk-TMKLHTCG.js +971 -0
- package/dist/chunk-TMKLHTCG.js.map +1 -0
- package/dist/chunk-TRJTTBE2.js +7 -0
- package/dist/chunk-TRJTTBE2.js.map +1 -0
- package/dist/chunk-U2CKL755.js +4 -0
- package/dist/chunk-U2CKL755.js.map +1 -0
- package/dist/chunk-UCQ2N3KO.js +9 -0
- package/dist/chunk-UCQ2N3KO.js.map +1 -0
- package/dist/chunk-W4YR2RO2.js +2 -0
- package/dist/chunk-W4YR2RO2.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/client-GGVVLOOF.js +2 -0
- package/dist/{compaction-BTUJJ3LJ.js → compaction-NWEMOSHA.js} +3 -3
- package/dist/config-7FSZFRTQ.js +2 -0
- package/dist/config-mutator-POCOYYJ4.js +2 -0
- package/dist/{context-planner-I7ID7XF3.js → context-planner-45GEWKGL.js} +2 -2
- package/dist/doctor-DHLBEKA4.js +2 -0
- package/dist/heal-HZ3EQD52.js +2 -0
- package/dist/index.d.ts +1244 -225
- package/dist/index.js +12 -12
- package/dist/index.js.map +1 -1
- package/dist/memory-extract-AENFVVO5.js +2 -0
- package/dist/processes-UIC4XXA6.js +2 -0
- package/dist/program-WVAVJFND.js +2 -0
- package/dist/projects-api-KRME2YKZ.js +2 -0
- package/dist/projects-api-KRME2YKZ.js.map +1 -0
- package/dist/projects-mutate-3NHVG437.js +7 -0
- package/dist/projects-mutate-3NHVG437.js.map +1 -0
- package/dist/providers-L27BJ5A6.js +2 -0
- package/dist/registry-T36ZIAWV.js +2 -0
- package/dist/rotation-memo-B3EXXSEH.js +13 -0
- package/dist/rotation-memo-B3EXXSEH.js.map +1 -0
- package/dist/scheduler-GH7TIKYS.js +2 -0
- package/dist/surface-usage-PRL6Z4U3.js +2 -0
- package/dist/wiki-M6YNRFC4.js +2 -0
- package/dist/workflows/templates/branching.yaml +75 -0
- package/dist/workflows/templates/extract.yaml +59 -0
- package/dist/workflows/templates/linear.yaml +37 -0
- package/dist/workflows/templates/retry.yaml +61 -0
- package/dist/workspace-setup-TBQAHYHJ.js +2 -0
- package/package.json +11 -3
- package/dist/board-dashboard-2CPBWFOB.js.map +0 -1
- package/dist/chunk-2HXSJYAJ.js +0 -15
- package/dist/chunk-2HXSJYAJ.js.map +0 -1
- package/dist/chunk-5F3J55LG.js +0 -2
- package/dist/chunk-5F3J55LG.js.map +0 -1
- package/dist/chunk-A6BKSZQA.js +0 -3
- package/dist/chunk-ABVZHJUO.js +0 -7
- package/dist/chunk-ABVZHJUO.js.map +0 -1
- package/dist/chunk-BHSPAP4U.js +0 -44
- package/dist/chunk-BHSPAP4U.js.map +0 -1
- package/dist/chunk-CDNWCYF7.js +0 -2
- package/dist/chunk-CDNWCYF7.js.map +0 -1
- package/dist/chunk-CNNLREHD.js.map +0 -1
- package/dist/chunk-CO4YCIIA.js +0 -13
- package/dist/chunk-CO4YCIIA.js.map +0 -1
- package/dist/chunk-EW27SCCC.js +0 -2
- package/dist/chunk-EW27SCCC.js.map +0 -1
- package/dist/chunk-GOLZ6NEL.js +0 -111
- package/dist/chunk-GOLZ6NEL.js.map +0 -1
- package/dist/chunk-JGXE2YNV.js +0 -46
- package/dist/chunk-JGXE2YNV.js.map +0 -1
- package/dist/chunk-KX626FN4.js +0 -6
- package/dist/chunk-KX626FN4.js.map +0 -1
- package/dist/chunk-MTRWXRFF.js +0 -380
- package/dist/chunk-MTRWXRFF.js.map +0 -1
- package/dist/chunk-N67JZAXU.js +0 -2
- package/dist/chunk-N67JZAXU.js.map +0 -1
- package/dist/chunk-OPKQNLGX.js +0 -450
- package/dist/chunk-OPKQNLGX.js.map +0 -1
- package/dist/chunk-OZOQKPJV.js +0 -102
- package/dist/chunk-OZOQKPJV.js.map +0 -1
- package/dist/chunk-VYLZ7UYM.js +0 -118
- package/dist/chunk-VYLZ7UYM.js.map +0 -1
- package/dist/chunk-WHP4I44J.js.map +0 -1
- package/dist/chunk-WJXU5F4Z.js +0 -53
- package/dist/chunk-WJXU5F4Z.js.map +0 -1
- package/dist/chunk-X5D5AV5N.js +0 -833
- package/dist/chunk-X5D5AV5N.js.map +0 -1
- package/dist/config-BLOKGMER.js +0 -2
- package/dist/config-mutator-RG7QY2EL.js +0 -2
- package/dist/doctor-ZTECTUHX.js +0 -2
- package/dist/editor-chat-RVLCKKB7.js +0 -137
- package/dist/editor-chat-RVLCKKB7.js.map +0 -1
- package/dist/heal-YEQYWX2L.js +0 -2
- package/dist/inbox-BIN2S5XN.js +0 -2
- package/dist/memory-extract-KRCHD3OP.js +0 -2
- package/dist/processes-RJ6RDP32.js +0 -2
- package/dist/program-GZOJIWTB.js +0 -2
- package/dist/providers-WVCMIW67.js +0 -2
- package/dist/registry-QZIUSQ5J.js +0 -2
- package/dist/rotation-memo-GMTZPMNE.js +0 -13
- package/dist/rotation-memo-GMTZPMNE.js.map +0 -1
- package/dist/scheduler-B2BQPV4M.js +0 -2
- package/dist/slack-6P7ZJ32Y.js +0 -3
- package/dist/slack-6P7ZJ32Y.js.map +0 -1
- package/dist/store-BAPDZHXE.js +0 -2
- package/dist/task-store-RHISEU6V.js +0 -2
- package/dist/telegram-PIYDRM46.js +0 -3
- package/dist/telegram-PIYDRM46.js.map +0 -1
- package/dist/types-ZRLHHCGB.js +0 -2
- package/dist/web/workflow-editor.global.js +0 -1452
- package/dist/web/workflow-editor.global.js.map +0 -1
- package/dist/whatsapp-PFRDMUMT.js +0 -3
- package/dist/whatsapp-PFRDMUMT.js.map +0 -1
- package/dist/wiki-53W5NLZV.js +0 -2
- package/dist/workspace-setup-DQCZX2NE.js +0 -2
- /package/dist/{agent-Y4U4R3ZZ.js.map → agent-QGZIAZMY.js.map} +0 -0
- /package/dist/{config-BLOKGMER.js.map → architect-SEPWMWS4.js.map} +0 -0
- /package/dist/{config-mutator-RG7QY2EL.js.map → builtin-FRKAQMFS.js.map} +0 -0
- /package/dist/{chunk-IKBHHTIT.js.map → chunk-5U35WAZF.js.map} +0 -0
- /package/dist/{chunk-7P5QRX5W.js.map → chunk-BFXONUUN.js.map} +0 -0
- /package/dist/{chunk-P3TJHGO5.js.map → chunk-FDMWOPEL.js.map} +0 -0
- /package/dist/{chunk-NOF4HSGK.js.map → chunk-H5D7ATXS.js.map} +0 -0
- /package/dist/{chunk-4ITQP3XL.js.map → chunk-JP5IEXR7.js.map} +0 -0
- /package/dist/{doctor-ZTECTUHX.js.map → client-GGVVLOOF.js.map} +0 -0
- /package/dist/{compaction-BTUJJ3LJ.js.map → compaction-NWEMOSHA.js.map} +0 -0
- /package/dist/{heal-YEQYWX2L.js.map → config-7FSZFRTQ.js.map} +0 -0
- /package/dist/{inbox-BIN2S5XN.js.map → config-mutator-POCOYYJ4.js.map} +0 -0
- /package/dist/{context-planner-I7ID7XF3.js.map → context-planner-45GEWKGL.js.map} +0 -0
- /package/dist/{memory-extract-KRCHD3OP.js.map → doctor-DHLBEKA4.js.map} +0 -0
- /package/dist/{processes-RJ6RDP32.js.map → heal-HZ3EQD52.js.map} +0 -0
- /package/dist/{program-GZOJIWTB.js.map → memory-extract-AENFVVO5.js.map} +0 -0
- /package/dist/{providers-WVCMIW67.js.map → processes-UIC4XXA6.js.map} +0 -0
- /package/dist/{registry-QZIUSQ5J.js.map → program-WVAVJFND.js.map} +0 -0
- /package/dist/{scheduler-B2BQPV4M.js.map → providers-L27BJ5A6.js.map} +0 -0
- /package/dist/{store-BAPDZHXE.js.map → registry-T36ZIAWV.js.map} +0 -0
- /package/dist/{task-store-RHISEU6V.js.map → scheduler-GH7TIKYS.js.map} +0 -0
- /package/dist/{types-ZRLHHCGB.js.map → surface-usage-PRL6Z4U3.js.map} +0 -0
- /package/dist/{wiki-53W5NLZV.js.map → wiki-M6YNRFC4.js.map} +0 -0
- /package/dist/{workspace-setup-DQCZX2NE.js.map → workspace-setup-TBQAHYHJ.js.map} +0 -0
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{a}from"./chunk-KUCCW2VG.js";import"./chunk-G57RZCXM.js";import"./chunk-TRJTTBE2.js";import"./chunk-LNXCJWOU.js";import"./chunk-UCQ2N3KO.js";import"./chunk-P4VWIVKN.js";import"./chunk-GV7S5BAX.js";import"./chunk-GSX4NK2N.js";import"./chunk-SZDACK6O.js";import"./chunk-TMKLHTCG.js";import"./chunk-3WQFCIB6.js";import"./chunk-HA3XI7Z2.js";import"./chunk-Q6YO7LYE.js";import"./chunk-M4AMWP3H.js";import"./chunk-NGSIRE4X.js";import"./chunk-SFQUP3BP.js";import"./chunk-I3SIXTJO.js";import"./chunk-67Q4DCPS.js";import"./chunk-LDHPMJ7M.js";import"./chunk-2WVX3LEC.js";import"./chunk-76Z4VHDX.js";import"./chunk-W4YR2RO2.js";import"./chunk-AFRHKA7B.js";import"./chunk-53GCFZPR.js";import"./chunk-FDMWOPEL.js";import"./chunk-JXWAGMI2.js";import"./chunk-ISEBLHWX.js";import"./chunk-HR4JSRZS.js";import"./chunk-VWTBHLMX.js";import"./chunk-BFXONUUN.js";import"./chunk-JTJIPNXY.js";import"./chunk-64P4QHSZ.js";import"./chunk-BFZUG66X.js";import"./chunk-SR3X25TN.js";import"./chunk-53NRX2NA.js";import"./chunk-FDWOECJY.js";import"./chunk-VJUTQAPJ.js";import"./chunk-DJEZ2HVU.js";import"./chunk-DSYKYZMT.js";import"./chunk-5U35WAZF.js";import"./chunk-QOYAX2FT.js";import"./chunk-4YCH6IZV.js";export{a as buildProgram};
|
|
2
|
+
//# sourceMappingURL=program-WVAVJFND.js.map
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{a as k}from"./chunk-2WVX3LEC.js";import"./chunk-4YCH6IZV.js";import{existsSync as v,readFileSync as M}from"fs";import{resolve as K}from"path";function W(i){let m=[],g=new Set,j=new Map;for(let t of i.rules.list())g.add(t.project),j.set(t.project,t);let u=new Map,R=i.workflowStore.list();for(let t of R){if(!t.project)continue;g.add(t.project),u.has(t.project)||u.set(t.project,[]);let n=(t.nodes||[]).find(o=>o?.type?.startsWith("trigger.")),s=n?n.config?.event??n.config?.source??n.type.replace("trigger.",""):void 0;u.get(t.project).push({id:t.id,title:t.title||t.id,status:t.status||"active",trigger:s})}let f=i.config.channels?.gitlab,d=i.config.channels?.github;for(let t of f?.routes??[])t.project&&t.project!=="*"&&g.add(t.project);for(let t of d?.routes??[])t.repo&&t.repo!=="*"&&g.add(t.repo);let w=(f?.agentMappings??[]).map(t=>({agentId:t.agentId,usernames:t.gitlabUsernames??[]})),I=(d?.agentMappings??[]).map(t=>({agentId:t.agentId,usernames:t.githubUsernames??[]})),b=K(i.cwd,".agentx/contacts.json"),h=[];if(v(b))try{let t=JSON.parse(M(b,"utf-8"));h=Array.isArray(t)?t:t.contacts??[]}catch(t){m.push(`failed to parse .agentx/contacts.json: ${t?.message??t}`)}let y=[];for(let t of Array.from(g).sort()){let n=j.get(t),s=[],o=[],P=(f?.routes??[]).filter(e=>e.project===t);P.length>0&&s.push("gitlab");let A=(d?.routes??[]).filter(e=>e.repo===t);A.length>0&&s.push("github"),s.length===0&&n&&s.push("gitlab");let a=new Set;if(n?.agent&&!a.has(n.agent)){a.add(n.agent);let e=w.find(r=>r.agentId===n.agent);o.push({agentId:n.agent,channel:s[0]??"gitlab",via:"rule.agent",gitlabUsername:e?.usernames[0]})}for(let e of P){if(a.has(e.agent))continue;a.add(e.agent);let r=w.find(p=>p.agentId===e.agent);o.push({agentId:e.agent,channel:"gitlab",via:"channels.gitlab.routes",gitlabUsername:r?.usernames[0]})}for(let e of A){let r=e.agent;if(a.has(r))continue;a.add(r);let p=I.find(U=>U.agentId===r);o.push({agentId:r,channel:"github",via:"channels.github.routes",gitlabUsername:p?.usernames[0]})}let S=h.filter(e=>e.project===t).map(e=>({id:e.id,name:e.name??e.id,channels:Object.keys(e.channels??{})})),c=new Map;for(let e of S)c.set(e.id,e);if(n?.contacts)for(let e of n.contacts){if(c.has(e))continue;let r=h.find(p=>p.id===e);r?c.set(e,{id:e,name:r.name??e,channels:Object.keys(r.channels??{})}):c.set(e,{id:e,name:e+" (unknown)",channels:[]})}let l={};n?.gitlab&&(l.gitlab=n.gitlab),n?.github&&(l.github=n.github),y.push({project:t,kind:n?k.inferKind(n):"other",displayName:n?.displayName,homeUrl:n?.homeUrl,channels:s,agents:o,workflows:u.get(t)??[],contacts:Array.from(c.values()),runbook:n?.runbook,rulePath:n?._path,clauses:l.gitlab||l.github?l:void 0})}return{projects:y,warnings:m}}export{W as computeProjectsAggregate};
|
|
2
|
+
//# sourceMappingURL=projects-api-KRME2YKZ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/daemon/projects-api.ts"],"sourcesContent":["// --- Projects API: aggregated per-project view ---\n//\n// The /admin/projects page asks \"for this project, what does the\n// daemon know?\" — agents, workflows, channels, contacts. This module\n// is the read-side aggregator: it stitches together\n//\n// - `.agentx/projects/<org>/<repo>.yaml` rule files (ProjectRulesStore)\n// - `channels.gitlab.routes` / `channels.gitlab.agentMappings`\n// - `channels.github.routes` / `channels.github.agentMappings`\n// - workflowStore filtered by `workflow.project == <project>`\n// - `.agentx/contacts.json` entries that name this project\n//\n// Output shape is one row per known project; the page renders a list,\n// click-through opens a detail panel that consumes the same row. The\n// answer is computed fresh on every request — projects rarely change\n// at high frequency, and the cost is a few small reads + an in-memory\n// filter over workflows.\n//\n// Gating: read-only. No mutations exposed yet — controls (edit rule,\n// add agent, etc.) come in a follow-up once the read view is shaped.\n\nimport { existsSync, readFileSync } from \"fs\"\nimport { resolve } from \"path\"\nimport type { DaemonConfig } from \"./config\"\nimport { ProjectRulesStore, type ProjectRule, type ProjectKind } from \"@/projects/rules\"\nimport type { WorkflowStore } from \"@/workflows\"\n\nexport interface ProjectsApiContext {\n config: DaemonConfig\n rules: ProjectRulesStore\n workflowStore: WorkflowStore\n /** Working directory of the daemon — used to resolve contacts.json and\n * any other relative path the aggregator needs to read. */\n cwd: string\n}\n\nexport interface ProjectAggregateRow {\n /** Canonical project key — `<org>/<repo>` for vcs, operator slug otherwise. */\n project: string\n /** Source of this project, declared on the rule or inferred from\n * clauses. Used for the badge/icon on the Projects page header. */\n kind: ProjectKind\n /** Optional human-friendly name. Falls back to `project` when unset. */\n displayName?: string\n /** Optional external URL — repo / board home. */\n homeUrl?: string\n /** Channel binding inferred from the project rule + channel routes. Often\n * \"gitlab\" — but could be \"github\" or both (\"gitlab,github\") when a\n * monorepo is mirrored. */\n channels: string[]\n /** Agents bound to this project. Sourced from rule.agent (preferred) +\n * channels.gitlab.routes / .github.routes lookups + agentMappings cross-\n * reference. Each entry includes the agent id, the channel that bound\n * it, and the agent's displayName when available. */\n agents: Array<{ agentId: string; channel: string; via: string; gitlabUsername?: string }>\n /** Workflows whose top-level `project` field equals this project. */\n workflows: Array<{ id: string; title: string; status: string; trigger?: string }>\n /** Contacts associated with this project. Comes from contacts.json\n * entries that carry a `project` field. Empty when the contact store\n * doesn't tag projects. */\n contacts: Array<{ id: string; name: string; channels: string[] }>\n /** Runbook path resolved by the rule, if any. */\n runbook?: string\n /** Path to the source rule YAML for diagnostics. */\n rulePath?: string\n /** Raw rule clauses (gitlab.* and github.*) — surfaced so the\n * channel-clause editor can pre-populate its form fields without a\n * second round-trip. Both keys absent when the rule has none. */\n clauses?: {\n gitlab?: ProjectRule[\"gitlab\"]\n github?: ProjectRule[\"github\"]\n }\n}\n\nexport interface ProjectsApiResponse {\n /** Sorted by project key for deterministic UI rendering. */\n projects: ProjectAggregateRow[]\n /** When the aggregator hit an unparseable contacts.json or similar\n * soft failure, the message lands here so the operator sees it in the\n * UI without surfacing a 500. Empty array on the happy path. */\n warnings: string[]\n}\n\n/**\n * Compute the per-project aggregate. Pure read — no side effects.\n *\n * Project membership rules:\n * 1. Every project that has a rule file in `.agentx/projects/` is a\n * project, even if no workflow points at it yet.\n * 2. Every distinct `workflow.project` value is also a project, even\n * when there's no rule file (legacy / WIP state).\n * 3. Every distinct `channels.gitlab.routes[].project` and\n * `channels.github.routes[].repo` value is also a project (gives\n * the operator a \"you have a route but no rule\" signal).\n *\n * The union becomes the project list. Each row's fields are filled in\n * from whichever sources had data; absent sources land as empty arrays\n * rather than missing keys, so the UI doesn't have to defensive-check.\n */\nexport function computeProjectsAggregate(ctx: ProjectsApiContext): ProjectsApiResponse {\n const warnings: string[] = []\n const projectKeys = new Set<string>()\n\n // 1. Rule-defined projects.\n const ruleByKey = new Map<string, ProjectRule>()\n for (const rule of ctx.rules.list()) {\n projectKeys.add(rule.project)\n ruleByKey.set(rule.project, rule)\n }\n\n // 2. Workflow-tagged projects.\n const workflowsByProject = new Map<string, ProjectAggregateRow[\"workflows\"]>()\n const allWorkflows = ctx.workflowStore.list()\n for (const wf of allWorkflows) {\n if (!wf.project) continue\n projectKeys.add(wf.project)\n if (!workflowsByProject.has(wf.project)) workflowsByProject.set(wf.project, [])\n const triggerNode = (wf.nodes || []).find((n) => n?.type?.startsWith(\"trigger.\"))\n const triggerKey = triggerNode\n ? (triggerNode.config as { event?: string; source?: string } | undefined)?.event\n ?? (triggerNode.config as { source?: string } | undefined)?.source\n ?? triggerNode.type.replace(\"trigger.\", \"\")\n : undefined\n workflowsByProject.get(wf.project)!.push({\n id: wf.id,\n title: wf.title || wf.id,\n status: wf.status || \"active\",\n trigger: triggerKey,\n })\n }\n\n // 3. Channel-route-defined projects.\n const gitlab = ctx.config.channels?.gitlab\n const github = ctx.config.channels?.github\n for (const r of gitlab?.routes ?? []) {\n if (r.project && r.project !== \"*\") projectKeys.add(r.project)\n }\n for (const r of github?.routes ?? []) {\n if ((r as { repo?: string }).repo && (r as { repo: string }).repo !== \"*\") {\n projectKeys.add((r as { repo: string }).repo)\n }\n }\n\n // Cache the agentMappings reverse-lookup once.\n const gitlabMappings = (gitlab?.agentMappings ?? []).map((m) => ({\n agentId: m.agentId,\n usernames: m.gitlabUsernames ?? [],\n }))\n const githubMappings = (github?.agentMappings ?? []).map((m) => ({\n agentId: m.agentId,\n usernames: (m as { githubUsernames?: string[] }).githubUsernames ?? [],\n }))\n\n // Contacts JSON — load once per call. Soft-fail.\n const contactsPath = resolve(ctx.cwd, \".agentx/contacts.json\")\n let allContacts: Array<{ id: string; name?: string; project?: string; channels?: Record<string, unknown> }> = []\n if (existsSync(contactsPath)) {\n try {\n const raw = JSON.parse(readFileSync(contactsPath, \"utf-8\")) as\n { contacts?: Array<{ id: string; name?: string; project?: string; channels?: Record<string, unknown> }> }\n | Array<{ id: string; name?: string; project?: string; channels?: Record<string, unknown> }>\n allContacts = Array.isArray(raw) ? raw : (raw.contacts ?? [])\n } catch (e: any) {\n warnings.push(`failed to parse .agentx/contacts.json: ${e?.message ?? e}`)\n }\n }\n\n const rows: ProjectAggregateRow[] = []\n for (const key of Array.from(projectKeys).sort()) {\n const rule = ruleByKey.get(key)\n const channels: string[] = []\n const agents: ProjectAggregateRow[\"agents\"] = []\n\n // Channel binding via routes.\n const glRoutes = (gitlab?.routes ?? []).filter((r) => r.project === key)\n if (glRoutes.length > 0) channels.push(\"gitlab\")\n const ghRoutes = (github?.routes ?? []).filter((r) => (r as { repo?: string }).repo === key)\n if (ghRoutes.length > 0) channels.push(\"github\")\n // Rule fallback when no explicit route — most rule projects are gitlab.\n if (channels.length === 0 && rule) channels.push(\"gitlab\")\n\n // Agents: dedupe by agentId across (rule.agent, gitlab routes, github routes).\n const seenAgents = new Set<string>()\n if (rule?.agent && !seenAgents.has(rule.agent)) {\n seenAgents.add(rule.agent)\n const mapping = gitlabMappings.find((m) => m.agentId === rule.agent)\n agents.push({\n agentId: rule.agent,\n channel: channels[0] ?? \"gitlab\",\n via: \"rule.agent\",\n gitlabUsername: mapping?.usernames[0],\n })\n }\n for (const r of glRoutes) {\n if (seenAgents.has(r.agent)) continue\n seenAgents.add(r.agent)\n const mapping = gitlabMappings.find((m) => m.agentId === r.agent)\n agents.push({\n agentId: r.agent,\n channel: \"gitlab\",\n via: \"channels.gitlab.routes\",\n gitlabUsername: mapping?.usernames[0],\n })\n }\n for (const r of ghRoutes) {\n const a = (r as { agent: string }).agent\n if (seenAgents.has(a)) continue\n seenAgents.add(a)\n const mapping = githubMappings.find((m) => m.agentId === a)\n agents.push({\n agentId: a,\n channel: \"github\",\n via: \"channels.github.routes\",\n gitlabUsername: mapping?.usernames[0],\n })\n }\n\n // Contacts — match by exact `project` field on the contact entry.\n const contacts = allContacts\n .filter((c) => c.project === key)\n .map((c) => ({\n id: c.id,\n name: c.name ?? c.id,\n channels: Object.keys(c.channels ?? {}),\n }))\n\n // Merge contacts from BOTH sides of the link: rule.contacts (ids) and\n // .agentx/contacts.json entries with `project: <key>`. Dedup by id —\n // either side declaring the relationship is enough.\n const contactsMap = new Map<string, ProjectAggregateRow[\"contacts\"][number]>()\n for (const c of contacts) contactsMap.set(c.id, c)\n if (rule?.contacts) {\n for (const cid of rule.contacts) {\n if (contactsMap.has(cid)) continue\n const found = allContacts.find((c) => c.id === cid)\n if (found) {\n contactsMap.set(cid, {\n id: cid,\n name: found.name ?? cid,\n channels: Object.keys(found.channels ?? {}),\n })\n } else {\n // Rule declares a contact id that doesn't exist in contacts.json.\n // Surface anyway so the operator can spot stale links.\n contactsMap.set(cid, { id: cid, name: cid + \" (unknown)\", channels: [] })\n }\n }\n }\n\n const clauses: ProjectAggregateRow[\"clauses\"] = {}\n if (rule?.gitlab) clauses.gitlab = rule.gitlab\n if (rule?.github) clauses.github = rule.github\n\n rows.push({\n project: key,\n kind: rule ? ProjectRulesStore.inferKind(rule) : \"other\",\n displayName: rule?.displayName,\n homeUrl: rule?.homeUrl,\n channels,\n agents,\n workflows: workflowsByProject.get(key) ?? [],\n contacts: Array.from(contactsMap.values()),\n runbook: rule?.runbook,\n rulePath: rule?._path,\n clauses: (clauses.gitlab || clauses.github) ? clauses : undefined,\n })\n }\n\n return { projects: rows, warnings }\n}\n"],"mappings":"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","names":["existsSync","readFileSync","resolve","computeProjectsAggregate","ctx","warnings","projectKeys","ruleByKey","rule","workflowsByProject","allWorkflows","wf","triggerNode","n","triggerKey","gitlab","github","r","gitlabMappings","m","githubMappings","contactsPath","resolve","allContacts","existsSync","raw","readFileSync","e","rows","key","channels","agents","glRoutes","ghRoutes","seenAgents","mapping","a","contacts","c","contactsMap","cid","found","clauses","ProjectRulesStore"]}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import"./chunk-4YCH6IZV.js";import{existsSync as m,mkdirSync as k,readFileSync as g,readdirSync as b,statSync as K,unlinkSync as P,writeFileSync as R,renameSync as x,copyFileSync as j}from"fs";import{dirname as $,extname as S,join as E,resolve as s}from"path";import u from"js-yaml";function I(t,e){return[{path:s(e,`${t}.yaml`),format:"yaml"},{path:s(e,`${t}.yml`),format:"yaml"},{path:s(e,`${t}.json`),format:"json"}].find(n=>m(n.path))??null}function A(t){return`${t}.bak.${Date.now()}`}function O(t,e){let r=t.split(`
|
|
2
|
+
`),n=r.findIndex(o=>/^project\s*:/.test(o));if(n>=0){if(e===null){let i=r[n];return r.splice(n,1),{text:r.join(`
|
|
3
|
+
`),changed:i!==""}}let o=`project: ${e}`;return r[n]===o?{text:t,changed:!1}:(r[n]=o,{text:r.join(`
|
|
4
|
+
`),changed:!0})}if(e===null)return{text:t,changed:!1};let c=r.findIndex(o=>/^id\s*:/.test(o)),a=`project: ${e}`;return c>=0?r.splice(c+1,0,a):r.unshift(a),{text:r.join(`
|
|
5
|
+
`),changed:!0}}function M(t,e){let r=JSON.parse(t),n=r.project??null;if(e===null){if(n===null)return{text:t,changed:!1};delete r.project}else{if(n===e)return{text:t,changed:!1};r.project=e}return{text:JSON.stringify(r,null,2)+`
|
|
6
|
+
`,changed:!0}}function p(t,e){let r=`${t}.tmp.${process.pid}.${Date.now()}`;R(r,e,"utf-8"),x(r,t)}var y=".agentx/projects";function h(t,e){let r=s(e,y);if(!m(r))return null;let n=[r];for(;n.length>0;){let c=n.pop(),a;try{a=b(c)}catch{continue}for(let o of a){let i=E(c,o),l;try{l=K(i)}catch{continue}if(l.isDirectory()){n.push(i);continue}let w=S(o).toLowerCase();if(!(w!==".yaml"&&w!==".yml"))try{if(u.load(g(i,"utf-8"))?.project===t)return i}catch{}}}return null}function N(t,e){let r=s(e,y),n=t.replace(/[^a-zA-Z0-9._/-]/g,"_");return s(r,n+".yaml")}function d(t){let e=h(t.projectKey,t.cwd);if(!e)throw new Error(`project rule for "${t.projectKey}" not found under ${y}`);let r=g(e,"utf-8"),n=u.load(r)??{};if(n.project!==t.projectKey)throw new Error(`rule file at ${e} project=${n.project} does not match requested ${t.projectKey}`);let c=t.mutator(structuredClone(n)),a=f(c),o=u.dump(a,{lineWidth:100,noRefs:!0,sortKeys:!1});if(o===r)return{projectKey:t.projectKey,path:e,backup:"",changed:!1};try{u.load(o)}catch(l){throw new Error(`refusing to write \u2014 YAML parse failed after mutation: ${l?.message??l}`)}let i=`${e}.bak.${Date.now()}`;return j(e,i),p(e,o),{projectKey:t.projectKey,path:e,backup:i,changed:!0}}function f(t){if(t!=null){if(typeof t=="string")return t===""?void 0:t;if(Array.isArray(t)){let e=t.map(f).filter(r=>r!==void 0);return e.length>0?e:void 0}if(typeof t=="object"){let e={},r=0;for(let[n,c]of Object.entries(t)){let a=f(c);a!==void 0&&(e[n]=a,r++)}return r>0?e:void 0}return t}}var C=["displayName","homeUrl","kind","runbook","agent"];function _(t){return d({projectKey:t.projectKey,cwd:t.cwd,mutator:e=>{for(let r of C){if(!(r in t.patch))continue;let n=t.patch[r];if(n===""||n===null)delete e[r];else if(typeof n=="string")if(r==="kind"){let c=["gitlab","github","jira","linear","other"];if(!c.includes(n))throw new Error(`invalid kind="${n}" \u2014 allowed: ${c.join(", ")}`);e.kind=n}else e[r]=n}return e}})}function v(t){return d({projectKey:t.projectKey,cwd:t.cwd,mutator:e=>{let r=Array.isArray(e.contacts)?e.contacts.slice():[];return r.includes(t.contactId)||r.push(t.contactId),e.contacts=r,e}})}function U(t){return d({projectKey:t.projectKey,cwd:t.cwd,mutator:e=>{let r=Array.isArray(e.contacts)?e.contacts.filter(n=>n!==t.contactId):[];return r.length>0?e.contacts=r:delete e.contacts,e}})}function H(t){return d({projectKey:t.projectKey,cwd:t.cwd,mutator:e=>(t.gitlab!==void 0&&(t.gitlab===null?delete e.gitlab:e.gitlab=t.gitlab),t.github!==void 0&&(t.github===null?delete e.github:e.github=t.github),e)})}function J(t){if(!/^[a-zA-Z0-9._-]+(\/[a-zA-Z0-9._-]+)*$/.test(t.projectKey))throw new Error("project key must be alphanumeric with optional / separators");if(h(t.projectKey,t.cwd))throw new Error(`project "${t.projectKey}" already exists`);let e=N(t.projectKey,t.cwd);k($(e),{recursive:!0});let r={project:t.projectKey,kind:t.kind};t.displayName&&(r.displayName=t.displayName),t.homeUrl&&(r.homeUrl=t.homeUrl),t.runbook&&(r.runbook=t.runbook),t.agent&&(r.agent=t.agent);let n=u.dump(r,{lineWidth:100,noRefs:!0,sortKeys:!1});return p(e,n),{path:e}}function z(t){let e=h(t.projectKey,t.cwd);if(!e)throw new Error(`project "${t.projectKey}" not found`);let r=`${e}.bak.${Date.now()}`;return j(e,r),P(e),{path:e,backup:r}}function Y(t){let e=s(t.cwd,".agentx/workflows"),r=I(t.workflowId,e);if(!r)throw new Error(`workflow "${t.workflowId}" not found in ${e}`);let n=g(r.path,"utf-8"),{text:c,changed:a}=r.format==="yaml"?O(n,t.projectKey):M(n,t.projectKey);if(r.format==="yaml")try{u.load(c)}catch(i){throw new Error(`refusing to write \u2014 YAML parse failed after mutation: ${i?.message??i}`)}else try{JSON.parse(c)}catch(i){throw new Error(`refusing to write \u2014 JSON parse failed after mutation: ${i?.message??i}`)}let o="";return a&&(o=A(r.path),j(r.path,o),p(r.path,c)),{workflowId:t.workflowId,path:r.path,format:r.format,backup:o,project:t.projectKey,changed:a}}export{J as createProjectRule,z as deleteProjectRule,v as linkContact,d as mutateProjectRule,_ as patchProjectHeader,H as setProjectClauses,Y as setWorkflowProject,U as unlinkContact};
|
|
7
|
+
//# sourceMappingURL=projects-mutate-3NHVG437.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/daemon/projects-mutate.ts"],"sourcesContent":["// --- Project ↔ workflow tag mutation ---\n//\n// Adds, replaces, or removes the top-level `project:` field on a\n// workflow YAML / JSON file in `.agentx/workflows/`. Surface used by\n// the Projects page's link/unlink controls so an operator can re-\n// associate a workflow without hand-editing its source.\n//\n// Stability concerns honored:\n// - YAML files: surgical text edit. Preserves comments + ordering +\n// existing fields; only the `project:` line is touched.\n// - JSON files: parse → mutate → write with stable key ordering.\n// - Atomic write via a sibling tempfile + rename so a partial-write\n// can never corrupt the source file.\n// - Backup written to `<file>.bak.<unix-ms>` BEFORE any write so the\n// operator can recover even if the new content is later regretted.\n//\n// What this DOES NOT do:\n// - Validate that projectKey is a known project. The Projects page\n// resolves the autocomplete from existing projects; the daemon\n// trusts the caller. Setting an unknown project just makes the\n// workflow show up as \"(no project)\" elsewhere — recoverable.\n// - Notify the workflow store of the change. workflowStore.list() is\n// stateless and re-reads on each call, so the next /api/workflows\n// request picks up the new value automatically.\n\nimport { existsSync, mkdirSync, readFileSync, readdirSync, statSync, unlinkSync, writeFileSync, renameSync, copyFileSync } from \"fs\"\nimport { dirname, extname, join, resolve } from \"path\"\nimport yaml from \"js-yaml\"\nimport type { ProjectRule, ProjectKind } from \"@/projects/rules\"\n\nexport interface SetWorkflowProjectOpts {\n workflowId: string\n /** Target project key, or null to strip the field entirely. */\n projectKey: string | null\n cwd: string\n}\n\nexport interface SetWorkflowProjectResult {\n workflowId: string\n /** Resolved file path that was rewritten — caller can surface it. */\n path: string\n /** Format detected on disk: \"yaml\" or \"json\". */\n format: \"yaml\" | \"json\"\n /** Backup file written before the mutation. */\n backup: string\n /** New value of `project:` after the mutation (null when unlinked). */\n project: string | null\n /** True when the file actually changed; false when the request was\n * idempotent (e.g. linking to the same project the workflow already\n * carries). */\n changed: boolean\n}\n\n/**\n * Pick the workflow file. Mirrors the WorkflowStore's\n * `<id>.yaml` > `<id>.yml` > `<id>.json` precedence — file format\n * uniqueness is enforced at WorkflowStore level (collisions cause the\n * id to drop out of list()), so we don't need to re-check that here.\n */\nfunction findWorkflowPath(workflowId: string, baseDir: string): { path: string; format: \"yaml\" | \"json\" } | null {\n const candidates: Array<{ path: string; format: \"yaml\" | \"json\" }> = [\n { path: resolve(baseDir, `${workflowId}.yaml`), format: \"yaml\" },\n { path: resolve(baseDir, `${workflowId}.yml`), format: \"yaml\" },\n { path: resolve(baseDir, `${workflowId}.json`), format: \"json\" },\n ]\n return candidates.find((c) => existsSync(c.path)) ?? null\n}\n\nfunction backupPath(p: string): string {\n return `${p}.bak.${Date.now()}`\n}\n\n/**\n * Atomically replace the top-level `project:` field in a YAML file.\n *\n * Strategy: scan top-level lines (zero indent). If a `project:` line\n * exists, replace the value (or remove the line when newValue=null).\n * If absent, insert it RIGHT AFTER the existing top-level `id:` line\n * — that's the conventional placement in our seeded workflows. Falls\n * back to inserting at the top of the file when no `id:` is found.\n *\n * Why not js-yaml round-trip: js-yaml drops comments and re-orders\n * keys. Workflow YAMLs are documentation-rich; preserving comments\n * matters for hand-readability. A line-level edit is sufficient\n * because `project:` is always top-level and a single line.\n */\nfunction rewriteYamlProject(text: string, newValue: string | null): { text: string; changed: boolean } {\n const lines = text.split(\"\\n\")\n // Top-level lines have no leading whitespace before the colon.\n const projectLineIdx = lines.findIndex((l) => /^project\\s*:/.test(l))\n if (projectLineIdx >= 0) {\n if (newValue === null) {\n // Remove. If the line is followed by a YAML block-scalar\n // continuation, that would be unusual for `project:` (always a\n // scalar string in our schema). We assume single-line.\n const before = lines[projectLineIdx]\n lines.splice(projectLineIdx, 1)\n const next = lines.join(\"\\n\")\n return { text: next, changed: before !== \"\" }\n }\n const replaced = `project: ${newValue}`\n if (lines[projectLineIdx] === replaced) {\n return { text, changed: false }\n }\n lines[projectLineIdx] = replaced\n return { text: lines.join(\"\\n\"), changed: true }\n }\n // Absent — insert. Prefer placing right after the `id:` line; fall\n // back to top.\n if (newValue === null) {\n return { text, changed: false } // nothing to remove\n }\n const idLineIdx = lines.findIndex((l) => /^id\\s*:/.test(l))\n const insertion = `project: ${newValue}`\n if (idLineIdx >= 0) {\n lines.splice(idLineIdx + 1, 0, insertion)\n } else {\n lines.unshift(insertion)\n }\n return { text: lines.join(\"\\n\"), changed: true }\n}\n\nfunction rewriteJsonProject(text: string, newValue: string | null): { text: string; changed: boolean } {\n const obj = JSON.parse(text) as Record<string, unknown>\n const current = (obj.project as string | undefined) ?? null\n if (newValue === null) {\n if (current === null) return { text, changed: false }\n delete obj.project\n } else {\n if (current === newValue) return { text, changed: false }\n obj.project = newValue\n }\n return { text: JSON.stringify(obj, null, 2) + \"\\n\", changed: true }\n}\n\nfunction atomicWrite(path: string, content: string): void {\n const tmp = `${path}.tmp.${process.pid}.${Date.now()}`\n writeFileSync(tmp, content, \"utf-8\")\n renameSync(tmp, path)\n}\n\n// ────────────────────────────────────────────────────────────────────────\n// Project rule file mutations\n// ────────────────────────────────────────────────────────────────────────\n//\n// Walks .agentx/projects/ to find the YAML for a given project key,\n// runs the operator's mutator on the parsed rule, validates that the\n// result still parses as YAML, then atomic-writes with a backup.\n//\n// Round-tripping: js-yaml's load/dump pair drops comments and may\n// re-order keys. For the operator-edit workflow we prefer that —\n// edits become canonical, predictable. The original is preserved in\n// the .bak.<unix-ms> sidecar, so anyone who hand-authored comments\n// has a recovery path.\n\nconst RULES_DIR_REL = \".agentx/projects\"\n\nfunction findRulePath(projectKey: string, cwd: string): string | null {\n const root = resolve(cwd, RULES_DIR_REL)\n if (!existsSync(root)) return null\n // Walk the tree — rule files live at any depth (org/repo/...).\n const stack: string[] = [root]\n while (stack.length > 0) {\n const dir = stack.pop()!\n let entries: string[]\n try { entries = readdirSync(dir) } catch { continue }\n for (const name of entries) {\n const full = join(dir, name)\n let s\n try { s = statSync(full) } catch { continue }\n if (s.isDirectory()) { stack.push(full); continue }\n const ext = extname(name).toLowerCase()\n if (ext !== \".yaml\" && ext !== \".yml\") continue\n try {\n const raw = yaml.load(readFileSync(full, \"utf-8\")) as { project?: string } | null\n if (raw?.project === projectKey) return full\n } catch { /* skip malformed */ }\n }\n }\n return null\n}\n\nfunction pathForNewRule(projectKey: string, cwd: string): string {\n // Layout matches the existing seeds: <root>/<org>/<repo>.yaml.\n // For non-VCS projects (jira / linear) the operator might pick a slug\n // without a slash; we still write under <root>/<slug>.yaml in that case.\n const root = resolve(cwd, RULES_DIR_REL)\n const safe = projectKey.replace(/[^a-zA-Z0-9._/-]/g, \"_\")\n return resolve(root, safe + \".yaml\")\n}\n\nexport interface MutateProjectRuleOpts {\n projectKey: string\n cwd: string\n mutator: (rule: ProjectRule) => ProjectRule\n}\n\nexport interface MutateProjectRuleResult {\n projectKey: string\n path: string\n backup: string\n changed: boolean\n}\n\nexport function mutateProjectRule(opts: MutateProjectRuleOpts): MutateProjectRuleResult {\n const path = findRulePath(opts.projectKey, opts.cwd)\n if (!path) {\n throw new Error(`project rule for \"${opts.projectKey}\" not found under ${RULES_DIR_REL}`)\n }\n const before = readFileSync(path, \"utf-8\")\n const rule = (yaml.load(before) ?? {}) as ProjectRule\n if (rule.project !== opts.projectKey) {\n // Defensive — finder matched on .project so this should be impossible.\n throw new Error(`rule file at ${path} project=${rule.project} does not match requested ${opts.projectKey}`)\n }\n const mutated = opts.mutator(structuredClone(rule))\n\n // Strip empty/undefined fields so the output stays clean. Operators\n // expect \"set field to empty string\" to mean \"remove the field\" —\n // matches the pattern across agentx config (cron prompts, channel\n // tokens, etc.).\n const cleaned = pruneEmpty(mutated)\n\n const after = yaml.dump(cleaned, { lineWidth: 100, noRefs: true, sortKeys: false })\n if (after === before) {\n return { projectKey: opts.projectKey, path, backup: \"\", changed: false }\n }\n // Validate the post-mutation YAML round-trips cleanly.\n try { yaml.load(after) } catch (e: any) {\n throw new Error(`refusing to write — YAML parse failed after mutation: ${e?.message ?? e}`)\n }\n const backup = `${path}.bak.${Date.now()}`\n copyFileSync(path, backup)\n atomicWrite(path, after)\n return { projectKey: opts.projectKey, path, backup, changed: true }\n}\n\n/** Strip undefined / null / empty-string / empty-array values recursively\n * so the YAML output stays compact. Empty objects become undefined and\n * the parent strips them on the next pass. */\nfunction pruneEmpty(input: unknown): unknown {\n if (input === null || input === undefined) return undefined\n if (typeof input === \"string\") return input === \"\" ? undefined : input\n if (Array.isArray(input)) {\n const out = input.map(pruneEmpty).filter((v) => v !== undefined)\n return out.length > 0 ? out : undefined\n }\n if (typeof input === \"object\") {\n const out: Record<string, unknown> = {}\n let kept = 0\n for (const [k, v] of Object.entries(input as Record<string, unknown>)) {\n const cleaned = pruneEmpty(v)\n if (cleaned !== undefined) { out[k] = cleaned; kept++ }\n }\n return kept > 0 ? out : undefined\n }\n return input\n}\n\n// ── Header-only mutations: shallow merge into the rule's top-level\n// scalar fields. The Project page's edit modal uses this — sending\n// only the fields the operator actually changed. Empty string =\n// remove field.\nconst HEADER_FIELDS = [\"displayName\", \"homeUrl\", \"kind\", \"runbook\", \"agent\"] as const\ntype HeaderField = typeof HEADER_FIELDS[number]\n\nexport function patchProjectHeader(opts: { projectKey: string; cwd: string; patch: Partial<Record<HeaderField, string | null>> }): MutateProjectRuleResult {\n return mutateProjectRule({\n projectKey: opts.projectKey,\n cwd: opts.cwd,\n mutator: (rule) => {\n for (const k of HEADER_FIELDS) {\n if (!(k in opts.patch)) continue\n const v = opts.patch[k]\n if (v === \"\" || v === null) {\n delete (rule as unknown as Record<string, unknown>)[k]\n } else if (typeof v === \"string\") {\n if (k === \"kind\") {\n const allowed: ProjectKind[] = [\"gitlab\", \"github\", \"jira\", \"linear\", \"other\"]\n if (!allowed.includes(v as ProjectKind)) {\n throw new Error(`invalid kind=\"${v}\" — allowed: ${allowed.join(\", \")}`)\n }\n rule.kind = v as ProjectKind\n } else {\n (rule as unknown as Record<string, unknown>)[k] = v\n }\n }\n }\n return rule\n },\n })\n}\n\n// ── Contact link/unlink: array surgery on rule.contacts. ──────────\n\nexport function linkContact(opts: { projectKey: string; cwd: string; contactId: string }): MutateProjectRuleResult {\n return mutateProjectRule({\n projectKey: opts.projectKey,\n cwd: opts.cwd,\n mutator: (rule) => {\n const list = Array.isArray(rule.contacts) ? rule.contacts.slice() : []\n if (!list.includes(opts.contactId)) list.push(opts.contactId)\n rule.contacts = list\n return rule\n },\n })\n}\n\nexport function unlinkContact(opts: { projectKey: string; cwd: string; contactId: string }): MutateProjectRuleResult {\n return mutateProjectRule({\n projectKey: opts.projectKey,\n cwd: opts.cwd,\n mutator: (rule) => {\n const list = Array.isArray(rule.contacts) ? rule.contacts.filter((c) => c !== opts.contactId) : []\n if (list.length > 0) rule.contacts = list\n else delete (rule as { contacts?: unknown }).contacts\n return rule\n },\n })\n}\n\n// ── Channel-clause replacement ────────────────────────────────────\n//\n// The Channels section of a rule (gitlab.issue/merge_request/note/\n// pipeline + github.issues/pull_request) is the most form-heavy edit\n// surface. Rather than per-clause partial merges (which hide the\n// final shape from the operator), we accept the FULL channel object\n// from the UI and replace the rule's gitlab / github top-level keys\n// in one shot. The form UI is responsible for sending what the\n// operator wants the rule to look like; we trust + validate.\n//\n// Empty objects / arrays are pruned by the generic pruneEmpty so a\n// \"remove this clause\" intent can be expressed by sending {} for it.\n\nexport interface SetProjectClausesOpts {\n projectKey: string\n cwd: string\n /** When provided, REPLACES the rule's top-level `gitlab` block in\n * full. Pass `null` to remove the entire gitlab section. Omit\n * (undefined) to leave it untouched. Same semantics for github. */\n gitlab?: ProjectRule[\"gitlab\"] | null\n github?: ProjectRule[\"github\"] | null\n}\n\nexport function setProjectClauses(opts: SetProjectClausesOpts): MutateProjectRuleResult {\n return mutateProjectRule({\n projectKey: opts.projectKey,\n cwd: opts.cwd,\n mutator: (rule) => {\n if (opts.gitlab !== undefined) {\n if (opts.gitlab === null) delete rule.gitlab\n else rule.gitlab = opts.gitlab\n }\n if (opts.github !== undefined) {\n if (opts.github === null) delete rule.github\n else rule.github = opts.github\n }\n return rule\n },\n })\n}\n\n// ── Create / delete project rule files ────────────────────────────\n\nexport interface CreateProjectOpts {\n projectKey: string\n cwd: string\n kind: ProjectKind\n displayName?: string\n homeUrl?: string\n runbook?: string\n agent?: string\n}\n\nexport function createProjectRule(opts: CreateProjectOpts): { path: string } {\n if (!/^[a-zA-Z0-9._-]+(\\/[a-zA-Z0-9._-]+)*$/.test(opts.projectKey)) {\n throw new Error(\"project key must be alphanumeric with optional / separators\")\n }\n if (findRulePath(opts.projectKey, opts.cwd)) {\n throw new Error(`project \"${opts.projectKey}\" already exists`)\n }\n const path = pathForNewRule(opts.projectKey, opts.cwd)\n mkdirSync(dirname(path), { recursive: true })\n const rule: Partial<ProjectRule> = {\n project: opts.projectKey,\n kind: opts.kind,\n }\n if (opts.displayName) rule.displayName = opts.displayName\n if (opts.homeUrl) rule.homeUrl = opts.homeUrl\n if (opts.runbook) rule.runbook = opts.runbook\n if (opts.agent) rule.agent = opts.agent\n const yamlText = yaml.dump(rule, { lineWidth: 100, noRefs: true, sortKeys: false })\n atomicWrite(path, yamlText)\n return { path }\n}\n\nexport function deleteProjectRule(opts: { projectKey: string; cwd: string }): { path: string; backup: string } {\n const path = findRulePath(opts.projectKey, opts.cwd)\n if (!path) {\n throw new Error(`project \"${opts.projectKey}\" not found`)\n }\n const backup = `${path}.bak.${Date.now()}`\n copyFileSync(path, backup)\n unlinkSync(path)\n return { path, backup }\n}\n\nexport function setWorkflowProject(opts: SetWorkflowProjectOpts): SetWorkflowProjectResult {\n const baseDir = resolve(opts.cwd, \".agentx/workflows\")\n const found = findWorkflowPath(opts.workflowId, baseDir)\n if (!found) {\n throw new Error(`workflow \"${opts.workflowId}\" not found in ${baseDir}`)\n }\n const text = readFileSync(found.path, \"utf-8\")\n const { text: nextText, changed } =\n found.format === \"yaml\"\n ? rewriteYamlProject(text, opts.projectKey)\n : rewriteJsonProject(text, opts.projectKey)\n\n // Sanity check: the new YAML must still parse. js-yaml.load throws\n // on malformed; we want to fail BEFORE writing, never after.\n if (found.format === \"yaml\") {\n try { yaml.load(nextText) } catch (e: any) {\n throw new Error(`refusing to write — YAML parse failed after mutation: ${e?.message ?? e}`)\n }\n } else {\n try { JSON.parse(nextText) } catch (e: any) {\n throw new Error(`refusing to write — JSON parse failed after mutation: ${e?.message ?? e}`)\n }\n }\n\n let backup = \"\"\n if (changed) {\n backup = backupPath(found.path)\n copyFileSync(found.path, backup)\n atomicWrite(found.path, nextText)\n }\n\n return {\n workflowId: opts.workflowId,\n path: found.path,\n format: found.format,\n backup,\n project: opts.projectKey,\n changed,\n }\n}\n"],"mappings":"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","names":["existsSync","mkdirSync","readFileSync","readdirSync","statSync","unlinkSync","writeFileSync","renameSync","copyFileSync","dirname","extname","join","resolve","yaml","findWorkflowPath","workflowId","baseDir","c","backupPath","p","rewriteYamlProject","text","newValue","lines","projectLineIdx","l","before","replaced","idLineIdx","insertion","rewriteJsonProject","obj","current","atomicWrite","path","content","tmp","RULES_DIR_REL","findRulePath","projectKey","cwd","root","stack","dir","entries","name","full","s","ext","pathForNewRule","safe","mutateProjectRule","opts","rule","mutated","cleaned","pruneEmpty","after","e","backup","input","out","v","kept","k","HEADER_FIELDS","patchProjectHeader","allowed","linkContact","list","unlinkContact","setProjectClauses","createProjectRule","yamlText","deleteProjectRule","setWorkflowProject","found","nextText","changed"]}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{A as f,B as g,C as h,v as a,w as b,x as c,y as d,z as e}from"./chunk-HA3XI7Z2.js";import"./chunk-Q6YO7LYE.js";import"./chunk-M4AMWP3H.js";import"./chunk-67Q4DCPS.js";import"./chunk-W4YR2RO2.js";import"./chunk-AFRHKA7B.js";import"./chunk-53GCFZPR.js";import"./chunk-FDMWOPEL.js";import"./chunk-JXWAGMI2.js";import"./chunk-ISEBLHWX.js";import"./chunk-HR4JSRZS.js";import"./chunk-VWTBHLMX.js";import"./chunk-BFXONUUN.js";import"./chunk-JTJIPNXY.js";import"./chunk-64P4QHSZ.js";import"./chunk-BFZUG66X.js";import"./chunk-SR3X25TN.js";import"./chunk-53NRX2NA.js";import"./chunk-FDWOECJY.js";import"./chunk-VJUTQAPJ.js";import"./chunk-DSYKYZMT.js";import"./chunk-5U35WAZF.js";import"./chunk-QOYAX2FT.js";import"./chunk-4YCH6IZV.js";export{h as AgentRegistry,c as TRACE_STEP_SUMMARY_BYTES,d as clipForTrace,g as emitTraceStepsFromStreamEvent,f as firstMissingRequiredTool,b as getGlobalRegistry,a as setGlobalRegistry,e as tallyToolUses};
|
|
2
|
+
//# sourceMappingURL=registry-T36ZIAWV.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import{a as l,b as m}from"./chunk-HR4JSRZS.js";import"./chunk-4YCH6IZV.js";import{execFile as f}from"child_process";var g=3e4,w="haiku",M=`You are about to lose this working session due to context-size rotation. Before that happens, write a compact memo of what you should carry forward to your next session on the same conversation.
|
|
2
|
+
|
|
3
|
+
Include only durable facts a future turn would otherwise have to rediscover:
|
|
4
|
+
- Names, handles, agent ids, telegram/whatsapp/gitlab usernames
|
|
5
|
+
- File paths, URLs, project namespaces, ssh hosts, ports
|
|
6
|
+
- Decisions made and their reasoning
|
|
7
|
+
- Open tasks or commitments to follow up on
|
|
8
|
+
- Identifiers (issue numbers, PR numbers, ticket ids) the user referred to
|
|
9
|
+
|
|
10
|
+
Format: 3-12 short bullet points. Each \u2264120 chars. No prose, no preamble, no header. If the session was uninformative or just casual chat, reply with exactly the literal text "(no memo)".
|
|
11
|
+
|
|
12
|
+
Begin the memo now.`;async function R(n,r){let c=Date.now();if(!r)return{memo:null,reason:"no-resume",durationMs:0};let i=["-p",M,"--output-format","json","--resume",r,"--model",w];n.permissionMode==="bypassPermissions"&&i.push("--dangerously-skip-permissions");let p=m(l(n.workspace));return new Promise(d=>{let a=!1,e=(t,s)=>{a||(a=!0,d({memo:t,reason:s,durationMs:Date.now()-c}))};f("claude",i,{cwd:n.workspace,env:p,timeout:g,maxBuffer:1024*1024},(t,s,b)=>{if(t)return t.killed?e(null,"timeout"):e(null,`error:${t.message.slice(0,80)}`);try{let u=JSON.parse(s||"{}"),o=typeof u.result=="string"?u.result.trim():"";if(!o||o==="(no memo)")return e(null,"empty");let h=o.length>4096?o.slice(0,4096)+"\u2026":o;return e(h,"ok")}catch{return e(null,"parse-error")}}).on("error",()=>e(null,"spawn-error"))})}export{R as extractRotationMemo};
|
|
13
|
+
//# sourceMappingURL=rotation-memo-B3EXXSEH.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/agents/rotation-memo.ts"],"sourcesContent":["import { execFile } from \"child_process\"\nimport type { AgentDef } from \"@/daemon/config\"\nimport { buildAgentEnv, stripAnthropicApiKey } from \"@/utils/workspace-env\"\n\n// --- Pre-rotation memo extraction ---\n//\n// Just before agentx clears `claudeSessionId` (tier-2 / max-turns / stale\n// rotation), we ask the dying Claude session to dump the facts and\n// decisions it should carry forward. The agent has full working-memory\n// of its tool-use history at this moment — the only chance to capture\n// it before --resume is dropped and the next turn starts with a blank\n// session.\n//\n// The memo persists into MemoryStore, where the existing findRelevant\n// path injects it into future turns naturally. No new wiring on the\n// read side; one-shot LLM call on the write side.\n//\n// Hard rule: never block rotation. Timeout, errors, empty memos all\n// mean \"skip this round of memo capture\". Memory continuity is a\n// best-effort enhancement, not a correctness requirement.\n\nconst MEMO_TIMEOUT_MS = 30_000\nconst MEMO_MODEL = \"haiku\" // cheapest fast model — memo is summarisation, not reasoning\n\nconst MEMO_PROMPT =\n \"You are about to lose this working session due to context-size rotation. \" +\n \"Before that happens, write a compact memo of what you should carry forward \" +\n \"to your next session on the same conversation.\\n\\n\" +\n \"Include only durable facts a future turn would otherwise have to rediscover:\\n\" +\n \"- Names, handles, agent ids, telegram/whatsapp/gitlab usernames\\n\" +\n \"- File paths, URLs, project namespaces, ssh hosts, ports\\n\" +\n \"- Decisions made and their reasoning\\n\" +\n \"- Open tasks or commitments to follow up on\\n\" +\n \"- Identifiers (issue numbers, PR numbers, ticket ids) the user referred to\\n\\n\" +\n \"Format: 3-12 short bullet points. Each ≤120 chars. No prose, no preamble, \" +\n \"no header. If the session was uninformative or just casual chat, reply with \" +\n \"exactly the literal text \\\"(no memo)\\\".\\n\\n\" +\n \"Begin the memo now.\"\n\nexport interface RotationMemoResult {\n /** The extracted memo text, or null when extraction failed/skipped. */\n memo: string | null\n /** Why we skipped or what happened — \"ok\", \"timeout\", \"error\", \"empty\", \"no-resume\". */\n reason: string\n /** Wall-clock time spent in the extractor. */\n durationMs: number\n}\n\n/** Ask the dying Claude session for a carry-forward memo. Spawns one\n * short claude CLI call with --resume on the about-to-be-dropped\n * session id, asks for a compact memo, returns the text. Never throws\n * — every failure mode resolves with `memo=null` so the caller can\n * proceed with the rotation regardless. */\nexport async function extractRotationMemo(\n agent: AgentDef,\n resumeSessionId: string | undefined,\n): Promise<RotationMemoResult> {\n const start = Date.now()\n if (!resumeSessionId) {\n return { memo: null, reason: \"no-resume\", durationMs: 0 }\n }\n\n const args: string[] = [\n \"-p\", MEMO_PROMPT,\n \"--output-format\", \"json\",\n \"--resume\", resumeSessionId,\n \"--model\", MEMO_MODEL,\n ]\n if (agent.permissionMode === \"bypassPermissions\") {\n args.push(\"--dangerously-skip-permissions\")\n }\n\n const env = stripAnthropicApiKey(buildAgentEnv(agent.workspace))\n\n return new Promise<RotationMemoResult>((resolve) => {\n let settled = false\n const finish = (memo: string | null, reason: string) => {\n if (settled) return\n settled = true\n resolve({ memo, reason, durationMs: Date.now() - start })\n }\n\n const child = execFile(\n \"claude\",\n args,\n {\n cwd: agent.workspace,\n env,\n timeout: MEMO_TIMEOUT_MS,\n maxBuffer: 1024 * 1024, // 1 MB; the memo itself is tiny\n },\n (err, stdout, _stderr) => {\n if (err) {\n // execFile signals timeout via signal SIGTERM + err.killed\n if ((err as NodeJS.ErrnoException & { killed?: boolean }).killed) {\n return finish(null, \"timeout\")\n }\n return finish(null, `error:${err.message.slice(0, 80)}`)\n }\n try {\n const data = JSON.parse(stdout || \"{}\")\n // claude -p --output-format json yields { result, ... }\n const text = typeof data.result === \"string\" ? data.result.trim() : \"\"\n if (!text || text === \"(no memo)\") return finish(null, \"empty\")\n // Defensive cap: memos > 4 KB are almost certainly the LLM\n // ignoring instructions and dumping prose. Truncate.\n const capped = text.length > 4096 ? text.slice(0, 4096) + \"…\" : text\n return finish(capped, \"ok\")\n } catch {\n return finish(null, \"parse-error\")\n }\n },\n )\n child.on(\"error\", () => finish(null, \"spawn-error\"))\n })\n}\n"],"mappings":"2EAAA,OAAS,YAAAA,MAAgB,gBAqBzB,IAAMC,EAAkB,IAClBC,EAAa,QAEbC,EACJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,qBA4BF,eAAsBC,EACpBC,EACAC,EAC6B,CAC7B,IAAMC,EAAQ,KAAK,IAAI,EACvB,GAAI,CAACD,EACH,MAAO,CAAE,KAAM,KAAM,OAAQ,YAAa,WAAY,CAAE,EAG1D,IAAME,EAAiB,CACrB,KAAML,EACN,kBAAmB,OACnB,WAAYG,EACZ,UAAWJ,CACb,EACIG,EAAM,iBAAmB,qBAC3BG,EAAK,KAAK,gCAAgC,EAG5C,IAAMC,EAAMC,EAAqBC,EAAcN,EAAM,SAAS,CAAC,EAE/D,OAAO,IAAI,QAA6BO,GAAY,CAClD,IAAIC,EAAU,GACRC,EAAS,CAACC,EAAqBC,IAAmB,CAClDH,IACJA,EAAU,GACVD,EAAQ,CAAE,KAAAG,EAAM,OAAAC,EAAQ,WAAY,KAAK,IAAI,EAAIT,CAAM,CAAC,EAC1D,EAEcU,EACZ,SACAT,EACA,CACE,IAAKH,EAAM,UACX,IAAAI,EACA,QAASR,EACT,UAAW,KAAO,IACpB,EACA,CAACiB,EAAKC,EAAQC,IAAY,CACxB,GAAIF,EAEF,OAAKA,EAAqD,OACjDJ,EAAO,KAAM,SAAS,EAExBA,EAAO,KAAM,SAASI,EAAI,QAAQ,MAAM,EAAG,EAAE,GAAG,EAEzD,GAAI,CACF,IAAMG,EAAO,KAAK,MAAMF,GAAU,IAAI,EAEhCG,EAAO,OAAOD,EAAK,QAAW,SAAWA,EAAK,OAAO,KAAK,EAAI,GACpE,GAAI,CAACC,GAAQA,IAAS,YAAa,OAAOR,EAAO,KAAM,OAAO,EAG9D,IAAMS,EAASD,EAAK,OAAS,KAAOA,EAAK,MAAM,EAAG,IAAI,EAAI,SAAMA,EAChE,OAAOR,EAAOS,EAAQ,IAAI,CAC5B,MAAE,CACA,OAAOT,EAAO,KAAM,aAAa,CACnC,CACF,CACF,EACM,GAAG,QAAS,IAAMA,EAAO,KAAM,aAAa,CAAC,CACrD,CAAC,CACH","names":["execFile","MEMO_TIMEOUT_MS","MEMO_MODEL","MEMO_PROMPT","extractRotationMemo","agent","resumeSessionId","start","args","env","stripAnthropicApiKey","buildAgentEnv","resolve","settled","finish","memo","reason","execFile","err","stdout","_stderr","data","text","capped"]}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import"./chunk-VWTBHLMX.js";import{e as a,f as b}from"./chunk-BFXONUUN.js";import"./chunk-JTJIPNXY.js";import"./chunk-64P4QHSZ.js";import{a as c}from"./chunk-BFZUG66X.js";import"./chunk-FDWOECJY.js";import"./chunk-VJUTQAPJ.js";import"./chunk-QOYAX2FT.js";import"./chunk-4YCH6IZV.js";export{b as WikiHub,a as WikiStore,c as agenticQuery};
|
|
2
|
+
//# sourceMappingURL=wiki-M6YNRFC4.js.map
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Branching workflow — classify, then route on the result.
|
|
2
|
+
#
|
|
3
|
+
# A `branch` node evaluates one of four condition kinds (equals,
|
|
4
|
+
# contains, matches, exists) against the run context and selects an
|
|
5
|
+
# outgoing edge by `fromPort`. The `default` port catches anything
|
|
6
|
+
# that doesn't match any case.
|
|
7
|
+
|
|
8
|
+
id: __ID__
|
|
9
|
+
version: 2
|
|
10
|
+
title: __TITLE__
|
|
11
|
+
|
|
12
|
+
nodes:
|
|
13
|
+
- id: start
|
|
14
|
+
type: trigger.manual
|
|
15
|
+
config: {}
|
|
16
|
+
|
|
17
|
+
- id: classify
|
|
18
|
+
type: agent
|
|
19
|
+
config:
|
|
20
|
+
agentId: __AGENT__
|
|
21
|
+
# Tell the agent to emit a structured RESULT token your workflow
|
|
22
|
+
# can branch on. The default `noqta-result-token` parser captures
|
|
23
|
+
# the line after `RESULT:` into `{{classify.result}}`.
|
|
24
|
+
prompt: |
|
|
25
|
+
Classify the message into one of: sales, support, newsletter, other.
|
|
26
|
+
Reply ONLY with: RESULT: <category>
|
|
27
|
+
|
|
28
|
+
Message: {{trigger.payload.text}}
|
|
29
|
+
|
|
30
|
+
- id: route
|
|
31
|
+
type: branch
|
|
32
|
+
config:
|
|
33
|
+
cases:
|
|
34
|
+
- when: { kind: equals, params: { path: classify.result, value: sales } }
|
|
35
|
+
to: sales_track
|
|
36
|
+
- when: { kind: equals, params: { path: classify.result, value: support } }
|
|
37
|
+
to: support_track
|
|
38
|
+
default: catchall
|
|
39
|
+
|
|
40
|
+
- id: sales_track
|
|
41
|
+
type: agent
|
|
42
|
+
config:
|
|
43
|
+
agentId: __AGENT__
|
|
44
|
+
prompt: |
|
|
45
|
+
Sales lead came in: {{trigger.payload.text}}
|
|
46
|
+
Draft a one-line acknowledgement.
|
|
47
|
+
|
|
48
|
+
- id: support_track
|
|
49
|
+
type: agent
|
|
50
|
+
config:
|
|
51
|
+
agentId: __AGENT__
|
|
52
|
+
prompt: |
|
|
53
|
+
Support request: {{trigger.payload.text}}
|
|
54
|
+
Reply with the next-step instruction.
|
|
55
|
+
|
|
56
|
+
- id: catchall
|
|
57
|
+
type: agent
|
|
58
|
+
config:
|
|
59
|
+
agentId: __AGENT__
|
|
60
|
+
prompt: |
|
|
61
|
+
Generic reply for: {{trigger.payload.text}}
|
|
62
|
+
|
|
63
|
+
- id: done
|
|
64
|
+
type: end
|
|
65
|
+
config: { status: completed }
|
|
66
|
+
|
|
67
|
+
edges:
|
|
68
|
+
- { from: start, to: classify }
|
|
69
|
+
- { from: classify, to: route }
|
|
70
|
+
- { from: route, fromPort: sales_track, to: sales_track }
|
|
71
|
+
- { from: route, fromPort: support_track, to: support_track }
|
|
72
|
+
- { from: route, fromPort: catchall, to: catchall }
|
|
73
|
+
- { from: sales_track, to: done }
|
|
74
|
+
- { from: support_track, to: done }
|
|
75
|
+
- { from: catchall, to: done }
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Structured extraction — pull typed fields from free-form text.
|
|
2
|
+
#
|
|
3
|
+
# Uses the `extract.structured` built-in via an `action.builtin` node.
|
|
4
|
+
# The output is validated against an inline JSON-schema-ish shape so
|
|
5
|
+
# downstream nodes can read `{{extract.data.<field>}}` with confidence.
|
|
6
|
+
|
|
7
|
+
id: __ID__
|
|
8
|
+
version: 2
|
|
9
|
+
title: __TITLE__
|
|
10
|
+
|
|
11
|
+
flow:
|
|
12
|
+
- start
|
|
13
|
+
- extract
|
|
14
|
+
- confirm
|
|
15
|
+
- done
|
|
16
|
+
|
|
17
|
+
nodes:
|
|
18
|
+
- id: start
|
|
19
|
+
type: trigger.manual
|
|
20
|
+
config: {}
|
|
21
|
+
|
|
22
|
+
- id: extract
|
|
23
|
+
type: action.builtin
|
|
24
|
+
config:
|
|
25
|
+
name: extract.structured
|
|
26
|
+
input:
|
|
27
|
+
# Replace with your model id; defaults to the agent provider's chosen one.
|
|
28
|
+
model: claude-sonnet-4-6
|
|
29
|
+
# Free-form text the model should pull fields from.
|
|
30
|
+
text: "{{trigger.payload.text}}"
|
|
31
|
+
# Schema is a Zod-validated shape descriptor (subset of JSON Schema).
|
|
32
|
+
# Required fields rejected if missing; optional fields default to null.
|
|
33
|
+
schema:
|
|
34
|
+
type: object
|
|
35
|
+
properties:
|
|
36
|
+
name: { type: string }
|
|
37
|
+
email: { type: string }
|
|
38
|
+
company: { type: string }
|
|
39
|
+
intent: { type: string, enum: [sales, support, newsletter, other] }
|
|
40
|
+
required: [name, email, intent]
|
|
41
|
+
# Retry the extraction once if the model's first response doesn't fit
|
|
42
|
+
# the schema — typed errors come back via `errorKind`.
|
|
43
|
+
retry:
|
|
44
|
+
maxAttempts: 2
|
|
45
|
+
backoffMs: 500
|
|
46
|
+
|
|
47
|
+
- id: confirm
|
|
48
|
+
type: agent
|
|
49
|
+
config:
|
|
50
|
+
agentId: __AGENT__
|
|
51
|
+
prompt: |
|
|
52
|
+
Send a one-line confirmation:
|
|
53
|
+
name = {{extract.data.name}}
|
|
54
|
+
email = {{extract.data.email}}
|
|
55
|
+
intent = {{extract.data.intent}}
|
|
56
|
+
|
|
57
|
+
- id: done
|
|
58
|
+
type: end
|
|
59
|
+
config: { status: completed }
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Linear workflow — the smallest workflow that actually runs.
|
|
2
|
+
# Trigger → Agent → End. Use this as the starting point when you
|
|
3
|
+
# don't know what shape you want yet.
|
|
4
|
+
|
|
5
|
+
id: __ID__
|
|
6
|
+
version: 2
|
|
7
|
+
title: __TITLE__
|
|
8
|
+
|
|
9
|
+
# Linear sequences are easier to read with `flow:` sugar — it expands
|
|
10
|
+
# into edges so you don't have to write `from: a, to: b` for every step.
|
|
11
|
+
# Branches and parallel gateways still need explicit `edges:`.
|
|
12
|
+
flow:
|
|
13
|
+
- start
|
|
14
|
+
- reply
|
|
15
|
+
- done
|
|
16
|
+
|
|
17
|
+
nodes:
|
|
18
|
+
- id: start
|
|
19
|
+
type: trigger.manual
|
|
20
|
+
config: {}
|
|
21
|
+
|
|
22
|
+
- id: reply
|
|
23
|
+
type: agent
|
|
24
|
+
config:
|
|
25
|
+
# Replace with one of your registered agents (`agentx agent list`).
|
|
26
|
+
agentId: __AGENT__
|
|
27
|
+
# Templates pull from the run context — `{{trigger.payload.text}}`,
|
|
28
|
+
# `{{<previousNodeId>.<field>}}`. See the reference for the full
|
|
29
|
+
# path syntax.
|
|
30
|
+
prompt: |
|
|
31
|
+
The user said: {{trigger.payload.text}}
|
|
32
|
+
Reply concisely.
|
|
33
|
+
|
|
34
|
+
- id: done
|
|
35
|
+
type: end
|
|
36
|
+
config:
|
|
37
|
+
status: completed
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Retry + fallback — defend against transient model errors.
|
|
2
|
+
#
|
|
3
|
+
# Per-node `retry: { maxAttempts, backoffMs }` re-runs hard failures
|
|
4
|
+
# (returned `{ error }` or thrown exceptions) with exponential backoff.
|
|
5
|
+
# Pause results (signalWait, timerWait) are NEVER retried —
|
|
6
|
+
# pausing is a normal lifecycle transition, not a failure.
|
|
7
|
+
|
|
8
|
+
id: __ID__
|
|
9
|
+
version: 2
|
|
10
|
+
title: __TITLE__
|
|
11
|
+
|
|
12
|
+
nodes:
|
|
13
|
+
- id: start
|
|
14
|
+
type: trigger.manual
|
|
15
|
+
config: {}
|
|
16
|
+
|
|
17
|
+
- id: classify
|
|
18
|
+
type: agent
|
|
19
|
+
config:
|
|
20
|
+
agentId: __AGENT__
|
|
21
|
+
prompt: |
|
|
22
|
+
Classify: {{trigger.payload.text}}
|
|
23
|
+
Reply ONLY with: RESULT: ok | RESULT: error
|
|
24
|
+
# Up to three attempts total; 1s, then 2s backoff.
|
|
25
|
+
retry:
|
|
26
|
+
maxAttempts: 3
|
|
27
|
+
backoffMs: 1000
|
|
28
|
+
|
|
29
|
+
- id: route
|
|
30
|
+
type: branch
|
|
31
|
+
config:
|
|
32
|
+
cases:
|
|
33
|
+
- when: { kind: equals, params: { path: classify.result, value: ok } }
|
|
34
|
+
to: ok_track
|
|
35
|
+
# Anything else (including post-retry failures) lands here.
|
|
36
|
+
default: error_track
|
|
37
|
+
|
|
38
|
+
- id: ok_track
|
|
39
|
+
type: agent
|
|
40
|
+
config:
|
|
41
|
+
agentId: __AGENT__
|
|
42
|
+
prompt: "Acknowledge: {{trigger.payload.text}}"
|
|
43
|
+
|
|
44
|
+
- id: error_track
|
|
45
|
+
type: agent
|
|
46
|
+
config:
|
|
47
|
+
agentId: __AGENT__
|
|
48
|
+
prompt: |
|
|
49
|
+
Apologize for the issue and ask the user to rephrase: {{trigger.payload.text}}
|
|
50
|
+
|
|
51
|
+
- id: done
|
|
52
|
+
type: end
|
|
53
|
+
config: { status: completed }
|
|
54
|
+
|
|
55
|
+
edges:
|
|
56
|
+
- { from: start, to: classify }
|
|
57
|
+
- { from: classify, to: route }
|
|
58
|
+
- { from: route, fromPort: ok_track, to: ok_track }
|
|
59
|
+
- { from: route, fromPort: error_track, to: error_track }
|
|
60
|
+
- { from: ok_track, to: done }
|
|
61
|
+
- { from: error_track, to: done }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentix-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "AI operations layer for small & medium businesses. Routes Telegram, WhatsApp, Slack, Discord, GitLab, crons, webhooks, and mesh tasks to AI agents on Claude, OpenAI, or any LLM. Web wizard for non-technical operators, CLI for engineers. Self-hosted.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"multi-agent",
|
|
34
34
|
"orchestrator",
|
|
35
35
|
"claude-code",
|
|
36
|
+
"opencode",
|
|
36
37
|
"claude",
|
|
37
38
|
"telegram",
|
|
38
39
|
"whatsapp",
|
|
@@ -59,7 +60,8 @@
|
|
|
59
60
|
"docs:dev": "vitepress dev docs",
|
|
60
61
|
"docs:build": "vitepress build docs",
|
|
61
62
|
"docs:preview": "vitepress preview docs",
|
|
62
|
-
"prepublishOnly": "npm run build"
|
|
63
|
+
"prepublishOnly": "npm run build",
|
|
64
|
+
"postinstall": "node scripts/postinstall.mjs"
|
|
63
65
|
},
|
|
64
66
|
"dependencies": {
|
|
65
67
|
"@roamhq/wrtc": "^0.10.0",
|
|
@@ -69,13 +71,18 @@
|
|
|
69
71
|
"@xyflow/react": "^12.10.2",
|
|
70
72
|
"better-sqlite3": "^12.9.0",
|
|
71
73
|
"chalk": "5.2.0",
|
|
74
|
+
"cli-highlight": "^2.1.11",
|
|
72
75
|
"commander": "^10.0.0",
|
|
73
76
|
"cosmiconfig": "^8.1.3",
|
|
74
77
|
"cronstrue": "^3.14.0",
|
|
75
78
|
"execa": "^7.0.0",
|
|
76
79
|
"fast-glob": "^3.3.2",
|
|
77
80
|
"fs-extra": "^11.2.0",
|
|
81
|
+
"ink": "^5.0.1",
|
|
78
82
|
"js-yaml": "^4.1.1",
|
|
83
|
+
"marked": "^15.0.12",
|
|
84
|
+
"marked-terminal": "^7.3.0",
|
|
85
|
+
"minisearch": "^7.2.0",
|
|
79
86
|
"node-fetch": "^3.3.0",
|
|
80
87
|
"ora": "^6.1.2",
|
|
81
88
|
"prompts": "^2.4.2",
|
|
@@ -84,6 +91,7 @@
|
|
|
84
91
|
"react": "^18.3.1",
|
|
85
92
|
"react-dom": "^18.3.1",
|
|
86
93
|
"undici": "^8.1.0",
|
|
94
|
+
"yaml": "^2.8.4",
|
|
87
95
|
"zod": "^3.20.2"
|
|
88
96
|
},
|
|
89
97
|
"devDependencies": {
|
|
@@ -105,7 +113,7 @@
|
|
|
105
113
|
"vitest": "^1.2.2"
|
|
106
114
|
},
|
|
107
115
|
"engines": {
|
|
108
|
-
"node": ">=22"
|
|
116
|
+
"node": ">=22 <23"
|
|
109
117
|
},
|
|
110
118
|
"pnpm": {
|
|
111
119
|
"onlyBuiltDependencies": [
|