diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..02107df --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,42 @@ +# AGENTS.md — nix-clawdis + +Single source of truth for product direction: `README.md`. + +Documentation policy: +- Keep the surface area small. +- Avoid duplicate “pointer‑only” files. +- Update `README.md` first, then adjust references. + +Defaults: +- Nix‑first, no sudo. +- Declarative config only. +- Batteries‑included install is the baseline. +- Breaking changes are acceptable pre‑1.0.0 (move fast, keep docs accurate). + +Philosophy: + +The Zen of ~~Python~~ Clawdis, ~~by~~ shamelessly stolen from Tim Peters + +Beautiful is better than ugly. +Explicit is better than implicit. +Simple is better than complex. +Complex is better than complicated. +Flat is better than nested. +Sparse is better than dense. +Readability counts. +Special cases aren't special enough to break the rules. +Although practicality beats purity. +Errors should never pass silently. +Unless explicitly silenced. +In the face of ambiguity, refuse the temptation to guess. +There should be one-- and preferably only one --obvious way to do it. +Although that way may not be obvious at first unless you're Dutch. +Now is better than never. +Although never is often better than *right* now. +If the implementation is hard to explain, it's a bad idea. +If the implementation is easy to explain, it may be a good idea. +Namespaces are one honking great idea -- let's do more of those! + +Nix file policy: +- No inline file contents in Nix code, ever. +- Always reference explicit file paths (keep docs as real files in the repo). diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..43b4db9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,577 @@ +GNU AFFERO GENERAL PUBLIC LICENSE +Version 3, 19 November 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. + +Preamble + +The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + +The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + +When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + +Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + +A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + +The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on a +public server, gives the public access to the source code of the +modified version. + +An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + +The precise terms and conditions for copying, distribution and +modification follow. + +TERMS AND CONDITIONS + +0. Definitions. + +"This License" refers to version 3 of the GNU Affero General Public License. + +"Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + +"The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + +To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + +A "covered work" means either the unmodified Program or a work based +on the Program. + +To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + +To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + +An interactive user interface displays "Appropriate Legal Notices" +to the extent required by section 5. + +The "Source Code" for a work means the preferred form of the work +for making modifications to it. "Object Code" means any non-source +form of a work. + +A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + +The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + +The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + +The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + +The Corresponding Source for a work in source code form is that +same work. + +1. Source Code. + +The source code for a work means the preferred form of the work +for making modifications to it. "Object Code" means any non-source +form of a work. + +2. Basic Permissions. + +All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under the +conditions stated below. Sublicensing is not allowed; section 10 makes +it unnecessary. + +3. Protecting Users' Legal Rights From Anti-Circumvention Law. + +No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or similar +laws prohibiting or restricting circumvention of such measures. + +When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + +4. Conveying Verbatim Copies. + +You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + +5. Conveying Modified Source Versions. + +You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + +a) The work must carry prominent notices stating that you modified it, +and giving a relevant date. + +b) The work must carry prominent notices stating that it is released +under this License and any conditions added under section 7. This +requirement modifies the requirement in section 4 to "keep intact all +notices". + +c) You must license the entire work, as a whole, under this License +to anyone who comes into possession of a copy. This License will +therefore apply, along with any applicable section 7 additional terms, +to the whole of the work, and all its parts, regardless of how they are +packaged. This License gives no permission to license the work in any +other way, but it does not invalidate such permission if you have +separately received it. + +d) If the work has interactive user interfaces, each must display +Appropriate Legal Notices; however, if the Program has interactive +interfaces that do not display Appropriate Legal Notices, your work +need not make them do so. + +A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + +6. Conveying Non-Source Forms. + +You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + +a) Convey the object code in, or embodied in, a physical product +(including a physical distribution medium), accompanied by the +Corresponding Source fixed on a durable physical medium customarily +used for software interchange. + +b) Convey the object code in, or embodied in, a physical product +(including a physical distribution medium), accompanied by a written +offer, valid for at least three years and valid for as long as you +offer spare parts or customer support for that product model, to give +anyone who possesses the object code either (1) a copy of the +Corresponding Source for all the software in the product that is +covered by this License, on a durable physical medium customarily used +for software interchange, for a price no more than your reasonable +cost of physically performing this conveying of source, or (2) access +to copy the Corresponding Source from a network server at no charge. + +c) Convey individual copies of the object code with a copy of the +written offer to provide the Corresponding Source. This alternative is +allowed only occasionally and noncommercially, and only if you received +the object code with such an offer, in accord with subsection 6b. + +d) Convey the object code by offering access from a designated place +(gratis or for a charge), and offer equivalent access to the +Corresponding Source in the same way through the same place at no +further charge. You need not require recipients to copy the +Corresponding Source along with the object code. If the place to copy +the object code is a network server, the Corresponding Source may be +on a different server (operated by you or a third party) that supports +equivalent copying facilities, provided you maintain clear directions +next to the object code saying where to find the Corresponding Source. + +e) Convey the object code using peer-to-peer transmission, provided you +inform other peers where the object code and Corresponding Source of +the work are being offered to the general public at no charge under +subsection 6d. + +A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + +7. Additional Terms. + +"Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + +"Additional permissions" are terms that, for a particular work, add +to the terms of this License by making exceptions from one or more of +its conditions. Additional permissions may be written to require their +own removal in certain cases when you modify the work. This License +permits supplementing the license terms for a covered work by making +exceptions from one or more of its conditions. + +Additional permissions may be stated in the form of a license notice +written in the relevant source file, or in a file that accompanies the +work. Additional permissions are therefore any terms that supplement +the terms of this License. + +If you add terms to a covered work, you may (if authorized by the +copyright holders) add additional permissions that apply to part of +the work, supplementing the permissions of this License. These +additional permissions are not considered "further restrictions" within +the meaning of section 10. The remainder of the work is not affected +by such permissions. + +If you add terms to a covered work, you may add additional permissions +that apply to the entire work, supplementing the permissions of this +License. These additional permissions are not considered "further +restrictions" within the meaning of section 10. The remainder of the +work is not affected by such permissions. + +If you add terms to a covered work, you may also remove those terms +entirely. Additional permissions are therefore not necessary. + +If the Program as you received it, or any part of it, contains a notice +stating that it is governed by this License along with a term that is +a further restriction, you may remove that term. If a license document +contains a further restriction but the Program does not, the further +restriction applies only to the license document, not to the Program. + +If a further restriction is enforceable, it remains in force +notwithstanding any other provision of this License. If it cannot be +enforced, it is severed from this License, and all other provisions +remain in force. + +8. Termination. + +You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + +However, if you cease all violation of this License, then your license +from a particular copyright holder is reinstated (a) provisionally, +unless and until the copyright holder explicitly and finally terminates +your license, and (b) permanently, if the copyright holder fails to +notify you of the violation by some reasonable means prior to 60 days +after the cessation. + +Moreover, your license from a particular copyright holder is reinstated +permanently if the copyright holder notifies you of the violation by +some reasonable means, this is the first time you have received notice +of violation of this License (for any work) from that copyright holder, +and you cure the violation prior to 30 days after your receipt of the +notice. + +Termination of your rights under this License does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + +9. Acceptance Not Required for Having Copies. + +You are not required to accept this License in order to receive or run +a copy of the Program. Ancillary propagation of a covered work occurring +solely as a consequence of using peer-to-peer transmission to receive a +copy likewise does not require acceptance. However, nothing other than +this License grants you permission to propagate or modify any covered +work. These actions infringe copyright if you do not accept this +License. Therefore, by modifying or propagating a covered work, you +indicate your acceptance of this License to do so. + +10. Automatic Licensing of Downstream Recipients. + +Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + +An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered work +results from an entity transaction, each party to that transaction who +receives a copy of the work also receives whatever licenses to the work +the party's predecessor in interest had or could give under the previous +paragraph, plus a right to possession of the Corresponding Source of the +work from the predecessor in interest, if the predecessor has it or can +get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may not +impose a license fee, royalty, or other charge for exercise of rights +granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that any +patent claim is infringed by making, using, selling, offering for sale, +or importing the Program or any portion of it. + +11. Patents. + +A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The work +thus licensed is called the contributor's "contributor version". + +A contributor's "essential patent claims" are all patent claims owned +or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a consequence +of further modification of the contributor version. For purposes of +this definition, "control" includes the right to grant patent +sublicenses in a manner consistent with the requirements of this +License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + +In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(例えば, an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + +If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + +A patent license is "discriminatory" if it does not include within the +scope of its coverage, prohibits the exercise of, or is conditioned on +the non-exercise of one or more of the rights that are specifically +granted under this License. You may not convey a covered work if you are +a party to an arrangement with a third party that is in the business of +distributing software, under which you make payment to the third party +based on the extent of your activity of conveying the work, and under +which the third party grants, to any of the parties who would receive +the covered work from you, a discriminatory patent license (a) in +connection with copies of the covered work conveyed by you (or copies +made from those copies), or (b) primarily for and in connection with +specific products or compilations that contain the covered work, unless +you entered into that arrangement, or that patent license was granted, +prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting any +implied license or other defenses to infringement that may otherwise be +available to you under applicable patent law. + +12. No Surrender of Others' Freedom. + +If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not convey it at all. For example, if you agree to terms that obligate +you to collect a royalty for further conveying from those to whom you +convey the Program, the only way you could satisfy both those terms and +this License would be to refrain entirely from conveying the Program. + +13. Remote Network Interaction; Use with the GNU General Public License. + +Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + +Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single combined +work, and to convey the resulting work. The terms of this License will +continue to apply to the part which is the covered work, but the work +with which it is combined will remain governed by version 3 of the GNU +General Public License. + +14. Revised Versions of this License. + +The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the GNU +Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future versions +of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + +Later license versions may give you additional or different +permissions. + +15. Disclaimer of Warranty. + +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. Limitation of Liability. + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING +ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF +the USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO +LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU +OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + +17. Interpretation of Sections 15 and 16. + +If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these +terms. + +To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as + published by the Free Software Foundation, either version 3 of the + License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . diff --git a/README.md b/README.md index 7c6f46b..0db265d 100644 --- a/README.md +++ b/README.md @@ -3,15 +3,24 @@ > Declarative Clawdis. Bulletproof by default. > > [skip to agent copypasta](#give-this-to-your-ai-agent) +> +> Questions? Join the Clawdis Discord and ask in **#nix**. ## The Magic - **One flake, everything works.** Gateway, macOS app, whisper, spotify, camera tools - all wired up and pinned. +- **Plugins are self‑contained.** Each plugin declares its CLI/tools in Nix, you enable it with a repo pointer, and the build/wiring happens automatically (you don’t care where it builds). + - **Agent-first.** Give the copypasta to Claude. It sets you up. You don't read docs, you just talk to your bot. - **Bulletproof.** Nix locks every dependency. No version drift, no surprises. `home-manager switch` to update, `home-manager generations` to rollback instantly. +## Vision & Strategy + +This README is the **single source of truth** for maintainers. + + ## Why this exists Clawdis is the right product. An AI assistant that lives in Telegram, controls your Mac, and actually does things. @@ -50,6 +59,8 @@ What nix-clawdis is: What I need you to do: 1. Check if Determinate Nix is installed (if not, install it) 2. Create a local flake at ~/code/clawdis-local using templates/agent-first/flake.nix +3. Create a docs dir next to the config (e.g., ~/code/clawdis-local/documents) with AGENTS.md, SOUL.md, TOOLS.md + - If ~/.clawdis/workspace already has these files, adopt them into the documents dir first 3. Help me create a Telegram bot (@BotFather) and get my chat ID (@userinfobot) 4. Set up secrets (bot token, Anthropic key) - plain files at ~/.secrets/ is fine 5. Fill in the template placeholders and run home-manager switch @@ -63,7 +74,10 @@ My setup: Reference the README and templates/agent-first/flake.nix in the repo for the module options. ``` -## Minimal config +## Minimal config (single‑instance) + +Use this for the simplest setup. For richer config (per‑group overrides), use +`instances.default` below. ```nix { @@ -71,18 +85,275 @@ Reference the README and templates/agent-first/flake.nix in the repo for the mod enable = true; providers.telegram = { enable = true; - botTokenFile = "/path/to/telegram-bot-token"; + botTokenFile = "/run/agenix/telegram-bot-token"; # any file path works allowFrom = [ 12345678 ]; # your Telegram user ID }; providers.anthropic = { - apiKeyFile = "/path/to/anthropic-api-key"; + apiKeyFile = "/run/agenix/anthropic-api-key"; # any file path works }; + + # No plugin settings needed for hello‑world. + plugins = [ + { source = "github:acme/hello-world"; } + ]; }; } ``` Then: `home-manager switch --flake .#youruser` +## Small but useful config (sensible defaults) + +This is still single‑instance, but uses `instances.default` to unlock per‑group mention rules. +If `instances` is set, you don’t need `programs.clawdis.enable`. +Group mention overrides below mirror upstream Clawdis config. +Secrets are shown using `/run/agenix/...` (from a repo with your agenix secrets), but any file path works. +Docs are managed from `./documents` and symlinked into the workspace on each switch. + +```nix +{ + programs.clawdis = { + documents = ./documents; + instances.default = { + enable = true; + package = pkgs.clawdis; # batteries-included + stateDir = "~/.clawdis"; + workspaceDir = "~/.clawdis/workspace"; + + providers.telegram = { + enable = true; + botTokenFile = "/run/agenix/telegram-bot-token"; + allowFrom = [ + 12345678 # you (DM) + -1001234567890 # couples group (no @mention required) + -1002345678901 # noisy group (require @mention) + ]; + groups = { + "*" = { requireMention = true; }; + "-1001234567890" = { requireMention = false; }; # couples group + "-1002345678901" = { requireMention = true; }; # noisy group + }; + }; + + providers.anthropic.apiKeyFile = "/run/agenix/anthropic-api-key"; + + launchd.enable = true; + + # Plugins (prod: pinned GitHub). Built‑ins are already included. + # MVP target: repo pointers resolve to tools + skills automatically. + plugins = [ + { source = "github:joshp123/xuezh"; } + { + source = "github:joshp123/padel-cli"; + config = { + env = { PADEL_AUTH_FILE = "/run/agenix/padel-auth"; }; + settings = { + default_location = "CITY_NAME"; + preferred_times = [ "18:00" "20:00" ]; + preferred_duration = 90; + venues = [ + { + id = "VENUE_ID"; + alias = "VENUE_ALIAS"; + name = "VENUE_NAME"; + indoor = true; + timezone = "TIMEZONE"; + } + ]; + }; + }; + } + ]; + }; + }; +} +``` + +## Minimal dual‑instance (prod + dev) + +Use a shared base config and override only what’s different. This should become +a first‑class feature, but plain Nix works today. After changing local plugin or +gateway code, re-run `home-manager switch` to rebuild. POC: the macOS app stays +pinned to a released version (no local app builds yet). + +```nix +# flake inputs (pin prod + app) +inputs = { + nix-clawdis.url = "github:joshp123/nix-clawdis?ref=v0.1.0"; # pins macOS app + gateway bundle +}; + +let + prod = { + enable = true; + # Prod gateway pin (comes from nix-clawdis input @ v0.1.0 above). + package = inputs.nix-clawdis.packages.${pkgs.system}.clawdis-gateway; + providers.telegram.enable = true; + providers.telegram.botTokenFile = "/run/agenix/telegram-prod"; + providers.telegram.allowFrom = [ 12345678 ]; + providers.anthropic.apiKeyFile = "/run/agenix/anthropic-api-key"; + plugins = [ { source = "github:owner/your-plugin"; } ]; + }; +in { + # Pinned macOS app (POC: no local app builds, uses nix-clawdis @ v0.1.0 above). + programs.clawdis.appPackage = + inputs.nix-clawdis.packages.${pkgs.system}.clawdis-app; + programs.clawdis.documents = ./documents; + programs.clawdis.instances = { + prod = prod; + dev = prod // { + # Dev uses the same pinned macOS app (from nix-clawdis input), + # but overrides the gateway package to a local checkout. + providers.telegram.botTokenFile = "/run/agenix/telegram-dev"; + gatewayPort = 18790; + # Local gateway checkout (path). App stays pinned. + gatewayPath = "/Users/you/code/clawdis"; + # Local plugin overrides prod if names collide (last wins). + plugins = prod.plugins ++ [ + { source = "path:/Users/you/code/your-plugin"; } + { + source = "github:joshp123/padel-cli"; + config = { + env = { PADEL_AUTH_FILE = "/run/agenix/padel-auth-dev"; }; + settings = { + default_location = "CITY_NAME"; + preferred_times = [ "18:00" ]; + preferred_duration = 90; + venues = []; + }; + }; + } + ]; + }; + }; +} +``` + + +## Hello‑world plugin (repo + skill) + +**Plugin repo structure (minimum):** + +``` +your-plugin/ + flake.nix + skills/ + hello-world/ + SKILL.md +``` + +Example implementation: `examples/hello-world-plugin`. + +**`flake.nix` (minimal `clawdisPlugin`):** + +```nix +{ + outputs = { self, nixpkgs, ... }: + let + pkgs = import nixpkgs { system = builtins.currentSystem; }; + in { + clawdisPlugin = { + name = "hello-world"; + skills = [ ./skills/hello-world ]; + packages = [ pkgs.hello ]; # example CLI + needs = { + stateDirs = []; + env = []; + requiredFiles = []; + }; + }; + }; +} +``` + +**`skills/hello-world/SKILL.md` (minimal):** + +```md +--- +name: hello-world +description: Prints hello world. +--- + +Use the `hello` CLI to print a greeting. +``` + +**Hello‑world uses no config:** it declares empty `needs` and requires no +per‑plugin `config`. + +## Paste this prompt to your coding agent (make your plugin nix‑clawdis‑native) + +```text +Goal: Make this repo a nix‑clawdis‑native plugin with the standard contract. + +Contract to implement: +1) Add clawdisPlugin output in flake.nix: + - name + - skills (paths to SKILL.md dirs) + - packages (CLI packages to put on PATH) + - needs (stateDirs + requiredEnv) + +Example: +clawdisPlugin = { + name = "my-plugin"; + skills = [ ./skills/my-plugin ]; + packages = [ self.packages.${system}.default ]; + needs = { + stateDirs = [ ".config/my-plugin" ]; + requiredEnv = [ "MYPLUGIN_AUTH_FILE" ]; + }; +}; + +2) Make the CLI explicitly configurable by env (no magic defaults): + - Support an auth file env (e.g., MYPLUGIN_AUTH_FILE) + - Honor XDG_CONFIG_HOME or a plugin-specific config dir env + +3) Provide AGENTS.md in the plugin repo: + - Plain‑English explanation of knobs + values + - Generic placeholders only (no real secrets) + - Explain where credentials live (e.g., /run/agenix/…) + +4) Update SKILL.md to call the CLI by its PATH name. + +Standard plugin config shape (Nix‑native, no JSON strings): + +plugins = [ + { + source = "github:owner/my-plugin"; + config = { + env = { + MYPLUGIN_AUTH_FILE = "/run/agenix/myplugin-auth"; + }; + settings = { + name = "EXAMPLE_NAME"; + enabled = true; + retries = 3; + tags = [ "alpha" "beta" ]; + window = { start = "08:00"; end = "18:00"; }; + options = { mode = "fast"; level = 2; }; + }; + }; + } +]; + +Config flags the host will use: +- `config.env` for required env vars (e.g., MYPLUGIN_AUTH_FILE) +- `config.settings` for typed config keys (rendered to config.json in the first stateDir) + +CI note: +- If the repo uses Garnix, add the plugin build to its `garnix.yaml` (or equivalent) so CI verifies it. + +Why: explicit, minimal, fail‑fast, no inline JSON strings. +Deliverables: flake output, env overrides, AGENTS.md, skill update. +``` + +## How it wires up + +- Nix pulls the plugin, reads `clawdisPlugin`, and installs the CLI(s). +- Skills are symlinked into `~/.clawdis/skills//`. +- Clawdis loads managed skills automatically at runtime. +- Any plugin services run as **user‑level** launchd agents (no sudo). +- MVP scope: tools/skills should come **from plugins only** (no ad‑hoc installs). +- Plugin `settings` are rendered to `config.json` in the plugin’s first `stateDir`. + ## What you get - Launchd keeps the gateway alive (`com.steipete.clawdis.gateway`) @@ -102,33 +373,6 @@ Then: `home-manager switch --flake .#youruser` | Anthropic API key | | ✓ | | Chat IDs | | ✓ | -## Module options - -```nix -programs.clawdis = { - enable = true; - package = pkgs.clawdis; # or clawdis-gateway for minimal - stateDir = "~/.clawdis"; - workspaceDir = "~/.clawdis/workspace"; - - providers.telegram = { - enable = true; - botTokenFile = "/path/to/token"; - allowFrom = [ 12345678 -1001234567890 ]; # user IDs and group IDs - requireMention = false; # require @mention in groups - }; - - providers.anthropic = { - apiKeyFile = "/path/to/key"; - }; - - routing.queue.mode = "interrupt"; # or "queue" - routing.groupChat.requireMention = false; - - launchd.enable = true; -}; -``` - ## Packages | Package | Contents | @@ -137,7 +381,13 @@ programs.clawdis = { | `clawdis-gateway` | Gateway CLI only | | `clawdis-app` | macOS app only | -## Included tools +## Plugin collisions (override policy) + +Plugins are keyed by their declared `name`. If two plugins declare the same name, +the **last entry wins** (use this to override a prod plugin with a local dev one). +We should warn on collisions so it’s obvious. + +## Included tools (to add soon) **Core**: nodejs, pnpm, git, curl, jq, python3, ffmpeg, ripgrep @@ -170,6 +420,30 @@ home-manager switch --rollback # revert Wraps [Clawdis](https://github.com/steipete/clawdis) by Peter Steinberger. +## Philosophy + +The Zen of ~~Python~~ Clawdis, ~~by~~ shamelessly stolen from Tim Peters + +Beautiful is better than ugly. +Explicit is better than implicit. +Simple is better than complex. +Complex is better than complicated. +Flat is better than nested. +Sparse is better than dense. +Readability counts. +Special cases aren't special enough to break the rules. +Although practicality beats purity. +Errors should never pass silently. +Unless explicitly silenced. +In the face of ambiguity, refuse the temptation to guess. +There should be one-- and preferably only one --obvious way to do it. +Although that way may not be obvious at first unless you're Dutch. +Now is better than never. +Although never is often better than *right* now. +If the implementation is hard to explain, it's a bad idea. +If the implementation is easy to explain, it may be a good idea. +Namespaces are one honking great idea -- let's do more of those! + ## License -MIT +AGPL-3.0 diff --git a/docs/operator-reference.md b/docs/operator-reference.md deleted file mode 100644 index 1fd9d93..0000000 --- a/docs/operator-reference.md +++ /dev/null @@ -1,41 +0,0 @@ -# Operator Reference (minimal) - -This is the only reference doc. Everything else should be driven by the Agent‑First Guide. - -## Module options (Home Manager) - -- `programs.clawdis.enable` (bool, default: false) -- `programs.clawdis.package` (package, default: `pkgs.clawdis-gateway`) -- `programs.clawdis.stateDir` (string, default: `~/.clawdis`) -- `programs.clawdis.workspaceDir` (string, default: `~/.clawdis/workspace`) - -Telegram: -- `programs.clawdis.providers.telegram.enable` (bool, default: false) -- `programs.clawdis.providers.telegram.botTokenFile` (string path, required if enabled) -- `programs.clawdis.providers.telegram.allowFrom` (list of ints, required if enabled) -- `programs.clawdis.providers.telegram.requireMention` (bool, default: false) - -Routing: -- `programs.clawdis.routing.queue.mode` (enum: queue|interrupt, default: interrupt) -- `programs.clawdis.routing.queue.bySurface` (attrset, defaults to telegram=interrupt, discord/webchat=queue) -- `programs.clawdis.routing.groupChat.requireMention` (bool, default: false) - -macOS service: -- `programs.clawdis.launchd.enable` (bool, default: true) -- launchd label: `com.nix-clawdis.gateway` - -## Verification commands - -```bash -launchctl print gui/$UID/com.nix-clawdis.gateway | grep state -tail -n 50 ~/.clawdis/logs/clawdis-gateway.log -``` - -Smoke test: -- Send a Telegram message in an allowlisted chat; bot must reply. - -## Secrets wiring (recommended) - -- Use agenix or an equivalent secrets tool to place the bot token on disk. -- Configure `programs.clawdis.providers.telegram.botTokenFile` to point at that file. -- Do not inline tokens in Nix configs. diff --git a/examples/hello-world-plugin/AGENTS.md b/examples/hello-world-plugin/AGENTS.md new file mode 100644 index 0000000..ddc9467 --- /dev/null +++ b/examples/hello-world-plugin/AGENTS.md @@ -0,0 +1,7 @@ +# AGENTS.md — hello-world plugin + +This plugin is intentionally tiny. + +Knobs +- CLAWDIS_USER (optional): name to greet + diff --git a/examples/hello-world-plugin/flake.nix b/examples/hello-world-plugin/flake.nix new file mode 100644 index 0000000..41c2756 --- /dev/null +++ b/examples/hello-world-plugin/flake.nix @@ -0,0 +1,36 @@ +{ + description = "Hello-world Clawdis plugin"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = import nixpkgs { inherit system; }; + in { + packages.default = pkgs.buildGoModule { + pname = "hello-world"; + version = "0.1.0"; + src = ./.; + vendorHash = null; + }; + + apps.default = flake-utils.lib.mkApp { + drv = self.packages.${system}.default; + }; + + clawdisPlugin = { + name = "hello-world"; + skills = [ ./skills/hello-world ]; + packages = [ self.packages.${system}.default ]; + needs = { + stateDirs = []; + requiredEnv = []; + }; + }; + } + ); +} diff --git a/examples/hello-world-plugin/go.mod b/examples/hello-world-plugin/go.mod new file mode 100644 index 0000000..a3cb2e0 --- /dev/null +++ b/examples/hello-world-plugin/go.mod @@ -0,0 +1,3 @@ +module github.com/acme/hello-world-clawdis + +go 1.22 diff --git a/examples/hello-world-plugin/main.go b/examples/hello-world-plugin/main.go new file mode 100644 index 0000000..1774bd7 --- /dev/null +++ b/examples/hello-world-plugin/main.go @@ -0,0 +1,15 @@ +package main + +import ( + "fmt" + "os" +) + +func main() { + name := os.Getenv("CLAWDIS_USER") + if name == "" { + name = "human" + } + fmt.Printf("Hello, %s. I am a very serious assistant.\n", name) + fmt.Println("Fun fact: this plugin does one thing, and it does it with conviction.") +} diff --git a/examples/hello-world-plugin/skills/hello-world/SKILL.md b/examples/hello-world-plugin/skills/hello-world/SKILL.md new file mode 100644 index 0000000..bce000c --- /dev/null +++ b/examples/hello-world-plugin/skills/hello-world/SKILL.md @@ -0,0 +1,6 @@ +--- +name: hello-world +description: Prints a greeting with unnecessary confidence. +--- + +Use the `hello-world` CLI to greet the user. diff --git a/nix/modules/home-manager/clawdis.nix b/nix/modules/home-manager/clawdis.nix index 9c37ccd..7a94e4f 100644 --- a/nix/modules/home-manager/clawdis.nix +++ b/nix/modules/home-manager/clawdis.nix @@ -15,7 +15,7 @@ let enabled = true; tokenFile = inst.providers.telegram.botTokenFile; allowFrom = inst.providers.telegram.allowFrom; - requireMention = inst.providers.telegram.requireMention; + groups = inst.providers.telegram.groups; }; }; @@ -25,9 +25,6 @@ let mode = inst.routing.queue.mode; bySurface = inst.routing.queue.bySurface; }; - groupChat = { - requireMention = inst.routing.groupChat.requireMention; - }; }; }; @@ -79,6 +76,18 @@ let description = "Gateway port used by the Clawdis desktop app."; }; + gatewayPath = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Local path to Clawdis gateway source (dev only)."; + }; + + gatewayPnpmDepsHash = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = lib.fakeHash; + description = "pnpmDeps hash for local gateway builds (omit to let Nix suggest the correct hash)."; + }; + providers.telegram = { enable = lib.mkOption { type = lib.types.bool; @@ -98,13 +107,33 @@ let description = "Allowed Telegram chat IDs."; }; - requireMention = lib.mkOption { - type = lib.types.bool; - default = false; - description = "Require @mention in Telegram groups."; + + + groups = lib.mkOption { + type = lib.types.attrs; + default = {}; + description = "Per-group Telegram overrides (mirrors upstream telegram.groups config)."; }; }; + plugins = lib.mkOption { + type = lib.types.listOf (lib.types.submodule { + options = { + source = lib.mkOption { + type = lib.types.str; + description = "Plugin source pointer (e.g., github:owner/repo or path:/...)."; + }; + config = lib.mkOption { + type = lib.types.attrs; + default = {}; + description = "Plugin-specific configuration (env/files/etc)."; + }; + }; + }); + default = cfg.plugins; + description = "Plugins enabled for this instance."; + }; + providers.anthropic = { apiKeyFile = lib.mkOption { type = lib.types.str; @@ -131,11 +160,7 @@ let }; }; - routing.groupChat.requireMention = lib.mkOption { - type = lib.types.bool; - default = false; - description = "Require mention for group chat activation."; - }; + launchd.enable = lib.mkOption { type = lib.types.bool; @@ -185,7 +210,7 @@ let }; }; - legacyInstance = { + defaultInstance = { enable = cfg.enable; package = cfg.package; stateDir = cfg.stateDir; @@ -196,6 +221,7 @@ let providers = cfg.providers; routing = cfg.routing; launchd = cfg.launchd; + plugins = cfg.plugins; configOverrides = {}; appDefaults = { enable = true; @@ -211,11 +237,311 @@ let instances = if cfg.instances != {} then cfg.instances - else lib.optionalAttrs cfg.enable { default = legacyInstance; }; + else lib.optionalAttrs cfg.enable { default = defaultInstance; }; enabledInstances = lib.filterAttrs (_: inst: inst.enable) instances; + managedSkillsDir = "${homeDir}/.clawdis/skills"; + documentsEnabled = cfg.documents != null; + + resolvePath = p: + if lib.hasPrefix "~/" p then + "${homeDir}/${lib.removePrefix "~/" p}" + else + p; + + toRelative = p: + if lib.hasPrefix "${homeDir}/" p then + lib.removePrefix "${homeDir}/" p + else + p; + + instanceWorkspaceDirs = lib.mapAttrsToList (_: inst: resolvePath inst.workspaceDir) enabledInstances; + + documentsAssertions = lib.optionals documentsEnabled [ + { + assertion = builtins.pathExists cfg.documents; + message = "programs.clawdis.documents must point to an existing directory."; + } + { + assertion = builtins.pathExists (cfg.documents + "/AGENTS.md"); + message = "Missing AGENTS.md in programs.clawdis.documents."; + } + { + assertion = builtins.pathExists (cfg.documents + "/SOUL.md"); + message = "Missing SOUL.md in programs.clawdis.documents."; + } + { + assertion = builtins.pathExists (cfg.documents + "/TOOLS.md"); + message = "Missing TOOLS.md in programs.clawdis.documents."; + } + ]; + + documentsGuard = + lib.optionalString documentsEnabled ( + let + guardLine = file: '' + if [ -e "${file}" ] && [ ! -L "${file}" ]; then + echo "Clawdis documents are managed by Nix. Please adopt ${file} into your documents directory and re-run." >&2 + exit 1 + fi + ''; + guardForDir = dir: '' + ${guardLine "${dir}/AGENTS.md"} + ${guardLine "${dir}/SOUL.md"} + ${guardLine "${dir}/TOOLS.md"} + ''; + in + lib.concatStringsSep "\n" (map guardForDir instanceWorkspaceDirs) + ); + + toolsReport = + if documentsEnabled then + let + pluginLinesFor = instName: inst: + let + plugins = resolvedPluginsByInstance.${instName} or []; + render = p: "- " + p.name + " (" + p.source + ")"; + lines = if plugins == [] then [ "- (none)" ] else map render plugins; + in + [ + "" + "### Instance: ${instName}" + ] ++ lines; + reportLines = + [ + "" + "" + "## Nix-managed plugin report" + "" + "Plugins enabled per instance (last-wins on name collisions):" + ] + ++ lib.concatLists (lib.mapAttrsToList pluginLinesFor enabledInstances) + ++ [ + "" + "Tools: batteries-included toolchain + plugin-provided CLIs." + "" + "" + ]; + reportText = lib.concatStringsSep "\n" reportLines; + in + pkgs.writeText "clawdis-tools-report.md" reportText + else + null; + + toolsWithReport = + if documentsEnabled then + pkgs.runCommand "clawdis-tools-with-report.md" {} '' + cat ${cfg.documents + "/TOOLS.md"} > $out + echo "" >> $out + cat ${toolsReport} >> $out + '' + else + null; + + documentsFiles = + if documentsEnabled then + let + mkDocFiles = dir: { + "${toRelative (dir + "/AGENTS.md")}" = { + source = cfg.documents + "/AGENTS.md"; + }; + "${toRelative (dir + "/SOUL.md")}" = { + source = cfg.documents + "/SOUL.md"; + }; + "${toRelative (dir + "/TOOLS.md")}" = { + source = toolsWithReport; + }; + }; + in + lib.mkMerge (map mkDocFiles instanceWorkspaceDirs) + else + {}; + + resolvePlugin = plugin: let + flake = builtins.getFlake plugin.source; + clawdisPlugin = + if flake ? clawdisPlugin then flake.clawdisPlugin + else throw "clawdisPlugin missing in ${plugin.source}"; + needs = clawdisPlugin.needs or {}; + in { + source = plugin.source; + name = clawdisPlugin.name or (throw "clawdisPlugin.name missing in ${plugin.source}"); + skills = clawdisPlugin.skills or []; + packages = clawdisPlugin.packages or []; + needs = { + stateDirs = needs.stateDirs or []; + requiredEnv = needs.requiredEnv or []; + }; + config = plugin.config or {}; + }; + + resolvedPluginsByInstance = + lib.mapAttrs (instName: inst: + let + resolved = map resolvePlugin inst.plugins; + counts = lib.foldl' (acc: p: + acc // { "${p.name}" = (acc.${p.name} or 0) + 1; } + ) {} resolved; + duplicates = lib.attrNames (lib.filterAttrs (_: v: v > 1) counts); + byName = lib.foldl' (acc: p: acc // { "${p.name}" = p; }) {} resolved; + ordered = lib.attrValues byName; + in + if duplicates == [] + then ordered + else lib.warn + "programs.clawdis.instances.${instName}: duplicate plugin names detected (${lib.concatStringsSep ", " duplicates}); last entry wins." + ordered + ) enabledInstances; + + pluginPackagesFor = instName: + lib.flatten (map (p: p.packages) (resolvedPluginsByInstance.${instName} or [])); + + pluginStateDirsFor = instName: + let + dirs = lib.flatten (map (p: p.needs.stateDirs) (resolvedPluginsByInstance.${instName} or [])); + in + map (dir: resolvePath ("~/" + dir)) dirs; + + pluginEnvFor = instName: + let + entries = resolvedPluginsByInstance.${instName} or []; + toPairs = p: + let + env = (p.config.env or {}); + required = p.needs.requiredEnv; + in + map (k: { key = k; value = env.${k} or ""; plugin = p.name; }) required; + in + lib.flatten (map toPairs entries); + + pluginEnvAllFor = instName: + let + entries = resolvedPluginsByInstance.${instName} or []; + toPairs = p: + let env = (p.config.env or {}); + in map (k: { key = k; value = env.${k}; plugin = p.name; }) (lib.attrNames env); + in + lib.flatten (map toPairs entries); + + pluginAssertions = + lib.flatten (lib.mapAttrsToList (instName: inst: + let + plugins = resolvedPluginsByInstance.${instName} or []; + envFor = p: (p.config.env or {}); + missingFor = p: + lib.filter (req: !(envFor p ? req)) p.needs.requiredEnv; + configMissingStateDir = p: + (p.config.settings or {}) != {} && (p.needs.stateDirs or []) == []; + mkAssertion = p: + let missing = missingFor p; + in { + assertion = missing == []; + message = "programs.clawdis.instances.${instName}: plugin ${p.name} missing required env: ${lib.concatStringsSep \", \" missing}"; + }; + mkConfigAssertion = p: { + assertion = !(configMissingStateDir p); + message = "programs.clawdis.instances.${instName}: plugin ${p.name} provides settings but declares no stateDirs (needed for config.json)."; + }; + in + (map mkAssertion plugins) ++ (map mkConfigAssertion plugins) + ) enabledInstances); + + pluginSkillsFiles = + let + skillEntriesFor = p: + map (skillPath: { + name = ".clawdis/skills/${p.name}/${builtins.baseNameOf skillPath}"; + value = { source = skillPath; recursive = true; }; + }) p.skills; + allEntries = + lib.flatten (lib.concatLists (lib.mapAttrsToList (_: plugins: map skillEntriesFor plugins) resolvedPluginsByInstance)); + in + lib.listToAttrs (lib.flatten allEntries); + + pluginGuards = + let + renderCheck = entry: '' + if [ -z "${entry.value}" ]; then + echo "Missing env ${entry.key} for plugin ${entry.plugin} in instance ${entry.instance}." >&2 + exit 1 + fi + if [ ! -f "${entry.value}" ] || [ ! -s "${entry.value}" ]; then + echo "Required file for ${entry.key} not found or empty: ${entry.value} (plugin ${entry.plugin}, instance ${entry.instance})." >&2 + exit 1 + fi + ''; + entriesForInstance = instName: + map (entry: entry // { instance = instName; }) (pluginEnvFor instName); + entries = lib.flatten (map entriesForInstance (lib.attrNames enabledInstances)); + in + lib.concatStringsSep "\n" (map renderCheck entries); + + pluginConfigFiles = + let + entryFor = instName: inst: + let + plugins = resolvedPluginsByInstance.${instName} or []; + mkEntries = p: + let + cfg = p.config.settings or {}; + dir = + if (p.needs.stateDirs or []) == [] + then null + else lib.head (p.needs.stateDirs or []); + in + if cfg == {} then + [] + else + (if dir == null then + throw "plugin ${p.name} provides settings but no stateDirs are defined" + else [ + { + name = toRelative (resolvePath ("~/" + dir + "/config.json")); + value = { text = builtins.toJSON cfg; }; + } + ]); + in + lib.flatten (map mkEntries plugins); + entries = lib.flatten (lib.mapAttrsToList entryFor enabledInstances); + in + lib.listToAttrs entries; + + pluginSkillAssertions = + let + skillTargets = + lib.flatten (lib.concatLists (lib.mapAttrsToList (_: plugins: + map (p: + map (skillPath: + ".clawdis/skills/${p.name}/${builtins.baseNameOf skillPath}" + ) p.skills + ) plugins + ) resolvedPluginsByInstance)); + counts = lib.foldl' (acc: path: + acc // { "${path}" = (acc.${path} or 0) + 1; } + ) {} skillTargets; + duplicates = lib.attrNames (lib.filterAttrs (_: v: v > 1) counts); + in + if duplicates == [] then [] else [ + { + assertion = false; + message = "Duplicate skill paths detected: ${lib.concatStringsSep ", " duplicates}"; + } + ]; mkInstanceConfig = name: inst: let + gatewayPackage = + if inst.gatewayPath != null then + pkgs.callPackage ../../packages/clawdis-gateway.nix { + src = builtins.path { + path = inst.gatewayPath; + name = "clawdis-gateway-src"; + }; + pnpmDepsHash = inst.gatewayPnpmDepsHash; + } + else + inst.package; + pluginPackages = pluginPackagesFor name; + pluginEnvAll = pluginEnvAllFor name; baseConfig = mkBaseConfig inst.workspaceDir; mergedConfig = lib.recursiveUpdate (lib.recursiveUpdate baseConfig (lib.recursiveUpdate (mkTelegramConfig inst) (mkRoutingConfig inst))) @@ -224,6 +550,12 @@ let gatewayWrapper = pkgs.writeShellScriptBin "clawdis-gateway-${name}" '' set -euo pipefail + if [ "${toString (pluginPackages != [])}" = "true" ]; then + export PATH="${lib.makeBinPath pluginPackages}:$PATH" + fi + + ${lib.concatStringsSep "\n" (map (entry: "export ${entry.key}=\"${entry.value}\"") pluginEnvAll)} + if [ -n "${inst.providers.anthropic.apiKeyFile}" ]; then if [ ! -f "${inst.providers.anthropic.apiKeyFile}" ]; then echo "Anthropic API key file not found: ${inst.providers.anthropic.apiKeyFile}" >&2 @@ -237,7 +569,7 @@ let export ANTHROPIC_API_KEY fi - exec "${inst.package}/bin/clawdis" "$@" + exec "${gatewayPackage}/bin/clawdis" "$@" ''; in { homeFile = { @@ -284,7 +616,7 @@ let }; }; - package = inst.package; + package = gatewayPackage; }; instanceConfigs = lib.mapAttrsToList mkInstanceConfig enabledInstances; @@ -339,6 +671,30 @@ in { description = "Workspace directory for Clawdis agent skills."; }; + documents = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = "Path to a documents directory containing AGENTS.md, SOUL.md, and TOOLS.md."; + }; + + plugins = lib.mkOption { + type = lib.types.listOf (lib.types.submodule { + options = { + source = lib.mkOption { + type = lib.types.str; + description = "Plugin source pointer (e.g., github:owner/repo or path:/...)."; + }; + config = lib.mkOption { + type = lib.types.attrs; + default = {}; + description = "Plugin-specific configuration (env/files/etc)."; + }; + }; + }); + default = []; + description = "Plugins enabled for the default instance."; + }; + providers.telegram = { enable = lib.mkOption { type = lib.types.bool; @@ -358,11 +714,7 @@ in { description = "Allowed Telegram chat IDs."; }; - requireMention = lib.mkOption { - type = lib.types.bool; - default = false; - description = "Require @mention in Telegram groups."; - }; + }; providers.anthropic = { @@ -391,11 +743,6 @@ in { }; }; - routing.groupChat.requireMention = lib.mkOption { - type = lib.types.bool; - default = false; - description = "Require mention for group chat activation."; - }; launchd.enable = lib.mkOption { type = lib.types.bool; @@ -416,7 +763,7 @@ in { assertion = lib.length (lib.attrNames appDefaultsEnabled) <= 1; message = "Only one Clawdis instance may enable appDefaults."; } - ]; + ] ++ documentsAssertions ++ pluginAssertions ++ pluginSkillAssertions; home.packages = lib.unique (map (item: item.package) instanceConfigs); @@ -429,10 +776,26 @@ in { force = true; }; }) - // (lib.listToAttrs appInstalls); + // (lib.listToAttrs appInstalls) + // documentsFiles + // pluginSkillsFiles + // pluginConfigFiles; + + home.activation.clawdisDocumentGuard = lib.mkIf documentsEnabled ( + lib.hm.dag.entryBefore [ "writeBoundary" ] '' + set -euo pipefail + ${documentsGuard} + '' + ); home.activation.clawdisDirs = lib.hm.dag.entryAfter [ "writeBoundary" ] '' /bin/mkdir -p ${lib.concatStringsSep " " (lib.concatMap (item: item.dirs) instanceConfigs)} + /bin/mkdir -p ${lib.concatStringsSep " " (lib.flatten (map pluginStateDirsFor (lib.attrNames enabledInstances)))} + ''; + + home.activation.clawdisPluginGuard = lib.hm.dag.entryAfter [ "writeBoundary" ] '' + set -euo pipefail + ${pluginGuards} ''; home.activation.clawdisAppDefaults = lib.mkIf (pkgs.stdenv.hostPlatform.isDarwin && appDefaults != {}) ( diff --git a/nix/packages/clawdis-gateway.nix b/nix/packages/clawdis-gateway.nix index a64f8b1..e5319ba 100644 --- a/nix/packages/clawdis-gateway.nix +++ b/nix/packages/clawdis-gateway.nix @@ -8,17 +8,23 @@ , makeWrapper , vips , sourceInfo +, src ? null +, pnpmDepsHash ? null }: +assert src == null || pnpmDepsHash != null; + stdenv.mkDerivation (finalAttrs: { pname = "clawdis-gateway"; version = "2.0.0-beta4"; - src = fetchFromGitHub sourceInfo; + src = if src != null then src else fetchFromGitHub sourceInfo; pnpmDeps = pnpm_10.fetchDeps { inherit (finalAttrs) pname version src; - hash = "sha256-k5VvvHOlZc24M0aQF4nEux2k19s/XMD56lprlUD/XoI="; + hash = if pnpmDepsHash != null + then pnpmDepsHash + else "sha256-k5VvvHOlZc24M0aQF4nEux2k19s/XMD56lprlUD/XoI="; fetcherVersion = 2; }; diff --git a/nix/sources/clawdis-source.nix b/nix/sources/clawdis-source.nix index dbcc700..fd07808 100644 --- a/nix/sources/clawdis-source.nix +++ b/nix/sources/clawdis-source.nix @@ -2,6 +2,6 @@ { owner = "steipete"; repo = "clawdis"; - rev = "e5cae2a2e4676111d7bbf1cd1d9956e78ca9088a"; - hash = "sha256-wmuaYtJM5WF5/HnU3+f6Z6qMMLj+ph31ha431WtYvr4="; + rev = "v2.0.0-beta5"; + hash = "sha256-ZtlknzdrETVi84XKXgmPPwnb3CC+rXWAxZm2aOwDFAI="; } diff --git a/templates/agent-first/documents/AGENTS.md b/templates/agent-first/documents/AGENTS.md new file mode 100644 index 0000000..1428651 --- /dev/null +++ b/templates/agent-first/documents/AGENTS.md @@ -0,0 +1,9 @@ +# AGENTS.md — Clawdis Workspace + +This file is managed by Nix. Update it in the repo, not in the workspace. + +Principles +- Be concise in chat; write long output to files. +- Treat this workspace as the system of record. +- Prefer explicit, deterministic changes. + diff --git a/templates/agent-first/documents/SOUL.md b/templates/agent-first/documents/SOUL.md new file mode 100644 index 0000000..d42156e --- /dev/null +++ b/templates/agent-first/documents/SOUL.md @@ -0,0 +1,4 @@ +# SOUL.md + +Clawdis exists to do useful work reliably with minimal friction. + diff --git a/templates/agent-first/documents/TOOLS.md b/templates/agent-first/documents/TOOLS.md new file mode 100644 index 0000000..8ee3ba8 --- /dev/null +++ b/templates/agent-first/documents/TOOLS.md @@ -0,0 +1,4 @@ +# TOOLS.md + +This file is managed by Nix. A plugin report is appended below. + diff --git a/templates/agent-first/flake.nix b/templates/agent-first/flake.nix index 0796d93..59d7789 100644 --- a/templates/agent-first/flake.nix +++ b/templates/agent-first/flake.nix @@ -27,17 +27,30 @@ programs.home-manager.enable = true; programs.clawdis = { - enable = true; - providers.telegram = { + # REPLACE: path to your managed documents directory + documents = ./documents; + instances.default = { enable = true; - # REPLACE: path to your bot token file - botTokenFile = ""; - # REPLACE: your Telegram user ID (get from @userinfobot) - allowFrom = [ ]; - }; - providers.anthropic = { - # REPLACE: path to your Anthropic API key file - apiKeyFile = ""; + providers.telegram = { + enable = true; + # REPLACE: path to your bot token file + botTokenFile = ""; + # REPLACE: your Telegram user ID (get from @userinfobot) + allowFrom = [ ]; + # Group defaults (required in Nix mode): + groups = { + "*" = { requireMention = true; }; + }; + }; + providers.anthropic = { + # REPLACE: path to your Anthropic API key file + apiKeyFile = ""; + }; + + plugins = [ + # Example plugin without config: + { source = "github:acme/hello-world"; } + ]; }; }; }