commit 930d56644577f94bc9781087139c5aa15dce7514 Spenser Truex <truex@equwal.com> 2026-09-20 22:36:40 -0700 Merge pull request #1 from equwal/remove-irc-recover-pages Remove irc recover pages
MISSING-ASSETS.md | 14 +- .../4fb0f92f5666de0e698ac6739e5f6aea0e3ceccf.html | 82 +- .../75492a1107063b545c771844e77c34a852554350.html | 96 +- .../c9441131467ba02e70e8cc466bfd2b769d09d985.html | 80 +- .../cecddfa6396707ab94ada6098f3668145df39511.html | 148 +- .../f3d174752dda70690cf073701c79901755cfc192.html | 72 +- .../5f016019ae005479871c8196e23bca98742615f6.html | 98 + .../814be9a54e4062da4972978745e63da86572bde8.html | 2331 +++++++++ .../file/.claude-plugin/marketplace.json.html | 32 + git/ideamine/file/.claude-plugin/plugin.json.html | 23 + git/ideamine/file/.gitattributes.html | 11 + git/ideamine/file/.gitignore.html | 14 + git/ideamine/file/.mcp.json.html | 18 + git/ideamine/file/LICENSE.html | 31 + git/ideamine/file/README.md.html | 149 + git/ideamine/file/bin/ideamine.js.html | 188 + git/ideamine/file/hooks/hooks.json.html | 27 + git/ideamine/file/package.json.html | 53 + git/ideamine/file/skills/idea-go/SKILL.md.html | 24 + git/ideamine/file/skills/idea-triage/SKILL.md.html | 22 + git/ideamine/file/skills/idea/SKILL.md.html | 23 + git/ideamine/file/skills/ideas/SKILL.md.html | 21 + git/ideamine/file/src/claude.js.html | 149 + git/ideamine/file/src/hook.js.html | 107 + git/ideamine/file/src/mcp.js.html | 319 ++ git/ideamine/file/src/render.js.html | 145 + git/ideamine/file/src/rubric.js.html | 111 + git/ideamine/file/src/store.js.html | 345 ++ git/ideamine/file/src/text.js.html | 98 + .../file/tests/fixtures/fake-claude.js.html | 44 + git/ideamine/file/tests/hook.test.js.html | 88 + git/ideamine/file/tests/mcp.test.js.html | 126 + git/ideamine/file/tests/store.test.js.html | 95 + git/ideamine/file/tests/text.test.js.html | 45 + git/ideamine/file/tests/triage.test.js.html | 78 + git/ideamine/files.html | 11 + git/ideamine/index.html | 11 + git/ideamine/refs.html | 11 + git/index.html | 2 +- .../1566c4a665139cd9c4952964446851f405f283ef.html | 60 +- .../13bf349f9ef17b7a013de81b723019a2e1bbf42b.html | 26 + .../145656d00c49cac3f9106294054ffd3d028a5109.html | 39 + .../56e8f9215885140b69db44c3da3b402f1345f196.html | 1183 +++++ .../6e716f1111181ed24c3ed1a707d188d44e008fd4.html | 2079 ++++++++ .../9cc74dbb83d83f1247c742f4dafe99eeb56cfc8b.html | 4048 ++++++++++++++++ .../adfeebb2bca27fb504441538957ab4ff1712b9ff.html | 38 + .../d560e1b926b9e93b66f9f853f0cb0ef362e7c0db.html | 5049 ++++++++++++++++++++ .../dd9977bf8e1e12e4e0a4a1ffc67f1a09d5eb5c26.html | 41 + git/recentlywritten/file/.gitattributes.html | 12 + git/recentlywritten/file/.gitignore.html | 19 + git/recentlywritten/file/MISSING-ASSETS.md.html | 1155 +++++ git/recentlywritten/file/build.sh.html | 305 +- git/recentlywritten/file/deploy-git.sh.html | 27 + git/recentlywritten/file/git.html.html | 11 + ...bb6dcc701ef4714ddcd2e83f5aaaa7d34f13f.html.html | 47 + ...0c21d376f5032246677667f456b1ac43018b1.html.html | 30 + ...8a2e296e94912acb9ec834a3373fa1aa7ea38.html.html | 30 + ...842de191e56f2f0f5e14100f093846068aaca.html.html | 52 + ...4cab04918986b3453b7e8c95999cfcf68134f.html.html | 92 + ...ca0d81e38d24fe7cd0bc062a53c1ff4d6818c.html.html | 107 + ...0ec1dfe5de713b99378928b361edc89c59ada.html.html | 31 + ...dae44e341aeb4e3840914f35608c1e5c1c2de.html.html | 707 +++ ...bf7108272763fa16a318d8cf7bb8f53c727cc.html.html | 209 + ...a1297fd38f0ad8dfab76377739f1bc08d47dd.html.html | 39 + ...2dd6eeb0eb3a971ae7b02bb3c9abc9c073c51.html.html | 39 + .../file/git/CSV/file/.gitignore.html.html | 38 + .../file/git/CSV/file/LICENSE.html.html | 694 +++ .../file/git/CSV/file/README.md.html.html | 22 + .../file/git/CSV/file/csv.asd.html.html | 29 + .../file/git/CSV/file/csv.lisp.html.html | 90 + .../file/git/CSV/file/example.csv.html.html | 21 + .../file/git/CSV/file/package.lisp.html.html | 24 + .../file/git/CSV/file/utils.lisp.html.html | 40 + git/recentlywritten/file/git/CSV/files.html.html | 21 + git/recentlywritten/file/git/CSV/index.html.html | 21 + git/recentlywritten/file/git/CSV/refs.html.html | 21 + ...4a89d7d4038d12413b67b47aababbd69fbd55.html.html | 41 + ...0bcfa3576c3a8cc2670bc6c0b0c3834c5ec7c.html.html | 57 + ...4aaaedc5a22cb1a9b74ffc67cc90cad21f473.html.html | 57 + ...60a6217ad7bcc00fdf69e4d90b3592f8d96dd.html.html | 41 + ...411ca22768e51528942867fe6ea73351fe264.html.html | 57 + ...5cc9aa160b85bcb45106fdf2e01b10010d99f.html.html | 254 + ...6f89f505ccbc3867fb6a41f9044a8af98d66b.html.html | 186 + ...0cfbea38ef909abd2074af84b32bda8a9a0d8.html.html | 98 + ...c183ffde0cbbbc8e94116315e8bd61356868a.html.html | 40 + ...cc87c19b74a4d5b0c955e7f8c28c94043bb5e.html.html | 289 ++ ...f2b968cd774cde433fdacc1850d5baf20082f.html.html | 61 + ...eef29928431347cecb19eef84cbd840ea7ffd.html.html | 65 + ...275f7aaed925608d727cd9781825a89551b48.html.html | 95 + ...443d01202ac7b38a75f00b073cdbeb0f77821.html.html | 124 + ...6400ba980a182fcc44ee23182c8fa1d08b961.html.html | 120 + ...8ec84a64960c8904afa45c50a3d27f4e9de88.html.html | 52 + ...6a4347cb57a7131a771dd24f318fe7c3e01fb.html.html | 49 + ...6bcdce5d8b5d741230e8cd2be5de48bd44c1a.html.html | 66 + ...ee8daf51a5eed6f3c4ece81dda5d979af74f7.html.html | 122 + ...334a413d72bcdb32c0c1dbbdbbfe84e41d8ed.html.html | 339 ++ ...a7061144f21e1be9ad735051ac9240e100734.html.html | 57 + ...3d8e1d627d6fb53b0212961ddb3d6fe017290.html.html | 38 + ...f405991c4c20c1e5f4b3c3ddffe7690aafcff.html.html | 64 + ...5e9ab3f22c00225e6d185575fdcec2c49f7eb.html.html | 181 + .../file/git/Forthe/file/.gitignore.html.html | 24 + .../file/git/Forthe/file/README.org.html.html | 116 + .../file/git/Forthe/file/forthe.el.html.html | 70 + .../file/git/Forthe/files.html.html | 21 + .../file/git/Forthe/index.html.html | 21 + git/recentlywritten/file/git/Forthe/refs.html.html | 21 + ...908646560a1cc22f73b7ff4424ecc4d8123cd.html.html | 41 + ...f6fc16a0c919cf53a3885f12cf06c3fe9ce65.html.html | 39 + ...ba1ab4351e6bfb4e371c4ac45266fd7e81a36.html.html | 39 + ...bf18ae048458f5b8c2f782823eccf2c4e428e.html.html | 65 + ...7b40586302af16f2b3f5cbbab95e637c07527.html.html | 37 + ...96b982b0de78768560524e8a97e0bf3b2642c.html.html | 40 + ...fd81a3e8a4fa7d4f04d984b7f3b5990743e26.html.html | 46 + ...e0f12a2cf796572494e7f0911696ef20ebb48.html.html | 99 + ...dca0129237967fe8a5d3343e90c01e8970499.html.html | 76 + ...e43f81c69b0fee234fe2133e6e378734ea269.html.html | 30 + ...1e41ee1948be15dcf947a85cc468b8c271ced.html.html | 197 + ...9fc2f581e6c4da61a402533fa16638da01448.html.html | 31 + ...d6237ff12a1cf1c8986040350a8d773caad18.html.html | 31 + ...390165df32ced4f0b18d3e99000fd70d99252.html.html | 38 + ...0281bd34d528dbdbb01e4d35559966150a964.html.html | 29 + ...0efc736a164ded0905c41baded5b2e6609e4c.html.html | 32 + ...5ab4988298d2b59d6cc356819a77c40fd870e.html.html | 41 + ...e3b13434a9049041fb8240b1588195ef0c025.html.html | 42 + ...239c52c9c5bd5fe200865798310b44811eb16.html.html | 61 + ...bce052b85220d10217e0934dc20e4fae15e1d.html.html | 112 + ...3df4bba8ff92dd8fd35de708cd7cf3f92c896.html.html | 36 + ...1de8e4513e6f867d24b46a9c95c89ef6c64f0.html.html | 164 + ...1a6bfae6c44af203194562aecd01ec4262437.html.html | 29 + ...740d71087904ca6c6b27d41051d22bf7fd859.html.html | 39 + ...f558115d2efca76bb29704e9198201edb3498.html.html | 65 + ...0f92f5666de0e698ac6739e5f6aea0e3ceccf.html.html | 51 + ...85d7bee8db521c4d55d99c18e7ce62ac6fd11.html.html | 42 + ...7b845168bc921396aa5f88435e09710fb924b.html.html | 45 + ...740dbc13f98e5012b57f6f9110d061a87ffd3.html.html | 240 + ...8995e6ddbdfc94444f6b76b1631b7cf7b3754.html.html | 40 + ...7a2bcf31e3ea28d09bd6533e92655f5f73d81.html.html | 39 + ...946db11383d2179f6f19171c7674e5a6a668c.html.html | 133 + ...695d85fc7fd858c19ee5a1ba334b0fa69344b.html.html | 30 + ...e9c86d22e9f84464835228f827a2446c03e93.html.html | 47 + ...3fc8e352fe33bbac130150d9b579a58b9988a.html.html | 97 + ...687c3ea2bf2d795e7743ad85bc0a88e21a58b.html.html | 99 + ...02567815cf2b6b68531e7df321515103c9d44.html.html | 55 + ...b1d4592084d8ab8447b658c7ed74ec55e05b3.html.html | 39 + ...dff2c1a6e7ed6923c687249c01887bfe224fa.html.html | 44 + ...5c2b01cdc1e156fc09d836d355d26b5ca579c.html.html | 40 + ...92a1107063b545c771844e77c34a852554350.html.html | 58 + ...83887c8ab9f0c14f8d4325a33274f10fff0b6.html.html | 31 + ...5a1c2f040033a8497cc09b31ce7982b102e7a.html.html | 110 + ...30084567c2d9d7bf53a7f9f60031dbc973bd2.html.html | 40 + ...dbe168a03e080a718afc37985150486b6d017.html.html | 72 + ...9cb012d9bac29766bffa28acb2ffc50861577.html.html | 77 + ...2e4719850f4b5597f23f965e7d33c1094420a.html.html | 184 + ...e12b652c9c559b66504dbaf4e82f44632daaf.html.html | 42 + ...22444e2a44d641e3f88c283c986e3d29ede4d.html.html | 49 + ...c8bca7724ecd65287612b74ef3c9c0eb84533.html.html | 40 + ...dd8c171bf76d6a660d132754822267519da33.html.html | 104 + ...40e4f3372be731b7dbbc6ee2dd4d001c4b625.html.html | 75 + ...ba3c5dbcada75f28fe2b3f019be3965661da5.html.html | 103 + ...cb1d37043e202b2b5d95948112ff38c6fa8bd.html.html | 44 + ...683377cb009b53c4d4dc7ef147659c99b0b1c.html.html | 45 + ...289065cca5a480468c6b34fa24a793a1df2ca.html.html | 61 + ...b57802ef6d2c4497543d7737b595758646eb6.html.html | 35 + ...1c8bcf872f0d7bd918964ec50dfc5de8f9511.html.html | 45 + ...b6f1430d4d267bd4995d5de1a6e288cbb0830.html.html | 39 + ...406b2e62d5107320992b60f1146728c39d6ab.html.html | 39 + ...cf255edadfabbac14a971596db6962a1c627b.html.html | 30 + ...7f4988d4ff6359c72bb6698c6758bd242c89c.html.html | 29 + ...42893e3a9aebfcd1507996e316526b3f270f7.html.html | 66 + ...e80cda38383f81ca2f9e437000ab39953fc13.html.html | 51 + ...5914fd12d652c5eca4f9f149e4e8d1b344864.html.html | 37 + ...8f059c149293fa07f1b19292a429f33fda9fa.html.html | 29 + ...46cfccd9215d2a0467e0c5342709f3aaae33f.html.html | 47 + ...1daa26dd795a2d10af64a7ee62afe5eec1236.html.html | 80 + ...4dfe85ad1b28c467888600ae5ebc5bcedeb25.html.html | 29 + ...7f999762522eae7528b8783d9b4abf36f179f.html.html | 29 + ...0ece5bb684f0bb90bf4eccfdda2fe6eb1065f.html.html | 40 + ...bbb5f1d75f135a65e72fc014223ab89762f34.html.html | 44 + ...b7cec39a5fe5314e27bf8e08d9c9d897341b3.html.html | 78 + ...61105ae0f20655ed463a22377418cdf742767.html.html | 113 + ...46532b754ff5bc325313b04199f9db5b1febf.html.html | 29 + ...14bf030dc9e309ca2c75c26c2f321a05086e7.html.html | 55 + ...4737c6ae29a8113b753f88177e732c922480e.html.html | 39 + ...41131467ba02e70e8cc466bfd2b769d09d985.html.html | 50 + ...d70c0939ee1e7ddcbc5451d69a01464813150.html.html | 40 + ...97a1dd549f87a5adbad0355f01bc50fddbf26.html.html | 37 + ...673be44e580091958d2c1fc7d894efc13d787.html.html | 88 + ...ddfa6396707ab94ada6098f3668145df39511.html.html | 84 + ...6ab6ea2011481016de2faae0bd5bda3ee88ac.html.html | 40 + ...ab753a2da0af67302a613ff7fa66f14e1b716.html.html | 39 + ...66062236f2a84ce31150831811b2c8fedbd13.html.html | 55 + ...521443b7ef0473d34f98752b3b236b94fd924.html.html | 40 + ...b3200e654c0a0aeb12b805e087074d2a959af.html.html | 29 + ...f52419551b828aa212021cf6ee6d11d880b94.html.html | 41 + ...5ca7704ca8770bc77ecce19f427a1cda0fec2.html.html | 40 + ...ba8a19d38f319e99030cccd2b52f7096e5e04.html.html | 50 + ...d1edeebe84c559b89ba6a362b7bf4b888f1f0.html.html | 40 + ...6ff11008deb5596c8c69e265ba7ca7fac9230.html.html | 45 + ...174752dda70690cf073701c79901755cfc192.html.html | 46 + ...75c84649e33d908b68ff62673dd65b2039d59.html.html | 39 + ...17b178152d88fb003b98968049dce2a58d7c4.html.html | 283 ++ ...cff18567c34ae52d79e4a6f2ebbc03defc861.html.html | 49 + ...5d9a6d7a8cc8bea7606e01233a4aa88b77f56.html.html | 42 + ...1bcef016ee01b9b40e39c2efec2007a8b66fb.html.html | 42 + ...2d3eba18d928cc2cce56d206c3ac1da172972.html.html | 40 + ...c54eb417e51b1733e02c4405aa557ab8a5e6a.html.html | 40 + .../file/git/LispBrain/file/.gitignore.html.html | 23 + .../file/git/LispBrain/file/LICENSE.html.html | 27 + .../file/git/LispBrain/file/README.md.html.html | 155 + .../git/LispBrain/file/code/brain.asd.html.html | 40 + .../LispBrain/file/code/interpreter.lisp.html.html | 244 + .../LispBrain/file/code/packages.lisp.html.html | 51 + .../git/LispBrain/file/code/utils.lisp.html.html | 53 + .../file/git/LispBrain/files.html.html | 21 + .../file/git/LispBrain/index.html.html | 21 + .../file/git/LispBrain/refs.html.html | 21 + ...ea2e69186cbd8d0371d92d30bf851adcd071a.html.html | 46 + ...ab91b92198a27b3b4a5a5ceffe7ca57a44009.html.html | 228 + ...104747a15c44356bf80d09b5b8e090641b108.html.html | 167 + ...52fb12d50b9662a2211a64d10c375845c1055.html.html | 37 + ...7da977eec04f4c9dab41074d8b26a18235564.html.html | 187 + ...3bfec4756cae6d46fec7fe72af6b6c003f586.html.html | 38 + ...ee8d7f5073caf378b80dc7353732e842da50e.html.html | 39 + ...8a1a559c108865ce86c4b5ed3f19ae500631a.html.html | 32 + ...cc124441d057ea6790de5816b6758d520e636.html.html | 38 + ...ae396c437b5651468ac95f730a02ff8f80c65.html.html | 38 + ...0ebf66a38bde08af9550d694226d1e8159454.html.html | 35 + ...4fd421f09de6867b4472a24794b034c878906.html.html | 46 + ...e0cc428f235cedd2afc35c19bdfe49a8cce1d.html.html | 40 + ...0727f84b7bcc281a58d075b3891102651f650.html.html | 38 + ...c00df51879fb9cb5232fce44e89c2d75c3fa1.html.html | 49 + ...01e236ee7fdc4c52ea057776c7679267bcbdc.html.html | 42 + ...ba9dc6a10cf11ff2085a95248b847f48bb1b9.html.html | 44 + .../file/git/alt-hack0/file/.gitignore.html.html | 21 + .../file/git/alt-hack0/file/README.md.html.html | 42 + .../git/alt-hack0/file/generator.ros.html.html | 178 + .../file/git/alt-hack0/file/glyphs.html.html | 21 + .../file/git/alt-hack0/file/hack.html.html | 21 + .../file/git/alt-hack0/file/source.html.html | 21 + .../file/git/alt-hack0/file/tt-hinting.html.html | 21 + .../file/git/alt-hack0/files.html.html | 21 + .../file/git/alt-hack0/index.html.html | 21 + .../file/git/alt-hack0/refs.html.html | 21 + ...7ebbaf4a8ab737a160e23c7e6406479f14738.html.html | 41 + ...015f48708922827272e81a7dccffdac249308.html.html | 130 + ...962991152919804a9c3a0b083bc3f2419370b.html.html | 41 + ...e5a01782a502412204fa0553da7c2f1a7d98e.html.html | 39 + ...943dfb83934848009948b42eaaa3df70005f4.html.html | 90 + ...074218498e15bead7b7bbbf9d94fcff12e761.html.html | 40 + .../asdf-registration/file/README.org.html.html | 42 + .../file/asdf-registration.asd.html.html | 28 + .../file/asdf-registration.lisp.html.html | 30 + .../asdf-registration/file/initfile.lisp.html.html | 30 + .../asdf-registration/file/install.sh.html.html | 35 + .../asdf-registration/file/package.lisp.html.html | 24 + .../file/git/asdf-registration/files.html.html | 21 + .../file/git/asdf-registration/index.html.html | 21 + .../file/git/asdf-registration/refs.html.html | 21 + ...6a0b09ec2c5412bb13b2df1d25b1d08d3e856.html.html | 132 + ...ea7f6c2894ae370964aff70f3a1549fe5e87b.html.html | 97 + ...877fdd0a7f514c1f56313142e25299f713cf3.html.html | 89 + ...7f40a54eaecb486be7e257d99065c6e15b516.html.html | 56 + ...e6953704a0ac5d8a0e9c871aa08050422e5c5.html.html | 149 + .../file/git/blaher/file/README.md.html.html | 55 + .../file/git/blaher/file/blah.lisp.html.html | 67 + .../file/git/blaher/files.html.html | 21 + .../file/git/blaher/index.html.html | 21 + git/recentlywritten/file/git/blaher/refs.html.html | 21 + ...ada82f339f61de56f426824c1c3c1a88bf9b1.html.html | 175 + ...ead38e438f920e8d0a841e7522ff8b54d1a58.html.html | 618 +++ ...ff6f3b85fa92ac00b5404a71ba08b9b2568d3.html.html | 438 ++ ...8f39713dfcadd7d1ba413e6ebd47430d92cf2.html.html | 1140 +++++ ...d807bb0a6deabd6c24d0797efb4d2e8b78bb6.html.html | 41 + ...437d4dc56d22165a7023a3767e0026d199784.html.html | 429 ++ ...b1a03cf6a8f8aa14c515b74938bddc7b33fbe.html.html | 69 + ...2b15a09fb5f63c3a59f69e87d9b279b6eba2d.html.html | 50 + ...1443cc603fc89d582b24d1c13fd49b1c89ea7.html.html | 79 + ...a39ff59ab40b71219168da182be4ea9aca12f.html.html | 65 + ...df3f28a508269546f0ce394cdc3f21254199f.html.html | 438 ++ ...12b6e84c471e0b76cdb6ea4f5df420365eb36.html.html | 45 + ...1d71528f9fe834fe23cde1000034cd1f92e8d.html.html | 44 + ...46e0deeb1d5fbd7ef5911a23d48529fbfd074.html.html | 121 + ...b84ab78cb64423e2781a3e520bbd1e9cb40cd.html.html | 277 ++ ...be730f3ec1a2869290b5c8072edd665f295d1.html.html | 91 + ...86b79e894959621e9c618a18974c2f34fa5d5.html.html | 225 + ...6417916771c10ed235899b94e333866d11042.html.html | 39 + ...6c036c8e4304b22492eaa231e68b509a5bedf.html.html | 171 + ...82ad800216c04f8f575ccb0e0a099a7897535.html.html | 70 + ...83f1ce1ad0e39f746364c88ce45fda78e4630.html.html | 284 ++ ...93e93f76c5274805158f8b513493f6aa39d04.html.html | 134 + ...46ce064617e43c80f9c41ceef007843bea20d.html.html | 60 + ...3e6ee5c94e72efdf49a1ebf6490529ee00bd8.html.html | 42 + ...431f30c1c9a6c14bd4c838908d10efa5a9121.html.html | 123 + ...30b3245704582341dfbb51a85d555182b3d9f.html.html | 64 + ...16326c51ea827b8049ab873ea266756ba5653.html.html | 73 + ...b7c35c6213148261bb9f7d44efa0e51e4f160.html.html | 74 + ...6d24c105c4b71ab70b7d3f7aa7025034d1974.html.html | 252 + ...8622d74a70911bb975204306929b11559be07.html.html | 40 + ...228a3803cf285d220aedb5caa92b731375b62.html.html | 63 + ...4f5a4e82381039878f615749ad4de40f29c30.html.html | 221 + ...8a06766051afce42b6eeec8f182ccb36b87f3.html.html | 50 + ...4296da286dd15e183886cd4110e05a6049078.html.html | 92 + ...4dea43a91267d621f6ed78fcea25ed5c67243.html.html | 41 + ...d9a828bdd422d4c4ec8fbd30ba8f5ef253e76.html.html | 340 ++ ...ae0469c01b746c84590685e649873d3e65728.html.html | 751 +++ ...a1c9632b5d0b6b0f21fb330dc5b19bcc6b244.html.html | 41 + ...b061bb6f819e9e5a3bd028c5683e3b6227815.html.html | 47 + ...abb608d7259d1c4acdd5be13a711fe4b49160.html.html | 60 + ...7098cb11f735b256fa522cbec30495000749a.html.html | 61 + ...f6f665dc9ad801d57ffb7716d9fda78661bd7.html.html | 48 + ...22b40904e9e0037a38117b45f9323de45ff70.html.html | 41 + ...f5e747aa837657993f852439c99063a416cf6.html.html | 669 +++ ...8c13e573503fb6d019a46e9dfc52bb54e63b6.html.html | 38 + ...af068a53ee155aa4850c0beac0ecfeb51b59d.html.html | 78 + ...fe4f9da795b24680eba4e8c940e042b1d6e11.html.html | 215 + ...29a763d4d7f92c4ad27db73d676009afb0654.html.html | 51 + ...0da9492c2971cdeaebdbdc9fad9367e4d1e7f.html.html | 42 + ...b5bf244f382c1bda11d4f05d1b5a0fcc77d43.html.html | 59 + ...8de9add9b35be4694980db5826a91e286cf7b.html.html | 52 + ...692a3001141f9e9350b888565c51b92851e10.html.html | 53 + ...2e99d94388a41c1fc5f0e0af15ee8e68e7e6e.html.html | 41 + ...be73f502660ada5aeb21bd6eaffec14f8fef3.html.html | 41 + ...f2224eb625f71b68cb00a77edc37068f0f059.html.html | 99 + ...6b3331dbd17c1be1c8cfcb255d62202cf8b2a.html.html | 226 + ...04d3bebc920469566de468423e5e8b8284932.html.html | 68 + ...6ddb2979cd75bf9a4b7501830209de9e248d5.html.html | 107 + ...d2d0347fa40a4598ddee005faeb1fd0f7438f.html.html | 563 +++ ...18524bb0bf1cb850a252249a52e86efa421bb.html.html | 41 + ...1b504887429c23aed643787dc55d10f1d501d.html.html | 53 + ...fa314e9911295a3c52318d4702e4761093e2f.html.html | 91 + ...c963e39455b4f75af129ca00d635a02fc1702.html.html | 244 + ...b013bac0ad806bc90e487b552d3affa7f7413.html.html | 53 + ...7516c1e1125ada8b96260e7681bcf5d1b21b7.html.html | 51 + ...cc28e578b4ce432d7e81dab33b60eb118ccaf.html.html | 316 ++ ...92c61d9ff8d0a7e7fd65c33b0fd26c9741e68.html.html | 44 + ...0687d3956613d10973d651dd4154337a34880.html.html | 178 + ...4af1d082f947760dff8f1bb65f614c5516b58.html.html | 30 + ...6ca9e3a4f11ebbd196c972f74612dfbff6b26.html.html | 81 + ...345cdbd29f5e9c0a91ca3666e4ff93590aba6.html.html | 124 + ...13b65aae06b43fa193df726c14f2afa2d1794.html.html | 57 + ...cb403a8bdc1efd8e38450aaaec4192d287674.html.html | 54 + ...6b1e0980f2284552f3bc13c7653b1db35e5a8.html.html | 39 + ...2eed76a9fcb1e083e428c159834cb18c936cd.html.html | 65 + ...6c1f37a5427cf4173b0385c3c31e84619aa8c.html.html | 76 + ...70d2472d54844ce90cf4e2e68eb816f8d8bab.html.html | 69 + ...37d30940cbeef0a60808bd835835a11ef4beb.html.html | 92 + ...df0f848a3779e778330fe558bd60abcd832a7.html.html | 129 + ...c55ff7e26410c8c8d7f06f68e1c0a246bcdd1.html.html | 46 + ...b405e3258de8b4331abf84ec791d40bda805d.html.html | 153 + ...7e771f1bad099c9f9ac8d98085aefa8998af9.html.html | 134 + ...4f830fa5c7fa15180caa3f3bd58ed8c4c9dfc.html.html | 69 + ...3198dd927d89f6eb08566bd88693aca39e435.html.html | 44 + ...b727d69131de4b7b2662825bc130e2bf0020d.html.html | 76 + ...c8662dd38b3c46ad2eeaa82c5fbde192c4515.html.html | 57 + ...1ee60dcbbe37b43d7cb6fb7a87bdfc847d990.html.html | 92 + ...8131035ae98f04288c319848f23acb16c35cd.html.html | 89 + ...082e674c3fa47cca6bd199631e561e0415375.html.html | 345 ++ ...4e943bcf03b62ddee3273b075dc25dbe69904.html.html | 41 + ...ae44a217cb53b2b38ebbbbe53c65779383802.html.html | 44 + ...6103e1a32e6e2b5b3891275218c0e7400bc0a.html.html | 750 +++ ...fe5f96516ad07e66b118d01fee022baa43d56.html.html | 49 + ...94f295129ed69babc3a11e99d0364458dc95b.html.html | 40 + ...d8912469f5ce1e210ded72b2f52f49d307352.html.html | 54 + ...37db849f0b058408e668af45d3f74bb656779.html.html | 27 + .../file/git/cl-yag/file/LICENSE.html.html | 33 + .../file/git/cl-yag/file/Makefile.html.html | 58 + .../file/git/cl-yag/file/README.md.html.html | 347 ++ .../file/git/cl-yag/file/cl-yag.asd.html.html | 37 + .../file/git/cl-yag/file/data/1.md.html.html | 23 + .../file/git/cl-yag/file/data/README.md.html.html | 309 ++ .../git/cl-yag/file/data/articles.lisp.html.html | 96 + .../file/git/cl-yag/file/data/css.md.html.html | 143 + .../file/git/cl-yag/file/data/t.org.html.html | 25 + .../cl-yag/file/generator-aux-pre.lisp.html.html | 94 + .../git/cl-yag/file/generator-aux.lisp.html.html | 165 + .../file/git/cl-yag/file/generator.lisp.html.html | 23 + .../git/cl-yag/file/generators-util.lisp.html.html | 66 + .../cl-yag/file/generators/gemini.lisp.html.html | 96 + .../cl-yag/file/generators/gopher.lisp.html.html | 119 + .../git/cl-yag/file/generators/html.lisp.html.html | 157 + .../file/git/cl-yag/file/packages.lisp.html.html | 23 + .../git/cl-yag/file/static/css/clym.css.html.html | 234 + .../cl-yag/file/static/css/custom.css.html.html | 36 + .../static/css/pure_r1.0.0/LICENSE.md.html.html | 72 + .../file/static/css/pure_r1.0.0/pure.css.html.html | 1528 ++++++ .../git/cl-yag/file/static/css/style.css.html.html | 37 + .../cl-yag/file/templates/article.tpl.html.html | 31 + .../file/templates/gemini_head.tpl.html.html | 26 + .../file/templates/gopher_head.tpl.html.html | 29 + .../git/cl-yag/file/templates/layout.tpl.html.html | 55 + .../file/templates/layout.tpl_original.html.html | 54 + .../cl-yag/file/templates/one-tag.tpl.html.html | 23 + .../cl-yag/file/templates/rss-item.tpl.html.html | 31 + .../git/cl-yag/file/templates/rss.tpl.html.html | 30 + .../file/git/cl-yag/files.html.html | 21 + .../file/git/cl-yag/index.html.html | 21 + git/recentlywritten/file/git/cl-yag/refs.html.html | 21 + ...032395e10b0b07ecdf40d7c7d6989bc342b14.html.html | 106 + ...1b24e8d4a7d8060b8affd59a3ab688c4a5e7f.html.html | 39 + ...8a0b923c6cec1976ee42a265b355c63fbf697.html.html | 124 + ...be54b9a6985b98b776fa1a36655dc8ce6220e.html.html | 57 + ...88f6019d18e2dcc8048dd9420bceae4583166.html.html | 52 + ...c13a5e3d6b0c1602765dd0927cd4764a9c098.html.html | 121 + ...18c00657169aa03a6084a6b11d6c06c584263.html.html | 180 + ...a5c4a2b3b74eac046c5ce0263d39e748418c4.html.html | 79 + ...151acf12781a5830cdb8eaeb46ea6d5010216.html.html | 35 + ...e13b1831b6b9fe5185054c06307129af3463a.html.html | 452 ++ ...e4453246832e1fa5927857ac41717456a4e2b.html.html | 193 + ...01ccfd33a9ff1c9832109a016d83cced97ef8.html.html | 200 + ...4441bc5d8f29d5388aba6dbdd6a36cca6d180.html.html | 41 + ...704df3b8ff36bd1e1a1910c955471b32ddc2f.html.html | 159 + ...96c467d7e7915ef3595b38baa23806abda530.html.html | 81 + ...f58983720b389f271d56c2785aebb8a979e74.html.html | 101 + ...dd0bec578b1fa62a1c9c7cf17fc4b02a769ea.html.html | 254 + ...e98a6f138b3e31fbb14e03d67191e485d0f6a.html.html | 43 + ...fcfc0b2f8c40d036158cc10faa4a4d8e8fb29.html.html | 31 + ...1cf1eb8a8a7f9edf9c70720b5cbf267d94f43.html.html | 31 + .../file/git/clapt/file/.gitattributes.html.html | 22 + .../file/git/clapt/file/README.org.html.html | 83 + .../file/git/clapt/file/TODO.org.html.html | 37 + .../file/git/clapt/file/clapt.asd.html.html | 33 + .../file/git/clapt/file/code/add.lisp.html.html | 50 + .../git/clapt/file/code/conditions.lisp.html.html | 26 + .../git/clapt/file/code/defaults.lisp.html.html | 27 + .../git/clapt/file/code/install.lisp.html.html | 141 + .../file/git/clapt/file/code/save.lisp.html.html | 55 + .../file/git/clapt/file/code/util.lisp.html.html | 27 + .../file/git/clapt/file/package.lisp.html.html | 24 + .../file/git/clapt/file/tests/cp.lisp.html.html | 23 + git/recentlywritten/file/git/clapt/files.html.html | 21 + git/recentlywritten/file/git/clapt/index.html.html | 21 + git/recentlywritten/file/git/clapt/refs.html.html | 21 + ...da5b5bc6564db46dde5a1a2e2f5e71a75321d.html.html | 70 + ...ea88d00b8330bdbde6e6fca0b926821d9f1f1.html.html | 109 + ...5a2a34e4dcd4f26e89de3dc0dd81032eeda10.html.html | 220 + ...a33517b1df4a4b15882240d18b7955c25e5ea.html.html | 45 + ...2da9d52dd55f3de944489c2314728f99099ee.html.html | 271 ++ ...bb9a2db7f522d83e612c35b24a19d5b673517.html.html | 49 + ...94d72260ef81d5053b8708dd06ebc65c209b8.html.html | 103 + ...eeef4e3b769c19b558f9218d618b59383a54d.html.html | 94 + ...b9dfd4a861e92479c21cda6fd228f9185d300.html.html | 54 + ...48dc9ab2895d7bcf8b3bcfcf76eb48ecc075f.html.html | 77 + ...c86d0df968a36cd567bb54c6cdb923523ca0f.html.html | 41 + ...87c3c561873fda304042df02ed8c68575ce20.html.html | 465 ++ ...184b296767f9f65ca7d63490d9718bf8ab06a.html.html | 54 + ...0eee532a51e2d82d31e7b8e791c34b1cc7cde.html.html | 63 + ...21d7d6a66be5fd4f18a8acc515387e5bb1564.html.html | 44 + ...a83010562a967a3b62029b4321184ce346254.html.html | 134 + git/recentlywritten/file/index.html.html | 43 - git/recentlywritten/file/posts/lisprant.md.html | 23 - git/recentlywritten/file/site/index.html.html | 43 - git/recentlywritten/file/site/lisprant.html.html | 60 - git/recentlywritten/file/site/style.css.html | 209 - git/recentlywritten/file/style.css.html | 209 - git/recentlywritten/files.html | 2 +- git/recentlywritten/index.html | 2 +- git/recentlywritten/refs.html | 2 +- .../031f781dfa1e657a9c9eb6677d78de4d82c455b1.html | 78 + .../0573022ce4ceab92394c35c5701abe061f2ec35a.html | 55 + .../0a187a5d95a708254d99a2cdec6bb54b732481a1.html | 83 + .../0a5b9f60e25411cf2fbd6688e8be5a7326f2b28d.html | 31 + .../0cb0ce0bb84aca55dd9d66d22fd6550ee0a58ada.html | 38 + .../0e5be464cdcab2d17d9f9ab5cf75015bd18c1632.html | 70 + .../0fce134e1baa2ae47aeb0ee7c091e6c25651bcea.html | 56 + .../100d8ae55fc598a2c898bd6b5d33e15f131cee0c.html | 36 + .../10ed3e55246b65f39aa19f3c63bb0241c62d4f6c.html | 41 + .../12b8b8a338d1791ce2d200f7cad99c9f0b5804d1.html | 52 + .../17674fcc4257afa53e81fdeab49b21f0f57528c7.html | 64 + .../1bd2e5661b9c3646a8ddf2862eadf766666f52c6.html | 30 + .../1e11d7ef08cfe5deb5e6adb2845fffd48d5f64d9.html | 42 + .../1f9509a7c5ab03ab79d64212113bd83f194dcbcb.html | 41 + .../20a51bafd3906aa0337fe221d0981293d94370a5.html | 51 + .../21d46d603a3697f04072e4c0edb0076b4a215a2e.html | 31 + .../239ecedf18b5fdb0650a1b5468d2b75de2a38731.html | 33 + .../250ea3842c49c0f1b8224a607e5a4d7fd5dd7ae3.html | 39 + .../26bc04284d9becc870a6a036a9a6f9bdfba8ba2a.html | 30 + .../2cc14415ea4515db3271d8af0cc8b36dc62b3f71.html | 31 + .../314af1528acc78f62256043cb8a9c81f8f7833b8.html | 31 + .../34fd4f17068b408353bb6ae1df3eb4a58c259e25.html | 56 + .../356726d76e8d082d6702401bbd6bffea47a84b1f.html | 46 + .../392dcb24732c88f500266b923bafdda8eb631220.html | 203 + .../3a53a319d4ade0631accac3fd9177e6fbfc49c7f.html | 88 + .../4003310026b26f9c81b44fb07686e7301064c4bf.html | 31 + .../49292644fd4a8c983928738fe36ac9daaebc755d.html | 35 + .../4aa1ecfaf6c97cb02c7e34c88cc67c070b600b67.html | 31 + .../4fe03eaa05536aac1704b92c0267fb099125a3a3.html | 45 + .../55a9e8d63e4a4ab5188eacbee3ef48f0170f7230.html | 52 + .../572915c3d29428773577739e54647c4dfd0daa5d.html | 34 + .../575c12bd0606717a2e7312669decc9a8197e41bd.html | 98 + .../5a5611b3293eaac0b26832a54b845c6822736b65.html | 597 +++ .../5b8bd6016a9bc52b643e55335b46de1aaeae2940.html | 34 + .../5b9bcbd0dcf3cf2c4edb995621de245ce6b80a9d.html | 78 + .../5c2c788d7cda17dfadca2d12738792be1b140387.html | 71 + .../6272b9a65812c9f653d693941b3317fe2f98a028.html | 198 + .../62d746cb6d34ed43068386bcd4a75f828055ee5e.html | 47 + .../64be22d4516d31613a4a2c4a4a746c7076b1bfab.html | 58 + .../6582965f7a534eb82339208980582fcb661fe7c3.html | 80 + .../68934acac9cff7b2b6e1ee42cb2dd912341c0f5c.html | 43 + .../69ee789f16787da6a3065d911a44fc7d4991e366.html | 42 + .../69f77f767e52e21b1a40182d03180da73f2f9021.html | 65 + .../6b0f1c92c58fcff0de4724a1c005264e86794244.html | 84 + .../6b520eb8b70b98a311a13a62606567095af28290.html | 55 + .../7ab95c658cf1dd546ec39603851742db65233204.html | 41 + .../7bd48fa9930dca90e24e17b78b451b014ae287e8.html | 43 + .../7d3b71e31c73e122b62fec68a7196586f1b93082.html | 117 + .../7e0793d8395184b5b21ecdd0b13bccb524d1e414.html | 141 + .../7f2e86d875364e7828b2af55721e784ca0de5298.html | 34 + .../80db85ac77a36eb3f591ea7f38816b6339607821.html | 185 + .../827820f5580b8880939337772d85e59ae87721b6.html | 38 + .../864e9c534956afb2a429174e10cc9b4d6196fa05.html | 35 + .../88d7f160cb44cdb08634b034b23d2f2d640bf7fd.html | 59 + .../89d3a0bd06d2345ffa98675e356a17be24a61210.html | 191 + .../8b2a2eaf79da098717fe9a7d87fca9fa1f6c6579.html | 53 + .../8c49538793ad14ace0230ada9f4fb5f0c951ba32.html | 35 + .../904cd0f37ae4028597cb85cb08190e998dfac62b.html | 31 + .../90a9d3c81f27588c62faa43f22d946740042a3ce.html | 1708 +++++++ .../92394502830c460c78633f56a69da4ce5f01d5fa.html | 298 ++ .../95c60192368eb92fd706aff387aa99df29c8f216.html | 35 + .../9dec9b200c1bddd6d12f4aa6b43e2c329f513235.html | 316 ++ .../a58414c67ddf05ff48b8a86fb2d3cf5e58cea4b9.html | 35 + .../aa35a9899f7ce8e67a4801ff131d0cf96f7dc37c.html | 31 + .../aeb6afa41ddf42eeb865079c7d218a2cbdfe2ef3.html | 45 + .../b15d084107da05069a3cf70cd81475f26d8e563e.html | 37 + .../b2b6ca98bae9bac2b36cb4a13f66adaf876743aa.html | 28 + .../b35d9b876df14d24dcd3e6fc98222b75955d0f6b.html | 78 + .../b4f6ed2bf2344c5717523313bc666eb297a6fe36.html | 63 + .../b567fedfe014dc5c795b5bc8703bc187efd13e27.html | 32 + .../c0a79c0424a99180ed4c79e3335dc3f7ced2322c.html | 39 + .../c29025f3cc818b8e0f43d41d9a7d4b3220503cfa.html | 30 + .../c2e060c906af4fd970cb2b9ee2e3c05c83db1582.html | 194 + .../c3638488ce9b1d5d23e33f292710a35d14a6cc72.html | 67 + .../c416c8c73d0a33eb8c428b1a9b9eaaffc098ee5b.html | 37 + .../c7ab5157ebc7e7d502e0970770a7e7be48e61ea2.html | 36 + .../cb05caeda420012e03188d8f2cde8b80a166aeb1.html | 52 + .../cc6a6881e06ab4b851c7778842374b953e51539c.html | 51 + .../cd386e712b7587740115c0fb0815a43052758ae2.html | 69 + .../cd9b3f4f082f0581942db3a1ecdc2bf520416ad9.html | 30 + .../cef484cbcf0ba83757f853f235a6cc7497448fa2.html | 60 + .../cf707964fe31a4987713b4ae89eea96e63640c6a.html | 99 + .../d16bb53db353da7b817b18809adf57b57c4f973e.html | 964 ++++ .../d70616c357a135b8e251fe1974cf88f529dbc67f.html | 31 + .../d9488c74f50b137dc91851437ffa4cbaf64c1d00.html | 346 ++ .../dba0b8d682e77ac33fbdc6f75dee9ee9f08553f9.html | 80 + .../df1d4c0a7b17bd56c27d3805c8316aab21d1c19d.html | 31 + .../e01ac82824ccd60b4a77498f8e49a443296fcba2.html | 31 + .../e3535c1405eac56a658886023b057944b4b91bc0.html | 212 + .../e6daee96e9c73d29f6e51e8400d66a3511d36526.html | 278 ++ .../e80d8bfc3241d197f387a7cbe40ec924853bdc83.html | 85 + .../e8930420e7c31a3cea0484314bd38aba5197fa39.html | 156 + .../eae2a995544b2b48c3a7debcb186a00aa0c400b3.html | 39 + .../ed632c2c8f6b6c64acda2883a04a2739ebf3cf0f.html | 31 + .../edab539b23594219bbfc83729822da917a18a243.html | 56 + .../edb683070a46f17242618569dacff02b6075115e.html | 44 + .../edbf8409a1e281d17422e7b12b5edd3832b4429d.html | 31 + .../f1432f5a5c56bad44485d9dee8fe92e6d09e1f4c.html | 37 + .../f2b48c669ee5b69e3b46f64979739d15f41338c2.html | 46 + .../fe6bd5b52afc219737bd22f94ec3d679ac624de3.html | 64 + git/sacc/file/LICENSE.html | 33 + git/sacc/file/Makefile.html | 65 + git/sacc/file/common.h.html | 54 + git/sacc/file/config.def.h.html | 71 + git/sacc/file/config.mk.html | 50 + git/sacc/file/io.h.html | 33 + git/sacc/file/io_clr.c.html | 80 + git/sacc/file/io_tls.c.html | 342 ++ git/sacc/file/sacc.1.html | 153 + git/sacc/file/sacc.c.html | 1149 +++++ git/sacc/file/ui_rogue.c.html | 1429 ++++++ git/sacc/file/ui_rogue_notes.html | 41 + git/sacc/file/ui_rogue_readme.html | 39 + git/sacc/file/ui_ti.c.html | 590 +++ git/sacc/file/ui_txt.c.html | 328 ++ git/sacc/files.html | 11 + git/sacc/index.html | 11 + git/sacc/refs.html | 11 + git/sbm/refs.html | 2 +- pages/call.md | 4 +- pages/cervido.md | 53 + posts/IRC-Bouncer.md | 31 - posts/IRC-bouncer-announced.md | 11 - site/IRC-Bouncer.html | 68 - site/IRC-bouncer-announced.html | 39 - site/call.html | 2 - site/cervido.html | 115 + site/index.html | 3 +- site/rss.xml | 64 +- 586 files changed, 71604 insertions(+), 1122 deletions(-)
diff --git a/MISSING-ASSETS.md b/MISSING-ASSETS.md index d1e2343..ebd2f65 100755 --- a/MISSING-ASSETS.md +++ b/MISSING-ASSETS.md @@ -13,18 +13,17 @@ edits to any post. truncated at exactly 1 MiB and are unreadable, so they were deleted rather than served as downloads that fail halfway. -## Pages never archived (6) +## Pages never archived (5) No copy of these survives online. Re-create each as `pages/<name>.md`. - arabic-poem.html -- cervido.html - deksesop2.html - idoj-de-orfisto.html - intervjuoj-de-aejk.html - noveloj-de-la-ondo.html -## Attachments referenced by the posts and pages (124) +## Attachments referenced by the posts and pages (123) - 25-noveloj/25-noveloj.epub — used by 25-noveloj.html - 25-noveloj/25-noveloj.mobi — used by 25-noveloj.html @@ -32,6 +31,10 @@ No copy of these survives online. Re-create each as `pages/<name>.md`. - 25-noveloj/25-noveloj.txt — used by 25-noveloj.html - 25-noveloj/25-noveloj.zip — used by 25-noveloj.html - book/bible-as-literature-bates.pdf — used by biblio.html +- book/budhismaj-rakontoj-16-malalta-kvalito.mp3 — used by cervido.html +- book/budhismaj-rakontoj-16-malalta-kvalito.ogg — used by cervido.html +- book/budhismaj-rakontoj-16.mp3 — used by cervido.html +- book/budhismaj-rakontoj-16.ogg — used by cervido.html - book/rut-lit.epub — used by rut.html - book/rut-lit.mobi — used by rut.html - book/rut-lit.pdf — used by rut.html @@ -69,8 +72,6 @@ No copy of these survives online. Re-create each as `pages/<name>.md`. - static/book/null-hyp.pdf — used by books.html - static/book/unabomber.pdf — used by books.html - static/book/zhuagzi.pdf — used by books.html -- static/channel.png — used by IRC-Bouncer.html -- static/channels.png — used by IRC-Bouncer.html - static/createfiltereddeck.png — used by Learn-Kanji-in-a-Fortnight.html - static/dbc — used by dmenu-scripts-repo-released.html - static/dbrowsel — used by dmenu-scripts-repo-released.html @@ -99,7 +100,6 @@ No copy of these survives online. Re-create each as `pages/<name>.md`. - static/git-update-worker.sh.webp — used by A-new-way-to-release-code.html - static/glider-catb.png — used by glider.html - static/glosbe-bird.svg — used by esperantaj-vortaroj.html -- static/hexchat.png — used by IRC-Bouncer.html - static/hilda-dresen.jpg — used by norda-naturo.html - static/ircchat.webp — used by iilchat-setup-too-easy.html - static/johansson.jpg — used by sten-johansson.html @@ -123,7 +123,6 @@ No copy of these survives online. Re-create each as `pages/<name>.md`. - static/language-podcast/BUYNOW.opus — used by New-podcast-about-polyglot-language-learning-LISTEN-NOW.html - static/language-podcast/How to Use the Internet Library Only.opus — used by New-podcast-about-polyglot-language-learning-LISTEN-NOW.html - static/language-podcast/Quixotic Self-Awareness.opus — used by New-podcast-about-polyglot-language-learning-LISTEN-NOW.html -- static/networks.png — used by IRC-Bouncer.html - static/norda-naturo/norda-naturo.epub — used by norda-naturo.html - static/norda-naturo/norda-naturo.mobi — used by norda-naturo.html - static/norda-naturo/norda-naturo.txt — used by norda-naturo.html @@ -145,7 +144,6 @@ No copy of these survives online. Re-create each as `pages/<name>.md`. - static/unnazitheworld.jpg — used by Unanglo-your-literacy-materials-and-your-life.html - static/vortaro.png — used by esperantaj-vortaroj.html - static/y-or-n — used by dmenu-scripts-repo-released.html -- static/yoursettings.png — used by IRC-Bouncer.html - sten-johansson/sten-johansson.epub — used by sten-johansson.html - sten-johansson/sten-johansson.mobi — used by sten-johansson.html - sten-johansson/sten-johansson.pdf — used by sten-johansson.html diff --git a/git/LispBrain/commit/4fb0f92f5666de0e698ac6739e5f6aea0e3ceccf.html b/git/LispBrain/commit/4fb0f92f5666de0e698ac6739e5f6aea0e3ceccf.html index d887301..7a72673 100644 --- a/git/LispBrain/commit/4fb0f92f5666de0e698ac6739e5f6aea0e3ceccf.html +++ b/git/LispBrain/commit/4fb0f92f5666de0e698ac6739e5f6aea0e3ceccf.html @@ -1,41 +1,41 @@ -<!DOCTYPE html> -<html lang="en"><head><meta charset="UTF-8" /> -<meta name="viewport" content="width=device-width, initial-scale=1" /> -<title>LispBrain 4fb0f92f - Recently Written</title> -<link rel="stylesheet" href="../../stagit.css" /> -</head><body> -<div id="top"><a href="../../../index.html">Recently Written</a> · <a href="../../index.html">git</a></div> -<h1>LispBrain</h1><span class="desc">The best Brainfuck debugger available, if you know how the Common Lisp debugger works. Uses Common Lisp's ease of debugging to make debugging Brainfuck code easy. Almost entirely useless.</span><p class="url">git clone https://github.com/equwal/LispBrain</p><p><a href="../../LispBrain/index.html">Log</a> | <a href="../../LispBrain/files.html">Files</a> | <a href="../../LispBrain/refs.html">Refs</a></p><hr/> -<div id="content"> -<pre>commit 4fb0f92f5666de0e698ac6739e5f6aea0e3ceccf -Spenser Truex <struex0@gmail.com> -2016-02-15 19:30:46 -0800 - -added space - -</pre><pre> README.md | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) -</pre><pre>diff --git a/README.md b/README.md -index f86df1a..e3c5246 100644 ---- a/README.md -+++ b/README.md -<span class="h">@@ -44,7 +44,7 @@ If everything runs smoothly you will be ready to brainfuck. If not then please *</span> - - - - !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ - ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ" - ``` - ``` -<span class="d">->#f.+[.+] ;Note that this comment is not part of the brainfuck because of the space between it and the brainfuck code.</span> -<span class="a">+> #f.+[.+] ;Note that this comment is not part of the brainfuck because of the space between it and the brainfuck code.</span> - "� - - - - - - - !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ - ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ"</pre> -</div></body></html> +<!DOCTYPE html> +<html lang="en"><head><meta charset="UTF-8" /> +<meta name="viewport" content="width=device-width, initial-scale=1" /> +<title>LispBrain 4fb0f92f - Recently Written</title> +<link rel="stylesheet" href="../../stagit.css" /> +</head><body> +<div id="top"><a href="../../../index.html">Recently Written</a> · <a href="../../index.html">git</a></div> +<h1>LispBrain</h1><span class="desc">The best Brainfuck debugger available, if you know how the Common Lisp debugger works. Uses Common Lisp's ease of debugging to make debugging Brainfuck code easy. Almost entirely useless.</span><p class="url">git clone https://github.com/equwal/LispBrain</p><p><a href="../../LispBrain/index.html">Log</a> | <a href="../../LispBrain/files.html">Files</a> | <a href="../../LispBrain/refs.html">Refs</a></p><hr/> +<div id="content"> +<pre>commit 4fb0f92f5666de0e698ac6739e5f6aea0e3ceccf +Spenser Truex <struex0@gmail.com> +2016-02-15 19:30:46 -0800 + +added space + +</pre><pre> README.md | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) +</pre><pre>diff --git a/README.md b/README.md +index f86df1a..e3c5246 100644 +--- a/README.md ++++ b/README.md +<span class="h">@@ -44,7 +44,7 @@ If everything runs smoothly you will be ready to brainfuck. If not then please *</span> + + + + !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ + ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ" + ``` + ``` +<span class="d">->#f.+[.+] ;Note that this comment is not part of the brainfuck because of the space between it and the brainfuck code.</span> +<span class="a">+> #f.+[.+] ;Note that this comment is not part of the brainfuck because of the space between it and the brainfuck code.</span> + "� + + + + + + + !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ + ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ"</pre> +</div></body></html> diff --git a/git/LispBrain/commit/75492a1107063b545c771844e77c34a852554350.html b/git/LispBrain/commit/75492a1107063b545c771844e77c34a852554350.html index 7201323..370822c 100644 --- a/git/LispBrain/commit/75492a1107063b545c771844e77c34a852554350.html +++ b/git/LispBrain/commit/75492a1107063b545c771844e77c34a852554350.html @@ -1,48 +1,48 @@ -<!DOCTYPE html> -<html lang="en"><head><meta charset="UTF-8" /> -<meta name="viewport" content="width=device-width, initial-scale=1" /> -<title>LispBrain 75492a11 - Recently Written</title> -<link rel="stylesheet" href="../../stagit.css" /> -</head><body> -<div id="top"><a href="../../../index.html">Recently Written</a> · <a href="../../index.html">git</a></div> -<h1>LispBrain</h1><span class="desc">The best Brainfuck debugger available, if you know how the Common Lisp debugger works. Uses Common Lisp's ease of debugging to make debugging Brainfuck code easy. Almost entirely useless.</span><p class="url">git clone https://github.com/equwal/LispBrain</p><p><a href="../../LispBrain/index.html">Log</a> | <a href="../../LispBrain/files.html">Files</a> | <a href="../../LispBrain/refs.html">Refs</a></p><hr/> -<div id="content"> -<pre>commit 75492a1107063b545c771844e77c34a852554350 -Spenser Truex <spensertruexonline@gmail.com> -2016-02-15 21:16:10 -0800 - -More pictures -</pre><pre> README.md | 2 ++ - 1 file changed, 2 insertions(+) -</pre><pre>diff --git a/README.md b/README.md -index ee16e8b..12dbd57 100644 ---- a/README.md -+++ b/README.md -<span class="h">@@ -44,12 +44,14 @@ If everything runs smoothly you will be ready to Brainfuck. If there are issues</span> - - - - - - - !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ - ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ" - ``` -<span class="a">+[](https://raw.githubusercontent.com/equwal/LispFuck/master/brain-fuck-notation.png)</span> - ``` - > #f.+[.+] [<This is not inside the Brainfuck code, nor is + or -.>] - "� - - - - - - - !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ - ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ" - ``` -<span class="a">+[](https://raw.githubusercontent.com/equwal/LispFuck/master/pound-f-notation.png)</span> - - #Debugging Brainfuck - </pre> -</div></body></html> +<!DOCTYPE html> +<html lang="en"><head><meta charset="UTF-8" /> +<meta name="viewport" content="width=device-width, initial-scale=1" /> +<title>LispBrain 75492a11 - Recently Written</title> +<link rel="stylesheet" href="../../stagit.css" /> +</head><body> +<div id="top"><a href="../../../index.html">Recently Written</a> · <a href="../../index.html">git</a></div> +<h1>LispBrain</h1><span class="desc">The best Brainfuck debugger available, if you know how the Common Lisp debugger works. Uses Common Lisp's ease of debugging to make debugging Brainfuck code easy. Almost entirely useless.</span><p class="url">git clone https://github.com/equwal/LispBrain</p><p><a href="../../LispBrain/index.html">Log</a> | <a href="../../LispBrain/files.html">Files</a> | <a href="../../LispBrain/refs.html">Refs</a></p><hr/> +<div id="content"> +<pre>commit 75492a1107063b545c771844e77c34a852554350 +Spenser Truex <spensertruexonline@gmail.com> +2016-02-15 21:16:10 -0800 + +More pictures +</pre><pre> README.md | 2 ++ + 1 file changed, 2 insertions(+) +</pre><pre>diff --git a/README.md b/README.md +index ee16e8b..12dbd57 100644 +--- a/README.md ++++ b/README.md +<span class="h">@@ -44,12 +44,14 @@ If everything runs smoothly you will be ready to Brainfuck. If there are issues</span> + + + + + + + !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ + ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ" + ``` +<span class="a">+[](https://raw.githubusercontent.com/equwal/LispFuck/master/brain-fuck-notation.png)</span> + ``` + > #f.+[.+] [<This is not inside the Brainfuck code, nor is + or -.>] + "� + + + + + + + !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ + ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ" + ``` +<span class="a">+[](https://raw.githubusercontent.com/equwal/LispFuck/master/pound-f-notation.png)</span> + + #Debugging Brainfuck + </pre> +</div></body></html> diff --git a/git/LispBrain/commit/c9441131467ba02e70e8cc466bfd2b769d09d985.html b/git/LispBrain/commit/c9441131467ba02e70e8cc466bfd2b769d09d985.html index a08c201..5de0ecb 100644 --- a/git/LispBrain/commit/c9441131467ba02e70e8cc466bfd2b769d09d985.html +++ b/git/LispBrain/commit/c9441131467ba02e70e8cc466bfd2b769d09d985.html @@ -1,40 +1,40 @@ -<!DOCTYPE html> -<html lang="en"><head><meta charset="UTF-8" /> -<meta name="viewport" content="width=device-width, initial-scale=1" /> -<title>LispBrain c9441131 - Recently Written</title> -<link rel="stylesheet" href="../../stagit.css" /> -</head><body> -<div id="top"><a href="../../../index.html">Recently Written</a> · <a href="../../index.html">git</a></div> -<h1>LispBrain</h1><span class="desc">The best Brainfuck debugger available, if you know how the Common Lisp debugger works. Uses Common Lisp's ease of debugging to make debugging Brainfuck code easy. Almost entirely useless.</span><p class="url">git clone https://github.com/equwal/LispBrain</p><p><a href="../../LispBrain/index.html">Log</a> | <a href="../../LispBrain/files.html">Files</a> | <a href="../../LispBrain/refs.html">Refs</a></p><hr/> -<div id="content"> -<pre>commit c9441131467ba02e70e8cc466bfd2b769d09d985 -Spenser Truex <struex0@gmail.com> -2016-02-15 19:29:29 -0800 - -Readability block - -</pre><pre> README.md | 3 ++- - 1 file changed, 2 insertions(+), 1 deletion(-) -</pre><pre>diff --git a/README.md b/README.md -index f805791..f86df1a 100644 ---- a/README.md -+++ b/README.md -<span class="h">@@ -42,7 +42,8 @@ If everything runs smoothly you will be ready to brainfuck. If not then please *</span> - "� - - - - - - - !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ - ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ" -<span class="d">-</span> -<span class="a">+```</span> -<span class="a">+```</span> - >#f.+[.+] ;Note that this comment is not part of the brainfuck because of the space between it and the brainfuck code. - "� - - -</pre> -</div></body></html> +<!DOCTYPE html> +<html lang="en"><head><meta charset="UTF-8" /> +<meta name="viewport" content="width=device-width, initial-scale=1" /> +<title>LispBrain c9441131 - Recently Written</title> +<link rel="stylesheet" href="../../stagit.css" /> +</head><body> +<div id="top"><a href="../../../index.html">Recently Written</a> · <a href="../../index.html">git</a></div> +<h1>LispBrain</h1><span class="desc">The best Brainfuck debugger available, if you know how the Common Lisp debugger works. Uses Common Lisp's ease of debugging to make debugging Brainfuck code easy. Almost entirely useless.</span><p class="url">git clone https://github.com/equwal/LispBrain</p><p><a href="../../LispBrain/index.html">Log</a> | <a href="../../LispBrain/files.html">Files</a> | <a href="../../LispBrain/refs.html">Refs</a></p><hr/> +<div id="content"> +<pre>commit c9441131467ba02e70e8cc466bfd2b769d09d985 +Spenser Truex <struex0@gmail.com> +2016-02-15 19:29:29 -0800 + +Readability block + +</pre><pre> README.md | 3 ++- + 1 file changed, 2 insertions(+), 1 deletion(-) +</pre><pre>diff --git a/README.md b/README.md +index f805791..f86df1a 100644 +--- a/README.md ++++ b/README.md +<span class="h">@@ -42,7 +42,8 @@ If everything runs smoothly you will be ready to brainfuck. If not then please *</span> + "� + + + + + + + !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ + ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ" +<span class="d">-</span> +<span class="a">+```</span> +<span class="a">+```</span> + >#f.+[.+] ;Note that this comment is not part of the brainfuck because of the space between it and the brainfuck code. + "� + + +</pre> +</div></body></html> diff --git a/git/LispBrain/commit/cecddfa6396707ab94ada6098f3668145df39511.html b/git/LispBrain/commit/cecddfa6396707ab94ada6098f3668145df39511.html index d19d039..57f2216 100644 --- a/git/LispBrain/commit/cecddfa6396707ab94ada6098f3668145df39511.html +++ b/git/LispBrain/commit/cecddfa6396707ab94ada6098f3668145df39511.html @@ -1,74 +1,74 @@ -<!DOCTYPE html> -<html lang="en"><head><meta charset="UTF-8" /> -<meta name="viewport" content="width=device-width, initial-scale=1" /> -<title>LispBrain cecddfa6 - Recently Written</title> -<link rel="stylesheet" href="../../stagit.css" /> -</head><body> -<div id="top"><a href="../../../index.html">Recently Written</a> · <a href="../../index.html">git</a></div> -<h1>LispBrain</h1><span class="desc">The best Brainfuck debugger available, if you know how the Common Lisp debugger works. Uses Common Lisp's ease of debugging to make debugging Brainfuck code easy. Almost entirely useless.</span><p class="url">git clone https://github.com/equwal/LispBrain</p><p><a href="../../LispBrain/index.html">Log</a> | <a href="../../LispBrain/files.html">Files</a> | <a href="../../LispBrain/refs.html">Refs</a></p><hr/> -<div id="content"> -<pre>commit cecddfa6396707ab94ada6098f3668145df39511 -Spenser Truex <spensertruexonline@gmail.com> -2016-03-03 12:13:46 -0800 - -clarification -</pre><pre> README.md | 12 ++++++------ - 1 file changed, 6 insertions(+), 6 deletions(-) -</pre><pre>diff --git a/README.md b/README.md -index dfba44d..2a253d3 100644 ---- a/README.md -+++ b/README.md -<span class="h">@@ -26,7 +26,7 @@ These can be combined into a string such as the following "Hello World!" program</span> - #How to install: - - Make sure you have a Common Lisp implementation installed. I recommend [Steel Bank Common Lisp](http://www.sbcl.org/). - - [ASDF](https://common-lisp.net/project/asdf/) must be installed. Many Lisps come with it installed (including [SBCL](http://www.sbcl.org/)). -<span class="d">-- Install this code into your ASDF load directory. The default on linux is `~/common-lisp/`:</span> -<span class="a">+- Install this code into your ASDF *load directory*. The default on linux is usually `~/common-lisp/`:</span> - ``` - me@linux:~$ mkdir common-lisp - me@linux:~$ cd common-lisp -<span class="h">@@ -45,18 +45,18 @@ If you are unable to find where the ASDF load directory is, you may choose to lo</span> - > (asdf:load-system :brain) - ``` - -<span class="d">-In Allegro common lisp one must first use `(require :asdf)` before executing any other commands in order to activate the ASDF system.</span> -<span class="a">+In Allegro common lisp one must first use `(require :asdf)` before executing any other commands in order to activate the preinstalled ASDF system.</span> - #How to Use: -<span class="d">-If everything runs smoothly you will be ready to Brainfuck. If there are issues then please *let it be known*. Now one must chooses between the `brain:fuck` and the `#F` notation when using the REPL. The `#F` notation is more concise but does not allow any whitespace in the Brainfuck code, while the `brain:fuck` notation allows any character except for an unescaped literal quote `"` inside of the Brainfuck code. Below they are both shown:</span> -<span class="a">+If everything runs smoothly you will be ready to Brainfuck. If there are issues then please *let it be known*. Now one must chooses between the `brain:fuck` and the `#F` notation when using the REPL. The `#F` notation is more concise but does not allow any whitespace in the Brainfuck code, while the `brain:fuck` notation allows any character except for an unescaped literal quote `"`, or an unescaped literal backward slash `\` inside of the Brainfuck code. Below they are both shown:</span> - ``` -<span class="d">-> (brain:fuck ".+[.+] Please escape your \" characters!")</span> -<span class="a">+> (brain:fuck ".+[.+] Please escape your \" and \\ characters!")</span> - "� - - - - - - - !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ - ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ" - ``` - [](https://raw.githubusercontent.com/equwal/LispFuck/master/brain-fuck-notation.png) - ``` -<span class="d">-> #f.+[.+] [<This is not inside the Brainfuck code, nor is + or -.>]</span> -<span class="a">+> #f.+[.+] <This is not inside the Brainfuck code, nor is + or -.>[]</span> - "� - - - - - - - !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ - ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ" -<span class="h">@@ -111,7 +111,7 @@ Suppose you want to find information about the execution of `incf-byte` and `dec</span> - ``` - - #Bugs/TODO: -<span class="d">-- None right now, feedback is encouraged! My email is spensertruexonline@gmail.com</span> -<span class="a">+- Include instructions for users on Windows.</span> - - #Congratulations! - You have reached the end of this README. Please sumbit any feedback to me via email.</pre> -</div></body></html> +<!DOCTYPE html> +<html lang="en"><head><meta charset="UTF-8" /> +<meta name="viewport" content="width=device-width, initial-scale=1" /> +<title>LispBrain cecddfa6 - Recently Written</title> +<link rel="stylesheet" href="../../stagit.css" /> +</head><body> +<div id="top"><a href="../../../index.html">Recently Written</a> · <a href="../../index.html">git</a></div> +<h1>LispBrain</h1><span class="desc">The best Brainfuck debugger available, if you know how the Common Lisp debugger works. Uses Common Lisp's ease of debugging to make debugging Brainfuck code easy. Almost entirely useless.</span><p class="url">git clone https://github.com/equwal/LispBrain</p><p><a href="../../LispBrain/index.html">Log</a> | <a href="../../LispBrain/files.html">Files</a> | <a href="../../LispBrain/refs.html">Refs</a></p><hr/> +<div id="content"> +<pre>commit cecddfa6396707ab94ada6098f3668145df39511 +Spenser Truex <spensertruexonline@gmail.com> +2016-03-03 12:13:46 -0800 + +clarification +</pre><pre> README.md | 12 ++++++------ + 1 file changed, 6 insertions(+), 6 deletions(-) +</pre><pre>diff --git a/README.md b/README.md +index dfba44d..2a253d3 100644 +--- a/README.md ++++ b/README.md +<span class="h">@@ -26,7 +26,7 @@ These can be combined into a string such as the following "Hello World!" program</span> + #How to install: + - Make sure you have a Common Lisp implementation installed. I recommend [Steel Bank Common Lisp](http://www.sbcl.org/). + - [ASDF](https://common-lisp.net/project/asdf/) must be installed. Many Lisps come with it installed (including [SBCL](http://www.sbcl.org/)). +<span class="d">-- Install this code into your ASDF load directory. The default on linux is `~/common-lisp/`:</span> +<span class="a">+- Install this code into your ASDF *load directory*. The default on linux is usually `~/common-lisp/`:</span> + ``` + me@linux:~$ mkdir common-lisp + me@linux:~$ cd common-lisp +<span class="h">@@ -45,18 +45,18 @@ If you are unable to find where the ASDF load directory is, you may choose to lo</span> + > (asdf:load-system :brain) + ``` + +<span class="d">-In Allegro common lisp one must first use `(require :asdf)` before executing any other commands in order to activate the ASDF system.</span> +<span class="a">+In Allegro common lisp one must first use `(require :asdf)` before executing any other commands in order to activate the preinstalled ASDF system.</span> + #How to Use: +<span class="d">-If everything runs smoothly you will be ready to Brainfuck. If there are issues then please *let it be known*. Now one must chooses between the `brain:fuck` and the `#F` notation when using the REPL. The `#F` notation is more concise but does not allow any whitespace in the Brainfuck code, while the `brain:fuck` notation allows any character except for an unescaped literal quote `"` inside of the Brainfuck code. Below they are both shown:</span> +<span class="a">+If everything runs smoothly you will be ready to Brainfuck. If there are issues then please *let it be known*. Now one must chooses between the `brain:fuck` and the `#F` notation when using the REPL. The `#F` notation is more concise but does not allow any whitespace in the Brainfuck code, while the `brain:fuck` notation allows any character except for an unescaped literal quote `"`, or an unescaped literal backward slash `\` inside of the Brainfuck code. Below they are both shown:</span> + ``` +<span class="d">-> (brain:fuck ".+[.+] Please escape your \" characters!")</span> +<span class="a">+> (brain:fuck ".+[.+] Please escape your \" and \\ characters!")</span> + "� + + + + + + + !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ + ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ" + ``` + [](https://raw.githubusercontent.com/equwal/LispFuck/master/brain-fuck-notation.png) + ``` +<span class="d">-> #f.+[.+] [<This is not inside the Brainfuck code, nor is + or -.>]</span> +<span class="a">+> #f.+[.+] <This is not inside the Brainfuck code, nor is + or -.>[]</span> + "� + + + + + + + !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ + ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ" +<span class="h">@@ -111,7 +111,7 @@ Suppose you want to find information about the execution of `incf-byte` and `dec</span> + ``` + + #Bugs/TODO: +<span class="d">-- None right now, feedback is encouraged! My email is spensertruexonline@gmail.com</span> +<span class="a">+- Include instructions for users on Windows.</span> + + #Congratulations! + You have reached the end of this README. Please sumbit any feedback to me via email.</pre> +</div></body></html> diff --git a/git/LispBrain/commit/f3d174752dda70690cf073701c79901755cfc192.html b/git/LispBrain/commit/f3d174752dda70690cf073701c79901755cfc192.html index 7d6fd42..e15bb7e 100644 --- a/git/LispBrain/commit/f3d174752dda70690cf073701c79901755cfc192.html +++ b/git/LispBrain/commit/f3d174752dda70690cf073701c79901755cfc192.html @@ -1,36 +1,36 @@ -<!DOCTYPE html> -<html lang="en"><head><meta charset="UTF-8" /> -<meta name="viewport" content="width=device-width, initial-scale=1" /> -<title>LispBrain f3d17475 - Recently Written</title> -<link rel="stylesheet" href="../../stagit.css" /> -</head><body> -<div id="top"><a href="../../../index.html">Recently Written</a> · <a href="../../index.html">git</a></div> -<h1>LispBrain</h1><span class="desc">The best Brainfuck debugger available, if you know how the Common Lisp debugger works. Uses Common Lisp's ease of debugging to make debugging Brainfuck code easy. Almost entirely useless.</span><p class="url">git clone https://github.com/equwal/LispBrain</p><p><a href="../../LispBrain/index.html">Log</a> | <a href="../../LispBrain/files.html">Files</a> | <a href="../../LispBrain/refs.html">Refs</a></p><hr/> -<div id="content"> -<pre>commit f3d174752dda70690cf073701c79901755cfc192 -Spenser Truex <struex0@gmail.com> -2016-02-14 09:39:05 -0800 - -added comment -</pre><pre> README.md | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) -</pre><pre>diff --git a/README.md b/README.md -index 1e19341..370dd65 100644 ---- a/README.md -+++ b/README.md -<span class="h">@@ -48,7 +48,7 @@ Another example:</span> - - - - - - - !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ - ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ - -<span class="d">-NIL</span> -<span class="a">+NIL ;note that NIL will always be returned by (interpret). The above text is written to *standard-output*</span> - ``` - #TODO: - - ASDF would be a better packaging system choice than the current arrangement.</pre> -</div></body></html> +<!DOCTYPE html> +<html lang="en"><head><meta charset="UTF-8" /> +<meta name="viewport" content="width=device-width, initial-scale=1" /> +<title>LispBrain f3d17475 - Recently Written</title> +<link rel="stylesheet" href="../../stagit.css" /> +</head><body> +<div id="top"><a href="../../../index.html">Recently Written</a> · <a href="../../index.html">git</a></div> +<h1>LispBrain</h1><span class="desc">The best Brainfuck debugger available, if you know how the Common Lisp debugger works. Uses Common Lisp's ease of debugging to make debugging Brainfuck code easy. Almost entirely useless.</span><p class="url">git clone https://github.com/equwal/LispBrain</p><p><a href="../../LispBrain/index.html">Log</a> | <a href="../../LispBrain/files.html">Files</a> | <a href="../../LispBrain/refs.html">Refs</a></p><hr/> +<div id="content"> +<pre>commit f3d174752dda70690cf073701c79901755cfc192 +Spenser Truex <struex0@gmail.com> +2016-02-14 09:39:05 -0800 + +added comment +</pre><pre> README.md | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) +</pre><pre>diff --git a/README.md b/README.md +index 1e19341..370dd65 100644 +--- a/README.md ++++ b/README.md +<span class="h">@@ -48,7 +48,7 @@ Another example:</span> + + + + + + + !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ + ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ + +<span class="d">-NIL</span> +<span class="a">+NIL ;note that NIL will always be returned by (interpret). The above text is written to *standard-output*</span> + ``` + #TODO: + - ASDF would be a better packaging system choice than the current arrangement.</pre> +</div></body></html> diff --git a/git/ideamine/commit/5f016019ae005479871c8196e23bca98742615f6.html b/git/ideamine/commit/5f016019ae005479871c8196e23bca98742615f6.html new file mode 100644 index 0000000..1d76980 --- /dev/null +++ b/git/ideamine/commit/5f016019ae005479871c8196e23bca98742615f6.html @@ -0,0 +1,98 @@ +<!DOCTYPE html> +<html lang="en"><head><meta charset="UTF-8" /> +<meta name="viewport" content="width=device-width, initial-scale=1" /> +<title>ideamine 5f016019 - Recently Written</title> +<link rel="stylesheet" href="../../stagit.css" /> +</head><body> +<div id="top"><a href="../../../index.html">Recently Written</a> · <a href="../../index.html">git</a></div> +<h1>ideamine</h1><span class="desc">An idea inbox for Claude Code: /idea saves ideas at zero tokens; Claude triages them and routes each to the cheapest model that can build it.</span><p class="url">git clone https://github.com/equwal/ideamine</p><p><a href="../../ideamine/index.html">Log</a> | <a href="../../ideamine/files.html">Files</a> | <a href="../../ideamine/refs.html">Refs</a></p><hr/> +<div id="content"> +<pre>commit 5f016019ae005479871c8196e23bca98742615f6 +equwal <13551856+equwal@users.noreply.github.com> +2026-09-20 22:11:02 -0700 + +Use equwal as the author name + +Change the author name in LICENSE, package.json, and the plugin +manifests to the GitHub account name, equwal. Increase the version +to 0.1.1, so that installed copies of the plugin get the change. + +</pre><pre> .claude-plugin/marketplace.json | 6 +++--- + .claude-plugin/plugin.json | 4 ++-- + LICENSE | 2 +- + package.json | 4 ++-- + 4 files changed, 8 insertions(+), 8 deletions(-) +</pre><pre>diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json +index df36de1..c44bedd 100644 +--- a/.claude-plugin/marketplace.json ++++ b/.claude-plugin/marketplace.json +<span class="h">@@ -3,16 +3,16 @@</span> + "name": "ideamine", + "description": "ideamine: an idea inbox for Claude Code, with model routing", + "owner": { +<span class="d">- "name": "Spenser Truex",</span> +<span class="a">+ "name": "equwal",</span> + "url": "https://github.com/equwal" + }, + "plugins": [ + { + "name": "ideamine", + "description": "An idea inbox for Claude Code. /idea saves an idea without a model call. Claude triages the ideas and picks the cheapest model that can build each one.", +<span class="d">- "version": "0.1.0",</span> +<span class="a">+ "version": "0.1.1",</span> + "author": { +<span class="d">- "name": "Spenser Truex"</span> +<span class="a">+ "name": "equwal"</span> + }, + "source": "./", + "category": "productivity", +diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json +index d577439..f66d7c7 100644 +--- a/.claude-plugin/plugin.json ++++ b/.claude-plugin/plugin.json +<span class="h">@@ -1,9 +1,9 @@</span> + { + "name": "ideamine", +<span class="d">- "version": "0.1.0",</span> +<span class="a">+ "version": "0.1.1",</span> + "description": "An idea inbox for Claude Code. /idea saves an idea without a model call, so it works at any time, even at your usage limit. Claude triages the ideas and picks the cheapest model that can build each one.", + "author": { +<span class="d">- "name": "Spenser Truex",</span> +<span class="a">+ "name": "equwal",</span> + "url": "https://github.com/equwal" + }, + "homepage": "https://github.com/equwal/ideamine", +diff --git a/LICENSE b/LICENSE +index da3111d..160e1db 100644 +--- a/LICENSE ++++ b/LICENSE +<span class="h">@@ -1,6 +1,6 @@</span> + MIT License + +<span class="d">-Copyright (c) 2026 Spenser Truex</span> +<span class="a">+Copyright (c) 2026 equwal</span> + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal +diff --git a/package.json b/package.json +index 5523c04..16f02c8 100644 +--- a/package.json ++++ b/package.json +<span class="h">@@ -1,6 +1,6 @@</span> + { + "name": "ideamine", +<span class="d">- "version": "0.1.0",</span> +<span class="a">+ "version": "0.1.1",</span> + "description": "An idea inbox for Claude Code. /idea saves an idea without calling the model; Claude later triages each idea and picks the cheapest model that can build it.", + "type": "module", + "bin": { +<span class="h">@@ -28,7 +28,7 @@</span> + }, + "homepage": "https://github.com/equwal/ideamine#readme", + "bugs": "https://github.com/equwal/ideamine/issues", +<span class="d">- "author": "Spenser Truex",</span> +<span class="a">+ "author": "equwal",</span> + "license": "MIT", + "keywords": [ + "claude",</pre> +</div></body></html> diff --git a/git/ideamine/commit/814be9a54e4062da4972978745e63da86572bde8.html b/git/ideamine/commit/814be9a54e4062da4972978745e63da86572bde8.html new file mode 100644 index 0000000..49f35be --- /dev/null +++ b/git/ideamine/commit/814be9a54e4062da4972978745e63da86572bde8.html @@ -0,0 +1,2331 @@ +<!DOCTYPE html> +<html lang="en"><head><meta charset="UTF-8" /> +<meta name="viewport" content="width=device-width, initial-scale=1" /> +<title>ideamine 814be9a5 - Recently Written</title> +<link rel="stylesheet" href="../../stagit.css" /> +</head><body> +<div id="top"><a href="../../../index.html">Recently Written</a> · <a href="../../index.html">git</a></div> +<h1>ideamine</h1><span class="desc">An idea inbox for Claude Code: /idea saves ideas at zero tokens; Claude triages them and routes each to the cheapest model that can build it.</span><p class="url">git clone https://github.com/equwal/ideamine</p><p><a href="../../ideamine/index.html">Log</a> | <a href="../../ideamine/files.html">Files</a> | <a href="../../ideamine/refs.html">Refs</a></p><hr/> +<div id="content"> +<pre>commit 814be9a54e4062da4972978745e63da86572bde8 +Spenser Truex <13551856+equwal@users.noreply.github.com> +2026-09-20 21:28:13 -0700 + +ideamine 0.1.0: an idea inbox for Claude Code + +- /idea and /ideas are answered by a UserPromptSubmit hook before any + API call: capture costs 0 tokens and works on any model, mid-task, + or at the usage limit. +- One global archive (~/.ideamine/ideas.json), locked atomic writes. +- /idea-triage scores ideas (verdict, impact, size) and picks the + cheapest capable model, via one minimal headless claude -p call. +- /idea-go builds an idea in a subagent on its recommended model. +- Zero-dependency MCP server, CLI, and plugin marketplace manifest. + +</pre><pre> .claude-plugin/marketplace.json | 22 +++ + .claude-plugin/plugin.json | 13 ++ + .gitattributes | 1 + + .gitignore | 4 + + .mcp.json | 8 + + LICENSE | 21 +++ + README.md | 139 +++++++++++++++++ + bin/ideamine.js | 178 +++++++++++++++++++++ + hooks/hooks.json | 17 ++ + package.json | 43 ++++++ + skills/idea-go/SKILL.md | 14 ++ + skills/idea-triage/SKILL.md | 12 ++ + skills/idea/SKILL.md | 13 ++ + skills/ideas/SKILL.md | 11 ++ + src/claude.js | 139 +++++++++++++++++ + src/hook.js | 97 ++++++++++++ + src/mcp.js | 309 ++++++++++++++++++++++++++++++++++++ + src/render.js | 135 ++++++++++++++++ + src/rubric.js | 101 ++++++++++++ + src/store.js | 335 ++++++++++++++++++++++++++++++++++++++++ + src/text.js | 88 +++++++++++ + tests/fixtures/fake-claude.js | 34 ++++ + tests/hook.test.js | 78 ++++++++++ + tests/mcp.test.js | 116 ++++++++++++++ + tests/store.test.js | 85 ++++++++++ + tests/text.test.js | 35 +++++ + tests/triage.test.js | 68 ++++++++ + 27 files changed, 2116 insertions(+) +</pre><pre>diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json +new file mode 100644 +index 0000000..df36de1 +--- /dev/null ++++ b/.claude-plugin/marketplace.json +<span class="h">@@ -0,0 +1,22 @@</span> +<span class="a">+{</span> +<span class="a">+ "$schema": "https://anthropic.com/claude-code/marketplace.schema.json",</span> +<span class="a">+ "name": "ideamine",</span> +<span class="a">+ "description": "ideamine: an idea inbox for Claude Code, with model routing",</span> +<span class="a">+ "owner": {</span> +<span class="a">+ "name": "Spenser Truex",</span> +<span class="a">+ "url": "https://github.com/equwal"</span> +<span class="a">+ },</span> +<span class="a">+ "plugins": [</span> +<span class="a">+ {</span> +<span class="a">+ "name": "ideamine",</span> +<span class="a">+ "description": "An idea inbox for Claude Code. /idea saves an idea without a model call. Claude triages the ideas and picks the cheapest model that can build each one.",</span> +<span class="a">+ "version": "0.1.0",</span> +<span class="a">+ "author": {</span> +<span class="a">+ "name": "Spenser Truex"</span> +<span class="a">+ },</span> +<span class="a">+ "source": "./",</span> +<span class="a">+ "category": "productivity",</span> +<span class="a">+ "homepage": "https://github.com/equwal/ideamine"</span> +<span class="a">+ }</span> +<span class="a">+ ]</span> +<span class="a">+}</span> +diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json +new file mode 100644 +index 0000000..d577439 +--- /dev/null ++++ b/.claude-plugin/plugin.json +<span class="h">@@ -0,0 +1,13 @@</span> +<span class="a">+{</span> +<span class="a">+ "name": "ideamine",</span> +<span class="a">+ "version": "0.1.0",</span> +<span class="a">+ "description": "An idea inbox for Claude Code. /idea saves an idea without a model call, so it works at any time, even at your usage limit. Claude triages the ideas and picks the cheapest model that can build each one.",</span> +<span class="a">+ "author": {</span> +<span class="a">+ "name": "Spenser Truex",</span> +<span class="a">+ "url": "https://github.com/equwal"</span> +<span class="a">+ },</span> +<span class="a">+ "homepage": "https://github.com/equwal/ideamine",</span> +<span class="a">+ "repository": "https://github.com/equwal/ideamine",</span> +<span class="a">+ "license": "MIT",</span> +<span class="a">+ "keywords": ["ideas", "backlog", "inbox", "triage", "model-routing", "mcp", "tokens"]</span> +<span class="a">+}</span> +diff --git a/.gitattributes b/.gitattributes +new file mode 100644 +index 0000000..6313b56 +--- /dev/null ++++ b/.gitattributes +<span class="h">@@ -0,0 +1 @@</span> +<span class="a">+* text=auto eol=lf</span> +diff --git a/.gitignore b/.gitignore +new file mode 100644 +index 0000000..d56c12b +--- /dev/null ++++ b/.gitignore +<span class="h">@@ -0,0 +1,4 @@</span> +<span class="a">+node_modules/</span> +<span class="a">+*.tmp</span> +<span class="a">+*.log</span> +<span class="a">+.DS_Store</span> +diff --git a/.mcp.json b/.mcp.json +new file mode 100644 +index 0000000..4bed0e0 +--- /dev/null ++++ b/.mcp.json +<span class="h">@@ -0,0 +1,8 @@</span> +<span class="a">+{</span> +<span class="a">+ "mcpServers": {</span> +<span class="a">+ "ideamine": {</span> +<span class="a">+ "command": "node",</span> +<span class="a">+ "args": ["${CLAUDE_PLUGIN_ROOT}/bin/ideamine.js", "mcp", "--plugin"]</span> +<span class="a">+ }</span> +<span class="a">+ }</span> +<span class="a">+}</span> +diff --git a/LICENSE b/LICENSE +new file mode 100644 +index 0000000..da3111d +--- /dev/null ++++ b/LICENSE +<span class="h">@@ -0,0 +1,21 @@</span> +<span class="a">+MIT License</span> +<span class="a">+</span> +<span class="a">+Copyright (c) 2026 Spenser Truex</span> +<span class="a">+</span> +<span class="a">+Permission is hereby granted, free of charge, to any person obtaining a copy</span> +<span class="a">+of this software and associated documentation files (the "Software"), to deal</span> +<span class="a">+in the Software without restriction, including without limitation the rights</span> +<span class="a">+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell</span> +<span class="a">+copies of the Software, and to permit persons to whom the Software is</span> +<span class="a">+furnished to do so, subject to the following conditions:</span> +<span class="a">+</span> +<span class="a">+The above copyright notice and this permission notice shall be included in all</span> +<span class="a">+copies or substantial portions of the Software.</span> +<span class="a">+</span> +<span class="a">+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR</span> +<span class="a">+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,</span> +<span class="a">+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE</span> +<span class="a">+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER</span> +<span class="a">+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,</span> +<span class="a">+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE</span> +<span class="a">+SOFTWARE.</span> +diff --git a/README.md b/README.md +new file mode 100644 +index 0000000..b4e321e +--- /dev/null ++++ b/README.md +<span class="h">@@ -0,0 +1,139 @@</span> +<span class="a">+# ideamine</span> +<span class="a">+</span> +<span class="a">+**An idea inbox for Claude Code.** Save ideas as fast as you have them. Claude sorts them out later.</span> +<span class="a">+</span> +<span class="a">+```</span> +<span class="a">+> /idea let the scroller pause when the mouse hovers over a link</span> +<span class="a">+ 💡 Saved #43 · let the scroller pause when the mouse hovers over a link (6 in inbox)</span> +<span class="a">+```</span> +<span class="a">+</span> +<span class="a">+That line did not call the model. A hook answers `/idea` on your machine before any request goes to Claude, so saving an idea:</span> +<span class="a">+</span> +<span class="a">+- **costs 0 tokens.** `claude -p "/idea …"` reports 0 turns, 0 input tokens, 0 output tokens, $0.</span> +<span class="a">+- **does not interrupt the current task.** Claude never sees the idea, so it cannot get distracted by it.</span> +<span class="a">+- **works on any model, in any session, and when you are out of usage.** Nothing is sent, so there is nothing to rate-limit.</span> +<span class="a">+</span> +<span class="a">+Every session writes to one archive, `~/.ideamine/ideas.json`, whatever project or model it uses. When you are ready, Claude triages the pile. It decides which ideas are worth doing and picks the **cheapest model that can build each one**: Haiku for a typo, Sonnet for a feature, Opus for a redesign. Fable is only for the hardest problems.</span> +<span class="a">+</span> +<span class="a">+## Install</span> +<span class="a">+</span> +<span class="a">+In Claude Code (CLI, desktop, or IDE):</span> +<span class="a">+</span> +<span class="a">+```bash</span> +<span class="a">+claude plugin marketplace add equwal/ideamine</span> +<span class="a">+```</span> +<span class="a">+</span> +<span class="a">+```bash</span> +<span class="a">+claude plugin install ideamine@ideamine</span> +<span class="a">+```</span> +<span class="a">+</span> +<span class="a">+Or from inside a session: `/plugin marketplace add equwal/ideamine`, then `/plugin install ideamine@ideamine`. Start a new session to load it. Requires Node.js 18 or later. There are no dependencies to install. Tested on Claude Code 2.1.224 (CLI) and 2.1.275 (desktop app) on Windows.</span> +<span class="a">+</span> +<span class="a">+Marketplaces you add yourself do not auto-update. To upgrade, run `claude plugin marketplace update ideamine`, then `claude plugin update ideamine@ideamine`.</span> +<span class="a">+</span> +<span class="a">+## Commands</span> +<span class="a">+</span> +<span class="a">+| Command | What it does | Calls the model? |</span> +<span class="a">+|---|---|---|</span> +<span class="a">+| `/idea <text>` | Save an idea. A pasted bulleted list saves one idea per bullet. `#tags` are recorded. | **No** |</span> +<span class="a">+| `/ideas` | Show the board: doing, do (best first), maybe, inbox | **No** |</span> +<span class="a">+| `/ideas #12` | Show one idea in full: brief, model, notes | **No** |</span> +<span class="a">+| `/ideas done 12 shipped it` | Quick edit. Also `drop`, `start`, `reopen` | **No** |</span> +<span class="a">+| `/ideas inbox` · `do` · `maybe` · `skip` · `done` · `all` · `here` | Filter the board. `here` = this project only | **No** |</span> +<span class="a">+| `/ideas <question>` | Ask about your ideas, e.g. "which ones fit in an hour?" | Yes, briefly |</span> +<span class="a">+| `/idea-triage` | Score the inbox and choose a model for each idea | Yes, briefly |</span> +<span class="a">+| `/idea-go [id]` | Build the top idea (or `#id`) with its recommended model | Yes, this is the build |</span> +<span class="a">+</span> +<span class="a">+In the plugin menu the commands also appear as `/ideamine:idea` and so on. Both forms work.</span> +<span class="a">+</span> +<span class="a">+Claude can also save ideas by itself. If you write "idea: dark mode for the popup" or "save that for later", it calls the `idea_add` tool and continues the current task.</span> +<span class="a">+</span> +<span class="a">+## Model routing</span> +<span class="a">+</span> +<span class="a">+`/idea-triage` gives each idea a verdict (`do`, `maybe`, `skip`), an impact from 1 to 5, a size from `xs` to `xl`, a one-line reason, and a short brief that an agent can act on without the original chat. It also picks the cheapest model that is likely to finish the idea in one pass. If a weaker model fails and has to retry, that costs more than using the right model once.</span> +<span class="a">+</span> +<span class="a">+| Model | $ in / out per 1M tokens | Gets ideas like |</span> +<span class="a">+|---|---|---|</span> +<span class="a">+| `haiku` (Haiku 4.5) | $1 / $5 | mechanical, fully specified, local work: typos, renames, config tweaks, boilerplate, small scripts |</span> +<span class="a">+| `sonnet` (Sonnet 5) | $2 / $10 | the default: ordinary features, bug fixes with a clear repro, tests, docs, contained refactors |</span> +<span class="a">+| `opus` (Opus 5) | $5 / $25 | ambiguous or cross-cutting work: architecture, hard debugging, performance, security |</span> +<span class="a">+| `fable` (Fable 5.1) | $10 / $50 | only the hardest long-horizon or research-grade problems |</span> +<span class="a">+</span> +<span class="a">+The triage does not run on your session's model. `/idea-triage` makes one tool call, and the MCP server hands the work to a separate, minimal `claude -p` run on Sonnet (see below). A session on Opus or Fable therefore pays the same few cents as a session on Haiku. If that CLI is not available, Claude does the triage itself.</span> +<span class="a">+</span> +<span class="a">+Recommendations are stored as aliases, so they stay valid when a newer model ships under the same name. `/idea-go` gives the build to a subagent on the recommended model. The subagent starts with a clean context, so the build does not also re-read your whole conversation. To override a recommendation, run `ideamine model 12 opus` or ask Claude.</span> +<span class="a">+</span> +<span class="a">+## Headless triage</span> +<span class="a">+</span> +<span class="a">+```bash</span> +<span class="a">+ideamine triage</span> +<span class="a">+```</span> +<span class="a">+</span> +<span class="a">+`/idea-triage` uses this same engine. It is one `claude -p` call on your normal Claude Code login, with no tools, no MCP servers, no settings, a two-line system prompt, and a JSON schema for the output. Four ideas take about 1,700 input tokens, roughly two cents on Sonnet. A normal model turn in a setup with a few MCP servers can re-read tens of thousands of tokens. To keep the inbox sorted while you sleep, schedule the command with cron or Task Scheduler. `--model haiku` makes it cheaper, and `--dry-run` shows the exact prompt.</span> +<span class="a">+</span> +<span class="a">+## Command line</span> +<span class="a">+</span> +<span class="a">+```bash</span> +<span class="a">+npm install -g github:equwal/ideamine</span> +<span class="a">+```</span> +<span class="a">+</span> +<span class="a">+```</span> +<span class="a">+ideamine add "support vim keys in the popup" # "-" reads stdin</span> +<span class="a">+ideamine ls [inbox|do|maybe|skip|doing|done|all] [here]</span> +<span class="a">+ideamine show 12</span> +<span class="a">+ideamine done 12 "shipped in v1.4" # also: drop, start, reopen, note</span> +<span class="a">+ideamine next # what to build next</span> +<span class="a">+ideamine go 12 # opens Claude Code on the right model, in the idea's project</span> +<span class="a">+ideamine triage # headless triage (see above)</span> +<span class="a">+ideamine export IDEAS.md # Markdown copy of everything</span> +<span class="a">+```</span> +<span class="a">+</span> +<span class="a">+## Other MCP clients</span> +<span class="a">+</span> +<span class="a">+The MCP server works without the plugin. For Claude Desktop, Cursor, or any stdio MCP client:</span> +<span class="a">+</span> +<span class="a">+```json</span> +<span class="a">+{</span> +<span class="a">+ "mcpServers": {</span> +<span class="a">+ "ideamine": { "command": "npx", "args": ["-y", "github:equwal/ideamine", "mcp"] }</span> +<span class="a">+ }</span> +<span class="a">+}</span> +<span class="a">+```</span> +<span class="a">+</span> +<span class="a">+Tools: `idea_add`, `idea_list`, `idea_triage`, `idea_update`, `idea_next`. Outside the plugin, the slash commands are MCP prompts. Without the hook, saving goes through the model, which costs a few tokens.</span> +<span class="a">+</span> +<span class="a">+## Where your ideas live</span> +<span class="a">+</span> +<span class="a">+`~/.ideamine/ideas.json` is plain, readable JSON. Set `IDEAMINE_HOME` to move it, for example into a synced folder. Each write takes a lock and then replaces the file in one step, so many sessions can write at the same time without losing an idea. The previous version is kept as `ideas.json.bak`. If the file becomes damaged, ideamine stops and does not overwrite it. Nothing leaves your machine, except when you run triage or a build, which go through Claude as usual.</span> +<span class="a">+</span> +<span class="a">+| Variable | Default | Purpose |</span> +<span class="a">+|---|---|---|</span> +<span class="a">+| `IDEAMINE_HOME` | `~/.ideamine` | archive location |</span> +<span class="a">+| `IDEAMINE_TRIAGE_MODEL` | `sonnet` | model for `ideamine triage` |</span> +<span class="a">+| `IDEAMINE_CLAUDE_BIN` | `claude` | Claude Code executable |</span> +<span class="a">+| `IDEAMINE_SETTING_SOURCES` | *(empty)* | set to `user` if your login needs `settings.json` (e.g. `apiKeyHelper`) |</span> +<span class="a">+</span> +<span class="a">+## How it works</span> +<span class="a">+</span> +<span class="a">+```</span> +<span class="a">+/idea … ──► UserPromptSubmit hook ──► ~/.ideamine/ideas.json ──► "💡 Saved #43" (model never called)</span> +<span class="a">+/ideas ──► same hook, renders the board locally</span> +<span class="a">+</span> +<span class="a">+/idea-triage ──► Claude ──► MCP idea_triage ──► claude -p (Sonnet, minimal context)</span> +<span class="a">+ ──► verdict · impact · size · cheapest capable model · brief</span> +<span class="a">+/idea-go ──► Claude ──► subagent on that model ──► builds it ──► idea_update: done</span> +<span class="a">+```</span> +<span class="a">+</span> +<span class="a">+The plugin contains a Node MCP server with no dependencies, four skills (the slash commands), and one hook. The hook ignores every prompt except `/idea` and `/ideas`, and those end before any API call. The hook runs directly, not through a shell, and takes about 130 ms per prompt on Windows. The skills are user-only, so their descriptions add no tokens to your sessions. If the archive cannot be read, the hook lets the prompt through, so the `/idea` skill can still save it with the MCP tool. Your text is never dropped.</span> +<span class="a">+</span> +<span class="a">+## Development</span> +<span class="a">+</span> +<span class="a">+```bash</span> +<span class="a">+npm test</span> +<span class="a">+```</span> +<span class="a">+</span> +<span class="a">+The tests use `node:test` only. They cover the store, including concurrent writers from several processes, the hook, the MCP protocol, and headless triage. Triage runs against a stand-in `claude`, so the tests spend no tokens.</span> +<span class="a">+</span> +<span class="a">+## License</span> +<span class="a">+</span> +<span class="a">+MIT</span> +diff --git a/bin/ideamine.js b/bin/ideamine.js +new file mode 100644 +index 0000000..5a38928 +--- /dev/null ++++ b/bin/ideamine.js +<span class="h">@@ -0,0 +1,178 @@</span> +<span class="a">+#!/usr/bin/env node</span> +<span class="a">+// ideamine CLI. Also the entry point the plugin uses for its MCP server (`mcp`) and hook (`hook`).</span> +<span class="a">+</span> +<span class="a">+import fs from 'node:fs';</span> +<span class="a">+</span> +<span class="a">+const HELP = `ideamine: an idea inbox for Claude Code</span> +<span class="a">+</span> +<span class="a">+ ideamine add <idea...> save an idea ("-" reads stdin; a bulleted list = one idea per bullet)</span> +<span class="a">+ ideamine ls [filter] [here] board. filters: open inbox do maybe skip doing done dropped all</span> +<span class="a">+ ideamine show <id> one idea in full</span> +<span class="a">+ ideamine done|drop|start|reopen <id> [note...]</span> +<span class="a">+ ideamine note <id> <text...> append a note</span> +<span class="a">+ ideamine model <id> <haiku|sonnet|opus|fable> override the recommended model</span> +<span class="a">+ ideamine next [--here] the idea to build next</span> +<span class="a">+ ideamine go [id] [--print] open Claude Code on the idea's recommended model, in its project</span> +<span class="a">+ ideamine triage [--model sonnet] [--limit 20] [--dry-run]</span> +<span class="a">+ score the inbox with one headless \`claude -p\` call</span> +<span class="a">+ ideamine export [file.md] Markdown export of the whole archive</span> +<span class="a">+ ideamine path where the archive lives (override with IDEAMINE_HOME)</span> +<span class="a">+ ideamine mcp run the MCP server on stdio</span> +<span class="a">+ ideamine hook run the UserPromptSubmit hook (reads the hook JSON on stdin)`;</span> +<span class="a">+</span> +<span class="a">+function parseArgs(argv) {</span> +<span class="a">+ const flags = {};</span> +<span class="a">+ const words = [];</span> +<span class="a">+ for (let i = 0; i < argv.length; i++) {</span> +<span class="a">+ const a = argv[i];</span> +<span class="a">+ if (a.startsWith('--')) {</span> +<span class="a">+ const [key, inline] = a.slice(2).split('=', 2);</span> +<span class="a">+ if (inline !== undefined) flags[key] = inline;</span> +<span class="a">+ else if (argv[i + 1] !== undefined && !argv[i + 1].startsWith('--') && ['model', 'limit', 'budget', 'query'].includes(key)) flags[key] = argv[++i];</span> +<span class="a">+ else flags[key] = true;</span> +<span class="a">+ } else words.push(a);</span> +<span class="a">+ }</span> +<span class="a">+ return { flags, words };</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+async function readStdin() {</span> +<span class="a">+ let data = '';</span> +<span class="a">+ for await (const chunk of process.stdin) data += chunk;</span> +<span class="a">+ return data;</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+function fail(message) {</span> +<span class="a">+ process.stderr.write(`ideamine: ${message}\n`);</span> +<span class="a">+ process.exit(1);</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+async function main() {</span> +<span class="a">+ const [command = 'help', ...rest] = process.argv.slice(2);</span> +<span class="a">+</span> +<span class="a">+ // The two machine entry points load only what they need.</span> +<span class="a">+ if (command === 'mcp') return (await import('../src/mcp.js')).serve({ prompts: !rest.includes('--plugin') });</span> +<span class="a">+ if (command === 'hook') return (await import('../src/hook.js')).runHook();</span> +<span class="a">+</span> +<span class="a">+ const store = await import('../src/store.js');</span> +<span class="a">+ const render = await import('../src/render.js');</span> +<span class="a">+ const { splitIdeas, clip } = await import('../src/text.js');</span> +<span class="a">+ const { flags, words } = parseArgs(rest);</span> +<span class="a">+ const cwd = process.cwd();</span> +<span class="a">+ const needId = () => words[0] || fail(`usage: ideamine ${command} <id>`);</span> +<span class="a">+</span> +<span class="a">+ switch (command) {</span> +<span class="a">+ case 'add': {</span> +<span class="a">+ const text = words.length === 1 && words[0] === '-' ? await readStdin() : words.join(' ');</span> +<span class="a">+ const results = store.addIdeas(splitIdeas(text), { source: 'cli', project: flags['no-project'] ? null : cwd });</span> +<span class="a">+ console.log(render.renderAdded(results, store.load()));</span> +<span class="a">+ break;</span> +<span class="a">+ }</span> +<span class="a">+ case 'ls':</span> +<span class="a">+ case 'list':</span> +<span class="a">+ case 'board': {</span> +<span class="a">+ const lower = words.map((w) => w.toLowerCase());</span> +<span class="a">+ const filter = lower.find((w) => store.FILTERS.includes(w)) || 'open';</span> +<span class="a">+ const here = lower.includes('here') || flags.here;</span> +<span class="a">+ console.log(render.renderBoard(store.load(), { filter, project: here ? cwd : null, query: flags.query || '', cwd }));</span> +<span class="a">+ break;</span> +<span class="a">+ }</span> +<span class="a">+ case 'show': {</span> +<span class="a">+ const idea = store.findIdea(store.load(), needId());</span> +<span class="a">+ if (!idea) fail(`no idea #${words[0]}`);</span> +<span class="a">+ console.log(render.renderIdea(idea));</span> +<span class="a">+ break;</span> +<span class="a">+ }</span> +<span class="a">+ case 'done':</span> +<span class="a">+ case 'drop':</span> +<span class="a">+ case 'start':</span> +<span class="a">+ case 'doing':</span> +<span class="a">+ case 'reopen': {</span> +<span class="a">+ const status = { drop: 'dropped', start: 'doing' }[command] || command;</span> +<span class="a">+ const note = words.slice(1).join(' ');</span> +<span class="a">+ const idea = store.updateIdea(needId(), { status, note: note || undefined });</span> +<span class="a">+ console.log(`#${idea.id} ${clip(idea.title, 60)} → ${store.lane(idea)}`);</span> +<span class="a">+ break;</span> +<span class="a">+ }</span> +<span class="a">+ case 'note': {</span> +<span class="a">+ const note = words.slice(1).join(' ') || fail('usage: ideamine note <id> <text>');</span> +<span class="a">+ const idea = store.updateIdea(needId(), { note });</span> +<span class="a">+ console.log(`#${idea.id}: note added`);</span> +<span class="a">+ break;</span> +<span class="a">+ }</span> +<span class="a">+ case 'model': {</span> +<span class="a">+ const idea = store.updateIdea(needId(), { model: words[1] || fail('usage: ideamine model <id> <haiku|sonnet|opus|fable>') });</span> +<span class="a">+ console.log(`#${idea.id} will be built with ${idea.triage.model}`);</span> +<span class="a">+ break;</span> +<span class="a">+ }</span> +<span class="a">+ case 'next': {</span> +<span class="a">+ const idea = store.pickNext(store.load(), { project: cwd, only: !!flags.here });</span> +<span class="a">+ if (!idea) {</span> +<span class="a">+ console.log('Nothing is ready to build. Triage the inbox first: ideamine triage');</span> +<span class="a">+ break;</span> +<span class="a">+ }</span> +<span class="a">+ console.log(render.renderIdea(idea));</span> +<span class="a">+ break;</span> +<span class="a">+ }</span> +<span class="a">+ case 'go': {</span> +<span class="a">+ const { buildPrompt, launchSession } = await import('../src/claude.js');</span> +<span class="a">+ const db = store.load();</span> +<span class="a">+ const idea = words[0] ? store.findIdea(db, words[0]) : store.pickNext(db, { project: cwd });</span> +<span class="a">+ if (!idea) fail(words[0] ? `no idea #${words[0]}` : 'nothing is ready to build; run: ideamine triage');</span> +<span class="a">+ const model = idea.triage?.model || 'sonnet';</span> +<span class="a">+ const dir = idea.project && fs.existsSync(idea.project) ? idea.project : cwd;</span> +<span class="a">+ const prompt = buildPrompt(idea);</span> +<span class="a">+ if (flags.print) {</span> +<span class="a">+ console.log(`directory: ${dir}\nmodel: ${model}\n\n${prompt}`);</span> +<span class="a">+ break;</span> +<span class="a">+ }</span> +<span class="a">+ store.updateIdea(idea.id, { status: 'doing' });</span> +<span class="a">+ console.log(`Opening Claude Code (${model}) in ${dir} for #${idea.id}…`);</span> +<span class="a">+ process.exitCode = launchSession({ model, prompt, cwd: dir });</span> +<span class="a">+ break;</span> +<span class="a">+ }</span> +<span class="a">+ case 'triage': {</span> +<span class="a">+ const { headlessTriage } = await import('../src/claude.js');</span> +<span class="a">+ const out = await headlessTriage({</span> +<span class="a">+ model: typeof flags.model === 'string' ? flags.model : undefined,</span> +<span class="a">+ limit: Number(flags.limit) || 20,</span> +<span class="a">+ budget: Number(flags.budget) || 1,</span> +<span class="a">+ dryRun: !!flags['dry-run'],</span> +<span class="a">+ });</span> +<span class="a">+ if (out.message) {</span> +<span class="a">+ console.log(out.message);</span> +<span class="a">+ break;</span> +<span class="a">+ }</span> +<span class="a">+ const ok = out.results.filter((r) => !r.error);</span> +<span class="a">+ const spend = out.tokens ? ` (${out.tokens.input} in / ${out.tokens.output} out tokens, ~$${Number(out.cost || 0).toFixed(4)})` : '';</span> +<span class="a">+ console.log(`Triaged ${ok.length}${spend}:`);</span> +<span class="a">+ for (const r of ok) console.log(` #${r.id} ${r.verdict.padEnd(5)} ${r.verdict === 'skip' ? '' : `${r.model.padEnd(6)} ${r.size.toUpperCase().padEnd(2)} `}${clip(r.title, 60)}`);</span> +<span class="a">+ for (const r of out.results.filter((x) => x.error)) console.log(` #${r.id} not saved: ${r.error}`);</span> +<span class="a">+ break;</span> +<span class="a">+ }</span> +<span class="a">+ case 'export': {</span> +<span class="a">+ const md = render.renderMarkdown(store.load());</span> +<span class="a">+ if (words[0]) {</span> +<span class="a">+ fs.writeFileSync(words[0], md);</span> +<span class="a">+ console.log(`Wrote ${words[0]}`);</span> +<span class="a">+ } else process.stdout.write(md);</span> +<span class="a">+ break;</span> +<span class="a">+ }</span> +<span class="a">+ case 'path':</span> +<span class="a">+ console.log(store.dbPath());</span> +<span class="a">+ break;</span> +<span class="a">+ case 'version':</span> +<span class="a">+ case '--version':</span> +<span class="a">+ case '-v':</span> +<span class="a">+ console.log(JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version);</span> +<span class="a">+ break;</span> +<span class="a">+ case 'help':</span> +<span class="a">+ case '--help':</span> +<span class="a">+ case '-h':</span> +<span class="a">+ console.log(HELP);</span> +<span class="a">+ break;</span> +<span class="a">+ default:</span> +<span class="a">+ fail(`unknown command "${command}". Try: ideamine help`);</span> +<span class="a">+ }</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+main().catch((e) => fail(e.message));</span> +diff --git a/hooks/hooks.json b/hooks/hooks.json +new file mode 100644 +index 0000000..88c4d5d +--- /dev/null ++++ b/hooks/hooks.json +<span class="h">@@ -0,0 +1,17 @@</span> +<span class="a">+{</span> +<span class="a">+ "description": "ideamine: answer /idea and /ideas locally, so they never call the model",</span> +<span class="a">+ "hooks": {</span> +<span class="a">+ "UserPromptSubmit": [</span> +<span class="a">+ {</span> +<span class="a">+ "hooks": [</span> +<span class="a">+ {</span> +<span class="a">+ "type": "command",</span> +<span class="a">+ "command": "node",</span> +<span class="a">+ "args": ["${CLAUDE_PLUGIN_ROOT}/bin/ideamine.js", "hook"],</span> +<span class="a">+ "timeout": 10</span> +<span class="a">+ }</span> +<span class="a">+ ]</span> +<span class="a">+ }</span> +<span class="a">+ ]</span> +<span class="a">+ }</span> +<span class="a">+}</span> +diff --git a/package.json b/package.json +new file mode 100644 +index 0000000..5523c04 +--- /dev/null ++++ b/package.json +<span class="h">@@ -0,0 +1,43 @@</span> +<span class="a">+{</span> +<span class="a">+ "name": "ideamine",</span> +<span class="a">+ "version": "0.1.0",</span> +<span class="a">+ "description": "An idea inbox for Claude Code. /idea saves an idea without calling the model; Claude later triages each idea and picks the cheapest model that can build it.",</span> +<span class="a">+ "type": "module",</span> +<span class="a">+ "bin": {</span> +<span class="a">+ "ideamine": "bin/ideamine.js"</span> +<span class="a">+ },</span> +<span class="a">+ "files": [</span> +<span class="a">+ "bin",</span> +<span class="a">+ "src",</span> +<span class="a">+ "skills",</span> +<span class="a">+ "hooks",</span> +<span class="a">+ ".claude-plugin",</span> +<span class="a">+ ".mcp.json",</span> +<span class="a">+ "README.md",</span> +<span class="a">+ "LICENSE"</span> +<span class="a">+ ],</span> +<span class="a">+ "engines": {</span> +<span class="a">+ "node": ">=18"</span> +<span class="a">+ },</span> +<span class="a">+ "scripts": {</span> +<span class="a">+ "test": "node --test"</span> +<span class="a">+ },</span> +<span class="a">+ "repository": {</span> +<span class="a">+ "type": "git",</span> +<span class="a">+ "url": "git+https://github.com/equwal/ideamine.git"</span> +<span class="a">+ },</span> +<span class="a">+ "homepage": "https://github.com/equwal/ideamine#readme",</span> +<span class="a">+ "bugs": "https://github.com/equwal/ideamine/issues",</span> +<span class="a">+ "author": "Spenser Truex",</span> +<span class="a">+ "license": "MIT",</span> +<span class="a">+ "keywords": [</span> +<span class="a">+ "claude",</span> +<span class="a">+ "claude-code",</span> +<span class="a">+ "claude-code-plugin",</span> +<span class="a">+ "mcp",</span> +<span class="a">+ "mcp-server",</span> +<span class="a">+ "ideas",</span> +<span class="a">+ "backlog",</span> +<span class="a">+ "vibecoding"</span> +<span class="a">+ ]</span> +<span class="a">+}</span> +diff --git a/skills/idea-go/SKILL.md b/skills/idea-go/SKILL.md +new file mode 100644 +index 0000000..2f928f6 +--- /dev/null ++++ b/skills/idea-go/SKILL.md +<span class="h">@@ -0,0 +1,14 @@</span> +<span class="a">+---</span> +<span class="a">+name: idea-go</span> +<span class="a">+description: Build the top ideamine idea, or a given #id, with the Claude model it was triaged for.</span> +<span class="a">+argument-hint: "[id]"</span> +<span class="a">+disable-model-invocation: true</span> +<span class="a">+allowed-tools: mcp__plugin_ideamine_ideamine__idea_next, mcp__plugin_ideamine_ideamine__idea_update</span> +<span class="a">+---</span> +<span class="a">+</span> +<span class="a">+Build one idea from the archive, using the model recommended for it. Requested idea: $ARGUMENTS (empty means the best pick).</span> +<span class="a">+</span> +<span class="a">+1. Call `idea_next`. Pass `id` if an id is given. If nothing is ready, say so and suggest /idea-triage. If the idea has no triage yet, ask whether to triage it first, then stop.</span> +<span class="a">+2. Tell the user the title and the recommended model in one line. Then call `idea_update` with status "doing".</span> +<span class="a">+3. Give the build to ONE subagent (Agent tool, general-purpose). Set its `model` to the recommended model (haiku, sonnet, opus, or fable). The purpose of this command is to send each idea to the cheapest model that can do it. In the subagent prompt, include the brief, the original note, and the project directory. Tell the subagent to work only in that directory, and to report what it changed and what is left to do.</span> +<span class="a">+4. When the subagent returns, call `idea_update`. Use status "done" with a one-line note. If work remains, keep status "doing" and write a note that says what remains. Then summarize in 2-3 lines.</span> +diff --git a/skills/idea-triage/SKILL.md b/skills/idea-triage/SKILL.md +new file mode 100644 +index 0000000..93cc9de +--- /dev/null ++++ b/skills/idea-triage/SKILL.md +<span class="h">@@ -0,0 +1,12 @@</span> +<span class="a">+---</span> +<span class="a">+name: idea-triage</span> +<span class="a">+description: Triage the ideamine inbox. Decide which ideas are worth doing, and pick the cheapest Claude model that can build each one.</span> +<span class="a">+argument-hint: "[max ideas]"</span> +<span class="a">+disable-model-invocation: true</span> +<span class="a">+allowed-tools: mcp__plugin_ideamine_ideamine__idea_triage</span> +<span class="a">+---</span> +<span class="a">+</span> +<span class="a">+1. Call `idea_triage` with `headless: true`. If a number is given here, pass it as `limit`: $ARGUMENTS</span> +<span class="a">+ This runs the triage in a separate, minimal Claude Code call. It does not use your context, so it costs little whatever model you are on.</span> +<span class="a">+2. If the call succeeds, show its summary as returned and name the single best pick. Stop.</span> +<span class="a">+3. If the call fails (for example, the Claude Code CLI cannot be found), do the triage yourself. Call `idea_triage` with no verdicts to get the rubric and the ideas. Judge every idea. Save all verdicts in ONE `idea_triage` call, with your model name as `by`. Then show the summary.</span> +diff --git a/skills/idea/SKILL.md b/skills/idea/SKILL.md +new file mode 100644 +index 0000000..a3de684 +--- /dev/null ++++ b/skills/idea/SKILL.md +<span class="h">@@ -0,0 +1,13 @@</span> +<span class="a">+---</span> +<span class="a">+name: idea</span> +<span class="a">+description: Save an idea to the ideamine archive for later without derailing the current task. The ideamine hook usually answers this instantly, with no model call.</span> +<span class="a">+argument-hint: <your idea></span> +<span class="a">+disable-model-invocation: true</span> +<span class="a">+allowed-tools: mcp__plugin_ideamine_ideamine__idea_add</span> +<span class="a">+---</span> +<span class="a">+</span> +<span class="a">+Save this idea with the `idea_add` tool. Pass it verbatim as `text`:</span> +<span class="a">+</span> +<span class="a">+$ARGUMENTS</span> +<span class="a">+</span> +<span class="a">+Only save it. Do not plan, discuss, or start building it. Reply with the one-line result from the tool, then continue what you were doing before.</span> +diff --git a/skills/ideas/SKILL.md b/skills/ideas/SKILL.md +new file mode 100644 +index 0000000..7e0e539 +--- /dev/null ++++ b/skills/ideas/SKILL.md +<span class="h">@@ -0,0 +1,11 @@</span> +<span class="a">+---</span> +<span class="a">+name: ideas</span> +<span class="a">+description: Show the ideamine board or one idea, or answer a question about your saved ideas. The ideamine hook answers board and edit requests instantly, with no model call.</span> +<span class="a">+argument-hint: "[open|inbox|do|maybe|done|all|here] | #id | done|drop|start|reopen <id> | a question"</span> +<span class="a">+disable-model-invocation: true</span> +<span class="a">+allowed-tools: mcp__plugin_ideamine_ideamine__idea_list, mcp__plugin_ideamine_ideamine__idea_update</span> +<span class="a">+---</span> +<span class="a">+</span> +<span class="a">+Request: $ARGUMENTS</span> +<span class="a">+</span> +<span class="a">+Call `idea_list`. Pass `filter`, `id`, `query`, or `here` when the request asks for them. Then answer the request from the result. If the request is empty or names only a filter, show the board exactly as returned. To change a status, use `idea_update`. Keep the answer short. Do not start building an idea.</span> +diff --git a/src/claude.js b/src/claude.js +new file mode 100644 +index 0000000..cc2bb15 +--- /dev/null ++++ b/src/claude.js +<span class="h">@@ -0,0 +1,139 @@</span> +<span class="a">+// Running the Claude Code CLI from ideamine: headless triage, and launching a session to build an idea.</span> +<span class="a">+</span> +<span class="a">+import { spawn, spawnSync } from 'node:child_process';</span> +<span class="a">+import { BATCH_SCHEMA, pendingIdeas, triagePrompt } from './rubric.js';</span> +<span class="a">+import { applyTriage, load, normalizeModel } from './store.js';</span> +<span class="a">+</span> +<span class="a">+/** Claude Code executable: explicit override, else the one running us (desktop app), else PATH. */</span> +<span class="a">+export function claudeBin() {</span> +<span class="a">+ return process.env.IDEAMINE_CLAUDE_BIN || process.env.CLAUDE_CODE_EXECPATH || 'claude';</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+function command(args) {</span> +<span class="a">+ const bin = claudeBin();</span> +<span class="a">+ // A .js file is run with node, which keeps a stand-in CLI portable (used by the tests).</span> +<span class="a">+ return /\.[cm]?js$/i.test(bin) ? [process.execPath, [bin, ...args]] : [bin, args];</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+/**</span> +<span class="a">+ * Environment for a separate, independent Claude Code process: drop the variables that tie a</span> +<span class="a">+ * child to the session that started us (nesting guard, host messaging, the parent's effort).</span> +<span class="a">+ */</span> +<span class="a">+function childEnv() {</span> +<span class="a">+ const env = { ...process.env };</span> +<span class="a">+ for (const key of Object.keys(env)) {</span> +<span class="a">+ if (</span> +<span class="a">+ key === 'CLAUDECODE' ||</span> +<span class="a">+ key === 'CLAUDE_PID' ||</span> +<span class="a">+ key === 'CLAUDE_EFFORT' ||</span> +<span class="a">+ key === 'AI_AGENT' ||</span> +<span class="a">+ /^CLAUDE_CODE_(ENTRYPOINT|CHILD_SESSION|HOST_SESSION_ID|SESSION_ID|SESSION_ATTENDED|MESSAGING_.*|SDK_.*|EXECPATH)$/.test(key)</span> +<span class="a">+ ) {</span> +<span class="a">+ delete env[key];</span> +<span class="a">+ }</span> +<span class="a">+ }</span> +<span class="a">+ return env;</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+function parseLooseJson(text) {</span> +<span class="a">+ const s = String(text || '').replace(/^\s*```(?:json)?\s*|\s*```\s*$/g, '');</span> +<span class="a">+ const start = s.indexOf('{');</span> +<span class="a">+ const end = s.lastIndexOf('}');</span> +<span class="a">+ if (start < 0 || end < start) return null;</span> +<span class="a">+ try {</span> +<span class="a">+ return JSON.parse(s.slice(start, end + 1));</span> +<span class="a">+ } catch {</span> +<span class="a">+ return null;</span> +<span class="a">+ }</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+function run(args, input, timeoutMs) {</span> +<span class="a">+ const [bin, argv] = command(args);</span> +<span class="a">+ return new Promise((resolve, reject) => {</span> +<span class="a">+ const child = spawn(bin, argv, { env: childEnv(), windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'] });</span> +<span class="a">+ let stdout = '';</span> +<span class="a">+ let stderr = '';</span> +<span class="a">+ const timer = setTimeout(() => child.kill(), timeoutMs);</span> +<span class="a">+ child.stdout.on('data', (d) => (stdout += d));</span> +<span class="a">+ child.stderr.on('data', (d) => (stderr += d));</span> +<span class="a">+ child.on('error', (e) => {</span> +<span class="a">+ clearTimeout(timer);</span> +<span class="a">+ reject(e.code === 'ENOENT' ? new Error(`Claude Code CLI not found ("${bin}"). Install it or set IDEAMINE_CLAUDE_BIN.`) : e);</span> +<span class="a">+ });</span> +<span class="a">+ child.on('close', (code) => {</span> +<span class="a">+ clearTimeout(timer);</span> +<span class="a">+ resolve({ code, stdout, stderr });</span> +<span class="a">+ });</span> +<span class="a">+ child.stdin.end(input);</span> +<span class="a">+ });</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+/**</span> +<span class="a">+ * Triage the inbox with a one-shot `claude -p` call: no tools, no MCP servers, no settings, a</span> +<span class="a">+ * two-line system prompt, and JSON-schema output. Runs on the user's normal Claude Code login and</span> +<span class="a">+ * costs about 400 tokens per idea, whatever model the calling session uses.</span> +<span class="a">+ */</span> +<span class="a">+export async function headlessTriage({ model = process.env.IDEAMINE_TRIAGE_MODEL || 'sonnet', limit = 20, dryRun = false, budget = 1 } = {}) {</span> +<span class="a">+ const alias = normalizeModel(model) || model;</span> +<span class="a">+ const db = load();</span> +<span class="a">+ const pending = pendingIdeas(db, { limit });</span> +<span class="a">+ if (!pending.length) return { message: 'Nothing to triage: the inbox is empty.' };</span> +<span class="a">+</span> +<span class="a">+ const prompt = `${triagePrompt(db, pending)}\n\nReturn one verdict for every idea listed above.`;</span> +<span class="a">+ // Skipping settings drops hooks, plugins, and skill listings: ~4k fewer input tokens per call.</span> +<span class="a">+ // Set IDEAMINE_SETTING_SOURCES=user if your login depends on settings.json (apiKeyHelper, env).</span> +<span class="a">+ const args = [</span> +<span class="a">+ '-p',</span> +<span class="a">+ '--model', alias,</span> +<span class="a">+ '--output-format', 'json',</span> +<span class="a">+ '--json-schema', JSON.stringify(BATCH_SCHEMA),</span> +<span class="a">+ '--tools', '',</span> +<span class="a">+ '--strict-mcp-config',</span> +<span class="a">+ '--setting-sources', process.env.IDEAMINE_SETTING_SOURCES ?? '',</span> +<span class="a">+ '--no-session-persistence',</span> +<span class="a">+ '--max-budget-usd', String(budget),</span> +<span class="a">+ '--system-prompt', 'You triage a developer\'s backlog of ideas. Follow the rubric exactly and answer only with the requested JSON.',</span> +<span class="a">+ ];</span> +<span class="a">+ if (alias !== 'haiku') args.push('--effort', 'low');</span> +<span class="a">+ if (dryRun) {</span> +<span class="a">+ const shown = args.map((a) => (/[\s"{]/.test(a) || !a ? JSON.stringify(a) : a)).join(' ');</span> +<span class="a">+ return { message: `${claudeBin()} ${shown}\n\n${prompt}` };</span> +<span class="a">+ }</span> +<span class="a">+</span> +<span class="a">+ const res = await run(args, prompt, 5 * 60 * 1000);</span> +<span class="a">+ let out;</span> +<span class="a">+ try {</span> +<span class="a">+ out = JSON.parse(res.stdout);</span> +<span class="a">+ } catch {</span> +<span class="a">+ const detail = (res.stderr || res.stdout || '').trim().split(/\r?\n/).slice(-5).join('\n');</span> +<span class="a">+ throw new Error(`claude exited with code ${res.code}: ${detail || 'no output'}`);</span> +<span class="a">+ }</span> +<span class="a">+ if (out.is_error) throw new Error(`claude: ${out.result || out.subtype || 'error'}`);</span> +<span class="a">+ const data = out.structured_output ?? parseLooseJson(out.result);</span> +<span class="a">+ if (!Array.isArray(data?.verdicts)) throw new Error('claude answered without verdicts');</span> +<span class="a">+ const results = applyTriage(data.verdicts, { by: `${alias} (headless)` });</span> +<span class="a">+ const u = out.usage || {};</span> +<span class="a">+ const input = (u.input_tokens || 0) + (u.cache_creation_input_tokens || 0) + (u.cache_read_input_tokens || 0);</span> +<span class="a">+ return { results, model: alias, cost: out.total_cost_usd, tokens: { input, output: u.output_tokens || 0 } };</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+/** The opening prompt for a fresh session that builds one idea. */</span> +<span class="a">+export function buildPrompt(idea) {</span> +<span class="a">+ const t = idea.triage;</span> +<span class="a">+ const lines = [`Build idea #${idea.id} from my ideamine archive: ${idea.title}`, ''];</span> +<span class="a">+ if (t?.brief) lines.push(t.brief, '');</span> +<span class="a">+ lines.push('My original note:', idea.text, '');</span> +<span class="a">+ lines.push(</span> +<span class="a">+ `When you finish, record the outcome with the ideamine idea_update tool (id ${idea.id}, status "done", a one-line note), ` +</span> +<span class="a">+ `or run: ideamine done ${idea.id} "<one-line outcome>"`,</span> +<span class="a">+ );</span> +<span class="a">+ return lines.join('\n');</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+/** Start an interactive Claude Code session on the recommended model. */</span> +<span class="a">+export function launchSession({ model, prompt, cwd }) {</span> +<span class="a">+ const [bin, argv] = command(['--model', model, prompt]);</span> +<span class="a">+ const res = spawnSync(bin, argv, { cwd, stdio: 'inherit', env: childEnv() });</span> +<span class="a">+ if (res.error) throw res.error;</span> +<span class="a">+ return res.status ?? 0;</span> +<span class="a">+}</span> +diff --git a/src/hook.js b/src/hook.js +new file mode 100644 +index 0000000..c5b6025 +--- /dev/null ++++ b/src/hook.js +<span class="h">@@ -0,0 +1,97 @@</span> +<span class="a">+// UserPromptSubmit hook: answers /idea and /ideas locally and blocks the prompt, so the model is</span> +<span class="a">+// never called. That makes capture free, instant, and possible even when the session is out of</span> +<span class="a">+// usage. Every other prompt passes through untouched.</span> +<span class="a">+</span> +<span class="a">+import { renderAdded, renderBoard, renderIdea } from './render.js';</span> +<span class="a">+import { addIdeas, FILTERS, findIdea, lane, load, updateIdea } from './store.js';</span> +<span class="a">+import { clip, splitIdeas } from './text.js';</span> +<span class="a">+</span> +<span class="a">+// `/idea ...`, `/ideas ...`, and the plugin-qualified `/ideamine:idea ...` forms.</span> +<span class="a">+const COMMAND = /^\s*\/(?:ideamine:)?(ideas?)(?=\s|$)([\s\S]*)$/i;</span> +<span class="a">+const EDIT_VERBS = { done: 'done', finish: 'done', drop: 'dropped', doing: 'doing', start: 'doing', reopen: 'reopen' };</span> +<span class="a">+</span> +<span class="a">+const USAGE =</span> +<span class="a">+ 'Usage: /idea <your idea> saves it without calling the model (a bulleted list saves one idea per bullet)\n' +</span> +<span class="a">+ ' /ideas [open|inbox|do|maybe|skip|doing|done|dropped|all|here] · /ideas #12 · /ideas done|drop|start|reopen 12 [note]';</span> +<span class="a">+</span> +<span class="a">+function readStdin() {</span> +<span class="a">+ return new Promise((resolve) => {</span> +<span class="a">+ if (process.stdin.isTTY) return resolve('');</span> +<span class="a">+ let data = '';</span> +<span class="a">+ process.stdin.setEncoding('utf8');</span> +<span class="a">+ process.stdin.on('data', (chunk) => (data += chunk));</span> +<span class="a">+ process.stdin.on('end', () => resolve(data));</span> +<span class="a">+ process.stdin.on('error', () => resolve(data));</span> +<span class="a">+ });</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+/** Handle one prompt. Returns the text to show the user, or null to let the prompt through. */</span> +<span class="a">+export function handlePrompt(prompt, { cwd = process.cwd(), session = null } = {}) {</span> +<span class="a">+ const m = COMMAND.exec(prompt || '');</span> +<span class="a">+ if (!m) return null;</span> +<span class="a">+ const command = m[1].toLowerCase();</span> +<span class="a">+ const arg = m[2].trim();</span> +<span class="a">+</span> +<span class="a">+ if (command === 'idea') {</span> +<span class="a">+ if (!arg) return USAGE;</span> +<span class="a">+ const results = addIdeas(splitIdeas(arg), { source: 'hook', project: cwd, session });</span> +<span class="a">+ return renderAdded(results, load());</span> +<span class="a">+ }</span> +<span class="a">+</span> +<span class="a">+ // /ideas</span> +<span class="a">+ const words = arg.split(/\s+/).filter(Boolean);</span> +<span class="a">+ const db = load();</span> +<span class="a">+</span> +<span class="a">+ if (!words.length) return renderBoard(db, { cwd, hints: true });</span> +<span class="a">+</span> +<span class="a">+ if (words.length === 1 && /^#?\d+$/.test(words[0])) {</span> +<span class="a">+ const idea = findIdea(db, words[0]);</span> +<span class="a">+ return idea ? renderIdea(idea) : `No idea ${words[0].startsWith('#') ? words[0] : '#' + words[0]}.`;</span> +<span class="a">+ }</span> +<span class="a">+</span> +<span class="a">+ const verb = EDIT_VERBS[words[0]?.toLowerCase()];</span> +<span class="a">+ if (verb && words[1] && /^#?\d+$/.test(words[1])) {</span> +<span class="a">+ const note = words.slice(2).join(' ');</span> +<span class="a">+ const idea = updateIdea(words[1], { status: verb, note: note || undefined });</span> +<span class="a">+ return `✓ #${idea.id} ${clip(idea.title, 60)} → ${lane(idea)}${note ? ' (note added)' : ''}`;</span> +<span class="a">+ }</span> +<span class="a">+</span> +<span class="a">+ const lower = words.map((w) => w.toLowerCase());</span> +<span class="a">+ const filters = lower.filter((w) => FILTERS.includes(w));</span> +<span class="a">+ const here = lower.includes('here');</span> +<span class="a">+ if (filters.length + (here ? 1 : 0) === lower.length && filters.length <= 1) {</span> +<span class="a">+ return renderBoard(db, { filter: filters[0] || 'open', project: here ? cwd : null, cwd, hints: true });</span> +<span class="a">+ }</span> +<span class="a">+ if (lower[0] === 'help') return USAGE;</span> +<span class="a">+</span> +<span class="a">+ return null; // a question about the ideas: let the model answer it (via the /ideas skill)</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+export async function runHook() {</span> +<span class="a">+ const raw = await readStdin();</span> +<span class="a">+ let input;</span> +<span class="a">+ try {</span> +<span class="a">+ input = JSON.parse(raw);</span> +<span class="a">+ } catch {</span> +<span class="a">+ return; // not a hook payload; stay out of the way</span> +<span class="a">+ }</span> +<span class="a">+ let message;</span> +<span class="a">+ try {</span> +<span class="a">+ message = handlePrompt(typeof input?.prompt === 'string' ? input.prompt : '', {</span> +<span class="a">+ cwd: input.cwd || process.cwd(),</span> +<span class="a">+ session: input.session_id || null,</span> +<span class="a">+ });</span> +<span class="a">+ } catch (e) {</span> +<span class="a">+ // Never swallow the user's text: let the prompt through so the /idea skill can save it via MCP.</span> +<span class="a">+ process.stderr.write(`ideamine: ${e.message}\n`);</span> +<span class="a">+ return;</span> +<span class="a">+ }</span> +<span class="a">+ if (message == null) return;</span> +<span class="a">+ // Blocking ends the turn before any API request. suppressOriginalPrompt keeps Claude Code from</span> +<span class="a">+ // echoing the command back under our message.</span> +<span class="a">+ process.stdout.write(JSON.stringify({</span> +<span class="a">+ decision: 'block',</span> +<span class="a">+ reason: message,</span> +<span class="a">+ hookSpecificOutput: { hookEventName: 'UserPromptSubmit', suppressOriginalPrompt: true },</span> +<span class="a">+ }));</span> +<span class="a">+}</span> +diff --git a/src/mcp.js b/src/mcp.js +new file mode 100644 +index 0000000..d41e83f +--- /dev/null ++++ b/src/mcp.js +<span class="h">@@ -0,0 +1,309 @@</span> +<span class="a">+// Minimal MCP server over stdio (newline-delimited JSON-RPC 2.0). No dependencies, so the plugin</span> +<span class="a">+// runs straight from a git checkout with nothing to install.</span> +<span class="a">+</span> +<span class="a">+import fs from 'node:fs';</span> +<span class="a">+import readline from 'node:readline';</span> +<span class="a">+import { fileURLToPath } from 'node:url';</span> +<span class="a">+import { headlessTriage } from './claude.js';</span> +<span class="a">+import { renderAdded, renderBoard, renderIdea } from './render.js';</span> +<span class="a">+import { BATCH_SCHEMA, MODELS, pendingIdeas, triagePrompt } from './rubric.js';</span> +<span class="a">+import { addIdeas, applyTriage, counts, FILTERS, findIdea, lane, load, pickNext, STATUSES, updateIdea } from './store.js';</span> +<span class="a">+import { clip, splitIdeas } from './text.js';</span> +<span class="a">+</span> +<span class="a">+const ROOT = new URL('..', import.meta.url);</span> +<span class="a">+const VERSION = JSON.parse(fs.readFileSync(new URL('package.json', ROOT), 'utf8')).version;</span> +<span class="a">+const PROTOCOL_VERSIONS = ['2025-11-25', '2025-06-18', '2025-03-26', '2024-11-05'];</span> +<span class="a">+</span> +<span class="a">+const INSTRUCTIONS =</span> +<span class="a">+ 'ideamine is the user\'s idea archive, shared by every session, project, and model. When the user tosses out an ' +</span> +<span class="a">+ 'idea for later ("idea: ...", "save this idea", "someday we should ..."), save it with idea_add and continue the ' +</span> +<span class="a">+ 'current task. Do not start building a saved idea unless the user asks.';</span> +<span class="a">+</span> +<span class="a">+const currentProject = () => process.env.CLAUDE_PROJECT_DIR || process.cwd();</span> +<span class="a">+</span> +<span class="a">+const MODEL_ENUM = MODELS.map((m) => m.alias);</span> +<span class="a">+</span> +<span class="a">+const TOOLS = [</span> +<span class="a">+ {</span> +<span class="a">+ name: 'idea_add',</span> +<span class="a">+ description:</span> +<span class="a">+ 'Save an idea to the ideamine archive. Use when the user shares an idea to keep for later. Only save it: ' +</span> +<span class="a">+ 'do not plan or build it, and continue what you were doing. A bulleted list becomes one idea per bullet.',</span> +<span class="a">+ inputSchema: {</span> +<span class="a">+ type: 'object',</span> +<span class="a">+ properties: {</span> +<span class="a">+ text: { type: 'string', description: 'The idea, verbatim.' },</span> +<span class="a">+ tags: { type: 'array', items: { type: 'string' } },</span> +<span class="a">+ project: { type: 'string', description: 'Project directory the idea belongs to. Default: current project.' },</span> +<span class="a">+ },</span> +<span class="a">+ required: ['text'],</span> +<span class="a">+ additionalProperties: false,</span> +<span class="a">+ },</span> +<span class="a">+ annotations: { title: 'Save idea', readOnlyHint: false, destructiveHint: false, openWorldHint: false },</span> +<span class="a">+ },</span> +<span class="a">+ {</span> +<span class="a">+ name: 'idea_list',</span> +<span class="a">+ description: 'Show the idea board, or one idea in full when id is given.',</span> +<span class="a">+ inputSchema: {</span> +<span class="a">+ type: 'object',</span> +<span class="a">+ properties: {</span> +<span class="a">+ id: { type: 'integer', description: 'Show this idea in full.' },</span> +<span class="a">+ filter: { type: 'string', enum: FILTERS, description: 'Default: open (doing, do, maybe, inbox).' },</span> +<span class="a">+ query: { type: 'string', description: 'Only ideas containing all of these words.' },</span> +<span class="a">+ here: { type: 'boolean', description: 'Only ideas from the current project.' },</span> +<span class="a">+ limit: { type: 'integer' },</span> +<span class="a">+ },</span> +<span class="a">+ additionalProperties: false,</span> +<span class="a">+ },</span> +<span class="a">+ annotations: { title: 'List ideas', readOnlyHint: true, openWorldHint: false },</span> +<span class="a">+ },</span> +<span class="a">+ {</span> +<span class="a">+ name: 'idea_triage',</span> +<span class="a">+ description:</span> +<span class="a">+ 'Triage ideas: decide which are worth doing and the cheapest model that can build each. headless=true ' +</span> +<span class="a">+ 'does it in a separate minimal Claude Code call (cheapest; your context is not used). Otherwise call with ' +</span> +<span class="a">+ 'no verdicts to get the rubric and the untriaged ideas, then call once more with all verdicts.',</span> +<span class="a">+ inputSchema: {</span> +<span class="a">+ type: 'object',</span> +<span class="a">+ properties: {</span> +<span class="a">+ headless: { type: 'boolean', description: 'Triage the inbox in a separate low-cost `claude -p` call.' },</span> +<span class="a">+ model: { type: 'string', enum: MODEL_ENUM, description: 'Model for headless triage (default sonnet).' },</span> +<span class="a">+ verdicts: BATCH_SCHEMA.properties.verdicts,</span> +<span class="a">+ ids: { type: 'array', items: { type: 'integer' }, description: 'Re-triage these ideas instead of the inbox.' },</span> +<span class="a">+ limit: { type: 'integer', description: 'Max ideas (default 20 headless, 30 otherwise).' },</span> +<span class="a">+ by: { type: 'string', description: 'Your model name, recorded with the verdicts.' },</span> +<span class="a">+ },</span> +<span class="a">+ additionalProperties: false,</span> +<span class="a">+ },</span> +<span class="a">+ annotations: { title: 'Triage ideas', readOnlyHint: false, destructiveHint: false, openWorldHint: false },</span> +<span class="a">+ },</span> +<span class="a">+ {</span> +<span class="a">+ name: 'idea_update',</span> +<span class="a">+ description: 'Change an idea: status, text, title, tags, model override, project, or append a note.',</span> +<span class="a">+ inputSchema: {</span> +<span class="a">+ type: 'object',</span> +<span class="a">+ properties: {</span> +<span class="a">+ id: { type: 'integer' },</span> +<span class="a">+ status: { type: 'string', enum: [...STATUSES, 'reopen'] },</span> +<span class="a">+ note: { type: 'string', description: 'Appended to the idea\'s notes, e.g. the outcome.' },</span> +<span class="a">+ title: { type: 'string' },</span> +<span class="a">+ text: { type: 'string' },</span> +<span class="a">+ tags: { type: 'array', items: { type: 'string' } },</span> +<span class="a">+ model: { type: 'string', enum: MODEL_ENUM },</span> +<span class="a">+ project: { type: 'string' },</span> +<span class="a">+ },</span> +<span class="a">+ required: ['id'],</span> +<span class="a">+ additionalProperties: false,</span> +<span class="a">+ },</span> +<span class="a">+ annotations: { title: 'Update idea', readOnlyHint: false, destructiveHint: false, openWorldHint: false },</span> +<span class="a">+ },</span> +<span class="a">+ {</span> +<span class="a">+ name: 'idea_next',</span> +<span class="a">+ description:</span> +<span class="a">+ 'Pick the idea to build next (verdict "do", best value per effort, current project first), or fetch one ' +</span> +<span class="a">+ 'by id. Returns its brief, project, and the model recommended to build it.',</span> +<span class="a">+ inputSchema: {</span> +<span class="a">+ type: 'object',</span> +<span class="a">+ properties: {</span> +<span class="a">+ id: { type: 'integer' },</span> +<span class="a">+ here: { type: 'boolean', description: 'Only consider ideas from the current project.' },</span> +<span class="a">+ },</span> +<span class="a">+ additionalProperties: false,</span> +<span class="a">+ },</span> +<span class="a">+ annotations: { title: 'Next idea', readOnlyHint: true, openWorldHint: false },</span> +<span class="a">+ },</span> +<span class="a">+];</span> +<span class="a">+</span> +<span class="a">+// Slash commands for clients that load this server without the plugin (the plugin ships skills instead).</span> +<span class="a">+// The prompt text is the matching SKILL.md body, so both stay in sync.</span> +<span class="a">+const PROMPTS = [</span> +<span class="a">+ { name: 'idea', skill: 'idea', description: 'Save an idea for later', arguments: [{ name: 'text', required: true }] },</span> +<span class="a">+ { name: 'ideas', skill: 'ideas', description: 'Show the idea board', arguments: [{ name: 'request', required: false }] },</span> +<span class="a">+ { name: 'idea-triage', skill: 'idea-triage', description: 'Score the untriaged ideas', arguments: [] },</span> +<span class="a">+ { name: 'idea-go', skill: 'idea-go', description: 'Build the top idea with its model', arguments: [{ name: 'id', required: false }] },</span> +<span class="a">+];</span> +<span class="a">+</span> +<span class="a">+function skillBody(name, args) {</span> +<span class="a">+ const file = new URL(`skills/${name}/SKILL.md`, ROOT);</span> +<span class="a">+ const text = fs.readFileSync(fileURLToPath(file), 'utf8').replace(/^---[\s\S]*?\n---\s*\n/, '');</span> +<span class="a">+ return text.replaceAll('$ARGUMENTS', args || '').trim();</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+function summarizeTriage(results, how = '') {</span> +<span class="a">+ const ok = results.filter((r) => !r.error);</span> +<span class="a">+ const tally = ['do', 'maybe', 'skip'].map((v) => `${ok.filter((r) => r.verdict === v).length} ${v}`).join(' · ');</span> +<span class="a">+ const lines = [`Saved ${ok.length} verdict${ok.length === 1 ? '' : 's'}: ${tally}${how ? ` (${how})` : ''}`];</span> +<span class="a">+ for (const r of ok) lines.push(` #${r.id} ${r.verdict}${r.verdict === 'skip' ? '' : ` · ${r.model} · ${r.size.toUpperCase()}`} ${clip(r.title, 60)}`);</span> +<span class="a">+ for (const r of results.filter((x) => x.error)) lines.push(` #${r.id} not saved: ${r.error}`);</span> +<span class="a">+ const left = counts(load()).inbox;</span> +<span class="a">+ if (left) lines.push(`${left} still in inbox.`);</span> +<span class="a">+ return lines.join('\n');</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+// ---------------------------------------------------------------------------------------------</span> +<span class="a">+// Tool handlers return plain text for the model.</span> +<span class="a">+</span> +<span class="a">+const handlers = {</span> +<span class="a">+ idea_add({ text, tags, project }) {</span> +<span class="a">+ const results = addIdeas(splitIdeas(text || ''), { source: 'mcp', project: project || currentProject(), tags });</span> +<span class="a">+ return renderAdded(results, load());</span> +<span class="a">+ },</span> +<span class="a">+</span> +<span class="a">+ idea_list({ id, filter = 'open', query = '', here = false, limit = 0 }) {</span> +<span class="a">+ const db = load();</span> +<span class="a">+ if (id != null) {</span> +<span class="a">+ const idea = findIdea(db, id);</span> +<span class="a">+ if (!idea) throw new Error(`no idea #${id}`);</span> +<span class="a">+ return renderIdea(idea);</span> +<span class="a">+ }</span> +<span class="a">+ return renderBoard(db, { filter, query, limit, project: here ? currentProject() : null, cwd: currentProject() });</span> +<span class="a">+ },</span> +<span class="a">+</span> +<span class="a">+ async idea_triage({ verdicts, ids, limit, by, headless = false, model }) {</span> +<span class="a">+ if (headless) {</span> +<span class="a">+ const out = await headlessTriage({ model, limit: limit || 20 });</span> +<span class="a">+ return out.message || summarizeTriage(out.results, `${out.model}, ${out.tokens.input} in / ${out.tokens.output} out tokens`);</span> +<span class="a">+ }</span> +<span class="a">+ if (Array.isArray(verdicts) && verdicts.length) {</span> +<span class="a">+ return summarizeTriage(applyTriage(verdicts, { by: by ? clip(by, 40) : 'claude' }));</span> +<span class="a">+ }</span> +<span class="a">+ limit ||= 30;</span> +<span class="a">+ const db = load();</span> +<span class="a">+ const pending = pendingIdeas(db, { ids, limit });</span> +<span class="a">+ if (!pending.length) return 'Nothing to triage: the inbox is empty.';</span> +<span class="a">+ return (</span> +<span class="a">+ triagePrompt(db, pending) +</span> +<span class="a">+ '\n\nSave every verdict in ONE idea_triage call: ' +</span> +<span class="a">+ '{"verdicts":[{"id":1,"verdict":"do","impact":3,"size":"s","model":"sonnet","title":"...","why":"...","brief":"..."}]}'</span> +<span class="a">+ );</span> +<span class="a">+ },</span> +<span class="a">+</span> +<span class="a">+ idea_update({ id, ...patch }) {</span> +<span class="a">+ const idea = updateIdea(id, patch);</span> +<span class="a">+ const bits = [`#${idea.id} ${clip(idea.title, 60)} · ${lane(idea)}`];</span> +<span class="a">+ if (patch.model) bits.push(`model ${idea.triage.model}`);</span> +<span class="a">+ if (patch.note) bits.push('note added');</span> +<span class="a">+ return `Updated ${bits.join(' · ')}`;</span> +<span class="a">+ },</span> +<span class="a">+</span> +<span class="a">+ idea_next({ id, here = false }) {</span> +<span class="a">+ const db = load();</span> +<span class="a">+ let idea;</span> +<span class="a">+ if (id != null) {</span> +<span class="a">+ idea = findIdea(db, id);</span> +<span class="a">+ if (!idea) throw new Error(`no idea #${id}`);</span> +<span class="a">+ } else {</span> +<span class="a">+ idea = pickNext(db, { project: currentProject(), only: here });</span> +<span class="a">+ }</span> +<span class="a">+ if (!idea) {</span> +<span class="a">+ const c = counts(db);</span> +<span class="a">+ return c.inbox</span> +<span class="a">+ ? `No idea is ready to build. ${c.inbox} untriaged in the inbox: triage them first.`</span> +<span class="a">+ : 'No idea is ready to build, and the inbox is empty.';</span> +<span class="a">+ }</span> +<span class="a">+ const t = idea.triage;</span> +<span class="a">+ const lines = [renderIdea(idea), ''];</span> +<span class="a">+ if (!t) lines.push('recommended model: none yet (untriaged)');</span> +<span class="a">+ else lines.push(`recommended model: ${t.model}`);</span> +<span class="a">+ lines.push(`project dir: ${idea.project || '(none recorded)'}`);</span> +<span class="a">+ return lines.join('\n');</span> +<span class="a">+ },</span> +<span class="a">+};</span> +<span class="a">+</span> +<span class="a">+// ---------------------------------------------------------------------------------------------</span> +<span class="a">+// JSON-RPC plumbing</span> +<span class="a">+</span> +<span class="a">+function send(msg) {</span> +<span class="a">+ process.stdout.write(JSON.stringify(msg) + '\n');</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+function result(id, value) {</span> +<span class="a">+ return { jsonrpc: '2.0', id, result: value };</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+function error(id, code, message) {</span> +<span class="a">+ return { jsonrpc: '2.0', id, error: { code, message } };</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+async function handleRequest(msg, { prompts }) {</span> +<span class="a">+ const { id, method, params = {} } = msg;</span> +<span class="a">+ switch (method) {</span> +<span class="a">+ case 'initialize': {</span> +<span class="a">+ const asked = params.protocolVersion;</span> +<span class="a">+ const protocolVersion = PROTOCOL_VERSIONS.includes(asked) ? asked : PROTOCOL_VERSIONS[0];</span> +<span class="a">+ const capabilities = { tools: { listChanged: false } };</span> +<span class="a">+ if (prompts) capabilities.prompts = { listChanged: false };</span> +<span class="a">+ return result(id, { protocolVersion, capabilities, serverInfo: { name: 'ideamine', version: VERSION }, instructions: INSTRUCTIONS });</span> +<span class="a">+ }</span> +<span class="a">+ case 'ping':</span> +<span class="a">+ return result(id, {});</span> +<span class="a">+ case 'tools/list':</span> +<span class="a">+ return result(id, { tools: TOOLS });</span> +<span class="a">+ case 'tools/call': {</span> +<span class="a">+ const handler = handlers[params.name];</span> +<span class="a">+ if (!handler) return error(id, -32602, `unknown tool: ${params.name}`);</span> +<span class="a">+ try {</span> +<span class="a">+ const text = await handler(params.arguments || {});</span> +<span class="a">+ return result(id, { content: [{ type: 'text', text }] });</span> +<span class="a">+ } catch (e) {</span> +<span class="a">+ return result(id, { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true });</span> +<span class="a">+ }</span> +<span class="a">+ }</span> +<span class="a">+ case 'prompts/list':</span> +<span class="a">+ return result(id, { prompts: prompts ? PROMPTS.map(({ skill, ...p }) => p) : [] });</span> +<span class="a">+ case 'prompts/get': {</span> +<span class="a">+ const p = prompts && PROMPTS.find((x) => x.name === params.name);</span> +<span class="a">+ if (!p) return error(id, -32602, `unknown prompt: ${params.name}`);</span> +<span class="a">+ const args = Object.values(params.arguments || {}).filter(Boolean).join(' ');</span> +<span class="a">+ return result(id, {</span> +<span class="a">+ description: p.description,</span> +<span class="a">+ messages: [{ role: 'user', content: { type: 'text', text: skillBody(p.skill, args) } }],</span> +<span class="a">+ });</span> +<span class="a">+ }</span> +<span class="a">+ case 'resources/list':</span> +<span class="a">+ return result(id, { resources: [] });</span> +<span class="a">+ case 'resources/templates/list':</span> +<span class="a">+ return result(id, { resourceTemplates: [] });</span> +<span class="a">+ case 'logging/setLevel':</span> +<span class="a">+ return result(id, {});</span> +<span class="a">+ default:</span> +<span class="a">+ return error(id, -32601, `method not found: ${method}`);</span> +<span class="a">+ }</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+async function handleMessage(msg, opts) {</span> +<span class="a">+ if (!msg || typeof msg !== 'object' || msg.jsonrpc !== '2.0') return error(null, -32600, 'invalid request');</span> +<span class="a">+ if (typeof msg.method !== 'string') return null; // a response to a request we never send</span> +<span class="a">+ if (msg.id === undefined) return null; // notification (initialized, cancelled, ...)</span> +<span class="a">+ try {</span> +<span class="a">+ return await handleRequest(msg, opts);</span> +<span class="a">+ } catch (e) {</span> +<span class="a">+ return error(msg.id, -32603, e.message);</span> +<span class="a">+ }</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+async function handleLine(line, opts) {</span> +<span class="a">+ let msg;</span> +<span class="a">+ try {</span> +<span class="a">+ msg = JSON.parse(line);</span> +<span class="a">+ } catch {</span> +<span class="a">+ return error(null, -32700, 'parse error');</span> +<span class="a">+ }</span> +<span class="a">+ if (!Array.isArray(msg)) return handleMessage(msg, opts);</span> +<span class="a">+ const replies = (await Promise.all(msg.map((m) => handleMessage(m, opts)))).filter(Boolean);</span> +<span class="a">+ return replies.length ? replies : null;</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+/** Serve MCP on stdin/stdout. `prompts: false` when running as a plugin (skills cover slash commands). */</span> +<span class="a">+export function serve({ prompts = true } = {}) {</span> +<span class="a">+ const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });</span> +<span class="a">+ const inFlight = new Set();</span> +<span class="a">+ // Requests run concurrently (a headless triage must not stall a ping); replies carry their ids.</span> +<span class="a">+ rl.on('line', (line) => {</span> +<span class="a">+ if (!line.trim()) return;</span> +<span class="a">+ const job = handleLine(line, { prompts }).then((reply) => reply && send(reply));</span> +<span class="a">+ inFlight.add(job);</span> +<span class="a">+ job.finally(() => inFlight.delete(job));</span> +<span class="a">+ });</span> +<span class="a">+ rl.on('close', () => Promise.allSettled([...inFlight]).then(() => process.exit(0)));</span> +<span class="a">+}</span> +diff --git a/src/render.js b/src/render.js +new file mode 100644 +index 0000000..32b121a +--- /dev/null ++++ b/src/render.js +<span class="h">@@ -0,0 +1,135 @@</span> +<span class="a">+// Plain-text views. Kept compact on purpose: every character here may end up in a model's context.</span> +<span class="a">+</span> +<span class="a">+import path from 'node:path';</span> +<span class="a">+import { counts, lane, listIdeas, samePath } from './store.js';</span> +<span class="a">+import { clip } from './text.js';</span> +<span class="a">+</span> +<span class="a">+const LANE_TITLES = {</span> +<span class="a">+ doing: 'DOING',</span> +<span class="a">+ do: 'DO (best first)',</span> +<span class="a">+ maybe: 'MAYBE',</span> +<span class="a">+ inbox: 'INBOX (untriaged)',</span> +<span class="a">+ skip: 'SKIP',</span> +<span class="a">+ done: 'DONE',</span> +<span class="a">+ dropped: 'DROPPED',</span> +<span class="a">+};</span> +<span class="a">+</span> +<span class="a">+function projectName(p) {</span> +<span class="a">+ return p ? path.basename(p) : '';</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+function stamp(iso) {</span> +<span class="a">+ if (!iso) return '';</span> +<span class="a">+ const d = new Date(iso);</span> +<span class="a">+ const pad = (n) => String(n).padStart(2, '0');</span> +<span class="a">+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+export function ideaLine(idea, { cwd = null } = {}) {</span> +<span class="a">+ const t = idea.triage;</span> +<span class="a">+ const id = `#${idea.id}`.padEnd(5);</span> +<span class="a">+ const meta = t && lane(idea) !== 'inbox' && t.verdict !== 'skip'</span> +<span class="a">+ ? `${t.model.padEnd(6)} ${t.size.toUpperCase().padEnd(2)} ▲${t.impact} `</span> +<span class="a">+ : '';</span> +<span class="a">+ const where = idea.project && !samePath(idea.project, cwd) ? ` · ${projectName(idea.project)}` : '';</span> +<span class="a">+ return ` ${id}${meta}${clip(idea.title, 64)}${where}`;</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+export function summaryLine(db) {</span> +<span class="a">+ const c = counts(db);</span> +<span class="a">+ const parts = ['doing', 'do', 'maybe', 'inbox'].filter((k) => c[k]).map((k) => `${c[k]} ${k}`);</span> +<span class="a">+ return `ideamine: ${c.open} open${parts.length ? ` (${parts.join(' · ')})` : ''}${c.done ? ` · ${c.done} done` : ''}`;</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+/** The board: ideas grouped by lane. */</span> +<span class="a">+export function renderBoard(db, { filter = 'open', project = null, query = '', cwd = null, limit = 0, hints = false } = {}) {</span> +<span class="a">+ const ideas = listIdeas(db, { filter, project, query, limit });</span> +<span class="a">+ const out = [summaryLine(db)];</span> +<span class="a">+ if (project || query || filter !== 'open') {</span> +<span class="a">+ const scope = [filter !== 'open' && filter, project && `project ${projectName(project)}`, query && `"${query}"`];</span> +<span class="a">+ out[0] += ` [showing: ${scope.filter(Boolean).join(', ')}]`;</span> +<span class="a">+ }</span> +<span class="a">+ if (!ideas.length) {</span> +<span class="a">+ out.push('', db.ideas.length ? ' (nothing here)' : ' (empty — add one with /idea <text>)');</span> +<span class="a">+ }</span> +<span class="a">+ let current = null;</span> +<span class="a">+ for (const idea of ideas) {</span> +<span class="a">+ const l = lane(idea);</span> +<span class="a">+ if (l !== current) {</span> +<span class="a">+ current = l;</span> +<span class="a">+ out.push('', LANE_TITLES[l] || l.toUpperCase());</span> +<span class="a">+ }</span> +<span class="a">+ out.push(ideaLine(idea, { cwd }));</span> +<span class="a">+ }</span> +<span class="a">+ if (hints) {</span> +<span class="a">+ const c = counts(db);</span> +<span class="a">+ const tips = ['/idea <text> to add', '/ideas #N for details'];</span> +<span class="a">+ if (c.inbox) tips.push('/idea-triage to score the inbox');</span> +<span class="a">+ if (c.do) tips.push('/idea-go to build the top pick');</span> +<span class="a">+ out.push('', tips.join(' · '));</span> +<span class="a">+ }</span> +<span class="a">+ return out.join('\n');</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+/** One idea in full. */</span> +<span class="a">+export function renderIdea(idea) {</span> +<span class="a">+ const t = idea.triage;</span> +<span class="a">+ const out = [`#${idea.id} ${idea.title}`];</span> +<span class="a">+ const status = [idea.status];</span> +<span class="a">+ if (t) status.push(t.verdict, t.model, `size ${t.size.toUpperCase()}`, `impact ${t.impact}/5`);</span> +<span class="a">+ out.push(`status ${status.join(' · ')}`);</span> +<span class="a">+ if (idea.project) out.push(`project ${idea.project}`);</span> +<span class="a">+ const added = [`${stamp(idea.created)} via ${idea.source}`];</span> +<span class="a">+ if (idea.tags?.length) added.push(`tags: ${idea.tags.join(', ')}`);</span> +<span class="a">+ out.push(`added ${added.join(' · ')}`);</span> +<span class="a">+ if (idea.dup_of) out.push(`dup of #${idea.dup_of}`);</span> +<span class="a">+ if (t?.why) out.push(`why ${t.why}`);</span> +<span class="a">+ if (t?.brief) out.push(`brief ${t.brief}`);</span> +<span class="a">+ if (t?.by) out.push(`triaged ${stamp(t.at)} by ${t.by}`);</span> +<span class="a">+ if (idea.text !== idea.title) out.push('', 'text', ...idea.text.split(/\r?\n/).map((l) => ` ${l}`));</span> +<span class="a">+ if (idea.notes?.length) {</span> +<span class="a">+ out.push('', 'notes');</span> +<span class="a">+ for (const n of idea.notes) out.push(` ${stamp(n.at)} ${n.text}`);</span> +<span class="a">+ }</span> +<span class="a">+ return out.join('\n');</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+/** Confirmation after capture, e.g. "💡 Saved #12 · Add dark mode (3 in inbox)". */</span> +<span class="a">+export function renderAdded(results, db) {</span> +<span class="a">+ const inbox = counts(db).inbox;</span> +<span class="a">+ const lines = [];</span> +<span class="a">+ if (results.length === 1) {</span> +<span class="a">+ const { idea } = results[0];</span> +<span class="a">+ lines.push(`💡 Saved #${idea.id} · ${clip(idea.title, 70)} (${inbox} in inbox)`);</span> +<span class="a">+ } else {</span> +<span class="a">+ lines.push(`💡 Saved ${results.length} ideas: ${results.map((r) => `#${r.idea.id}`).join(', ')} (${inbox} in inbox)`);</span> +<span class="a">+ }</span> +<span class="a">+ const seen = new Set(results.map((r) => r.idea.id));</span> +<span class="a">+ for (const r of results) {</span> +<span class="a">+ for (const s of r.similar) {</span> +<span class="a">+ if (seen.has(s.id)) continue;</span> +<span class="a">+ seen.add(s.id);</span> +<span class="a">+ lines.push(` similar to #${s.id} (${s.status}): ${clip(s.title, 60)}`);</span> +<span class="a">+ }</span> +<span class="a">+ }</span> +<span class="a">+ return lines.join('\n');</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+/** Markdown export of the whole archive. */</span> +<span class="a">+export function renderMarkdown(db) {</span> +<span class="a">+ const out = [`# Ideas`, '', `_${summaryLine(db)} · exported ${stamp(new Date().toISOString())}_`];</span> +<span class="a">+ let current = null;</span> +<span class="a">+ for (const idea of listIdeas(db, { filter: 'all' })) {</span> +<span class="a">+ const l = lane(idea);</span> +<span class="a">+ if (l !== current) {</span> +<span class="a">+ current = l;</span> +<span class="a">+ out.push('', `## ${LANE_TITLES[l] || l}`, '');</span> +<span class="a">+ }</span> +<span class="a">+ const t = idea.triage;</span> +<span class="a">+ const meta = t && l !== 'inbox' ? ` — ${t.verdict}, ${t.model}, size ${t.size.toUpperCase()}, impact ${t.impact}/5` : '';</span> +<span class="a">+ out.push(`- **#${idea.id} ${idea.title}**${meta}${idea.project ? ` · \`${projectName(idea.project)}\`` : ''}`);</span> +<span class="a">+ if (t?.brief) out.push(` - ${t.brief}`);</span> +<span class="a">+ if (idea.text !== idea.title) out.push(` - > ${idea.text.replace(/\r?\n/g, ' ')}`);</span> +<span class="a">+ }</span> +<span class="a">+ return out.join('\n') + '\n';</span> +<span class="a">+}</span> +diff --git a/src/rubric.js b/src/rubric.js +new file mode 100644 +index 0000000..c28fb33 +--- /dev/null ++++ b/src/rubric.js +<span class="h">@@ -0,0 +1,101 @@</span> +<span class="a">+// How ideas are judged and which model builds them. Both triage paths (the MCP tool used by</span> +<span class="a">+// /idea-triage and the headless `ideamine triage`) read this file.</span> +<span class="a">+</span> +<span class="a">+import path from 'node:path';</span> +<span class="a">+import { lane, listIdeas } from './store.js';</span> +<span class="a">+import { clip } from './text.js';</span> +<span class="a">+</span> +<span class="a">+// Aliases are what Claude Code accepts (`claude --model sonnet`, Agent tool `model: "sonnet"`),</span> +<span class="a">+// so a recommendation stays valid when a new generation ships under the same alias.</span> +<span class="a">+// Prices: USD per million input/output tokens.</span> +<span class="a">+export const MODELS = [</span> +<span class="a">+ {</span> +<span class="a">+ alias: 'haiku',</span> +<span class="a">+ name: 'Claude Haiku 4.5',</span> +<span class="a">+ price: '$1/$5',</span> +<span class="a">+ use: 'mechanical, fully specified, local work: typos, renames, config tweaks, boilerplate, small scripts, lookups',</span> +<span class="a">+ },</span> +<span class="a">+ {</span> +<span class="a">+ alias: 'sonnet',</span> +<span class="a">+ name: 'Claude Sonnet 5',</span> +<span class="a">+ price: '$2/$10',</span> +<span class="a">+ use: 'the default: ordinary features, bug fixes with a clear repro, tests, docs, contained refactors',</span> +<span class="a">+ },</span> +<span class="a">+ {</span> +<span class="a">+ alias: 'opus',</span> +<span class="a">+ name: 'Claude Opus 5',</span> +<span class="a">+ price: '$5/$25',</span> +<span class="a">+ use: 'ambiguous or cross-cutting work: architecture, hard debugging, performance, security, unfamiliar domains',</span> +<span class="a">+ },</span> +<span class="a">+ {</span> +<span class="a">+ alias: 'fable',</span> +<span class="a">+ name: 'Claude Fable 5.1',</span> +<span class="a">+ price: '$10/$50',</span> +<span class="a">+ use: 'only the hardest long-horizon or research-grade problems, where Opus would likely fail',</span> +<span class="a">+ },</span> +<span class="a">+];</span> +<span class="a">+</span> +<span class="a">+export const RUBRIC = `Triage each idea. Be decisive: the user wants a short list worth building, not encouragement.</span> +<span class="a">+</span> +<span class="a">+For every idea return:</span> +<span class="a">+- verdict: "do" (clear value, worth building soon) | "maybe" (unclear value, blocked, or needs a decision from the user) | "skip" (low value, already exists, or costs far more than it returns)</span> +<span class="a">+- impact: 1-5, how much it would matter to the user or project if it shipped</span> +<span class="a">+- size: xs (<15 min) | s (<1 h) | m (a few hours) | l (1-2 days) | xl (multi-day, should be split)</span> +<span class="a">+- model: the CHEAPEST model likely to finish it in one pass. A failed attempt on a weaker model plus a retry costs more than the right model once.</span> +<span class="a">+${MODELS.map((m) => ` ${m.alias.padEnd(6)} ${m.price.padEnd(7)} ${m.use}`).join('\n')}</span> +<span class="a">+ (prices in USD per million input/output tokens)</span> +<span class="a">+- title: imperative, at most 60 characters ("Add ...", "Fix ...")</span> +<span class="a">+- why: one sentence with the deciding reason</span> +<span class="a">+- brief: 1-4 sentences an agent can act on without this conversation: goal, scope, and "done when". Leave empty for skip.</span> +<span class="a">+- dup_of: id of an existing idea this one duplicates (use verdict "skip"), if any`;</span> +<span class="a">+</span> +<span class="a">+/** JSON Schema for a batch of verdicts; used for `claude -p --json-schema` and the MCP tool. */</span> +<span class="a">+export const VERDICT_SCHEMA = {</span> +<span class="a">+ type: 'object',</span> +<span class="a">+ properties: {</span> +<span class="a">+ id: { type: 'integer' },</span> +<span class="a">+ verdict: { type: 'string', enum: ['do', 'maybe', 'skip'] },</span> +<span class="a">+ impact: { type: 'integer', minimum: 1, maximum: 5 },</span> +<span class="a">+ size: { type: 'string', enum: ['xs', 's', 'm', 'l', 'xl'] },</span> +<span class="a">+ model: { type: 'string', enum: MODELS.map((m) => m.alias) },</span> +<span class="a">+ title: { type: 'string' },</span> +<span class="a">+ why: { type: 'string' },</span> +<span class="a">+ brief: { type: 'string' },</span> +<span class="a">+ dup_of: { type: 'integer' },</span> +<span class="a">+ },</span> +<span class="a">+ required: ['id', 'verdict', 'impact', 'size', 'model', 'title', 'why', 'brief'],</span> +<span class="a">+};</span> +<span class="a">+</span> +<span class="a">+export const BATCH_SCHEMA = {</span> +<span class="a">+ type: 'object',</span> +<span class="a">+ properties: { verdicts: { type: 'array', items: VERDICT_SCHEMA } },</span> +<span class="a">+ required: ['verdicts'],</span> +<span class="a">+};</span> +<span class="a">+</span> +<span class="a">+/** Ideas waiting for a verdict: the inbox, or the given ids (re-triage). */</span> +<span class="a">+export function pendingIdeas(db, { ids = null, limit = 30 } = {}) {</span> +<span class="a">+ if (Array.isArray(ids) && ids.length) {</span> +<span class="a">+ const wanted = new Set(ids.map(Number));</span> +<span class="a">+ return db.ideas.filter((i) => wanted.has(i.id));</span> +<span class="a">+ }</span> +<span class="a">+ return listIdeas(db, { filter: 'inbox' }).reverse().slice(0, limit); // oldest first</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+/** The rubric plus the ideas to judge, as one prompt. */</span> +<span class="a">+export function triagePrompt(db, pending) {</span> +<span class="a">+ const pendingIds = new Set(pending.map((i) => i.id));</span> +<span class="a">+ const existing = db.ideas</span> +<span class="a">+ .filter((i) => !pendingIds.has(i.id) && ['do', 'maybe', 'doing', 'done'].includes(lane(i)))</span> +<span class="a">+ .slice(-60)</span> +<span class="a">+ .map((i) => `#${i.id} ${clip(i.title, 70)} (${lane(i)})`);</span> +<span class="a">+ const lines = [RUBRIC, ''];</span> +<span class="a">+ if (existing.length) lines.push('Existing ideas, for duplicate checks:', ...existing, '');</span> +<span class="a">+ lines.push(`Ideas to triage (${pending.length}):`);</span> +<span class="a">+ for (const i of pending) {</span> +<span class="a">+ const ctx = [i.project && `project: ${path.basename(i.project)}`, i.tags?.length && `tags: ${i.tags.join(', ')}`]</span> +<span class="a">+ .filter(Boolean)</span> +<span class="a">+ .join('; ');</span> +<span class="a">+ lines.push(`#${i.id}${ctx ? ` [${ctx}]` : ''}: ${i.text.replace(/\r?\n/g, ' / ')}`);</span> +<span class="a">+ }</span> +<span class="a">+ return lines.join('\n');</span> +<span class="a">+}</span> +diff --git a/src/store.js b/src/store.js +new file mode 100644 +index 0000000..56742c3 +--- /dev/null ++++ b/src/store.js +<span class="h">@@ -0,0 +1,335 @@</span> +<span class="a">+// The idea archive: one JSON file shared by every Claude session, project, and model.</span> +<span class="a">+// Writers take a directory lock and replace the file atomically, so concurrent sessions,</span> +<span class="a">+// the hook, and the CLI can all write at once without losing ideas.</span> +<span class="a">+</span> +<span class="a">+import fs from 'node:fs';</span> +<span class="a">+import os from 'node:os';</span> +<span class="a">+import path from 'node:path';</span> +<span class="a">+import { clip, deriveTitle, extractTags, isSimilar, normalizeTags, wordSet } from './text.js';</span> +<span class="a">+</span> +<span class="a">+export const VERDICTS = ['do', 'maybe', 'skip'];</span> +<span class="a">+export const SIZES = ['xs', 's', 'm', 'l', 'xl'];</span> +<span class="a">+export const SIZE_COST = { xs: 1, s: 2, m: 3, l: 5, xl: 8 };</span> +<span class="a">+export const MODEL_ALIASES = ['haiku', 'sonnet', 'opus', 'fable'];</span> +<span class="a">+export const STATUSES = ['inbox', 'triaged', 'doing', 'done', 'dropped'];</span> +<span class="a">+export const FILTERS = ['open', 'inbox', 'do', 'maybe', 'skip', 'doing', 'done', 'dropped', 'all'];</span> +<span class="a">+</span> +<span class="a">+const LOCK_STALE_MS = 5000;</span> +<span class="a">+const LOCK_WAIT_MS = 8000;</span> +<span class="a">+</span> +<span class="a">+export function home() {</span> +<span class="a">+ return process.env.IDEAMINE_HOME || path.join(os.homedir(), '.ideamine');</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+export function dbPath() {</span> +<span class="a">+ return path.join(home(), 'ideas.json');</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+function sleepSync(ms) {</span> +<span class="a">+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+const now = () => new Date().toISOString();</span> +<span class="a">+</span> +<span class="a">+function emptyDb() {</span> +<span class="a">+ return { version: 1, next_id: 1, ideas: [] };</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+/** Read-only snapshot. Safe without the lock because writes are atomic renames. */</span> +<span class="a">+export function load() {</span> +<span class="a">+ const file = dbPath();</span> +<span class="a">+ let raw;</span> +<span class="a">+ try {</span> +<span class="a">+ raw = fs.readFileSync(file, 'utf8').replace(/^/, '');</span> +<span class="a">+ } catch (e) {</span> +<span class="a">+ if (e.code === 'ENOENT') return emptyDb();</span> +<span class="a">+ throw e;</span> +<span class="a">+ }</span> +<span class="a">+ if (!raw.trim()) return emptyDb();</span> +<span class="a">+ let db;</span> +<span class="a">+ try {</span> +<span class="a">+ db = JSON.parse(raw);</span> +<span class="a">+ } catch (e) {</span> +<span class="a">+ throw new Error(`${file} is not valid JSON (${e.message}). Fix it, or restore ${file}.bak`);</span> +<span class="a">+ }</span> +<span class="a">+ if (!db || !Array.isArray(db.ideas)) throw new Error(`${file} has no "ideas" array`);</span> +<span class="a">+ const maxId = db.ideas.reduce((m, i) => Math.max(m, Number(i.id) || 0), 0);</span> +<span class="a">+ db.next_id = Math.max(Number(db.next_id) || 1, maxId + 1);</span> +<span class="a">+ return db;</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+function acquireLock() {</span> +<span class="a">+ fs.mkdirSync(home(), { recursive: true });</span> +<span class="a">+ const lockDir = path.join(home(), '.lock');</span> +<span class="a">+ const deadline = Date.now() + LOCK_WAIT_MS;</span> +<span class="a">+ for (let attempt = 0; ; attempt++) {</span> +<span class="a">+ try {</span> +<span class="a">+ fs.mkdirSync(lockDir);</span> +<span class="a">+ return () => fs.rmSync(lockDir, { recursive: true, force: true });</span> +<span class="a">+ } catch (e) {</span> +<span class="a">+ if (e.code !== 'EEXIST') throw e;</span> +<span class="a">+ }</span> +<span class="a">+ try {</span> +<span class="a">+ // A writer holds the lock for milliseconds; an old lock means a crashed process.</span> +<span class="a">+ if (Date.now() - fs.statSync(lockDir).mtimeMs > LOCK_STALE_MS) {</span> +<span class="a">+ fs.rmSync(lockDir, { recursive: true, force: true });</span> +<span class="a">+ continue;</span> +<span class="a">+ }</span> +<span class="a">+ } catch {</span> +<span class="a">+ // The lock vanished between mkdir and stat; the next attempt will likely get it.</span> +<span class="a">+ }</span> +<span class="a">+ if (Date.now() > deadline) {</span> +<span class="a">+ throw new Error(`idea store is busy (lock: ${lockDir}). Delete that folder if no ideamine process is running.`);</span> +<span class="a">+ }</span> +<span class="a">+ sleepSync(Math.min(10 + attempt * 5, 100));</span> +<span class="a">+ }</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+function renameWithRetry(from, to) {</span> +<span class="a">+ // Windows refuses to replace a file that another program (editor, antivirus, sync) has open.</span> +<span class="a">+ for (let attempt = 0; ; attempt++) {</span> +<span class="a">+ try {</span> +<span class="a">+ fs.renameSync(from, to);</span> +<span class="a">+ return;</span> +<span class="a">+ } catch (e) {</span> +<span class="a">+ if (attempt >= 40 || !['EPERM', 'EBUSY', 'EACCES'].includes(e.code)) {</span> +<span class="a">+ fs.rmSync(from, { force: true });</span> +<span class="a">+ throw e;</span> +<span class="a">+ }</span> +<span class="a">+ sleepSync(25);</span> +<span class="a">+ }</span> +<span class="a">+ }</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+function save(db) {</span> +<span class="a">+ const file = dbPath();</span> +<span class="a">+ const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;</span> +<span class="a">+ fs.writeFileSync(tmp, JSON.stringify(db, null, 2) + '\n');</span> +<span class="a">+ try {</span> +<span class="a">+ fs.copyFileSync(file, `${file}.bak`);</span> +<span class="a">+ } catch (e) {</span> +<span class="a">+ if (e.code !== 'ENOENT') throw e;</span> +<span class="a">+ }</span> +<span class="a">+ renameWithRetry(tmp, file);</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+/** Lock, read, apply `fn(db)`, write. Nothing is written if `fn` throws. */</span> +<span class="a">+export function mutate(fn) {</span> +<span class="a">+ const release = acquireLock();</span> +<span class="a">+ try {</span> +<span class="a">+ const db = load();</span> +<span class="a">+ const result = fn(db);</span> +<span class="a">+ save(db);</span> +<span class="a">+ return result;</span> +<span class="a">+ } finally {</span> +<span class="a">+ release();</span> +<span class="a">+ }</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+// ---------------------------------------------------------------------------------------------</span> +<span class="a">+// Queries</span> +<span class="a">+</span> +<span class="a">+export function samePath(a, b) {</span> +<span class="a">+ if (!a || !b) return false;</span> +<span class="a">+ const norm = (p) => {</span> +<span class="a">+ const r = path.resolve(p).replace(/[\\/]+$/, '');</span> +<span class="a">+ return process.platform === 'win32' ? r.toLowerCase() : r;</span> +<span class="a">+ };</span> +<span class="a">+ return norm(a) === norm(b);</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+export function findIdea(db, id) {</span> +<span class="a">+ const n = Number(String(id).replace(/^#/, ''));</span> +<span class="a">+ return db.ideas.find((i) => i.id === n) || null;</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+export function verdictOf(idea) {</span> +<span class="a">+ return idea.status === 'triaged' ? idea.triage?.verdict || null : null;</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+/** Value per unit of effort; untriaged ideas sort last. */</span> +<span class="a">+export function priority(idea) {</span> +<span class="a">+ const t = idea.triage;</span> +<span class="a">+ if (!t) return 0;</span> +<span class="a">+ return (Number(t.impact) || 3) / (SIZE_COST[t.size] || 3);</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+/** Lane an idea shows up in on the board. */</span> +<span class="a">+export function lane(idea) {</span> +<span class="a">+ if (idea.status === 'triaged') return idea.triage?.verdict || 'maybe';</span> +<span class="a">+ return idea.status;</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+const LANE_ORDER = ['doing', 'do', 'maybe', 'inbox', 'skip', 'done', 'dropped'];</span> +<span class="a">+</span> +<span class="a">+export function matchesFilter(idea, filter) {</span> +<span class="a">+ const l = lane(idea);</span> +<span class="a">+ switch (filter) {</span> +<span class="a">+ case 'all':</span> +<span class="a">+ return true;</span> +<span class="a">+ case 'open':</span> +<span class="a">+ return ['doing', 'do', 'maybe', 'inbox'].includes(l);</span> +<span class="a">+ default:</span> +<span class="a">+ return l === filter;</span> +<span class="a">+ }</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+export function listIdeas(db, { filter = 'open', project = null, query = '', limit = 0 } = {}) {</span> +<span class="a">+ if (!FILTERS.includes(filter)) throw new Error(`unknown filter "${filter}" (use: ${FILTERS.join(', ')})`);</span> +<span class="a">+ const words = String(query || '').toLowerCase().split(/\s+/).filter(Boolean);</span> +<span class="a">+ let ideas = db.ideas.filter((i) => matchesFilter(i, filter));</span> +<span class="a">+ if (project) ideas = ideas.filter((i) => samePath(i.project, project));</span> +<span class="a">+ if (words.length) {</span> +<span class="a">+ ideas = ideas.filter((i) => {</span> +<span class="a">+ const hay = `${i.title}\n${i.text}\n${(i.tags || []).join(' ')}`.toLowerCase();</span> +<span class="a">+ return words.every((w) => hay.includes(w));</span> +<span class="a">+ });</span> +<span class="a">+ }</span> +<span class="a">+ ideas.sort((a, b) => {</span> +<span class="a">+ const la = LANE_ORDER.indexOf(lane(a));</span> +<span class="a">+ const lb = LANE_ORDER.indexOf(lane(b));</span> +<span class="a">+ if (la !== lb) return la - lb;</span> +<span class="a">+ if (['done', 'dropped', 'inbox'].includes(lane(a))) return b.id - a.id; // newest first</span> +<span class="a">+ return priority(b) - priority(a) || a.id - b.id;</span> +<span class="a">+ });</span> +<span class="a">+ return limit > 0 ? ideas.slice(0, limit) : ideas;</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+export function counts(db) {</span> +<span class="a">+ const c = { inbox: 0, do: 0, maybe: 0, skip: 0, doing: 0, done: 0, dropped: 0 };</span> +<span class="a">+ for (const i of db.ideas) c[lane(i)] = (c[lane(i)] || 0) + 1;</span> +<span class="a">+ c.open = c.inbox + c.do + c.maybe + c.doing;</span> +<span class="a">+ return c;</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+/** Best idea to build next: verdict "do", not started, current project first, then value per effort. */</span> +<span class="a">+export function pickNext(db, { project = null, only = false } = {}) {</span> +<span class="a">+ let ready = db.ideas.filter((i) => lane(i) === 'do');</span> +<span class="a">+ if (only) ready = ready.filter((i) => samePath(i.project, project));</span> +<span class="a">+ ready.sort((a, b) => {</span> +<span class="a">+ const pa = project && samePath(a.project, project) ? 1 : 0;</span> +<span class="a">+ const pb = project && samePath(b.project, project) ? 1 : 0;</span> +<span class="a">+ return pb - pa || priority(b) - priority(a) || a.id - b.id;</span> +<span class="a">+ });</span> +<span class="a">+ return ready[0] || null;</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+function similarTo(db, text, excludeId) {</span> +<span class="a">+ const words = wordSet(text);</span> +<span class="a">+ if (words.size < 2) return [];</span> +<span class="a">+ return db.ideas</span> +<span class="a">+ .filter((i) => i.id !== excludeId && i.status !== 'dropped' && isSimilar(words, wordSet(`${i.title} ${i.text}`)))</span> +<span class="a">+ .slice(-2)</span> +<span class="a">+ .map((i) => ({ id: i.id, title: i.title, status: lane(i) }));</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+// ---------------------------------------------------------------------------------------------</span> +<span class="a">+// Mutations</span> +<span class="a">+</span> +<span class="a">+export function addIdeas(texts, { source = 'mcp', project = null, session = null, tags = [] } = {}) {</span> +<span class="a">+ const items = (Array.isArray(texts) ? texts : [texts]).map((t) => String(t ?? '').trim()).filter(Boolean);</span> +<span class="a">+ if (!items.length) throw new Error('idea text is empty');</span> +<span class="a">+ return mutate((db) =></span> +<span class="a">+ items.map((text) => {</span> +<span class="a">+ const similar = similarTo(db, text, null);</span> +<span class="a">+ const stamp = now();</span> +<span class="a">+ const idea = {</span> +<span class="a">+ id: db.next_id++,</span> +<span class="a">+ title: deriveTitle(text),</span> +<span class="a">+ text,</span> +<span class="a">+ status: 'inbox',</span> +<span class="a">+ tags: normalizeTags([...extractTags(text), ...normalizeTags(tags)]),</span> +<span class="a">+ project: project ? path.resolve(project) : null,</span> +<span class="a">+ source,</span> +<span class="a">+ created: stamp,</span> +<span class="a">+ updated: stamp,</span> +<span class="a">+ triage: null,</span> +<span class="a">+ notes: [],</span> +<span class="a">+ };</span> +<span class="a">+ if (session) idea.session = String(session);</span> +<span class="a">+ db.ideas.push(idea);</span> +<span class="a">+ return { idea, similar };</span> +<span class="a">+ }),</span> +<span class="a">+ );</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+export function normalizeModel(model) {</span> +<span class="a">+ const m = String(model || '').toLowerCase();</span> +<span class="a">+ return MODEL_ALIASES.find((alias) => m.includes(alias)) || null;</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+function cleanVerdict(v) {</span> +<span class="a">+ const verdict = String(v.verdict || '').toLowerCase();</span> +<span class="a">+ if (!VERDICTS.includes(verdict)) throw new Error(`verdict must be one of ${VERDICTS.join('/')}`);</span> +<span class="a">+ const impact = Math.min(5, Math.max(1, Math.round(Number(v.impact) || 3)));</span> +<span class="a">+ const size = SIZES.includes(String(v.size || '').toLowerCase()) ? String(v.size).toLowerCase() : 'm';</span> +<span class="a">+ const model = normalizeModel(v.model) || 'sonnet';</span> +<span class="a">+ return {</span> +<span class="a">+ verdict,</span> +<span class="a">+ impact,</span> +<span class="a">+ size,</span> +<span class="a">+ model,</span> +<span class="a">+ why: clip(v.why || '', 300),</span> +<span class="a">+ brief: verdict === 'skip' ? '' : String(v.brief || '').trim().slice(0, 2000),</span> +<span class="a">+ };</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+/** Save triage verdicts. Bad items are reported individually instead of failing the batch. */</span> +<span class="a">+export function applyTriage(verdicts, { by = null } = {}) {</span> +<span class="a">+ if (!Array.isArray(verdicts) || !verdicts.length) throw new Error('no verdicts given');</span> +<span class="a">+ return mutate((db) =></span> +<span class="a">+ verdicts.map((v) => {</span> +<span class="a">+ const idea = findIdea(db, v?.id);</span> +<span class="a">+ if (!idea) return { id: v?.id, error: 'no such idea' };</span> +<span class="a">+ let t;</span> +<span class="a">+ try {</span> +<span class="a">+ t = cleanVerdict(v);</span> +<span class="a">+ } catch (e) {</span> +<span class="a">+ return { id: idea.id, error: e.message };</span> +<span class="a">+ }</span> +<span class="a">+ idea.triage = { ...t, at: now(), ...(by ? { by } : {}) };</span> +<span class="a">+ if (v.title) idea.title = clip(v.title, 90);</span> +<span class="a">+ if (Array.isArray(v.tags)) idea.tags = normalizeTags([...(idea.tags || []), ...v.tags]);</span> +<span class="a">+ const dup = Number(v.dup_of);</span> +<span class="a">+ if (dup && dup !== idea.id && findIdea(db, dup)) idea.dup_of = dup;</span> +<span class="a">+ if (idea.status === 'inbox') idea.status = 'triaged';</span> +<span class="a">+ idea.updated = now();</span> +<span class="a">+ return { id: idea.id, verdict: t.verdict, model: t.model, size: t.size, title: idea.title };</span> +<span class="a">+ }),</span> +<span class="a">+ );</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+const STATUS_ALIASES = { start: 'doing', started: 'doing', finish: 'done', finished: 'done', drop: 'dropped' };</span> +<span class="a">+</span> +<span class="a">+export function updateIdea(id, patch = {}) {</span> +<span class="a">+ return mutate((db) => {</span> +<span class="a">+ const idea = findIdea(db, id);</span> +<span class="a">+ if (!idea) throw new Error(`no idea #${id}`);</span> +<span class="a">+ if (patch.status) {</span> +<span class="a">+ let status = String(patch.status).toLowerCase();</span> +<span class="a">+ status = STATUS_ALIASES[status] || status;</span> +<span class="a">+ if (status === 'reopen' || status === 'open') status = idea.triage ? 'triaged' : 'inbox';</span> +<span class="a">+ if (!STATUSES.includes(status)) throw new Error(`status must be one of ${STATUSES.join(', ')}, or reopen`);</span> +<span class="a">+ idea.status = status;</span> +<span class="a">+ if (status === 'done' || status === 'dropped') idea.closed = now();</span> +<span class="a">+ else delete idea.closed;</span> +<span class="a">+ }</span> +<span class="a">+ if (patch.text) idea.text = String(patch.text).trim();</span> +<span class="a">+ if (patch.title) idea.title = clip(patch.title, 90);</span> +<span class="a">+ else if (patch.text) idea.title = deriveTitle(idea.text);</span> +<span class="a">+ if (Array.isArray(patch.tags)) idea.tags = normalizeTags(patch.tags);</span> +<span class="a">+ if (patch.project !== undefined) idea.project = patch.project ? path.resolve(patch.project) : null;</span> +<span class="a">+ if (patch.model) {</span> +<span class="a">+ const model = normalizeModel(patch.model);</span> +<span class="a">+ if (!model) throw new Error(`model must be one of ${MODEL_ALIASES.join(', ')}`);</span> +<span class="a">+ idea.triage = { ...(idea.triage || { verdict: 'do', impact: 3, size: 'm', why: '', brief: '' }), model };</span> +<span class="a">+ if (idea.status === 'inbox') idea.status = 'triaged';</span> +<span class="a">+ }</span> +<span class="a">+ if (patch.note) {</span> +<span class="a">+ idea.notes ||= [];</span> +<span class="a">+ idea.notes.push({ at: now(), text: String(patch.note).trim() });</span> +<span class="a">+ }</span> +<span class="a">+ idea.updated = now();</span> +<span class="a">+ return idea;</span> +<span class="a">+ });</span> +<span class="a">+}</span> +diff --git a/src/text.js b/src/text.js +new file mode 100644 +index 0000000..500bd78 +--- /dev/null ++++ b/src/text.js +<span class="h">@@ -0,0 +1,88 @@</span> +<span class="a">+// Pure text helpers: titles, tags, bullet splitting, and cheap duplicate detection.</span> +<span class="a">+</span> +<span class="a">+const STOPWORDS = new Set(</span> +<span class="a">+ ('a an and any are as at be but by can could do does for from get got has have how i if in into is it its ' +</span> +<span class="a">+ 'just like make me more my of on or our out so some than that the their them then there these they this ' +</span> +<span class="a">+ 'to too up use using via was way we what when where which while who why will with would you your')</span> +<span class="a">+ .split(' '),</span> +<span class="a">+);</span> +<span class="a">+</span> +<span class="a">+/** First non-empty line, whitespace-collapsed, cut at a word boundary. */</span> +<span class="a">+export function deriveTitle(text, max = 72) {</span> +<span class="a">+ const first = String(text).split(/\r?\n/).map((s) => s.trim()).find(Boolean) || '';</span> +<span class="a">+ const clean = first</span> +<span class="a">+ .replace(/^(?:[-*•]|\d+[.)])\s+/, '')</span> +<span class="a">+ .replace(/(?:^|\s)#[a-z][\w-]{0,31}(?=\s|$)/gi, ' ')</span> +<span class="a">+ .replace(/\s+/g, ' ')</span> +<span class="a">+ .trim() || first;</span> +<span class="a">+ if (clean.length <= max) return clean;</span> +<span class="a">+ const cut = clean.slice(0, max - 1);</span> +<span class="a">+ const space = cut.lastIndexOf(' ');</span> +<span class="a">+ return (space > max * 0.6 ? cut.slice(0, space) : cut).replace(/[\s,.;:-]+$/, '') + '…';</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+/** `#tag` words (must start with a letter, so `#12` and `C#` are not tags). */</span> +<span class="a">+export function extractTags(text) {</span> +<span class="a">+ const tags = new Set();</span> +<span class="a">+ for (const m of String(text).matchAll(/(?:^|\s)#([a-z][\w-]{0,31})/gi)) tags.add(m[1].toLowerCase());</span> +<span class="a">+ return [...tags];</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+export function normalizeTags(tags) {</span> +<span class="a">+ if (!Array.isArray(tags)) return [];</span> +<span class="a">+ return [...new Set(tags.map((t) => String(t).trim().replace(/^#/, '').toLowerCase()).filter(Boolean))];</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+const BULLET = /^(\s{0,3})(?:[-*•]|\d+[.)])\s+/;</span> +<span class="a">+</span> +<span class="a">+/**</span> +<span class="a">+ * A pasted bulleted list becomes one idea per bullet; anything else is one idea.</span> +<span class="a">+ * Indented lines under a bullet are kept with that bullet.</span> +<span class="a">+ */</span> +<span class="a">+export function splitIdeas(text) {</span> +<span class="a">+ const lines = String(text).split(/\r?\n/);</span> +<span class="a">+ const items = [];</span> +<span class="a">+ let pureList = true;</span> +<span class="a">+ for (const line of lines) {</span> +<span class="a">+ if (!line.trim()) continue;</span> +<span class="a">+ if (BULLET.test(line)) items.push(line.replace(BULLET, '').trim());</span> +<span class="a">+ else if (items.length && /^\s+/.test(line)) items[items.length - 1] += '\n' + line.trim();</span> +<span class="a">+ else {</span> +<span class="a">+ pureList = false;</span> +<span class="a">+ break;</span> +<span class="a">+ }</span> +<span class="a">+ }</span> +<span class="a">+ if (pureList && items.length >= 2) return items.filter(Boolean);</span> +<span class="a">+ const whole = String(text).trim();</span> +<span class="a">+ return whole ? [whole] : [];</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+export function wordSet(text) {</span> +<span class="a">+ const words = String(text)</span> +<span class="a">+ .toLowerCase()</span> +<span class="a">+ .normalize('NFKD')</span> +<span class="a">+ .replace(/\p{M}/gu, '')</span> +<span class="a">+ .replace(/[^\p{L}\p{N}\s]/gu, ' ')</span> +<span class="a">+ .split(/\s+/)</span> +<span class="a">+ .filter((w) => w.length > 2 && !STOPWORDS.has(w))</span> +<span class="a">+ .map((w) => (w.length > 4 && w.endsWith('s') && !w.endsWith('ss') ? w.slice(0, -1) : w));</span> +<span class="a">+ return new Set(words);</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+/** Jaccard similarity of two word sets, plus the shared-word count. */</span> +<span class="a">+export function similarity(a, b) {</span> +<span class="a">+ let shared = 0;</span> +<span class="a">+ for (const w of a) if (b.has(w)) shared++;</span> +<span class="a">+ const union = a.size + b.size - shared;</span> +<span class="a">+ return { score: union ? shared / union : 0, shared };</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+export function isSimilar(a, b) {</span> +<span class="a">+ const { score, shared } = similarity(a, b);</span> +<span class="a">+ return (score >= 0.55 && shared >= 2) || score >= 0.8;</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+export function clip(text, max) {</span> +<span class="a">+ const s = String(text ?? '').replace(/\s+/g, ' ').trim();</span> +<span class="a">+ return s.length <= max ? s : s.slice(0, max - 1).trimEnd() + '…';</span> +<span class="a">+}</span> +diff --git a/tests/fixtures/fake-claude.js b/tests/fixtures/fake-claude.js +new file mode 100644 +index 0000000..0ec4c49 +--- /dev/null ++++ b/tests/fixtures/fake-claude.js +<span class="h">@@ -0,0 +1,34 @@</span> +<span class="a">+// Stand-in for the Claude Code CLI: returns a verdict for every idea in the prompt and records how</span> +<span class="a">+// it was called, so tests can check flags and environment without spending tokens.</span> +<span class="a">+import fs from 'node:fs';</span> +<span class="a">+</span> +<span class="a">+let input = '';</span> +<span class="a">+for await (const chunk of process.stdin) input += chunk;</span> +<span class="a">+</span> +<span class="a">+if (process.env.FAKE_CLAUDE_LOG) {</span> +<span class="a">+ const env = {};</span> +<span class="a">+ for (const key of ['CLAUDECODE', 'CLAUDE_EFFORT', 'CLAUDE_CODE_MESSAGING_SOCKET', 'ANTHROPIC_BASE_URL']) env[key] = process.env[key] ?? null;</span> +<span class="a">+ fs.writeFileSync(process.env.FAKE_CLAUDE_LOG, JSON.stringify({ args: process.argv.slice(2), env, input }));</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+const ids = [...(input.split('Ideas to triage')[1] || '').matchAll(/^#(\d+)/gm)].map((m) => Number(m[1]));</span> +<span class="a">+const verdicts = ids.map((id, i) => ({</span> +<span class="a">+ id,</span> +<span class="a">+ verdict: i === 2 ? 'skip' : 'do',</span> +<span class="a">+ impact: 3,</span> +<span class="a">+ size: 's',</span> +<span class="a">+ model: i === 1 ? 'haiku' : 'sonnet',</span> +<span class="a">+ title: `Idea ${id}`,</span> +<span class="a">+ why: 'test',</span> +<span class="a">+ brief: `Build ${id}.`,</span> +<span class="a">+}));</span> +<span class="a">+</span> +<span class="a">+process.stdout.write(JSON.stringify({</span> +<span class="a">+ type: 'result',</span> +<span class="a">+ subtype: 'success',</span> +<span class="a">+ is_error: false,</span> +<span class="a">+ result: '',</span> +<span class="a">+ structured_output: { verdicts },</span> +<span class="a">+ total_cost_usd: 0.0012,</span> +<span class="a">+ usage: { input_tokens: 321, output_tokens: 45 },</span> +<span class="a">+}));</span> +diff --git a/tests/hook.test.js b/tests/hook.test.js +new file mode 100644 +index 0000000..8265a4c +--- /dev/null ++++ b/tests/hook.test.js +<span class="h">@@ -0,0 +1,78 @@</span> +<span class="a">+import assert from 'node:assert/strict';</span> +<span class="a">+import { spawnSync } from 'node:child_process';</span> +<span class="a">+import fs from 'node:fs';</span> +<span class="a">+import os from 'node:os';</span> +<span class="a">+import path from 'node:path';</span> +<span class="a">+import { beforeEach, test } from 'node:test';</span> +<span class="a">+import { fileURLToPath } from 'node:url';</span> +<span class="a">+import { handlePrompt } from '../src/hook.js';</span> +<span class="a">+import * as store from '../src/store.js';</span> +<span class="a">+</span> +<span class="a">+const BIN = fileURLToPath(new URL('../bin/ideamine.js', import.meta.url));</span> +<span class="a">+</span> +<span class="a">+beforeEach(() => {</span> +<span class="a">+ process.env.IDEAMINE_HOME = fs.mkdtempSync(path.join(os.tmpdir(), 'ideamine-hook-'));</span> +<span class="a">+});</span> +<span class="a">+</span> +<span class="a">+/** Run the real hook process the way Claude Code does: JSON on stdin, decision on stdout. */</span> +<span class="a">+function runHook(prompt, cwd = '/work/app') {</span> +<span class="a">+ const input = JSON.stringify({ session_id: 's1', hook_event_name: 'UserPromptSubmit', cwd, prompt });</span> +<span class="a">+ const r = spawnSync(process.execPath, [BIN, 'hook'], { input, encoding: 'utf8', env: process.env });</span> +<span class="a">+ assert.equal(r.status, 0, r.stderr);</span> +<span class="a">+ return r.stdout ? JSON.parse(r.stdout) : null;</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+test('/idea saves the idea and blocks the prompt so no model call happens', () => {</span> +<span class="a">+ const out = runHook('/idea add a read-aloud mode #a11y');</span> +<span class="a">+ assert.equal(out.decision, 'block');</span> +<span class="a">+ assert.match(out.reason, /Saved #1 · add a read-aloud mode/);</span> +<span class="a">+ assert.deepEqual(out.hookSpecificOutput, { hookEventName: 'UserPromptSubmit', suppressOriginalPrompt: true });</span> +<span class="a">+ const [idea] = store.load().ideas;</span> +<span class="a">+ assert.equal(idea.source, 'hook');</span> +<span class="a">+ assert.equal(idea.session, 's1');</span> +<span class="a">+ assert.deepEqual(idea.tags, ['a11y']);</span> +<span class="a">+ assert.equal(idea.project, path.resolve('/work/app'));</span> +<span class="a">+});</span> +<span class="a">+</span> +<span class="a">+test('plugin-qualified and multi-line forms work', () => {</span> +<span class="a">+ assert.match(runHook('/ideamine:idea first').reason, /Saved #1/);</span> +<span class="a">+ assert.match(runHook('/idea - two\n- three').reason, /Saved 2 ideas: #2, #3/);</span> +<span class="a">+});</span> +<span class="a">+</span> +<span class="a">+test('ordinary prompts, other commands, and questions pass straight through', () => {</span> +<span class="a">+ for (const p of ['fix the tests', '/idea-triage', '/idea-go 3', '/ideasx', 'an /idea in the middle', '/ideas what should I build today?']) {</span> +<span class="a">+ assert.equal(runHook(p), null, p);</span> +<span class="a">+ }</span> +<span class="a">+ assert.equal(store.load().ideas.length, 0);</span> +<span class="a">+});</span> +<span class="a">+</span> +<span class="a">+test('/ideas shows the board, details, and applies quick edits', () => {</span> +<span class="a">+ store.addIdeas(['alpha', 'beta'], { project: '/work/app' });</span> +<span class="a">+ assert.match(runHook('/ideas').reason, /2 open[\s\S]*#2 {3}beta[\s\S]*#1 {3}alpha/);</span> +<span class="a">+ assert.match(runHook('/ideas #1').reason, /^#1 alpha/);</span> +<span class="a">+ assert.match(runHook('/ideas done 1 shipped it').reason, /#1 alpha → done/);</span> +<span class="a">+ assert.match(runHook('/ideas done').reason, /DONE[\s\S]*#1/);</span> +<span class="a">+ assert.match(runHook('/ideas 9').reason, /No idea #9/);</span> +<span class="a">+ assert.match(runHook('/idea').reason, /^Usage/);</span> +<span class="a">+});</span> +<span class="a">+</span> +<span class="a">+test('handlePrompt filters by project with "here"', () => {</span> +<span class="a">+ store.addIdeas(['mine'], { project: '/a' });</span> +<span class="a">+ store.addIdeas(['theirs'], { project: '/b' });</span> +<span class="a">+ const board = handlePrompt('/ideas here', { cwd: '/a' });</span> +<span class="a">+ assert.match(board, /mine/);</span> +<span class="a">+ assert.doesNotMatch(board, /theirs/);</span> +<span class="a">+});</span> +<span class="a">+</span> +<span class="a">+test('when the archive is unreadable the prompt goes through instead of being lost', () => {</span> +<span class="a">+ fs.mkdirSync(store.home(), { recursive: true });</span> +<span class="a">+ fs.writeFileSync(store.dbPath(), 'not json');</span> +<span class="a">+ const r = spawnSync(process.execPath, [BIN, 'hook'], {</span> +<span class="a">+ input: JSON.stringify({ prompt: '/idea keep me', cwd: '/x' }),</span> +<span class="a">+ encoding: 'utf8',</span> +<span class="a">+ env: process.env,</span> +<span class="a">+ });</span> +<span class="a">+ assert.equal(r.status, 0);</span> +<span class="a">+ assert.equal(r.stdout, '');</span> +<span class="a">+ assert.match(r.stderr, /not valid JSON/);</span> +<span class="a">+});</span> +diff --git a/tests/mcp.test.js b/tests/mcp.test.js +new file mode 100644 +index 0000000..68197cf +--- /dev/null ++++ b/tests/mcp.test.js +<span class="h">@@ -0,0 +1,116 @@</span> +<span class="a">+import assert from 'node:assert/strict';</span> +<span class="a">+import { spawn } from 'node:child_process';</span> +<span class="a">+import fs from 'node:fs';</span> +<span class="a">+import os from 'node:os';</span> +<span class="a">+import path from 'node:path';</span> +<span class="a">+import readline from 'node:readline';</span> +<span class="a">+import { after, before, test } from 'node:test';</span> +<span class="a">+import { fileURLToPath } from 'node:url';</span> +<span class="a">+</span> +<span class="a">+const BIN = fileURLToPath(new URL('../bin/ideamine.js', import.meta.url));</span> +<span class="a">+const project = fs.mkdtempSync(path.join(os.tmpdir(), 'ideamine-proj-'));</span> +<span class="a">+let server;</span> +<span class="a">+let nextId = 1;</span> +<span class="a">+const pending = new Map();</span> +<span class="a">+</span> +<span class="a">+function request(method, params = {}) {</span> +<span class="a">+ const id = nextId++;</span> +<span class="a">+ server.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n');</span> +<span class="a">+ return new Promise((resolve) => pending.set(id, resolve));</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+async function call(name, args = {}) {</span> +<span class="a">+ const res = await request('tools/call', { name, arguments: args });</span> +<span class="a">+ return { text: res.result.content[0].text, isError: !!res.result.isError };</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+before(async () => {</span> +<span class="a">+ const env = {</span> +<span class="a">+ ...process.env,</span> +<span class="a">+ IDEAMINE_HOME: fs.mkdtempSync(path.join(os.tmpdir(), 'ideamine-mcp-')),</span> +<span class="a">+ IDEAMINE_CLAUDE_BIN: fileURLToPath(new URL('./fixtures/fake-claude.js', import.meta.url)),</span> +<span class="a">+ };</span> +<span class="a">+ server = spawn(process.execPath, [BIN, 'mcp'], { cwd: project, env, stdio: ['pipe', 'pipe', 'inherit'] });</span> +<span class="a">+ readline.createInterface({ input: server.stdout }).on('line', (line) => {</span> +<span class="a">+ const msg = JSON.parse(line); // stdout must carry nothing but JSON-RPC</span> +<span class="a">+ pending.get(msg.id)?.(msg);</span> +<span class="a">+ });</span> +<span class="a">+ const init = await request('initialize', {</span> +<span class="a">+ protocolVersion: '2025-06-18',</span> +<span class="a">+ capabilities: {},</span> +<span class="a">+ clientInfo: { name: 'test', version: '0' },</span> +<span class="a">+ });</span> +<span class="a">+ assert.equal(init.result.protocolVersion, '2025-06-18');</span> +<span class="a">+ assert.equal(init.result.serverInfo.name, 'ideamine');</span> +<span class="a">+ assert.ok(init.result.capabilities.tools);</span> +<span class="a">+ assert.ok(init.result.capabilities.prompts);</span> +<span class="a">+ server.stdin.write(JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }) + '\n');</span> +<span class="a">+});</span> +<span class="a">+</span> +<span class="a">+after(() => server.kill());</span> +<span class="a">+</span> +<span class="a">+test('lists the five tools with schemas', async () => {</span> +<span class="a">+ const res = await request('tools/list');</span> +<span class="a">+ assert.deepEqual(res.result.tools.map((t) => t.name), ['idea_add', 'idea_list', 'idea_triage', 'idea_update', 'idea_next']);</span> +<span class="a">+ for (const t of res.result.tools) assert.equal(t.inputSchema.type, 'object');</span> +<span class="a">+});</span> +<span class="a">+</span> +<span class="a">+test('full loop: add, triage, next, update', async () => {</span> +<span class="a">+ const added = await call('idea_add', { text: '- cache the API responses\n- rename helpers.js to utils.js' });</span> +<span class="a">+ assert.match(added.text, /Saved 2 ideas: #1, #2/);</span> +<span class="a">+</span> +<span class="a">+ const work = await call('idea_triage');</span> +<span class="a">+ assert.match(work.text, /Ideas to triage \(2\)/);</span> +<span class="a">+ assert.match(work.text, /haiku/);</span> +<span class="a">+ assert.match(work.text, new RegExp(`project: ${path.basename(project)}`));</span> +<span class="a">+</span> +<span class="a">+ const saved = await call('idea_triage', {</span> +<span class="a">+ by: 'test-model',</span> +<span class="a">+ verdicts: [</span> +<span class="a">+ { id: 1, verdict: 'do', impact: 4, size: 'm', model: 'sonnet', title: 'Cache API responses', why: 'slow pages', brief: 'Add an LRU cache.' },</span> +<span class="a">+ { id: 2, verdict: 'do', impact: 2, size: 'xs', model: 'haiku', title: 'Rename helpers.js', why: 'trivial', brief: 'Rename and fix imports.' },</span> +<span class="a">+ ],</span> +<span class="a">+ });</span> +<span class="a">+ assert.match(saved.text, /Saved 2 verdicts: 2 do · 0 maybe · 0 skip/);</span> +<span class="a">+</span> +<span class="a">+ const next = await call('idea_next');</span> +<span class="a">+ assert.match(next.text, /^#2 Rename helpers\.js/); // 2/1 value per effort beats 4/3</span> +<span class="a">+ assert.match(next.text, /recommended model: haiku/);</span> +<span class="a">+</span> +<span class="a">+ const upd = await call('idea_update', { id: 2, status: 'done', note: 'renamed' });</span> +<span class="a">+ assert.match(upd.text, /Updated #2 Rename helpers\.js · done · note added/);</span> +<span class="a">+</span> +<span class="a">+ const board = await call('idea_list');</span> +<span class="a">+ assert.match(board.text, /1 open \(1 do\) · 1 done/);</span> +<span class="a">+ const one = await call('idea_list', { id: 1 });</span> +<span class="a">+ assert.match(one.text, /brief {4}Add an LRU cache\./);</span> +<span class="a">+ assert.match(one.text, /triaged .* by test-model/);</span> +<span class="a">+});</span> +<span class="a">+</span> +<span class="a">+test('headless triage through the tool, while other requests stay responsive', async () => {</span> +<span class="a">+ await call('idea_add', { text: '- idea x\n- idea y' });</span> +<span class="a">+ const [triaged, pong] = await Promise.all([call('idea_triage', { headless: true }), request('ping')]);</span> +<span class="a">+ assert.deepEqual(pong.result, {});</span> +<span class="a">+ assert.match(triaged.text, /Saved 2 verdicts: 2 do · 0 maybe · 0 skip \(sonnet, 321 in \/ 45 out tokens\)/);</span> +<span class="a">+ assert.match((await call('idea_triage', { headless: true })).text, /inbox is empty/);</span> +<span class="a">+});</span> +<span class="a">+</span> +<span class="a">+test('tool errors come back as isError results, not protocol errors', async () => {</span> +<span class="a">+ const res = await call('idea_update', { id: 999, status: 'done' });</span> +<span class="a">+ assert.equal(res.isError, true);</span> +<span class="a">+ assert.match(res.text, /no idea #999/);</span> +<span class="a">+ const unknown = await request('tools/call', { name: 'nope', arguments: {} });</span> +<span class="a">+ assert.equal(unknown.error.code, -32602);</span> +<span class="a">+ const method = await request('does/not/exist');</span> +<span class="a">+ assert.equal(method.error.code, -32601);</span> +<span class="a">+});</span> +<span class="a">+</span> +<span class="a">+test('prompts mirror the skills', async () => {</span> +<span class="a">+ const list = await request('prompts/list');</span> +<span class="a">+ assert.deepEqual(list.result.prompts.map((p) => p.name), ['idea', 'ideas', 'idea-triage', 'idea-go']);</span> +<span class="a">+ const got = await request('prompts/get', { name: 'idea', arguments: { text: 'teleport the cat' } });</span> +<span class="a">+ const text = got.result.messages[0].content.text;</span> +<span class="a">+ assert.match(text, /teleport the cat/);</span> +<span class="a">+ assert.doesNotMatch(text, /^---/);</span> +<span class="a">+ assert.match(text, /idea_add/);</span> +<span class="a">+});</span> +diff --git a/tests/store.test.js b/tests/store.test.js +new file mode 100644 +index 0000000..e17631b +--- /dev/null ++++ b/tests/store.test.js +<span class="h">@@ -0,0 +1,85 @@</span> +<span class="a">+import assert from 'node:assert/strict';</span> +<span class="a">+import { spawn } from 'node:child_process';</span> +<span class="a">+import fs from 'node:fs';</span> +<span class="a">+import os from 'node:os';</span> +<span class="a">+import path from 'node:path';</span> +<span class="a">+import { beforeEach, test } from 'node:test';</span> +<span class="a">+import { fileURLToPath } from 'node:url';</span> +<span class="a">+import * as store from '../src/store.js';</span> +<span class="a">+</span> +<span class="a">+const BIN = fileURLToPath(new URL('../bin/ideamine.js', import.meta.url));</span> +<span class="a">+</span> +<span class="a">+beforeEach(() => {</span> +<span class="a">+ process.env.IDEAMINE_HOME = fs.mkdtempSync(path.join(os.tmpdir(), 'ideamine-test-'));</span> +<span class="a">+});</span> +<span class="a">+</span> +<span class="a">+test('add, triage, and list ideas by lane and priority', () => {</span> +<span class="a">+ store.addIdeas(['small win', 'big but slow', 'meh', 'later'], { project: '/p' });</span> +<span class="a">+ store.applyTriage([</span> +<span class="a">+ { id: 1, verdict: 'do', impact: 3, size: 'xs', model: 'haiku', title: 'Small win', why: 'cheap', brief: 'b' },</span> +<span class="a">+ { id: 2, verdict: 'do', impact: 5, size: 'xl', model: 'claude-opus-5', title: 'Big', why: 'w', brief: 'b' },</span> +<span class="a">+ { id: 3, verdict: 'skip', impact: 1, size: 'm', model: 'sonnet', title: 'Meh', why: 'no', brief: 'ignored' },</span> +<span class="a">+ ]);</span> +<span class="a">+ const db = store.load();</span> +<span class="a">+ assert.deepEqual(store.listIdeas(db).map((i) => i.id), [1, 2, 4]); // do by value/effort, then inbox</span> +<span class="a">+ assert.deepEqual(store.listIdeas(db, { filter: 'skip' }).map((i) => i.id), [3]);</span> +<span class="a">+ assert.equal(store.findIdea(db, 2).triage.model, 'opus'); // full model ids normalize to aliases</span> +<span class="a">+ assert.equal(store.findIdea(db, 3).triage.brief, ''); // skip verdicts carry no brief</span> +<span class="a">+ assert.equal(store.pickNext(db, { project: '/p' }).id, 1);</span> +<span class="a">+ assert.deepEqual(store.counts(db), { inbox: 1, do: 2, maybe: 0, skip: 1, doing: 0, done: 0, dropped: 0, open: 3 });</span> +<span class="a">+});</span> +<span class="a">+</span> +<span class="a">+test('bad verdicts are reported per item, not fatal', () => {</span> +<span class="a">+ store.addIdeas(['one']);</span> +<span class="a">+ const res = store.applyTriage([{ id: 1, verdict: 'yes' }, { id: 99, verdict: 'do' }]);</span> +<span class="a">+ assert.match(res[0].error, /verdict/);</span> +<span class="a">+ assert.equal(res[1].error, 'no such idea');</span> +<span class="a">+ assert.equal(store.findIdea(store.load(), 1).status, 'inbox');</span> +<span class="a">+});</span> +<span class="a">+</span> +<span class="a">+test('status changes, notes, model override, and reopen', () => {</span> +<span class="a">+ store.addIdeas(['thing']);</span> +<span class="a">+ store.updateIdea(1, { status: 'start', note: 'on it' });</span> +<span class="a">+ let idea = store.updateIdea('#1', { status: 'done', note: 'shipped' });</span> +<span class="a">+ assert.equal(idea.status, 'done');</span> +<span class="a">+ assert.ok(idea.closed);</span> +<span class="a">+ assert.deepEqual(idea.notes.map((n) => n.text), ['on it', 'shipped']);</span> +<span class="a">+ idea = store.updateIdea(1, { status: 'reopen', model: 'opus' });</span> +<span class="a">+ assert.equal(idea.status, 'triaged');</span> +<span class="a">+ assert.equal(idea.triage.model, 'opus');</span> +<span class="a">+ assert.equal(idea.closed, undefined);</span> +<span class="a">+ assert.throws(() => store.updateIdea(1, { status: 'nope' }), /status must be/);</span> +<span class="a">+ assert.throws(() => store.updateIdea(42, { note: 'x' }), /no idea #42/);</span> +<span class="a">+});</span> +<span class="a">+</span> +<span class="a">+test('a corrupt archive is never overwritten', () => {</span> +<span class="a">+ fs.mkdirSync(store.home(), { recursive: true });</span> +<span class="a">+ fs.writeFileSync(store.dbPath(), '{"ideas": [oops');</span> +<span class="a">+ assert.throws(() => store.addIdeas(['x']), /not valid JSON/);</span> +<span class="a">+ assert.equal(fs.readFileSync(store.dbPath(), 'utf8'), '{"ideas": [oops');</span> +<span class="a">+});</span> +<span class="a">+</span> +<span class="a">+test('a stale lock from a crashed writer is recovered', () => {</span> +<span class="a">+ fs.mkdirSync(path.join(store.home(), '.lock'), { recursive: true });</span> +<span class="a">+ const old = new Date(Date.now() - 60_000);</span> +<span class="a">+ fs.utimesSync(path.join(store.home(), '.lock'), old, old);</span> +<span class="a">+ assert.equal(store.addIdeas(['after crash'])[0].idea.id, 1);</span> +<span class="a">+});</span> +<span class="a">+</span> +<span class="a">+test('concurrent writers from many processes never lose an idea', async () => {</span> +<span class="a">+ const procs = 8;</span> +<span class="a">+ const each = 6;</span> +<span class="a">+ const run = (n) =></span> +<span class="a">+ new Promise((resolve, reject) => {</span> +<span class="a">+ // Each process appends its ideas through the CLI, all racing for the same archive.</span> +<span class="a">+ const script = `for (let i = 0; i < ${each}; i++) require('node:child_process').execFileSync(process.execPath, [${JSON.stringify(BIN)}, 'add', 'proc ${n} idea ' + i]);`;</span> +<span class="a">+ const child = spawn(process.execPath, ['-e', script], { env: process.env, stdio: ['ignore', 'ignore', 'pipe'] });</span> +<span class="a">+ let err = '';</span> +<span class="a">+ child.stderr.on('data', (d) => (err += d));</span> +<span class="a">+ child.on('exit', (code) => (code === 0 ? resolve() : reject(new Error(err))));</span> +<span class="a">+ });</span> +<span class="a">+ await Promise.all(Array.from({ length: procs }, (_, n) => run(n)));</span> +<span class="a">+ const ids = store.load().ideas.map((i) => i.id).sort((a, b) => a - b);</span> +<span class="a">+ assert.equal(ids.length, procs * each);</span> +<span class="a">+ assert.deepEqual(ids, Array.from({ length: procs * each }, (_, i) => i + 1));</span> +<span class="a">+});</span> +diff --git a/tests/text.test.js b/tests/text.test.js +new file mode 100644 +index 0000000..325038b +--- /dev/null ++++ b/tests/text.test.js +<span class="h">@@ -0,0 +1,35 @@</span> +<span class="a">+import assert from 'node:assert/strict';</span> +<span class="a">+import { test } from 'node:test';</span> +<span class="a">+import { deriveTitle, extractTags, isSimilar, splitIdeas, wordSet } from '../src/text.js';</span> +<span class="a">+</span> +<span class="a">+test('deriveTitle uses the first line, drops bullets and tags, and cuts at a word', () => {</span> +<span class="a">+ assert.equal(deriveTitle(' Add dark mode #ui\nmore detail'), 'Add dark mode');</span> +<span class="a">+ assert.equal(deriveTitle('- a bullet idea'), 'a bullet idea');</span> +<span class="a">+ const long = 'Build a thing that does many things '.repeat(5);</span> +<span class="a">+ const t = deriveTitle(long, 40);</span> +<span class="a">+ assert.ok(t.length <= 40, t);</span> +<span class="a">+ assert.ok(t.endsWith('…'));</span> +<span class="a">+ assert.ok(!t.includes(' '));</span> +<span class="a">+});</span> +<span class="a">+</span> +<span class="a">+test('extractTags finds #words but not #numbers or C#', () => {</span> +<span class="a">+ assert.deepEqual(extractTags('ship it #UI #perf-fix, see #12 and C# code'), ['ui', 'perf-fix']);</span> +<span class="a">+});</span> +<span class="a">+</span> +<span class="a">+test('splitIdeas splits a bulleted list and keeps continuation lines', () => {</span> +<span class="a">+ assert.deepEqual(splitIdeas('- one\n- two\n more about two\n* three'), ['one', 'two\nmore about two', 'three']);</span> +<span class="a">+ assert.deepEqual(splitIdeas('1. first\n2) second'), ['first', 'second']);</span> +<span class="a">+});</span> +<span class="a">+</span> +<span class="a">+test('splitIdeas keeps prose (or a single bullet) as one idea', () => {</span> +<span class="a">+ assert.deepEqual(splitIdeas('An idea\n- with a bullet inside'), ['An idea\n- with a bullet inside']);</span> +<span class="a">+ assert.deepEqual(splitIdeas('- only one'), ['- only one']);</span> +<span class="a">+ assert.deepEqual(splitIdeas(' '), []);</span> +<span class="a">+});</span> +<span class="a">+</span> +<span class="a">+test('isSimilar flags near-duplicates and not mere topic overlap', () => {</span> +<span class="a">+ const a = wordSet('Add dark mode toggle to the popup');</span> +<span class="a">+ assert.ok(isSimilar(a, wordSet('add a dark mode toggle in settings')));</span> +<span class="a">+ assert.ok(!isSimilar(wordSet('fix login bug'), wordSet('fix signup bug')));</span> +<span class="a">+ assert.ok(!isSimilar(a, wordSet('export ideas to notion')));</span> +<span class="a">+});</span> +diff --git a/tests/triage.test.js b/tests/triage.test.js +new file mode 100644 +index 0000000..50a5f3f +--- /dev/null ++++ b/tests/triage.test.js +<span class="h">@@ -0,0 +1,68 @@</span> +<span class="a">+import assert from 'node:assert/strict';</span> +<span class="a">+import fs from 'node:fs';</span> +<span class="a">+import os from 'node:os';</span> +<span class="a">+import path from 'node:path';</span> +<span class="a">+import { beforeEach, test } from 'node:test';</span> +<span class="a">+import { fileURLToPath } from 'node:url';</span> +<span class="a">+import { headlessTriage } from '../src/claude.js';</span> +<span class="a">+import * as store from '../src/store.js';</span> +<span class="a">+</span> +<span class="a">+const FAKE = fileURLToPath(new URL('./fixtures/fake-claude.js', import.meta.url));</span> +<span class="a">+let log;</span> +<span class="a">+</span> +<span class="a">+beforeEach(() => {</span> +<span class="a">+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ideamine-triage-'));</span> +<span class="a">+ process.env.IDEAMINE_HOME = dir;</span> +<span class="a">+ process.env.IDEAMINE_CLAUDE_BIN = FAKE;</span> +<span class="a">+ process.env.FAKE_CLAUDE_LOG = log = path.join(dir, 'call.json');</span> +<span class="a">+});</span> +<span class="a">+</span> +<span class="a">+test('headless triage runs one minimal claude -p call and saves every verdict', async () => {</span> +<span class="a">+ store.addIdeas(['cache responses', 'rename helpers', 'nft thing'], { project: '/p' });</span> +<span class="a">+ // Pretend we are inside a desktop-app session at max effort.</span> +<span class="a">+ process.env.CLAUDECODE = '1';</span> +<span class="a">+ process.env.CLAUDE_EFFORT = 'max';</span> +<span class="a">+ process.env.CLAUDE_CODE_MESSAGING_SOCKET = '\\\\.\\pipe\\x';</span> +<span class="a">+ const out = await headlessTriage();</span> +<span class="a">+ delete process.env.CLAUDECODE;</span> +<span class="a">+ delete process.env.CLAUDE_EFFORT;</span> +<span class="a">+ delete process.env.CLAUDE_CODE_MESSAGING_SOCKET;</span> +<span class="a">+</span> +<span class="a">+ assert.equal(out.results.length, 3);</span> +<span class="a">+ assert.deepEqual(out.tokens, { input: 321, output: 45 });</span> +<span class="a">+ const db = store.load();</span> +<span class="a">+ assert.deepEqual(db.ideas.map((i) => store.lane(i)), ['do', 'do', 'skip']);</span> +<span class="a">+ assert.equal(store.findIdea(db, 2).triage.model, 'haiku');</span> +<span class="a">+ assert.equal(store.findIdea(db, 1).triage.by, 'sonnet (headless)');</span> +<span class="a">+</span> +<span class="a">+ const call = JSON.parse(fs.readFileSync(log, 'utf8'));</span> +<span class="a">+ const flag = (name) => call.args[call.args.indexOf(name) + 1];</span> +<span class="a">+ assert.equal(call.args[0], '-p');</span> +<span class="a">+ assert.equal(flag('--model'), 'sonnet');</span> +<span class="a">+ assert.equal(flag('--tools'), '');</span> +<span class="a">+ assert.equal(flag('--setting-sources'), '');</span> +<span class="a">+ assert.equal(flag('--effort'), 'low');</span> +<span class="a">+ assert.ok(call.args.includes('--strict-mcp-config'));</span> +<span class="a">+ assert.ok(call.args.includes('--no-session-persistence'));</span> +<span class="a">+ assert.ok(JSON.parse(flag('--json-schema')).properties.verdicts);</span> +<span class="a">+ // The child is an independent session: no nesting marker, no inherited effort or host socket.</span> +<span class="a">+ assert.deepEqual(call.env, { CLAUDECODE: null, CLAUDE_EFFORT: null, CLAUDE_CODE_MESSAGING_SOCKET: null, ANTHROPIC_BASE_URL: process.env.ANTHROPIC_BASE_URL ?? null });</span> +<span class="a">+ assert.match(call.input, /Ideas to triage \(3\)/);</span> +<span class="a">+});</span> +<span class="a">+</span> +<span class="a">+test('haiku gets no effort flag, and an empty inbox makes no call', async () => {</span> +<span class="a">+ assert.equal((await headlessTriage()).message, 'Nothing to triage: the inbox is empty.');</span> +<span class="a">+ assert.ok(!fs.existsSync(log));</span> +<span class="a">+ store.addIdeas(['one']);</span> +<span class="a">+ await headlessTriage({ model: 'claude-haiku-4-5' });</span> +<span class="a">+ const call = JSON.parse(fs.readFileSync(log, 'utf8'));</span> +<span class="a">+ assert.equal(call.args[call.args.indexOf('--model') + 1], 'haiku');</span> +<span class="a">+ assert.ok(!call.args.includes('--effort'));</span> +<span class="a">+});</span> +<span class="a">+</span> +<span class="a">+test('a missing CLI gives a clear error', async () => {</span> +<span class="a">+ store.addIdeas(['one']);</span> +<span class="a">+ process.env.IDEAMINE_CLAUDE_BIN = path.join(os.tmpdir(), 'definitely-not-claude-xyz');</span> +<span class="a">+ await assert.rejects(headlessTriage(), /Claude Code CLI not found/);</span> +<span class="a" [diff truncated]