quirl-cli 0.1.5 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/quirl.mjs +1 -1
- package/package.json +1 -1
package/dist/quirl.mjs
CHANGED
|
@@ -181,7 +181,7 @@ if(!Array.isArray(r))throw Error(`ajv implementation error`);let a=n.name(`valid
|
|
|
181
181
|
- Correct what's wrong instead of leaving it: update_feature or withdraw_feature for a feature, update_task or archive_task for a task, propose_capability_update (also its name) for a capability. list_capabilities gives them as data.
|
|
182
182
|
- Resources (databases, hosted services, external APIs) come from quirl sync. If the project runs on something sync can't see from the code, add it with add_resource; correct or relabel one with update_resource (e.g. its environment), and drop a wrong one with remove_resource. Never pass secret values.
|
|
183
183
|
- If the project has no capabilities yet, offer to read the codebase and draft them with propose_capability.
|
|
184
|
-
- If the project briefing isn't already in your context, call quirl_context first.`}),r={p_project:e.project},i=()=>({tool:n.server.getClientVersion()?.name??`mcp`,model:process.env.QUIRL_AGENT_MODEL}),a=e=>async t=>{try{return Ib(await e(t))}catch(e){if(e instanceof Hr)return{...Ib(e.message),isError:!0};throw e}};n.registerTool(`quirl_context`,{title:`Project briefing`,description:`The project briefing: always-loaded docs in full, glossary, capabilities, your open tasks and the list of other docs. Usually already loaded at session start.`,inputSchema:{}},a(async()=>Fb(await t.context(e.project)))),n.registerTool(`search_docs`,{title:`Search docs`,description:`Full-text search across the project's docs (README, ADRs, guides, runbooks). Returns matching sections with their content. Use for anything not in the briefing.`,inputSchema:{query:V().describe(`Words to search for, e.g. 'refund approval limits'`),limit:H().int().min(1).max(20).optional()}},a(({query:e,limit:n})=>t.call(`search_docs`,{...r,p_query:e,p_limit:n??6}))),n.registerTool(`read_doc`,{title:`Read a doc`,description:`Read one doc in full, by id, title or source path.`,inputSchema:{doc:V().describe(`Doc id, title or path, e.g. 'docs/adr/004-queues.md'`)}},a(({doc:e})=>t.call(`read_doc`,{...r,p_doc:e}))),n.registerTool(`lookup`,{title:`Look up a term, capability or contract`,description:`Find glossary terms, capabilities, features and API/table contracts matching a name. Use it to check what a word means here, or the request/response shape of an endpoint or table.`,inputSchema:{query:V().describe(`A term, capability, endpoint or table name, e.g. 'Visit' or 'POST /login'`)}},a(({query:e})=>t.call(`lookup`,{...r,p_query:e})));let o=(e,t,r,i)=>{n.registerTool(e,r,i),n.registerTool(t,{...r,description:`Old name for ${e}; use ${e}. ${r.description}`},i)};o(`list_tasks`,`list_cards`,{title:`List tasks`,description:`List tasks on the board. Defaults to your own tasks that aren't done.`,inputSchema:{onlyMine:$p().optional(),includeDone:$p().optional()}},a(({onlyMine:e,includeDone:n})=>t.call(`list_cards`,{...r,p_only_mine:e??!0,p_include_done:n??!1})));let s=`Markdown: short paragraphs, and lists for steps or parts, not one long block.`,c=`Kind of work: e.g. Frontend, Backend, Database, Design, DevOps, Testing, Docs, Research. Only tags from the task set are accepted (see tagSets in quirl_context); propose a new value with propose_term (kind tag, tagSet task_work).`,l=V().describe(`Task number, e.g. 'K-104' or '104'`);return o(`get_task`,`get_card`,{title:`Get a task`,description:`A task with its feature, docs, dependencies and evidence.`,inputSchema:{task:l.optional(),card:l.optional()}},a(({task:e,card:n})=>t.call(`get_card`,{...r,p_card:e??n}))),o(`create_task`,`create_card`,{title:`Create a task`,description:`Add a task to the board (status: todo). Use it for work you discover that should be tracked. Put it under the feature it serves.`,inputSchema:{title:V(),description:V().optional().describe(`What to do and why. ${s}`),type:vm([`feature_work`,`bug`,`chore`,`research`]).optional(),feature:V().optional().describe(`Feature name or id (e.g. F-4) this task belongs to`),tags:U(V()).optional().describe(c)}},a(async({title:e,description:n,type:a,feature:o,tags:s})=>{let c=await t.call(`create_card`,{...r,p_title:e,p_description:n??``,p_type:a??`feature_work`,p_feature:o??null,p_agent:i()});return s?.length&&await t.call(`update_card`,{...r,p_card:c,p_patch:{tags:s},p_agent:i()}),c})),o(`update_task`,`update_card`,{title:`Update a task`,description:`Move a task, mark it blocked (with a reason) or unblocked, put it under a feature, set its tags, rewrite its title or description, and attach evidence: a branch, commit or pull request. Repos are referenced by name.`,inputSchema:{task:l.optional(),card:l.optional(),status:vm([`backlog`,`todo`,`in_progress`,`review`,`done`]).optional(),feature:V().optional().describe(`Feature name or id (e.g. F-4) to put the task under; an empty string takes it off its feature`),title:V().optional(),description:V().optional().describe(`Replaces the description. ${s}`),tags:U(V()).optional().describe(`${c} Replaces the current tags.`),blocked:$p().optional(),blockedReason:V().optional(),addBranch:W({repo:V(),name:V()}).optional(),addCommit:W({repo:V(),sha:V(),message:V().optional()}).optional(),addPullRequest:W({repo:V(),number:H().int(),url:V(),state:vm([`open`,`merged`,`closed`]).optional()}).optional()}},a(({task:e,card:n,...a})=>t.call(`update_card`,{...r,p_card:e??n,p_patch:a,p_agent:i()}))),n.registerTool(`archive_task`,{title:`Archive a task`,description:`Remove a task that shouldn't be there: a duplicate or a mistake. It's archived and stays in history, and its feature's status is worked out without it. For work that's finished, move it to done instead.`,inputSchema:{task:l,reason:V().optional().describe(`Why, for history`)}},a(({task:e,reason:n})=>t.call(`archive_card`,{...r,p_card:e,p_reason:n??null,p_agent:i()}))),n.registerTool(`propose_term`,{title:`Propose a glossary term`,description:`Suggest a new glossary term. It shows up for approval; it isn't used until a person approves it.`,inputSchema:{term:V(),plain:V().describe(`What it means, for someone non-technical`),technical:V().describe(`What it means in the code`),kind:vm([`concept`,`tag`,`name`]).optional(),tagSet:V().optional().describe(`Required for kind 'tag': the set the value belongs to, e.g. feature_change, task_work, capability_audience, capability_kind (see tagSets in quirl_context)`),reason:V().optional()}},a(({term:e,plain:n,technical:a,kind:o,tagSet:s,reason:c})=>t.call(`propose_term`,{...r,p_term:e,p_plain:n,p_technical:a,p_kind:o??`concept`,p_tag_set:s??null,p_reason:c??null,p_agent:i()}))),n.registerTool(`propose_term_retire`,{title:`Retire a glossary term`,description:`Propose retiring a glossary term or tag that's no longer used or was a mistake. With replaceWith, the term stays as one not to say (agents are told the replacement); without it, it's removed. Either way it comes off every capability, task and feature that used it. A person approves it. Built-in tag values can't be retired.`,inputSchema:{term:V().describe(`The term or tag to retire`),replaceWith:V().optional().describe(`An approved term to say instead, if there is one`),reason:V().optional()}},a(({term:e,replaceWith:n,reason:a})=>t.call(`propose_term_retire`,{...r,p_term:e,p_replace_with:n??null,p_reason:a??null,p_agent:i()}))),n.registerTool(`list_flags`,{title:`List flags`,description:`Flags are messages people send about a section of the project: a capability, feature, task, doc or glossary term. Lists open (waiting for an admin) and approved (to act on) flags, or only those with the given status.`,inputSchema:{status:vm([`open`,`approved`,`removed`,`done`]).optional()}},a(({status:e})=>t.call(`list_flags`,{...r,p_status:e??null}))),n.registerTool(`decide_flag`,{title:`Approve or remove a flag`,description:`Admins only. Approve a flag so agents act on it at the start of their sessions, or remove it. An approved flag can still be removed. Only do this when the person you work for asks you to.`,inputSchema:{flag:V().describe(`The flag's id, from the briefing or list_flags`),decision:vm([`approve`,`remove`])}},a(({flag:e,decision:n})=>t.call(`decide_flag`,{...r,p_flag:e,p_approve:n===`approve`,p_agent:i()}))),n.registerTool(`resolve_flag`,{title:`Mark a flag done`,description:`Mark an approved flag done once you've handled it, so it leaves the briefing. Say briefly what you did.`,inputSchema:{flag:V().describe(`The flag's id, from the briefing or list_flags`),note:V().optional().describe(`What was done, e.g. 'Updated C-12 to mention SMS'`)}},a(({flag:e,note:n})=>t.call(`resolve_flag`,{...r,p_flag:e,p_note:n??null,p_agent:i()}))),n.registerTool(`set_capability_icon`,{title:`Set a capability's icon`,description:`Give a capability an icon that fits what it does; it's shown on the capability's card. Use the same icon a related feature has when one fits both.`,inputSchema:{capability:V().describe(`Capability name or id (e.g. C-12)`),icon:vm(Nb)}},a(({capability:e,icon:n})=>t.call(`set_capability_icon`,{...r,p_capability:e,p_icon:n,p_agent:i()}))),n.registerTool(`add_resource`,{title:`Add or fill in a resource`,description:"Record a service the project runs on that `quirl sync` can't see from the code: a hosted database another team runs, a SaaS named only in docs or the console, a staging copy. Sync already finds most things from env templates, docker-compose and client packages, so check Stack first (lookup) and don't add what it found. A resource with the same provider and environment is filled in rather than duplicated: the fields you give are set, and repos and capabilities are added to its links. Never pass URLs with credentials, keys or other secret values.",inputSchema:{name:V().describe(`What the team calls it, e.g. "Postgres", "Stripe"`),type:vm(Lb),provider:V().optional().describe(`Who runs it, e.g. "Google Cloud SQL", "Stripe". Resources are matched on this. Defaults to the name.`),description:V().optional().describe(`One plain sentence on what the project uses it for`),technical:V().optional().describe(`Technical notes for developers: setup, access, limits`),environment:V().optional().describe(`Defaults to "production"; e.g. "staging"`),repos:U(V()).optional().describe(`Repos that use it, by name`),capabilities:U(V()).optional().describe(`Capabilities that run on it, by name or id (e.g. C-12)`)}},a(e=>t.call(`add_resource`,{...r,p_name:e.name,p_type:e.type,p_provider:e.provider??null,p_description:e.description??null,p_technical:e.technical??null,p_environment:e.environment??null,p_repos:e.repos??null,p_capabilities:e.capabilities??null,p_agent:i()}))),n.registerTool(`list_resources`,{title:`List the project's resources`,description:`Every resource on Stack: name, type, provider, environment, whether sync found it or someone added it, the repos that use it and the capabilities that run on it.`,inputSchema:{}},a(()=>t.call(`list_resources`,r))),n.registerTool(`update_resource`,{title:`Change a resource`,description:`Correct a resource: rename it, relabel its environment (a scanned one that's really the local docker database becomes 'local'), or change its type, provider, descriptions or links. Only the fields you pass change. repos and capabilities replace its links; use add_resource to add to them. Sync keeps updating a relabelled resource instead of adding it again. Never pass secret values.`,inputSchema:{...Rb,name:V().optional(),type:vm(Lb).optional(),provider:V().optional(),environment:V().optional().describe(`e.g. "production", "staging", "dev", "local"`),description:V().optional().describe(`One plain sentence on what the project uses it for`),technical:V().optional().describe(`Notes for developers`),repos:U(V()).optional().describe(`Replaces the repos that use it`),capabilities:U(V()).optional().describe(`Replaces the capabilities that run on it (names or C-12)`)}},a(e=>t.call(`update_resource`,{...r,p_resource:e.resource,p_in_environment:e.inEnvironment??null,p_name:e.name??null,p_type:e.type??null,p_provider:e.provider??null,p_environment:e.environment??null,p_description:e.description??null,p_technical:e.technical??null,p_repos:e.repos??null,p_capabilities:e.capabilities??null,p_agent:i()}))),n.registerTool(`remove_resource`,{title:`Remove a wrong resource`,description:`Remove a resource that isn't one: a false match from sync, a duplicate, something no longer used. It's archived, stays in history, and sync won't add it again. A resource holding endpoints or tables from sync can't be removed; relabel it instead.`,inputSchema:{...Rb,reason:V().optional().describe(`Why it's wrong, for history`)}},a(e=>t.call(`remove_resource`,{...r,p_resource:e.resource,p_in_environment:e.inEnvironment??null,p_reason:e.reason??null,p_agent:i()}))),n.registerTool(`list_features`,{title:`List features`,description:`List the project's features with their status, the capability each changes (or createsCapability when it has none), and task counts. Shipped ones are left out unless includeShipped. Check here before requesting a feature.`,inputSchema:{includeShipped:$p().optional()}},a(({includeShipped:e})=>t.call(`list_features`,{...r,p_include_shipped:e??!1}))),n.registerTool(`get_feature`,{title:`Get a feature`,description:`A feature with its description, status, the capability it changes (and what that capability does) and its tasks.`,inputSchema:{feature:V().describe(`Feature name or id (e.g. F-4)`)}},a(({feature:e})=>t.call(`get_feature`,{...r,p_feature:e}))),n.registerTool(`request_feature`,{title:`Request a feature`,description:`Request a feature: a change to the product that stakeholders follow on the Features page, usually made of several tasks. Check list_features first so you don't request one twice. It shows as requested straight away and a person accepts or declines it. Put tasks under it with create_task or update_task.`,inputSchema:{name:V().describe(`Short name, e.g. 'Repeat appointments'`),description:V().describe(`What changes for the user, in plain words. Markdown: short paragraphs, and lists for parts`),capability:V().optional().describe(`Capability name or id (e.g. C-12) it changes. Leave it out when the feature creates a new capability`),tags:U(V()).optional().describe(`Kind of change: e.g. New, Improvement, Visual, Fix, Performance, Security, Removal, Internal (the feature_change set in quirl_context)`),reason:V().optional().describe(`Why it's worth doing`),icon:vm(Nb).optional().describe(`An icon that fits the feature, shown on its card`)}},a(async({name:e,description:n,capability:a,tags:o,reason:s,icon:c})=>{let l=await t.call(`request_feature`,{...r,p_name:e,p_description:n,p_capability:a??null,p_tags:o??null,p_reason:s??null,p_agent:i()});return c&&l===`requested`&&await t.call(`set_feature_icon`,{...r,p_feature:e,p_icon:c,p_agent:i()}),l})),n.registerTool(`set_feature_icon`,{title:`Set a feature's icon`,description:`Give a feature an icon that fits what it does; it's shown on the feature's card. list_features shows which features have none.`,inputSchema:{feature:V().describe(`Feature name or id (e.g. F-4)`),icon:vm(Nb)}},a(({feature:e,icon:n})=>t.call(`set_feature_icon`,{...r,p_feature:e,p_icon:n,p_agent:i()}))),n.registerTool(`update_feature`,{title:`Correct a feature`,description:`Fix a feature's name, description or the capability it changes, e.g. after requesting it with the wrong one. Only the fields you pass change; it shows in the feature's history. Tags: tag_feature; icon: set_feature_icon.`,inputSchema:{feature:V().describe(`Feature name or id (e.g. F-4)`),name:V().optional(),description:V().optional().describe(`What changes for the user, in plain words. Markdown: short paragraphs, and lists for parts`),capability:V().optional().describe(`The capability it changes (name or C-12), or "none" when it creates a new one`)}},a(({feature:e,name:n,description:a,capability:o})=>t.call(`update_feature`,{...r,p_feature:e,p_name:n??null,p_description:a??null,p_capability:o??null,p_agent:i()}))),n.registerTool(`withdraw_feature`,{title:`Withdraw a feature`,description:`Remove a feature that shouldn't be there: a duplicate, a mistake, or an idea dropped before any work. It's archived and stays in history. One that shipped can't be withdrawn; move or archive its tasks first (update_task, archive_task).`,inputSchema:{feature:V().describe(`Feature name or id (e.g. F-4)`),reason:V().optional().describe(`Why, for history`)}},a(({feature:e,reason:n})=>t.call(`withdraw_feature`,{...r,p_feature:e,p_reason:n??null,p_agent:i()}))),n.registerTool(`tag_feature`,{title:`Tag a feature`,description:`Set a feature's kind-of-change tags (replaces the current ones). Only values from the feature_change tag set are accepted.`,inputSchema:{feature:V().describe(`Feature name or id (e.g. F-4)`),tags:U(V()).describe(`Kind of change: e.g. New, Improvement, Visual, Fix, Performance, Security, Removal, Internal`)}},a(({feature:e,tags:n})=>t.call(`tag_feature`,{...r,p_feature:e,p_tags:n,p_agent:i()}))),n.registerTool(`list_capabilities`,{title:`List the capabilities`,description:`Every capability as data: id (C-12), name, status, icon, the capability it's part of, what it does, what it relies on, its tags and the features changing it. Retired ones only with includeRetired.`,inputSchema:{includeRetired:$p().optional()}},a(({includeRetired:e})=>t.call(`list_capabilities`,{...r,p_include_retired:e??!1}))),n.registerTool(`propose_capability`,{title:`Propose a capability`,description:"Draft a capability: one thing the product does today, described for a non-technical reader. Use it when the project has none yet (for example right after `quirl init`), when you find one that's missing, or when a feature that creates a new capability ships (pass it in features). Never for something not built yet: request a feature instead. A person approves it before it shows up.",inputSchema:{name:V().describe(`Short name, e.g. 'Online booking'`),whatItDoes:V().describe(`One or two plain sentences, no jargon`),technical:V().optional().describe(`Where it lives in the code and how it works. Markdown: short paragraphs, and lists for parts, not one long block`),features:U(V()).optional().describe(`Feature names or ids (e.g. F-4) that create or change this capability; they're linked to it once it's approved`),reason:V().optional()}},a(({name:e,whatItDoes:n,technical:a,features:o,reason:s})=>t.call(`propose_capability`,{...r,p_name:e,p_what_it_does:n,p_technical:a??``,p_features:o??null,p_reason:s??null,p_agent:i()}))),n.registerTool(`propose_capability_archive`,{title:`Propose archiving a capability`,description:`Suggest retiring a capability that no longer describes what the product does, or that was drafted for something not built yet. Unshipped features linked to it will create a new capability when they ship. A person approves it.`,inputSchema:{capability:V().describe(`Capability name or id (e.g. C-12)`),reason:V().describe(`Why it should go`)}},a(({capability:e,reason:n})=>t.call(`propose_capability_archive`,{...r,p_capability:e,p_reason:n,p_agent:i()}))),n.registerTool(`propose_capability_update`,{title:`Propose a capability correction`,description:`Suggest a corrected name or description for a capability when the code no longer matches it. A person approves it.`,inputSchema:{capability:V().describe(`Capability name or id (e.g. C-12)`),field:vm([`name`,`what_it_does`,`technical_description`]),after:V().describe(`The new name, or the corrected text. Markdown: short paragraphs, and lists for parts, not one long block`),reason:V().optional()}},a(({capability:e,field:n,after:a,reason:o})=>t.call(`propose_capability_update`,{...r,p_capability:e,p_field:n,p_after:a,p_reason:o??null,p_agent:i()}))),n.registerTool(`propose_capability_details`,{title:`Propose how a capability works and its connections`,description:`Suggest a capability's 'How it works' steps, its tags (glossary terms of kind 'tag'), the capability it's part of, and its connections: capabilities it relies on, repos, resources it runs on, glossary terms and docs. Each part you pass replaces that part when a person approves it; parts you leave out are kept. Names are matched to existing, approved items. What it relies on draws the arrows on the Home map, so link the steps a user goes through (e.g. invoicing relies on booking).`,inputSchema:{capability:V().describe(`Capability name or id (e.g. C-12)`),howItWorks:U(V()).optional().describe(`3 to 5 plain-language steps, as the user experiences it`),reliesOn:U(W({capability:V().describe(`Capability name or id (e.g. C-12)`),label:V().describe(`How it relies on it, read before the name, e.g. 'extends'`)})).optional(),parent:V().optional().describe(`The capability this one is part of (name or id, e.g. C-2), or 'none' to make it top-level. Parts are listed under their parent instead of on their own`),repos:U(V()).optional().describe(`Repo names`),resources:U(V()).optional().describe(`Resource names it runs on`),terms:U(V()).optional().describe(`Glossary terms`),docs:U(V()).optional().describe(`Doc titles, or <repo>/<path> (e.g. quirl/AGENTS.md) when titles repeat`),tags:U(V()).optional().describe(`Who it's for (Customer, Admin, Developer, Agent) and its kind (Interface, Integration, Data, Automation, Infrastructure, Access and security, AI): the capability tag sets in quirl_context`),reason:V().optional()}},a(({capability:e,howItWorks:n,reliesOn:a,parent:o,repos:s,resources:c,terms:l,docs:u,tags:d,reason:f})=>t.call(`propose_capability_details`,{...r,p_capability:e,p_how_it_works:n??null,p_relies_on:a??null,p_parent:o??null,p_repos:s??null,p_resources:c??null,p_terms:l??null,p_docs:u??null,p_tags:d??null,p_reason:f??null,p_agent:i()}))),n}async function Bb(e){await zb(e).connect(new Mb)}const Vb=`0.1.5`,Hb=process.env.QUIRL_SERVER===`local`?{apiUrl:`http://127.0.0.1:54321`,publishableKey:`sb_publishable_ACJWlzQHlZjBrEguHvfOxg_3BJgxAaH`,appUrl:`http://localhost:5173`}:{apiUrl:`https://dugevhzlzrdtasuibdzr.supabase.co`,publishableKey:`sb_publishable_45P7GyyZoJsN0AO3wfmqNA_CXbx1iCW`,appUrl:`https://app.quirl.io`};function Ub(){return{apiUrl:process.env.QUIRL_API_URL??Hb.apiUrl,publishableKey:process.env.QUIRL_PUBLISHABLE_KEY??Hb.publishableKey,appUrl:process.env.QUIRL_APP_URL??Hb.appUrl}}const Wb=()=>x(process.env.XDG_CONFIG_HOME??x(h(),`.config`),`quirl`),Gb=()=>x(Wb(),`credentials.json`);async function Kb(){try{return JSON.parse(await u(Gb(),`utf8`))}catch{return{}}}async function qb(){return process.env.QUIRL_KEY?{apiUrl:Ub().apiUrl,key:process.env.QUIRL_KEY,email:``}:(await Kb())[Ub().apiUrl]??null}async function Jb(e){let t=await Kb();t[e.apiUrl]=e,await l(Wb(),{recursive:!0,mode:448}),await m(Gb(),`${JSON.stringify(t,null,` `)}\n`,{mode:384}),await s(Gb(),384)}async function Yb(){let e=await Kb();delete e[Ub().apiUrl],Object.keys(e).length?await m(Gb(),`${JSON.stringify(e,null,` `)}\n`,{mode:384}):await f(Gb(),{force:!0})}async function Xb(){let e=Ub();return{apiUrl:e.apiUrl,publishableKey:e.publishableKey,key:(await qb())?.key??``}}function Zb(e=process.cwd()){try{return a(`git`,[`rev-parse`,`--show-toplevel`],{cwd:e,stdio:[`ignore`,`pipe`,`ignore`]}).toString().trim()}catch{return e}}const Qb=e=>x(e,`.quirl`,`project.json`);async function $b(e=process.cwd()){let t=e;for(;;)try{return{root:t,link:JSON.parse(await u(Qb(t),`utf8`))}}catch{let e=y(t);if(e===t)return null;t=e}}async function ex(e,t){await l(x(e,`.quirl`),{recursive:!0}),await m(Qb(e),`${JSON.stringify(t,null,` `)}\n`)}function tx(e){let t=``;for(let n=0;n<e.length;n++){let r=e[n];r===`*`?e[n+1]===`*`?(t+=e[n+2]===`/`?`(?:.*/)?`:`.*`,n+=e[n+2]===`/`?2:1):t+=`[^/]*`:t+=r===`?`?`[^/]`:r.replace(/[.+^${}()|[\]\\]/g,`\\$&`)}return RegExp(`^${t}$`)}function nx(e){let t=[];for(let n of e.split(/\r?\n/)){let e=n.replace(/\s+$/,``);if(!e||e.startsWith(`#`))continue;let r=e.startsWith(`!`);r&&(e=e.slice(1));let i=e.endsWith(`/`);i&&(e=e.slice(0,-1));let a=e.includes(`/`);e=e.replace(/^\//,``);let o=tx(e).source.slice(1,-1);t.push({regex:RegExp(a?`^${o}$`:`^(?:.*/)?${o}$`),negate:r,dirOnly:i})}return t}function rx(e,t){if(!e.length)return!1;let n=t.split(`/`);for(let t=1;t<=n.length;t++){let r=n.slice(0,t).join(`/`),i=t<n.length,a=!1;for(let t of e)(!t.dirOnly||i)&&t.regex.test(r)&&(a=!t.negate);if(a)return!0}return!1}async function ix(e){try{return nx(await u(_.join(e,`.quirlignore`),`utf8`))}catch{return[]}}function ax(e,t){return new Promise(n=>{if(!t.length)return n(/* @__PURE__ */ new Set);i(`git`,[`check-ignore`,`--stdin`,`-z`],{cwd:e,maxBuffer:268435456,timeout:6e4},(e,t)=>n(new Set(String(t??``).split(`\0`).filter(Boolean)))).stdin?.end(`${t.join(`\0`)}\0`)})}const ox=n(i),sx=/* @__PURE__ */ new Set([`.git`,`node_modules`,`dist`,`build`,`.next`,`vendor`,`.venv`,`venv`,`__pycache__`,`coverage`,`.turbo`,`.svelte-kit`,`.nuxt`,`.output`,`.cache`,`.vercel`,`.pnpm-store`,`fixtures`,`test-fixtures`,`__fixtures__`,`testdata`]),cx=1048576,lx=2e5;async function ux(e){let t=[],n=await ix(e),r=(await fx(e,t)).filter(e=>!rx(n,e.path)),i=await ax(e,r.map(e=>e.path)),a=r.filter(e=>!i.has(e.path)),o=new Map(a.map(e=>[e.path,e]));return{root:e,files:a,warnings:t,file:e=>o.get(e),async readText(e,n=cx){if(e.size>n)return t.push(`${e.path}: skipped, ${yx(e.size)} is over the ${yx(n)} limit`),null;try{return await u(e.absolutePath,`utf8`)}catch(n){return t.push(`${e.path}: could not be read (${_x(n)})`),null}}}}async function dx(e,t){let n=[],r=await ix(e),{stdout:i}=await ox(`git`,[`ls-tree`,`-r`,`-l`,`-z`,`--full-tree`,t],{cwd:e,maxBuffer:268435456,timeout:6e4}),a=[];for(let e of i.split(`\0`)){let t=e.indexOf(` `);if(t===-1)continue;let[i,o,s,c]=e.slice(0,t).trim().split(/\s+/),l=e.slice(t+1);if(o===`blob`&&i!==`120000`&&s&&!l.split(`/`).some(e=>sx.has(e))&&!rx(r,l)){if(a.length>=lx){n.push(`Stopped after ${lx} files; the rest of the tree was not scanned`);break}a.push({path:l,absolutePath:s,size:Number(c)||0})}}px(a);let o=new Map(a.map(e=>[e.path,e]));return{root:e,files:a,warnings:n,file:e=>o.get(e),async readText(t,r=cx){if(t.size>r)return n.push(`${t.path}: skipped, ${yx(t.size)} is over the ${yx(r)} limit`),null;try{let{stdout:n}=await ox(`git`,[`cat-file`,`blob`,t.absolutePath],{cwd:e,maxBuffer:r+1024,timeout:3e4});return n}catch(e){return n.push(`${t.path}: could not be read (${_x(e)})`),null}}}}async function fx(e,t){let n=[],r=[``];for(;r.length>0;){let i=r.pop(),a;try{a=await d(_.join(e,i),{withFileTypes:!0})}catch(e){t.push(`${i||`.`}: folder could not be read (${_x(e)})`);continue}for(let o of a){let a=i?`${i}/${o.name}`:o.name;if(o.isSymbolicLink())continue;if(o.isDirectory()){!sx.has(o.name)&&!C(_.join(e,a,`.git`))&&r.push(a);continue}if(!o.isFile())continue;if(n.length>=lx)return t.push(`Stopped after ${lx} files; the rest of the tree was not scanned`),px(n);let s=_.join(e,a);try{let e=await c(s);n.push({path:a,absolutePath:s,size:e.size})}catch(e){t.push(`${a}: could not be read (${_x(e)})`)}}}return px(n)}function px(e){return e.sort((e,t)=>e.path<t.path?-1:+(e.path>t.path))}function mx(e){let t=e.lastIndexOf(`/`);return t===-1?e:e.slice(t+1)}function hx(e){let t=e.lastIndexOf(`/`);return t===-1?``:e.slice(0,t)}function gx(e,t){return e?`${e}/${t}`:t}function _x(e){return Gr(e instanceof Error?e.message:String(e))}function Z(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function vx(e){return typeof e==`string`?e:void 0}function yx(e){return e>=1048576?`${(e/1048576).toFixed(1)} MB`:`${Math.ceil(e/1024)} KB`}async function bx({hook:e=!1}={}){let t=await $b();if(!t){if(e)return;throw Error("This folder isn't linked to a Quirl project. Run `quirl init`.")}try{let n=await new Ur(await Xb()).context(t.link.projectId);process.stdout.write(`${Fb(n,e?9500:void 0)}\n`)}catch(n){if(!e)throw n;process.stdout.write(`Quirl project "${t.link.name}": couldn't load the briefing (${_x(n)}). The quirl MCP tools may still work.\n`)}}async function xx(e,t=3){let n=[],r=async(e,i)=>{if(C(_.join(e,`.git`))&&n.push(e),i>=t)return;let a;try{a=await d(e,{withFileTypes:!0})}catch{return}for(let t of a)!t.isDirectory()||t.name.startsWith(`.`)||sx.has(t.name)||await r(_.join(e,t.name),i+1)};return await r(_.resolve(e),0),n.sort((e,t)=>e.localeCompare(t))}const Sx=process.stdout.isTTY&&!process.env.NO_COLOR,Cx=e=>t=>Sx?`\x1b[${e}m${t}\x1b[0m`:t,wx=Cx(1),Q=Cx(2),Tx=Cx(32),Ex=Cx(31),Dx=Cx(36),Ox=e=>console.log(`${Tx(`✓`)} ${e}`),kx=e=>console.log(` ${e}`),Ax=(e,t)=>console.log(`\n${e} ${t}`),jx=e=>console.log(` ${e}`);function Mx(e){if(!Sx)return jx(Q(e)),{stop:()=>{}};let t=0,n=setInterval(()=>{process.stdout.write(`\r ${Dx(`⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏`[t++%10]??``)} ${Q(e)}`)},80);return{stop:()=>{clearInterval(n),process.stdout.write(`\r\x1B[2K`)}}}var Nx=class extends Error{};async function Px(e,t=``){if(!process.stdin.isTTY)return t;let n=E({input:process.stdin,output:process.stdout});try{return(await n.question(e)).trim()||t}finally{n.close()}}async function Fx(e,t){let n=await Px(`${e} ${Q(t?`[Y/n]`:`[y/N]`)} `,t?`y`:`n`);return/^y/i.test(n)}function Ix(e){if(process.env.QUIRL_NO_BROWSER)return;let t=process.platform===`darwin`?`open`:process.platform===`win32`?`cmd`:`xdg-open`,n=process.platform===`win32`?[`/c`,`start`,``,e]:[e];try{o(t,n,{stdio:`ignore`,detached:!0}).on(`error`,()=>{}).unref()}catch{}}const Lx=`<!-- quirl:start -->`,Rx=`<!-- quirl:end -->`,zx=`# quirl: keep the project in sync`;function Bx(){try{let e=a(process.platform===`win32`?`where`:`which`,[`quirl`],{stdio:[`ignore`,`pipe`,`ignore`]}).toString();if(!/[\\/]_npx[\\/]/.test(e))return{command:`quirl`,args:[],shell:`quirl`,portable:!0}}catch{}return{command:`npx`,args:[`-y`,`quirl-cli`],shell:`npx -y quirl-cli`,portable:!0}}const Vx=[`QUIRL_SERVER`,`QUIRL_API_URL`,`QUIRL_PUBLISHABLE_KEY`,`QUIRL_APP_URL`];function Hx(){return Object.fromEntries(Vx.flatMap(e=>process.env[e]?[[e,process.env[e]]]:[]))}function Ux(){return`${Object.entries(Hx()).map(([e,t])=>`${e}="${t}" `).join(``)}${Bx().shell}`}async function Wx(e){try{return JSON.parse(await u(e,`utf8`))}catch(t){if(t.code===`ENOENT`)return{};throw Error(`${e} isn't valid JSON; fix it and run \`quirl init\` again.`)}}const Gx=async(e,t)=>{await l(y(e),{recursive:!0}),await m(e,`${JSON.stringify(t,null,2)}\n`)};async function Kx(e){let t=x(e,`.mcp.json`),n=await Wx(t),r=Bx();n.mcpServers={...n.mcpServers,quirl:{command:r.command,args:[...r.args,`mcp`],...Object.keys(Hx()).length?{env:Hx()}:{}}},await Gx(t,n)}async function qx(e){let t=x(e,`.claude`,`settings.json`),n=await Wx(t),r=n.hooks??{},i=`${Ux()} context --hook`;r.SessionStart=[...(r.SessionStart??[]).map(e=>({...e,hooks:e.hooks.filter(e=>!/\bcontext --hook$/.test(e.command))})).filter(e=>e.hooks.length),{hooks:[{type:`command`,command:i,timeout:15}]}],n.hooks=r,await Gx(t,n)}async function Jx(e,t,n=[]){let r=x(e,`CLAUDE.md`),i=``;try{i=await u(r,`utf8`)}catch{}let a=`${Lx}
|
|
184
|
+
- If the project briefing isn't already in your context, call quirl_context first.`}),r={p_project:e.project},i=()=>({tool:n.server.getClientVersion()?.name??`mcp`,model:process.env.QUIRL_AGENT_MODEL}),a=e=>async t=>{try{return Ib(await e(t))}catch(e){if(e instanceof Hr)return{...Ib(e.message),isError:!0};throw e}};n.registerTool(`quirl_context`,{title:`Project briefing`,description:`The project briefing: always-loaded docs in full, glossary, capabilities, your open tasks and the list of other docs. Usually already loaded at session start.`,inputSchema:{}},a(async()=>Fb(await t.context(e.project)))),n.registerTool(`search_docs`,{title:`Search docs`,description:`Full-text search across the project's docs (README, ADRs, guides, runbooks). Returns matching sections with their content. Use for anything not in the briefing.`,inputSchema:{query:V().describe(`Words to search for, e.g. 'refund approval limits'`),limit:H().int().min(1).max(20).optional()}},a(({query:e,limit:n})=>t.call(`search_docs`,{...r,p_query:e,p_limit:n??6}))),n.registerTool(`read_doc`,{title:`Read a doc`,description:`Read one doc in full, by id, title or source path.`,inputSchema:{doc:V().describe(`Doc id, title or path, e.g. 'docs/adr/004-queues.md'`)}},a(({doc:e})=>t.call(`read_doc`,{...r,p_doc:e}))),n.registerTool(`lookup`,{title:`Look up a term, capability or contract`,description:`Find glossary terms, capabilities, features and API/table contracts matching a name. Use it to check what a word means here, or the request/response shape of an endpoint or table.`,inputSchema:{query:V().describe(`A term, capability, endpoint or table name, e.g. 'Visit' or 'POST /login'`)}},a(({query:e})=>t.call(`lookup`,{...r,p_query:e})));let o=(e,t,r,i)=>{n.registerTool(e,r,i),n.registerTool(t,{...r,description:`Old name for ${e}; use ${e}. ${r.description}`},i)};o(`list_tasks`,`list_cards`,{title:`List tasks`,description:`List tasks on the board. Defaults to your own tasks that aren't done.`,inputSchema:{onlyMine:$p().optional(),includeDone:$p().optional()}},a(({onlyMine:e,includeDone:n})=>t.call(`list_cards`,{...r,p_only_mine:e??!0,p_include_done:n??!1})));let s=`Markdown: short paragraphs, and lists for steps or parts, not one long block.`,c=`Kind of work: e.g. Frontend, Backend, Database, Design, DevOps, Testing, Docs, Research. Only tags from the task set are accepted (see tagSets in quirl_context); propose a new value with propose_term (kind tag, tagSet task_work).`,l=V().describe(`Task number, e.g. 'K-104' or '104'`),u=V().optional().describe(`Who does it: "me", a member's email or name (only people on the project), or "none"`);return o(`get_task`,`get_card`,{title:`Get a task`,description:`A task with its feature, docs, dependencies and evidence.`,inputSchema:{task:l.optional(),card:l.optional()}},a(({task:e,card:n})=>t.call(`get_card`,{...r,p_card:e??n}))),o(`create_task`,`create_card`,{title:`Create a task`,description:`Add a task to the board (status: todo). Use it for work you discover that should be tracked. Put it under the feature it serves.`,inputSchema:{title:V(),description:V().optional().describe(`What to do and why. ${s}`),type:vm([`feature_work`,`bug`,`chore`,`research`]).optional(),feature:V().optional().describe(`Feature name or id (e.g. F-4) this task belongs to`),tags:U(V()).optional().describe(c),assignee:u.describe(`Who does it: "me" (the default), a member's email or name, or "none"`)}},a(async({title:e,description:n,type:a,feature:o,tags:s,assignee:c})=>{let l=await t.call(`create_card`,{...r,p_title:e,p_description:n??``,p_type:a??`feature_work`,p_feature:o??null,p_agent:i()});return(s?.length||c)&&await t.call(`update_card`,{...r,p_card:l,p_patch:{...s?.length?{tags:s}:{},...c?{assignee:c}:{}},p_agent:i()}),l})),o(`update_task`,`update_card`,{title:`Update a task`,description:`Move a task, mark it blocked (with a reason) or unblocked, assign it, put it under a feature, set its tags, rewrite its title or description, and attach evidence: a branch, commit or pull request. Repos are referenced by name.`,inputSchema:{task:l.optional(),card:l.optional(),assignee:u,status:vm([`backlog`,`todo`,`in_progress`,`review`,`done`]).optional(),feature:V().optional().describe(`Feature name or id (e.g. F-4) to put the task under; an empty string takes it off its feature`),title:V().optional(),description:V().optional().describe(`Replaces the description. ${s}`),tags:U(V()).optional().describe(`${c} Replaces the current tags.`),blocked:$p().optional(),blockedReason:V().optional(),addBranch:W({repo:V(),name:V()}).optional(),addCommit:W({repo:V(),sha:V(),message:V().optional()}).optional(),addPullRequest:W({repo:V(),number:H().int(),url:V(),state:vm([`open`,`merged`,`closed`]).optional()}).optional()}},a(({task:e,card:n,...a})=>t.call(`update_card`,{...r,p_card:e??n,p_patch:a,p_agent:i()}))),n.registerTool(`archive_task`,{title:`Archive a task`,description:`Remove a task that shouldn't be there: a duplicate or a mistake. It's archived and stays in history, and its feature's status is worked out without it. For work that's finished, move it to done instead.`,inputSchema:{task:l,reason:V().optional().describe(`Why, for history`)}},a(({task:e,reason:n})=>t.call(`archive_card`,{...r,p_card:e,p_reason:n??null,p_agent:i()}))),n.registerTool(`propose_term`,{title:`Propose a glossary term`,description:`Suggest a new glossary term. It shows up for approval; it isn't used until a person approves it.`,inputSchema:{term:V(),plain:V().describe(`What it means, for someone non-technical`),technical:V().describe(`What it means in the code`),kind:vm([`concept`,`tag`,`name`]).optional(),tagSet:V().optional().describe(`Required for kind 'tag': the set the value belongs to, e.g. feature_change, task_work, capability_audience, capability_kind (see tagSets in quirl_context)`),reason:V().optional()}},a(({term:e,plain:n,technical:a,kind:o,tagSet:s,reason:c})=>t.call(`propose_term`,{...r,p_term:e,p_plain:n,p_technical:a,p_kind:o??`concept`,p_tag_set:s??null,p_reason:c??null,p_agent:i()}))),n.registerTool(`propose_term_retire`,{title:`Retire a glossary term`,description:`Propose retiring a glossary term or tag that's no longer used or was a mistake. With replaceWith, the term stays as one not to say (agents are told the replacement); without it, it's removed. Either way it comes off every capability, task and feature that used it. A person approves it. Built-in tag values can't be retired.`,inputSchema:{term:V().describe(`The term or tag to retire`),replaceWith:V().optional().describe(`An approved term to say instead, if there is one`),reason:V().optional()}},a(({term:e,replaceWith:n,reason:a})=>t.call(`propose_term_retire`,{...r,p_term:e,p_replace_with:n??null,p_reason:a??null,p_agent:i()}))),n.registerTool(`list_flags`,{title:`List flags`,description:`Flags are messages people send about a section of the project: a capability, feature, task, doc or glossary term. Lists open (waiting for an admin) and approved (to act on) flags, or only those with the given status.`,inputSchema:{status:vm([`open`,`approved`,`removed`,`done`]).optional()}},a(({status:e})=>t.call(`list_flags`,{...r,p_status:e??null}))),n.registerTool(`decide_flag`,{title:`Approve or remove a flag`,description:`Admins only. Approve a flag so agents act on it at the start of their sessions, or remove it. An approved flag can still be removed. Only do this when the person you work for asks you to.`,inputSchema:{flag:V().describe(`The flag's id, from the briefing or list_flags`),decision:vm([`approve`,`remove`])}},a(({flag:e,decision:n})=>t.call(`decide_flag`,{...r,p_flag:e,p_approve:n===`approve`,p_agent:i()}))),n.registerTool(`resolve_flag`,{title:`Mark a flag done`,description:`Mark an approved flag done once you've handled it, so it leaves the briefing. Say briefly what you did.`,inputSchema:{flag:V().describe(`The flag's id, from the briefing or list_flags`),note:V().optional().describe(`What was done, e.g. 'Updated C-12 to mention SMS'`)}},a(({flag:e,note:n})=>t.call(`resolve_flag`,{...r,p_flag:e,p_note:n??null,p_agent:i()}))),n.registerTool(`set_capability_icon`,{title:`Set a capability's icon`,description:`Give a capability an icon that fits what it does; it's shown on the capability's card. Use the same icon a related feature has when one fits both.`,inputSchema:{capability:V().describe(`Capability name or id (e.g. C-12)`),icon:vm(Nb)}},a(({capability:e,icon:n})=>t.call(`set_capability_icon`,{...r,p_capability:e,p_icon:n,p_agent:i()}))),n.registerTool(`add_resource`,{title:`Add or fill in a resource`,description:"Record a service the project runs on that `quirl sync` can't see from the code: a hosted database another team runs, a SaaS named only in docs or the console, a staging copy. Sync already finds most things from env templates, docker-compose and client packages, so check Stack first (lookup) and don't add what it found. A resource with the same provider and environment is filled in rather than duplicated: the fields you give are set, and repos and capabilities are added to its links. Never pass URLs with credentials, keys or other secret values.",inputSchema:{name:V().describe(`What the team calls it, e.g. "Postgres", "Stripe"`),type:vm(Lb),provider:V().optional().describe(`Who runs it, e.g. "Google Cloud SQL", "Stripe". Resources are matched on this. Defaults to the name.`),description:V().optional().describe(`One plain sentence on what the project uses it for`),technical:V().optional().describe(`Technical notes for developers: setup, access, limits`),environment:V().optional().describe(`Defaults to "production"; e.g. "staging"`),repos:U(V()).optional().describe(`Repos that use it, by name`),capabilities:U(V()).optional().describe(`Capabilities that run on it, by name or id (e.g. C-12)`)}},a(e=>t.call(`add_resource`,{...r,p_name:e.name,p_type:e.type,p_provider:e.provider??null,p_description:e.description??null,p_technical:e.technical??null,p_environment:e.environment??null,p_repos:e.repos??null,p_capabilities:e.capabilities??null,p_agent:i()}))),n.registerTool(`list_resources`,{title:`List the project's resources`,description:`Every resource on Stack: name, type, provider, environment, whether sync found it or someone added it, the repos that use it and the capabilities that run on it.`,inputSchema:{}},a(()=>t.call(`list_resources`,r))),n.registerTool(`update_resource`,{title:`Change a resource`,description:`Correct a resource: rename it, relabel its environment (a scanned one that's really the local docker database becomes 'local'), or change its type, provider, descriptions or links. Only the fields you pass change. repos and capabilities replace its links; use add_resource to add to them. Sync keeps updating a relabelled resource instead of adding it again. Never pass secret values.`,inputSchema:{...Rb,name:V().optional(),type:vm(Lb).optional(),provider:V().optional(),environment:V().optional().describe(`e.g. "production", "staging", "dev", "local"`),description:V().optional().describe(`One plain sentence on what the project uses it for`),technical:V().optional().describe(`Notes for developers`),repos:U(V()).optional().describe(`Replaces the repos that use it`),capabilities:U(V()).optional().describe(`Replaces the capabilities that run on it (names or C-12)`)}},a(e=>t.call(`update_resource`,{...r,p_resource:e.resource,p_in_environment:e.inEnvironment??null,p_name:e.name??null,p_type:e.type??null,p_provider:e.provider??null,p_environment:e.environment??null,p_description:e.description??null,p_technical:e.technical??null,p_repos:e.repos??null,p_capabilities:e.capabilities??null,p_agent:i()}))),n.registerTool(`remove_resource`,{title:`Remove a wrong resource`,description:`Remove a resource that isn't one: a false match from sync, a duplicate, something no longer used. It's archived, stays in history, and sync won't add it again. A resource holding endpoints or tables from sync can't be removed; relabel it instead.`,inputSchema:{...Rb,reason:V().optional().describe(`Why it's wrong, for history`)}},a(e=>t.call(`remove_resource`,{...r,p_resource:e.resource,p_in_environment:e.inEnvironment??null,p_reason:e.reason??null,p_agent:i()}))),n.registerTool(`list_features`,{title:`List features`,description:`List the project's features with their status, the capability each changes (or createsCapability when it has none), and task counts. Shipped ones are left out unless includeShipped. Check here before requesting a feature.`,inputSchema:{includeShipped:$p().optional()}},a(({includeShipped:e})=>t.call(`list_features`,{...r,p_include_shipped:e??!1}))),n.registerTool(`get_feature`,{title:`Get a feature`,description:`A feature with its description, status, the capability it changes (and what that capability does) and its tasks.`,inputSchema:{feature:V().describe(`Feature name or id (e.g. F-4)`)}},a(({feature:e})=>t.call(`get_feature`,{...r,p_feature:e}))),n.registerTool(`request_feature`,{title:`Request a feature`,description:`Request a feature: a change to the product that stakeholders follow on the Features page, usually made of several tasks. Check list_features first so you don't request one twice. It shows as requested straight away and a person accepts or declines it. Put tasks under it with create_task or update_task.`,inputSchema:{name:V().describe(`Short name, e.g. 'Repeat appointments'`),description:V().describe(`What changes for the user, in plain words. Markdown: short paragraphs, and lists for parts`),capability:V().optional().describe(`Capability name or id (e.g. C-12) it changes. Leave it out when the feature creates a new capability`),tags:U(V()).optional().describe(`Kind of change: e.g. New, Improvement, Visual, Fix, Performance, Security, Removal, Internal (the feature_change set in quirl_context)`),reason:V().optional().describe(`Why it's worth doing`),icon:vm(Nb).optional().describe(`An icon that fits the feature, shown on its card`)}},a(async({name:e,description:n,capability:a,tags:o,reason:s,icon:c})=>{let l=await t.call(`request_feature`,{...r,p_name:e,p_description:n,p_capability:a??null,p_tags:o??null,p_reason:s??null,p_agent:i()});return c&&l===`requested`&&await t.call(`set_feature_icon`,{...r,p_feature:e,p_icon:c,p_agent:i()}),l})),n.registerTool(`set_feature_icon`,{title:`Set a feature's icon`,description:`Give a feature an icon that fits what it does; it's shown on the feature's card. list_features shows which features have none.`,inputSchema:{feature:V().describe(`Feature name or id (e.g. F-4)`),icon:vm(Nb)}},a(({feature:e,icon:n})=>t.call(`set_feature_icon`,{...r,p_feature:e,p_icon:n,p_agent:i()}))),n.registerTool(`update_feature`,{title:`Correct a feature`,description:`Fix a feature's name, description or the capability it changes, e.g. after requesting it with the wrong one. Only the fields you pass change; it shows in the feature's history. Tags: tag_feature; icon: set_feature_icon.`,inputSchema:{feature:V().describe(`Feature name or id (e.g. F-4)`),name:V().optional(),description:V().optional().describe(`What changes for the user, in plain words. Markdown: short paragraphs, and lists for parts`),capability:V().optional().describe(`The capability it changes (name or C-12), or "none" when it creates a new one`)}},a(({feature:e,name:n,description:a,capability:o})=>t.call(`update_feature`,{...r,p_feature:e,p_name:n??null,p_description:a??null,p_capability:o??null,p_agent:i()}))),n.registerTool(`withdraw_feature`,{title:`Withdraw a feature`,description:`Remove a feature that shouldn't be there: a duplicate, a mistake, or an idea dropped before any work. It's archived and stays in history. One that shipped can't be withdrawn; move or archive its tasks first (update_task, archive_task).`,inputSchema:{feature:V().describe(`Feature name or id (e.g. F-4)`),reason:V().optional().describe(`Why, for history`)}},a(({feature:e,reason:n})=>t.call(`withdraw_feature`,{...r,p_feature:e,p_reason:n??null,p_agent:i()}))),n.registerTool(`tag_feature`,{title:`Tag a feature`,description:`Set a feature's kind-of-change tags (replaces the current ones). Only values from the feature_change tag set are accepted.`,inputSchema:{feature:V().describe(`Feature name or id (e.g. F-4)`),tags:U(V()).describe(`Kind of change: e.g. New, Improvement, Visual, Fix, Performance, Security, Removal, Internal`)}},a(({feature:e,tags:n})=>t.call(`tag_feature`,{...r,p_feature:e,p_tags:n,p_agent:i()}))),n.registerTool(`list_capabilities`,{title:`List the capabilities`,description:`Every capability as data: id (C-12), name, status, icon, the capability it's part of, what it does, what it relies on, its tags and the features changing it. Retired ones only with includeRetired.`,inputSchema:{includeRetired:$p().optional()}},a(({includeRetired:e})=>t.call(`list_capabilities`,{...r,p_include_retired:e??!1}))),n.registerTool(`propose_capability`,{title:`Propose a capability`,description:"Draft a capability: one thing the product does today, described for a non-technical reader. Use it when the project has none yet (for example right after `quirl init`), when you find one that's missing, or when a feature that creates a new capability ships (pass it in features). Never for something not built yet: request a feature instead. A person approves it before it shows up.",inputSchema:{name:V().describe(`Short name, e.g. 'Online booking'`),whatItDoes:V().describe(`One or two plain sentences, no jargon`),technical:V().optional().describe(`Where it lives in the code and how it works. Markdown: short paragraphs, and lists for parts, not one long block`),features:U(V()).optional().describe(`Feature names or ids (e.g. F-4) that create or change this capability; they're linked to it once it's approved`),reason:V().optional()}},a(({name:e,whatItDoes:n,technical:a,features:o,reason:s})=>t.call(`propose_capability`,{...r,p_name:e,p_what_it_does:n,p_technical:a??``,p_features:o??null,p_reason:s??null,p_agent:i()}))),n.registerTool(`propose_capability_archive`,{title:`Propose archiving a capability`,description:`Suggest retiring a capability that no longer describes what the product does, or that was drafted for something not built yet. Unshipped features linked to it will create a new capability when they ship. A person approves it.`,inputSchema:{capability:V().describe(`Capability name or id (e.g. C-12)`),reason:V().describe(`Why it should go`)}},a(({capability:e,reason:n})=>t.call(`propose_capability_archive`,{...r,p_capability:e,p_reason:n,p_agent:i()}))),n.registerTool(`propose_capability_update`,{title:`Propose a capability correction`,description:`Suggest a corrected name or description for a capability when the code no longer matches it. A person approves it.`,inputSchema:{capability:V().describe(`Capability name or id (e.g. C-12)`),field:vm([`name`,`what_it_does`,`technical_description`]),after:V().describe(`The new name, or the corrected text. Markdown: short paragraphs, and lists for parts, not one long block`),reason:V().optional()}},a(({capability:e,field:n,after:a,reason:o})=>t.call(`propose_capability_update`,{...r,p_capability:e,p_field:n,p_after:a,p_reason:o??null,p_agent:i()}))),n.registerTool(`propose_capability_details`,{title:`Propose how a capability works and its connections`,description:`Suggest a capability's 'How it works' steps, its tags (glossary terms of kind 'tag'), the capability it's part of, and its connections: capabilities it relies on, repos, resources it runs on, glossary terms and docs. Each part you pass replaces that part when a person approves it; parts you leave out are kept. Names are matched to existing, approved items. What it relies on draws the arrows on the Home map, so link the steps a user goes through (e.g. invoicing relies on booking).`,inputSchema:{capability:V().describe(`Capability name or id (e.g. C-12)`),howItWorks:U(V()).optional().describe(`3 to 5 plain-language steps, as the user experiences it`),reliesOn:U(W({capability:V().describe(`Capability name or id (e.g. C-12)`),label:V().describe(`How it relies on it, read before the name, e.g. 'extends'`)})).optional(),parent:V().optional().describe(`The capability this one is part of (name or id, e.g. C-2), or 'none' to make it top-level. Parts are listed under their parent instead of on their own`),repos:U(V()).optional().describe(`Repo names`),resources:U(V()).optional().describe(`Resource names it runs on`),terms:U(V()).optional().describe(`Glossary terms`),docs:U(V()).optional().describe(`Doc titles, or <repo>/<path> (e.g. quirl/AGENTS.md) when titles repeat`),tags:U(V()).optional().describe(`Who it's for (Customer, Admin, Developer, Agent) and its kind (Interface, Integration, Data, Automation, Infrastructure, Access and security, AI): the capability tag sets in quirl_context`),reason:V().optional()}},a(({capability:e,howItWorks:n,reliesOn:a,parent:o,repos:s,resources:c,terms:l,docs:u,tags:d,reason:f})=>t.call(`propose_capability_details`,{...r,p_capability:e,p_how_it_works:n??null,p_relies_on:a??null,p_parent:o??null,p_repos:s??null,p_resources:c??null,p_terms:l??null,p_docs:u??null,p_tags:d??null,p_reason:f??null,p_agent:i()}))),n}async function Bb(e){await zb(e).connect(new Mb)}const Vb=`0.1.6`,Hb=process.env.QUIRL_SERVER===`local`?{apiUrl:`http://127.0.0.1:54321`,publishableKey:`sb_publishable_ACJWlzQHlZjBrEguHvfOxg_3BJgxAaH`,appUrl:`http://localhost:5173`}:{apiUrl:`https://dugevhzlzrdtasuibdzr.supabase.co`,publishableKey:`sb_publishable_45P7GyyZoJsN0AO3wfmqNA_CXbx1iCW`,appUrl:`https://app.quirl.io`};function Ub(){return{apiUrl:process.env.QUIRL_API_URL??Hb.apiUrl,publishableKey:process.env.QUIRL_PUBLISHABLE_KEY??Hb.publishableKey,appUrl:process.env.QUIRL_APP_URL??Hb.appUrl}}const Wb=()=>x(process.env.XDG_CONFIG_HOME??x(h(),`.config`),`quirl`),Gb=()=>x(Wb(),`credentials.json`);async function Kb(){try{return JSON.parse(await u(Gb(),`utf8`))}catch{return{}}}async function qb(){return process.env.QUIRL_KEY?{apiUrl:Ub().apiUrl,key:process.env.QUIRL_KEY,email:``}:(await Kb())[Ub().apiUrl]??null}async function Jb(e){let t=await Kb();t[e.apiUrl]=e,await l(Wb(),{recursive:!0,mode:448}),await m(Gb(),`${JSON.stringify(t,null,` `)}\n`,{mode:384}),await s(Gb(),384)}async function Yb(){let e=await Kb();delete e[Ub().apiUrl],Object.keys(e).length?await m(Gb(),`${JSON.stringify(e,null,` `)}\n`,{mode:384}):await f(Gb(),{force:!0})}async function Xb(){let e=Ub();return{apiUrl:e.apiUrl,publishableKey:e.publishableKey,key:(await qb())?.key??``}}function Zb(e=process.cwd()){try{return a(`git`,[`rev-parse`,`--show-toplevel`],{cwd:e,stdio:[`ignore`,`pipe`,`ignore`]}).toString().trim()}catch{return e}}const Qb=e=>x(e,`.quirl`,`project.json`);async function $b(e=process.cwd()){let t=e;for(;;)try{return{root:t,link:JSON.parse(await u(Qb(t),`utf8`))}}catch{let e=y(t);if(e===t)return null;t=e}}async function ex(e,t){await l(x(e,`.quirl`),{recursive:!0}),await m(Qb(e),`${JSON.stringify(t,null,` `)}\n`)}function tx(e){let t=``;for(let n=0;n<e.length;n++){let r=e[n];r===`*`?e[n+1]===`*`?(t+=e[n+2]===`/`?`(?:.*/)?`:`.*`,n+=e[n+2]===`/`?2:1):t+=`[^/]*`:t+=r===`?`?`[^/]`:r.replace(/[.+^${}()|[\]\\]/g,`\\$&`)}return RegExp(`^${t}$`)}function nx(e){let t=[];for(let n of e.split(/\r?\n/)){let e=n.replace(/\s+$/,``);if(!e||e.startsWith(`#`))continue;let r=e.startsWith(`!`);r&&(e=e.slice(1));let i=e.endsWith(`/`);i&&(e=e.slice(0,-1));let a=e.includes(`/`);e=e.replace(/^\//,``);let o=tx(e).source.slice(1,-1);t.push({regex:RegExp(a?`^${o}$`:`^(?:.*/)?${o}$`),negate:r,dirOnly:i})}return t}function rx(e,t){if(!e.length)return!1;let n=t.split(`/`);for(let t=1;t<=n.length;t++){let r=n.slice(0,t).join(`/`),i=t<n.length,a=!1;for(let t of e)(!t.dirOnly||i)&&t.regex.test(r)&&(a=!t.negate);if(a)return!0}return!1}async function ix(e){try{return nx(await u(_.join(e,`.quirlignore`),`utf8`))}catch{return[]}}function ax(e,t){return new Promise(n=>{if(!t.length)return n(/* @__PURE__ */ new Set);i(`git`,[`check-ignore`,`--stdin`,`-z`],{cwd:e,maxBuffer:268435456,timeout:6e4},(e,t)=>n(new Set(String(t??``).split(`\0`).filter(Boolean)))).stdin?.end(`${t.join(`\0`)}\0`)})}const ox=n(i),sx=/* @__PURE__ */ new Set([`.git`,`node_modules`,`dist`,`build`,`.next`,`vendor`,`.venv`,`venv`,`__pycache__`,`coverage`,`.turbo`,`.svelte-kit`,`.nuxt`,`.output`,`.cache`,`.vercel`,`.pnpm-store`,`fixtures`,`test-fixtures`,`__fixtures__`,`testdata`]),cx=1048576,lx=2e5;async function ux(e){let t=[],n=await ix(e),r=(await fx(e,t)).filter(e=>!rx(n,e.path)),i=await ax(e,r.map(e=>e.path)),a=r.filter(e=>!i.has(e.path)),o=new Map(a.map(e=>[e.path,e]));return{root:e,files:a,warnings:t,file:e=>o.get(e),async readText(e,n=cx){if(e.size>n)return t.push(`${e.path}: skipped, ${yx(e.size)} is over the ${yx(n)} limit`),null;try{return await u(e.absolutePath,`utf8`)}catch(n){return t.push(`${e.path}: could not be read (${_x(n)})`),null}}}}async function dx(e,t){let n=[],r=await ix(e),{stdout:i}=await ox(`git`,[`ls-tree`,`-r`,`-l`,`-z`,`--full-tree`,t],{cwd:e,maxBuffer:268435456,timeout:6e4}),a=[];for(let e of i.split(`\0`)){let t=e.indexOf(` `);if(t===-1)continue;let[i,o,s,c]=e.slice(0,t).trim().split(/\s+/),l=e.slice(t+1);if(o===`blob`&&i!==`120000`&&s&&!l.split(`/`).some(e=>sx.has(e))&&!rx(r,l)){if(a.length>=lx){n.push(`Stopped after ${lx} files; the rest of the tree was not scanned`);break}a.push({path:l,absolutePath:s,size:Number(c)||0})}}px(a);let o=new Map(a.map(e=>[e.path,e]));return{root:e,files:a,warnings:n,file:e=>o.get(e),async readText(t,r=cx){if(t.size>r)return n.push(`${t.path}: skipped, ${yx(t.size)} is over the ${yx(r)} limit`),null;try{let{stdout:n}=await ox(`git`,[`cat-file`,`blob`,t.absolutePath],{cwd:e,maxBuffer:r+1024,timeout:3e4});return n}catch(e){return n.push(`${t.path}: could not be read (${_x(e)})`),null}}}}async function fx(e,t){let n=[],r=[``];for(;r.length>0;){let i=r.pop(),a;try{a=await d(_.join(e,i),{withFileTypes:!0})}catch(e){t.push(`${i||`.`}: folder could not be read (${_x(e)})`);continue}for(let o of a){let a=i?`${i}/${o.name}`:o.name;if(o.isSymbolicLink())continue;if(o.isDirectory()){!sx.has(o.name)&&!C(_.join(e,a,`.git`))&&r.push(a);continue}if(!o.isFile())continue;if(n.length>=lx)return t.push(`Stopped after ${lx} files; the rest of the tree was not scanned`),px(n);let s=_.join(e,a);try{let e=await c(s);n.push({path:a,absolutePath:s,size:e.size})}catch(e){t.push(`${a}: could not be read (${_x(e)})`)}}}return px(n)}function px(e){return e.sort((e,t)=>e.path<t.path?-1:+(e.path>t.path))}function mx(e){let t=e.lastIndexOf(`/`);return t===-1?e:e.slice(t+1)}function hx(e){let t=e.lastIndexOf(`/`);return t===-1?``:e.slice(0,t)}function gx(e,t){return e?`${e}/${t}`:t}function _x(e){return Gr(e instanceof Error?e.message:String(e))}function Z(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function vx(e){return typeof e==`string`?e:void 0}function yx(e){return e>=1048576?`${(e/1048576).toFixed(1)} MB`:`${Math.ceil(e/1024)} KB`}async function bx({hook:e=!1}={}){let t=await $b();if(!t){if(e)return;throw Error("This folder isn't linked to a Quirl project. Run `quirl init`.")}try{let n=await new Ur(await Xb()).context(t.link.projectId);process.stdout.write(`${Fb(n,e?9500:void 0)}\n`)}catch(n){if(!e)throw n;process.stdout.write(`Quirl project "${t.link.name}": couldn't load the briefing (${_x(n)}). The quirl MCP tools may still work.\n`)}}async function xx(e,t=3){let n=[],r=async(e,i)=>{if(C(_.join(e,`.git`))&&n.push(e),i>=t)return;let a;try{a=await d(e,{withFileTypes:!0})}catch{return}for(let t of a)!t.isDirectory()||t.name.startsWith(`.`)||sx.has(t.name)||await r(_.join(e,t.name),i+1)};return await r(_.resolve(e),0),n.sort((e,t)=>e.localeCompare(t))}const Sx=process.stdout.isTTY&&!process.env.NO_COLOR,Cx=e=>t=>Sx?`\x1b[${e}m${t}\x1b[0m`:t,wx=Cx(1),Q=Cx(2),Tx=Cx(32),Ex=Cx(31),Dx=Cx(36),Ox=e=>console.log(`${Tx(`✓`)} ${e}`),kx=e=>console.log(` ${e}`),Ax=(e,t)=>console.log(`\n${e} ${t}`),jx=e=>console.log(` ${e}`);function Mx(e){if(!Sx)return jx(Q(e)),{stop:()=>{}};let t=0,n=setInterval(()=>{process.stdout.write(`\r ${Dx(`⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏`[t++%10]??``)} ${Q(e)}`)},80);return{stop:()=>{clearInterval(n),process.stdout.write(`\r\x1B[2K`)}}}var Nx=class extends Error{};async function Px(e,t=``){if(!process.stdin.isTTY)return t;let n=E({input:process.stdin,output:process.stdout});try{return(await n.question(e)).trim()||t}finally{n.close()}}async function Fx(e,t){let n=await Px(`${e} ${Q(t?`[Y/n]`:`[y/N]`)} `,t?`y`:`n`);return/^y/i.test(n)}function Ix(e){if(process.env.QUIRL_NO_BROWSER)return;let t=process.platform===`darwin`?`open`:process.platform===`win32`?`cmd`:`xdg-open`,n=process.platform===`win32`?[`/c`,`start`,``,e]:[e];try{o(t,n,{stdio:`ignore`,detached:!0}).on(`error`,()=>{}).unref()}catch{}}const Lx=`<!-- quirl:start -->`,Rx=`<!-- quirl:end -->`,zx=`# quirl: keep the project in sync`;function Bx(){try{let e=a(process.platform===`win32`?`where`:`which`,[`quirl`],{stdio:[`ignore`,`pipe`,`ignore`]}).toString();if(!/[\\/]_npx[\\/]/.test(e))return{command:`quirl`,args:[],shell:`quirl`,portable:!0}}catch{}return{command:`npx`,args:[`-y`,`quirl-cli`],shell:`npx -y quirl-cli`,portable:!0}}const Vx=[`QUIRL_SERVER`,`QUIRL_API_URL`,`QUIRL_PUBLISHABLE_KEY`,`QUIRL_APP_URL`];function Hx(){return Object.fromEntries(Vx.flatMap(e=>process.env[e]?[[e,process.env[e]]]:[]))}function Ux(){return`${Object.entries(Hx()).map(([e,t])=>`${e}="${t}" `).join(``)}${Bx().shell}`}async function Wx(e){try{return JSON.parse(await u(e,`utf8`))}catch(t){if(t.code===`ENOENT`)return{};throw Error(`${e} isn't valid JSON; fix it and run \`quirl init\` again.`)}}const Gx=async(e,t)=>{await l(y(e),{recursive:!0}),await m(e,`${JSON.stringify(t,null,2)}\n`)};async function Kx(e){let t=x(e,`.mcp.json`),n=await Wx(t),r=Bx();n.mcpServers={...n.mcpServers,quirl:{command:r.command,args:[...r.args,`mcp`],...Object.keys(Hx()).length?{env:Hx()}:{}}},await Gx(t,n)}async function qx(e){let t=x(e,`.claude`,`settings.json`),n=await Wx(t),r=n.hooks??{},i=`${Ux()} context --hook`;r.SessionStart=[...(r.SessionStart??[]).map(e=>({...e,hooks:e.hooks.filter(e=>!/\bcontext --hook$/.test(e.command))})).filter(e=>e.hooks.length),{hooks:[{type:`command`,command:i,timeout:15}]}],n.hooks=r,await Gx(t,n)}async function Jx(e,t,n=[]){let r=x(e,`CLAUDE.md`),i=``;try{i=await u(r,`utf8`)}catch{}let a=`${Lx}
|
|
185
185
|
## Quirl
|
|
186
186
|
|
|
187
187
|
${n.length?`This folder holds the repos of the Quirl project **${t}** (${n.join(`, `)}). Quirl is`:`This repo is part of the Quirl project **${t}**,`} the team's source of truth for what the product does. The project briefing (always-loaded docs, glossary, capabilities, your tasks) is loaded at the start of each session. Use the \`quirl\` MCP tools:
|