@asterxsk/kiln 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (303) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +3 -18
  3. package/agent/extensions/AGENTS.md +68 -68
  4. package/agent/extensions/ask-user/install.ps1 +4 -0
  5. package/agent/extensions/ask-user/install.sh +3 -0
  6. package/agent/extensions/background-terminals/install.ps1 +4 -0
  7. package/agent/extensions/background-terminals/install.sh +3 -0
  8. package/agent/extensions/destructive/install.ps1 +4 -0
  9. package/agent/extensions/destructive/install.sh +3 -0
  10. package/agent/extensions/file-search/install.ps1 +4 -0
  11. package/agent/extensions/file-search/install.sh +3 -0
  12. package/agent/extensions/goal/install.ps1 +4 -0
  13. package/agent/extensions/goal/install.sh +3 -0
  14. package/agent/extensions/herdr-agent-state.ts +257 -0
  15. package/agent/extensions/kiln-update/README.md +25 -0
  16. package/agent/extensions/kiln-update/index.ts +99 -0
  17. package/agent/extensions/kiln-update/install.ps1 +27 -0
  18. package/agent/extensions/kiln-update/install.sh +24 -0
  19. package/agent/extensions/modelconf/install.ps1 +4 -0
  20. package/agent/extensions/modelconf/install.sh +3 -0
  21. package/agent/extensions/shared/install.ps1 +4 -0
  22. package/agent/extensions/shared/install.sh +3 -0
  23. package/agent/extensions/skillsconf/install.ps1 +4 -0
  24. package/agent/extensions/skillsconf/install.sh +3 -0
  25. package/agent/extensions/statusline/install.ps1 +4 -0
  26. package/agent/extensions/statusline/install.sh +3 -0
  27. package/agent/extensions/subagents/install.ps1 +4 -0
  28. package/agent/extensions/subagents/install.sh +3 -0
  29. package/agent/extensions/taste/index.ts +443 -0
  30. package/agent/extensions/taste/install.ps1 +27 -0
  31. package/agent/extensions/taste/install.sh +24 -0
  32. package/agent/extensions/todo/install.ps1 +4 -0
  33. package/agent/extensions/todo/install.sh +3 -0
  34. package/agent/extensions/trim-context/install.ps1 +4 -0
  35. package/agent/extensions/trim-context/install.sh +3 -0
  36. package/agent/version.txt +1 -0
  37. package/agent.bak.20260905-190020/AGENTS.md +67 -0
  38. package/agent.bak.20260905-190020/README.md +5 -0
  39. package/agent.bak.20260905-190020/extensions/ask-user/index.ts +418 -0
  40. package/agent.bak.20260905-190020/extensions/ask-user/install.ps1 +23 -0
  41. package/agent.bak.20260905-190020/extensions/ask-user/install.sh +21 -0
  42. package/agent.bak.20260905-190020/extensions/ask-user/package-lock.json +769 -0
  43. package/agent.bak.20260905-190020/extensions/ask-user/package.json +19 -0
  44. package/agent.bak.20260905-190020/extensions/ask-user/prompt.ts +45 -0
  45. package/agent.bak.20260905-190020/extensions/ask-user/tsconfig.json +7 -0
  46. package/agent.bak.20260905-190020/extensions/background-terminals/docs/implementation-guide.md +942 -0
  47. package/agent.bak.20260905-190020/extensions/background-terminals/index.ts +629 -0
  48. package/agent.bak.20260905-190020/extensions/background-terminals/install.ps1 +23 -0
  49. package/agent.bak.20260905-190020/extensions/background-terminals/install.sh +21 -0
  50. package/agent.bak.20260905-190020/extensions/background-terminals/package-lock.json +769 -0
  51. package/agent.bak.20260905-190020/extensions/background-terminals/package.json +17 -0
  52. package/agent.bak.20260905-190020/extensions/background-terminals/src/domain.ts +87 -0
  53. package/agent.bak.20260905-190020/extensions/background-terminals/src/manager.ts +907 -0
  54. package/agent.bak.20260905-190020/extensions/background-terminals/src/output.ts +84 -0
  55. package/agent.bak.20260905-190020/extensions/background-terminals/src/prompt.ts +142 -0
  56. package/agent.bak.20260905-190020/extensions/background-terminals/src/result-delivery.ts +27 -0
  57. package/agent.bak.20260905-190020/extensions/background-terminals/src/runtime.ts +36 -0
  58. package/agent.bak.20260905-190020/extensions/background-terminals/src/ui/output-view.ts +79 -0
  59. package/agent.bak.20260905-190020/extensions/background-terminals/src/ui/ps.ts +621 -0
  60. package/agent.bak.20260905-190020/extensions/background-terminals/src/widget-order.ts +121 -0
  61. package/agent.bak.20260905-190020/extensions/background-terminals/test/manager.test.ts +735 -0
  62. package/agent.bak.20260905-190020/extensions/background-terminals/test/output.test.ts +109 -0
  63. package/agent.bak.20260905-190020/extensions/background-terminals/test/prompt.test.ts +125 -0
  64. package/agent.bak.20260905-190020/extensions/background-terminals/test/ps.test.ts +82 -0
  65. package/agent.bak.20260905-190020/extensions/background-terminals/test/result-delivery.test.ts +44 -0
  66. package/agent.bak.20260905-190020/extensions/background-terminals/tsconfig.json +7 -0
  67. package/agent.bak.20260905-190020/extensions/destructive/README.md +34 -0
  68. package/agent.bak.20260905-190020/extensions/destructive/index.ts +124 -0
  69. package/agent.bak.20260905-190020/extensions/destructive/install.ps1 +23 -0
  70. package/agent.bak.20260905-190020/extensions/destructive/install.sh +21 -0
  71. package/agent.bak.20260905-190020/extensions/file-search/index.ts +459 -0
  72. package/agent.bak.20260905-190020/extensions/file-search/install.ps1 +23 -0
  73. package/agent.bak.20260905-190020/extensions/file-search/install.sh +21 -0
  74. package/agent.bak.20260905-190020/extensions/file-search/package-lock.json +2253 -0
  75. package/agent.bak.20260905-190020/extensions/file-search/package.json +23 -0
  76. package/agent.bak.20260905-190020/extensions/file-search/src/args.ts +122 -0
  77. package/agent.bak.20260905-190020/extensions/file-search/src/binaries.ts +422 -0
  78. package/agent.bak.20260905-190020/extensions/file-search/src/output.ts +126 -0
  79. package/agent.bak.20260905-190020/extensions/file-search/src/process.ts +146 -0
  80. package/agent.bak.20260905-190020/extensions/file-search/src/prompt.ts +52 -0
  81. package/agent.bak.20260905-190020/extensions/file-search/test/index.spec.ts +443 -0
  82. package/agent.bak.20260905-190020/extensions/file-search/tsconfig.json +7 -0
  83. package/agent.bak.20260905-190020/extensions/goal/README.md +50 -0
  84. package/agent.bak.20260905-190020/extensions/goal/index.ts +155 -0
  85. package/agent.bak.20260905-190020/extensions/goal/install.ps1 +23 -0
  86. package/agent.bak.20260905-190020/extensions/goal/install.sh +21 -0
  87. package/agent.bak.20260905-190020/extensions/kiln-update/README.md +25 -0
  88. package/agent.bak.20260905-190020/extensions/kiln-update/index.ts +99 -0
  89. package/agent.bak.20260905-190020/extensions/kiln-update/install.ps1 +23 -0
  90. package/agent.bak.20260905-190020/extensions/kiln-update/install.sh +21 -0
  91. package/agent.bak.20260905-190020/extensions/modelconf/PLAN.md +915 -0
  92. package/agent.bak.20260905-190020/extensions/modelconf/README.md +66 -0
  93. package/agent.bak.20260905-190020/extensions/modelconf/index.ts +296 -0
  94. package/agent.bak.20260905-190020/extensions/modelconf/install.ps1 +23 -0
  95. package/agent.bak.20260905-190020/extensions/modelconf/install.sh +21 -0
  96. package/agent.bak.20260905-190020/extensions/modelconf/package-lock.json +1809 -0
  97. package/agent.bak.20260905-190020/extensions/modelconf/package.json +13 -0
  98. package/agent.bak.20260905-190020/extensions/modelconf/src/fuzzy.ts +26 -0
  99. package/agent.bak.20260905-190020/extensions/modelconf/src/glob.ts +13 -0
  100. package/agent.bak.20260905-190020/extensions/modelconf/src/persistence.test.ts +46 -0
  101. package/agent.bak.20260905-190020/extensions/modelconf/src/persistence.ts +164 -0
  102. package/agent.bak.20260905-190020/extensions/modelconf/src/ui/ModelConfView.test.ts +75 -0
  103. package/agent.bak.20260905-190020/extensions/modelconf/src/ui/ModelConfView.ts +1101 -0
  104. package/agent.bak.20260905-190020/extensions/modelconf/tsconfig.json +15 -0
  105. package/agent.bak.20260905-190020/extensions/pi-web-access/CHANGELOG.md +690 -0
  106. package/agent.bak.20260905-190020/extensions/pi-web-access/LICENSE +21 -0
  107. package/agent.bak.20260905-190020/extensions/pi-web-access/README.md +470 -0
  108. package/agent.bak.20260905-190020/extensions/pi-web-access/SECURITY.md +5 -0
  109. package/agent.bak.20260905-190020/extensions/pi-web-access/activity.ts +101 -0
  110. package/agent.bak.20260905-190020/extensions/pi-web-access/auth-fetch.ts +148 -0
  111. package/agent.bak.20260905-190020/extensions/pi-web-access/banner.png +0 -0
  112. package/agent.bak.20260905-190020/extensions/pi-web-access/brightdata-unlocker.ts +272 -0
  113. package/agent.bak.20260905-190020/extensions/pi-web-access/chrome-cookies.ts +669 -0
  114. package/agent.bak.20260905-190020/extensions/pi-web-access/content-find.ts +139 -0
  115. package/agent.bak.20260905-190020/extensions/pi-web-access/credential-source.ts +191 -0
  116. package/agent.bak.20260905-190020/extensions/pi-web-access/data-uri-sanitize.ts +406 -0
  117. package/agent.bak.20260905-190020/extensions/pi-web-access/datalab-pdf-extract.ts +568 -0
  118. package/agent.bak.20260905-190020/extensions/pi-web-access/declared-web-links.ts +173 -0
  119. package/agent.bak.20260905-190020/extensions/pi-web-access/evidence/CONTRACT-EVIDENCE.md +496 -0
  120. package/agent.bak.20260905-190020/extensions/pi-web-access/evidence/contract-probe.mjs +140 -0
  121. package/agent.bak.20260905-190020/extensions/pi-web-access/exa.ts +526 -0
  122. package/agent.bak.20260905-190020/extensions/pi-web-access/extract.ts +1196 -0
  123. package/agent.bak.20260905-190020/extensions/pi-web-access/feature-config.ts +29 -0
  124. package/agent.bak.20260905-190020/extensions/pi-web-access/fetch-params.ts +111 -0
  125. package/agent.bak.20260905-190020/extensions/pi-web-access/gemini-adc.ts +298 -0
  126. package/agent.bak.20260905-190020/extensions/pi-web-access/gemini-api.ts +353 -0
  127. package/agent.bak.20260905-190020/extensions/pi-web-access/gemini-pdf-extract.ts +108 -0
  128. package/agent.bak.20260905-190020/extensions/pi-web-access/gemini-search.ts +21 -0
  129. package/agent.bak.20260905-190020/extensions/pi-web-access/gemini-url-context.ts +128 -0
  130. package/agent.bak.20260905-190020/extensions/pi-web-access/gemini-web-config.ts +101 -0
  131. package/agent.bak.20260905-190020/extensions/pi-web-access/gemini-web.ts +487 -0
  132. package/agent.bak.20260905-190020/extensions/pi-web-access/github-api.ts +197 -0
  133. package/agent.bak.20260905-190020/extensions/pi-web-access/github-extract.ts +746 -0
  134. package/agent.bak.20260905-190020/extensions/pi-web-access/github-issue-pr.ts +700 -0
  135. package/agent.bak.20260905-190020/extensions/pi-web-access/index.ts +1737 -0
  136. package/agent.bak.20260905-190020/extensions/pi-web-access/package-lock.json +5808 -0
  137. package/agent.bak.20260905-190020/extensions/pi-web-access/package.json +64 -0
  138. package/agent.bak.20260905-190020/extensions/pi-web-access/page-query.ts +96 -0
  139. package/agent.bak.20260905-190020/extensions/pi-web-access/pdf-extract.ts +409 -0
  140. package/agent.bak.20260905-190020/extensions/pi-web-access/pi-web-fetch-demo.mp4 +0 -0
  141. package/agent.bak.20260905-190020/extensions/pi-web-access/promise-try.d.ts +7 -0
  142. package/agent.bak.20260905-190020/extensions/pi-web-access/query-rewrite.ts +51 -0
  143. package/agent.bak.20260905-190020/extensions/pi-web-access/render-search-error.ts +170 -0
  144. package/agent.bak.20260905-190020/extensions/pi-web-access/rsc-extract.ts +338 -0
  145. package/agent.bak.20260905-190020/extensions/pi-web-access/search-types.ts +20 -0
  146. package/agent.bak.20260905-190020/extensions/pi-web-access/source-check.ts +282 -0
  147. package/agent.bak.20260905-190020/extensions/pi-web-access/ssrf-protection.ts +526 -0
  148. package/agent.bak.20260905-190020/extensions/pi-web-access/storage.ts +521 -0
  149. package/agent.bak.20260905-190020/extensions/pi-web-access/summary-model-scope.ts +125 -0
  150. package/agent.bak.20260905-190020/extensions/pi-web-access/test/auth-fetch.test.mjs +208 -0
  151. package/agent.bak.20260905-190020/extensions/pi-web-access/test/brightdata-unlocker.test.mjs +840 -0
  152. package/agent.bak.20260905-190020/extensions/pi-web-access/test/chrome-cookie-extraction.test.mjs +441 -0
  153. package/agent.bak.20260905-190020/extensions/pi-web-access/test/config-path.test.mjs +283 -0
  154. package/agent.bak.20260905-190020/extensions/pi-web-access/test/content-find.test.mjs +25 -0
  155. package/agent.bak.20260905-190020/extensions/pi-web-access/test/credential-source.test.mjs +118 -0
  156. package/agent.bak.20260905-190020/extensions/pi-web-access/test/data-uri-sanitize.test.mjs +210 -0
  157. package/agent.bak.20260905-190020/extensions/pi-web-access/test/datalab-pdf-extract.test.mjs +552 -0
  158. package/agent.bak.20260905-190020/extensions/pi-web-access/test/declared-web-links.test.mjs +212 -0
  159. package/agent.bak.20260905-190020/extensions/pi-web-access/test/fetch-answer-storage.test.mjs +40 -0
  160. package/agent.bak.20260905-190020/extensions/pi-web-access/test/fetch-cache-storage.test.mjs +334 -0
  161. package/agent.bak.20260905-190020/extensions/pi-web-access/test/fetch-content-domain-policy.test.mjs +95 -0
  162. package/agent.bak.20260905-190020/extensions/pi-web-access/test/fetch-modes.test.mjs +53 -0
  163. package/agent.bak.20260905-190020/extensions/pi-web-access/test/fetch-not-found-guidance.test.mjs +92 -0
  164. package/agent.bak.20260905-190020/extensions/pi-web-access/test/fetch-params.test.mjs +86 -0
  165. package/agent.bak.20260905-190020/extensions/pi-web-access/test/fetch-render-call.test.mjs +34 -0
  166. package/agent.bak.20260905-190020/extensions/pi-web-access/test/fetch-routing.test.mjs +173 -0
  167. package/agent.bak.20260905-190020/extensions/pi-web-access/test/gemini-adc-auth.test.mjs +257 -0
  168. package/agent.bak.20260905-190020/extensions/pi-web-access/test/gemini-api-transport.test.mjs +170 -0
  169. package/agent.bak.20260905-190020/extensions/pi-web-access/test/gemini-pdf-extract.test.mjs +133 -0
  170. package/agent.bak.20260905-190020/extensions/pi-web-access/test/gemini-web-cookie-opt-in.test.mjs +178 -0
  171. package/agent.bak.20260905-190020/extensions/pi-web-access/test/gemini-web-header-overflow.test.mjs +148 -0
  172. package/agent.bak.20260905-190020/extensions/pi-web-access/test/get-search-content.test.mjs +223 -0
  173. package/agent.bak.20260905-190020/extensions/pi-web-access/test/github-extract.test.mjs +378 -0
  174. package/agent.bak.20260905-190020/extensions/pi-web-access/test/github-issue-pr.test.mjs +565 -0
  175. package/agent.bak.20260905-190020/extensions/pi-web-access/test/inline-content-config.test.mjs +99 -0
  176. package/agent.bak.20260905-190020/extensions/pi-web-access/test/lazy-extract-load.test.mjs +118 -0
  177. package/agent.bak.20260905-190020/extensions/pi-web-access/test/local-video-oversize.test.mjs +52 -0
  178. package/agent.bak.20260905-190020/extensions/pi-web-access/test/package-typebox-dependency.test.mjs +50 -0
  179. package/agent.bak.20260905-190020/extensions/pi-web-access/test/page-query.test.mjs +51 -0
  180. package/agent.bak.20260905-190020/extensions/pi-web-access/test/pdf-config.test.mjs +140 -0
  181. package/agent.bak.20260905-190020/extensions/pi-web-access/test/pdf-extract.test.mjs +500 -0
  182. package/agent.bak.20260905-190020/extensions/pi-web-access/test/proxy-transport.test.mjs +286 -0
  183. package/agent.bak.20260905-190020/extensions/pi-web-access/test/query-rewrite.test.mjs +52 -0
  184. package/agent.bak.20260905-190020/extensions/pi-web-access/test/rsc-fallback.test.mjs +102 -0
  185. package/agent.bak.20260905-190020/extensions/pi-web-access/test/search-error-render.test.mjs +152 -0
  186. package/agent.bak.20260905-190020/extensions/pi-web-access/test/search-providers.test.mjs +274 -0
  187. package/agent.bak.20260905-190020/extensions/pi-web-access/test/source-check.test.mjs +179 -0
  188. package/agent.bak.20260905-190020/extensions/pi-web-access/test/ssrf-allow-ranges-config.test.mjs +205 -0
  189. package/agent.bak.20260905-190020/extensions/pi-web-access/test/ssrf-protection.test.mjs +456 -0
  190. package/agent.bak.20260905-190020/extensions/pi-web-access/test/summary-model-scope.test.mjs +106 -0
  191. package/agent.bak.20260905-190020/extensions/pi-web-access/test/tool-registration-config.test.mjs +182 -0
  192. package/agent.bak.20260905-190020/extensions/pi-web-access/test/web-search-answer-render.test.mjs +66 -0
  193. package/agent.bak.20260905-190020/extensions/pi-web-access/test/youtube-extract-errors.test.mjs +64 -0
  194. package/agent.bak.20260905-190020/extensions/pi-web-access/tsconfig.json +11 -0
  195. package/agent.bak.20260905-190020/extensions/pi-web-access/utils.ts +451 -0
  196. package/agent.bak.20260905-190020/extensions/pi-web-access/video-extract.ts +392 -0
  197. package/agent.bak.20260905-190020/extensions/pi-web-access/youtube-extract.ts +328 -0
  198. package/agent.bak.20260905-190020/extensions/shared/activity-status.ts +31 -0
  199. package/agent.bak.20260905-190020/extensions/shared/child-session.ts +148 -0
  200. package/agent.bak.20260905-190020/extensions/shared/context-utilization.ts +47 -0
  201. package/agent.bak.20260905-190020/extensions/shared/dashboard-state.ts +99 -0
  202. package/agent.bak.20260905-190020/extensions/shared/install.ps1 +23 -0
  203. package/agent.bak.20260905-190020/extensions/shared/install.sh +21 -0
  204. package/agent.bak.20260905-190020/extensions/shared/test/child-session.test.ts +270 -0
  205. package/agent.bak.20260905-190020/extensions/shared/test/context-utilization.test.ts +48 -0
  206. package/agent.bak.20260905-190020/extensions/shared/test/tool-call-timeout.test.ts +117 -0
  207. package/agent.bak.20260905-190020/extensions/shared/test/widget-order.test.ts +167 -0
  208. package/agent.bak.20260905-190020/extensions/shared/tool-call-timeout.ts +104 -0
  209. package/agent.bak.20260905-190020/extensions/skillsconf/README.md +74 -0
  210. package/agent.bak.20260905-190020/extensions/skillsconf/index.ts +92 -0
  211. package/agent.bak.20260905-190020/extensions/skillsconf/install.ps1 +23 -0
  212. package/agent.bak.20260905-190020/extensions/skillsconf/install.sh +21 -0
  213. package/agent.bak.20260905-190020/extensions/skillsconf/package-lock.json +1809 -0
  214. package/agent.bak.20260905-190020/extensions/skillsconf/package.json +18 -0
  215. package/agent.bak.20260905-190020/extensions/skillsconf/src/delete-skill.test.ts +64 -0
  216. package/agent.bak.20260905-190020/extensions/skillsconf/src/delete-skill.ts +45 -0
  217. package/agent.bak.20260905-190020/extensions/skillsconf/src/filter.test.ts +80 -0
  218. package/agent.bak.20260905-190020/extensions/skillsconf/src/filter.ts +74 -0
  219. package/agent.bak.20260905-190020/extensions/skillsconf/src/fuzzy.ts +26 -0
  220. package/agent.bak.20260905-190020/extensions/skillsconf/src/persistence.test.ts +72 -0
  221. package/agent.bak.20260905-190020/extensions/skillsconf/src/persistence.ts +169 -0
  222. package/agent.bak.20260905-190020/extensions/skillsconf/src/ui/SkillConfView.test.ts +337 -0
  223. package/agent.bak.20260905-190020/extensions/skillsconf/src/ui/SkillConfView.ts +758 -0
  224. package/agent.bak.20260905-190020/extensions/skillsconf/src/ui/text-input.ts +91 -0
  225. package/agent.bak.20260905-190020/extensions/skillsconf/src/ui/tui-helpers.ts +144 -0
  226. package/agent.bak.20260905-190020/extensions/skillsconf/tsconfig.json +15 -0
  227. package/agent.bak.20260905-190020/extensions/statusline/index.ts +282 -0
  228. package/agent.bak.20260905-190020/extensions/statusline/install.ps1 +23 -0
  229. package/agent.bak.20260905-190020/extensions/statusline/install.sh +21 -0
  230. package/agent.bak.20260905-190020/extensions/subagents/docs/design-plan.md +568 -0
  231. package/agent.bak.20260905-190020/extensions/subagents/docs/effect-v4-extension-guide.md +354 -0
  232. package/agent.bak.20260905-190020/extensions/subagents/docs/effect-v4-notes.md +571 -0
  233. package/agent.bak.20260905-190020/extensions/subagents/index.ts +826 -0
  234. package/agent.bak.20260905-190020/extensions/subagents/install.ps1 +23 -0
  235. package/agent.bak.20260905-190020/extensions/subagents/install.sh +21 -0
  236. package/agent.bak.20260905-190020/extensions/subagents/package-lock.json +2244 -0
  237. package/agent.bak.20260905-190020/extensions/subagents/package.json +19 -0
  238. package/agent.bak.20260905-190020/extensions/subagents/src/backend.ts +73 -0
  239. package/agent.bak.20260905-190020/extensions/subagents/src/backends/claude.ts +701 -0
  240. package/agent.bak.20260905-190020/extensions/subagents/src/backends/codex.ts +1060 -0
  241. package/agent.bak.20260905-190020/extensions/subagents/src/backends/pi.ts +575 -0
  242. package/agent.bak.20260905-190020/extensions/subagents/src/backends/stub.ts +300 -0
  243. package/agent.bak.20260905-190020/extensions/subagents/src/by-the-way.ts +21 -0
  244. package/agent.bak.20260905-190020/extensions/subagents/src/domain.ts +253 -0
  245. package/agent.bak.20260905-190020/extensions/subagents/src/format.ts +74 -0
  246. package/agent.bak.20260905-190020/extensions/subagents/src/manager.ts +736 -0
  247. package/agent.bak.20260905-190020/extensions/subagents/src/prompt.ts +92 -0
  248. package/agent.bak.20260905-190020/extensions/subagents/src/result-delivery.ts +20 -0
  249. package/agent.bak.20260905-190020/extensions/subagents/src/runtime.ts +53 -0
  250. package/agent.bak.20260905-190020/extensions/subagents/src/ui/takeover.ts +583 -0
  251. package/agent.bak.20260905-190020/extensions/subagents/src/ui/transcript.ts +201 -0
  252. package/agent.bak.20260905-190020/extensions/subagents/src/widget-order.ts +121 -0
  253. package/agent.bak.20260905-190020/extensions/subagents/test/by-the-way.test.ts +29 -0
  254. package/agent.bak.20260905-190020/extensions/subagents/test/claude.test.ts +119 -0
  255. package/agent.bak.20260905-190020/extensions/subagents/test/codex.test.ts +102 -0
  256. package/agent.bak.20260905-190020/extensions/subagents/test/context-usage.test.ts +107 -0
  257. package/agent.bak.20260905-190020/extensions/subagents/test/manager.test.ts +276 -0
  258. package/agent.bak.20260905-190020/extensions/subagents/test/result-delivery.test.ts +27 -0
  259. package/agent.bak.20260905-190020/extensions/subagents/test/takeover.test.ts +29 -0
  260. package/agent.bak.20260905-190020/extensions/subagents/tsconfig.json +7 -0
  261. package/agent.bak.20260905-190020/extensions/taste/index.ts +443 -0
  262. package/agent.bak.20260905-190020/extensions/taste/install.ps1 +23 -0
  263. package/agent.bak.20260905-190020/extensions/taste/install.sh +21 -0
  264. package/agent.bak.20260905-190020/extensions/todo/AGENTS.md +38 -0
  265. package/agent.bak.20260905-190020/extensions/todo/LICENSE +21 -0
  266. package/agent.bak.20260905-190020/extensions/todo/config.ts +55 -0
  267. package/agent.bak.20260905-190020/extensions/todo/index.ts +151 -0
  268. package/agent.bak.20260905-190020/extensions/todo/install.ps1 +23 -0
  269. package/agent.bak.20260905-190020/extensions/todo/install.sh +21 -0
  270. package/agent.bak.20260905-190020/extensions/todo/locales/de.json +17 -0
  271. package/agent.bak.20260905-190020/extensions/todo/locales/en.json +15 -0
  272. package/agent.bak.20260905-190020/extensions/todo/locales/es.json +17 -0
  273. package/agent.bak.20260905-190020/extensions/todo/locales/fr.json +17 -0
  274. package/agent.bak.20260905-190020/extensions/todo/locales/pt-BR.json +17 -0
  275. package/agent.bak.20260905-190020/extensions/todo/locales/pt.json +17 -0
  276. package/agent.bak.20260905-190020/extensions/todo/locales/ru.json +17 -0
  277. package/agent.bak.20260905-190020/extensions/todo/locales/uk.json +17 -0
  278. package/agent.bak.20260905-190020/extensions/todo/locales/zh.json +17 -0
  279. package/agent.bak.20260905-190020/extensions/todo/package-lock.json +3358 -0
  280. package/agent.bak.20260905-190020/extensions/todo/package.json +67 -0
  281. package/agent.bak.20260905-190020/extensions/todo/state/i18n-bridge.ts +64 -0
  282. package/agent.bak.20260905-190020/extensions/todo/state/invariants.ts +20 -0
  283. package/agent.bak.20260905-190020/extensions/todo/state/replay.ts +38 -0
  284. package/agent.bak.20260905-190020/extensions/todo/state/selectors.ts +107 -0
  285. package/agent.bak.20260905-190020/extensions/todo/state/state-reducer.ts +326 -0
  286. package/agent.bak.20260905-190020/extensions/todo/state/state.ts +18 -0
  287. package/agent.bak.20260905-190020/extensions/todo/state/store.ts +82 -0
  288. package/agent.bak.20260905-190020/extensions/todo/state/task-graph.ts +57 -0
  289. package/agent.bak.20260905-190020/extensions/todo/todo-overlay.ts +204 -0
  290. package/agent.bak.20260905-190020/extensions/todo/todo.ts +155 -0
  291. package/agent.bak.20260905-190020/extensions/todo/tool/response-envelope.ts +109 -0
  292. package/agent.bak.20260905-190020/extensions/todo/tool/types.ts +206 -0
  293. package/agent.bak.20260905-190020/extensions/todo/view/format.ts +177 -0
  294. package/agent.bak.20260905-190020/extensions/todo/widget-order.ts +121 -0
  295. package/agent.bak.20260905-190020/extensions/trim-context/README.md +54 -0
  296. package/agent.bak.20260905-190020/extensions/trim-context/index.ts +487 -0
  297. package/agent.bak.20260905-190020/extensions/trim-context/install.ps1 +23 -0
  298. package/agent.bak.20260905-190020/extensions/trim-context/install.sh +21 -0
  299. package/agent.bak.20260905-190020/keybindings.json +7 -0
  300. package/bin/kiln.js +588 -471
  301. package/package.json +27 -27
  302. package/agent/install.ps1 +0 -637
  303. package/agent/install.sh +0 -620
package/bin/kiln.js CHANGED
@@ -1,472 +1,589 @@
1
- #!/usr/bin/env node
2
- // kiln installer — cross-platform Node port of agent/install.sh + agent/install.ps1.
3
- // 1) Install/update pi (@earendil-works/pi-coding-agent)
4
- // 2) Install pi packages (pi-context-usage, @baretread/pi-forge)
5
- // 3) Install custom config (AGENTS.md, keybindings, extensions …)
6
- // 4) Install per-extension npm deps via each extension's own installer
7
- //
8
- // Usage:
9
- // npx @asterxsk/kiln --yes
10
- // npm i -g @asterxsk/kiln && kiln --yes
11
- // bunx @asterxsk/kiln --yes
12
- // node bin/kiln.js --target /tmp/pi-test --skip-pi --skip-packages --yes
13
- // node bin/kiln.js --overwrite-settings # replace settings.json (asks by default)
14
- //
15
- // Already-installed items are skipped; outdated global packages are updated.
16
- // Only one timestamped backup (<target>.bak.*) is kept; the clone temp dir
17
- // under ~/.pi/tmp/kiln-* is always removed.
18
- //
19
- // Safe to re-run. Managed files are force-overwritten; per-user files
20
- // (settings.json, taste.md, auth.json, sessions, etc.) are preserved.
21
- "use strict";
22
-
23
- const { spawn, spawnSync } = require("node:child_process");
24
- const fs = require("node:fs");
25
- const os = require("node:os");
26
- const path = require("node:path");
27
- const crypto = require("node:crypto");
28
-
29
- const PI_PACKAGE = "@earendil-works/pi-coding-agent";
30
- const NODE_MIN = [22, 19, 0];
31
- const DEFAULT_REPO = "https://github.com/asterxsk/kiln";
32
- const DEFAULT_BRANCH = "main";
33
- const PACKAGES = ["pi-context-usage@latest", "@baretread/pi-forge@latest"];
34
- const SETTINGS_PACKAGES = ["npm:pi-context-usage", "npm:@baretread/pi-forge"];
35
-
36
- const startTime = Date.now();
37
- let clonedTmp = "";
38
- let tmpLog = "";
39
- let ok = true; // hard failures flip this; warnings don't
40
-
41
- // ── args ────────────────────────────────────────────────────────────────
42
- function parseArgs(argv) {
43
- const o = {
44
- repo: process.env.PI_CONFIG_REPO || "",
45
- branch: process.env.PI_CONFIG_BRANCH || DEFAULT_BRANCH,
46
- target: process.env.PI_AGENT_DIR || "",
47
- skipPi: false, skipPackages: false, yes: false, local: false,
48
- settingsMode: "ask", // ask | overwrite | keep
49
- };
50
- for (let i = 0; i < argv.length; i++) {
51
- const a = argv[i];
52
- if (a === "--repo") o.repo = argv[++i] ?? "";
53
- else if (a === "--branch") o.branch = argv[++i] ?? "";
54
- else if (a === "--target") o.target = argv[++i] ?? "";
55
- else if (a === "--skip-pi") o.skipPi = true;
56
- else if (a === "--skip-packages") o.skipPackages = true;
57
- else if (a === "--local") o.local = true;
58
- else if (a === "--overwrite-settings") o.settingsMode = "overwrite";
59
- else if (a === "--keep-settings") o.settingsMode = "keep";
60
- else if (a === "--yes" || a === "-y") o.yes = true;
61
- else if (a === "--help" || a === "-h") {
62
- console.log("Usage: kiln [--repo URL] [--branch BRANCH] [--target DIR] [--local] [--skip-pi] [--skip-packages] [--overwrite-settings|--keep-settings] [--yes]");
63
- process.exit(0);
64
- } else if (a === "--") break;
65
- else if (a.startsWith("-")) { console.error(`unknown arg: ${a}`); process.exit(1); }
66
- }
67
- if (!o.repo) o.repo = DEFAULT_REPO;
68
- return o;
69
- }
70
-
71
- // ── output ──────────────────────────────────────────────────────────────
72
- const isTTY = !!process.stdout.isTTY && process.env.TERM !== "dumb";
73
- const C = isTTY
74
- ? { reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m", green: "\x1b[32m", yellow: "\x1b[33m", red: "\x1b[31m", cyan: "\x1b[36m", gray: "\x1b[90m" }
75
- : { reset: "", bold: "", dim: "", green: "", yellow: "", red: "", cyan: "", gray: "" };
76
- const line = (s) => process.stdout.write(s + "\n");
77
- const log = (s) => { try { fs.appendFileSync(tmpLog, s + "\n"); } catch {} };
78
-
79
- function title() {
80
- line("");
81
- line(` ${C.bold}◆ Pi Setup${C.reset}`);
82
- line(` ${C.dim}custom agent config · pi + forge + extensions${C.reset}`);
83
- line("");
84
- }
85
-
86
- // Run a command: spinner on TTY, quiet otherwise. Output always → log.
87
- function runStep(step, label, cmd, args, opts = {}) {
88
- return new Promise((resolve) => {
89
- const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"], shell: false, ...opts });
90
- let frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠰", "⠠", "⠦", "⠧", "⠇", "⠏"];
91
- let i = 0, timer = null;
92
- if (isTTY) {
93
- timer = setInterval(() => {
94
- process.stdout.write(`\r\x1b[K ${C.yellow}${frames[i++ % frames.length]}${C.reset} ${C.dim}[${step}/4]${C.reset} ${label}`);
95
- }, 80);
96
- }
97
- child.stdout.on("data", (d) => log(d.toString().trimEnd()));
98
- child.stderr.on("data", (d) => log(d.toString().trimEnd()));
99
- child.on("error", (e) => { log(`spawn failed: ${e.message}`); if (timer) clearInterval(timer); if (isTTY) process.stdout.write("\r\x1b[K"); resolve(1); });
100
- child.on("close", (code) => { if (timer) clearInterval(timer); if (isTTY) process.stdout.write("\r\x1b[K"); resolve(code ?? 1); });
101
- });
102
- }
103
-
104
- function have(cmd) {
105
- try {
106
- if (process.platform === "win32") return spawnSync("where", [cmd], { stdio: "ignore" }).status === 0;
107
- return spawnSync("sh", ["-c", `command -v "${cmd}"`], { stdio: "ignore" }).status === 0;
108
- } catch { return false; }
109
- }
110
- function cmdOut(cmd, args) {
111
- try { return spawnSync(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).stdout.trim(); }
112
- catch { return ""; }
113
- }
114
- function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
115
- // Quote one shell word (cmd.exe + sh compatible for our controlled args).
116
- const q = (a) => (/[\s"']/.test(a) ? `"${a.replace(/"/g, '\\"')}"` : a);
117
- function cmdOutShell(command) {
118
- try { return spawnSync(command, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], shell: true }).stdout.trim(); }
119
- catch { return ""; }
120
- }
121
- // npm on Windows is npm.cmd (a batch shim) — it cannot be spawned directly,
122
- // so run it through the shell as a single pre-quoted command string.
123
- function npmStep(step, label, args) {
124
- if (process.platform === "win32") return runStep(step, label, "npm.cmd " + args.map(q).join(" "), [], { shell: true });
125
- return runStep(step, label, "npm", args);
126
- }
127
- const npmVer = process.platform === "win32" ? cmdOutShell("npm.cmd --version") : cmdOut("npm", ["--version"]);
128
-
129
- // Installed global version of a package ("" when not installed).
130
- function globalPkgVersion(name) {
131
- const out = process.platform === "win32"
132
- ? cmdOutShell(`npm.cmd ls -g "${name}" --depth=0`)
133
- : cmdOut("npm", ["ls", "-g", name, "--depth=0"]);
134
- const m = out.match(new RegExp(name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "@(\\S+)"));
135
- return m ? m[1].replace(/[^0-9A-Za-z.+-].*$/, "") : "";
136
- }
137
- // Latest registry version ("" when offline / unknown).
138
- function latestPkgVersion(name) {
139
- const out = process.platform === "win32"
140
- ? cmdOutShell(`npm.cmd view "${name}" version`)
141
- : cmdOut("npm", ["view", name, "version"]);
142
- const v = (out || "").split(/\s+/)[0].trim();
143
- return /^[0-9][0-9A-Za-z.+-]*$/.test(v) ? v : "";
144
- }
145
- // Synchronous Y/N prompt (default N). False when non-interactive.
146
- function askYN(question) {
147
- try {
148
- if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
149
- process.stdout.write(question);
150
- const buf = Buffer.alloc(16);
151
- const n = fs.readSync(0, buf, 0, 16);
152
- const ans = buf.slice(0, n).toString().trim().toLowerCase();
153
- return ans === "y" || ans === "yes";
154
- } catch { return false; }
155
- }
156
- // Keep only the newest sibling backup (<target>.bak.*); delete the rest.
157
- function pruneBackups(targetDir, keep) {
158
- try {
159
- const parent = path.dirname(targetDir);
160
- const base = path.basename(targetDir);
161
- const keepBase = keep ? path.basename(keep) : "";
162
- for (const b of fs.readdirSync(parent)) {
163
- if (!b.startsWith(base + ".bak.")) continue;
164
- if (b === keepBase) continue;
165
- try { fs.rmSync(path.join(parent, b), { recursive: true, force: true }); } catch {}
166
- }
167
- } catch (e) { log(`prune backups failed: ${e.message}`); }
168
- }
169
- // Remove stale clone dirs (~/.pi/tmp/kiln-*) except the active one.
170
- function sweepStaleCloneTmp(tmpBase, active) {
171
- try {
172
- for (const e of fs.readdirSync(tmpBase)) {
173
- if (!e.startsWith("kiln-")) continue;
174
- if (active && e === path.basename(active)) continue;
175
- try { fs.rmSync(path.join(tmpBase, e), { recursive: true, force: true }); } catch {}
176
- }
177
- } catch {}
178
- }
179
-
180
- // Recursive copy, excluding names (node_modules, .git). Returns false on error.
181
- function copyTree(src, dst, exclude = new Set(["node_modules", ".git"])) {
182
- try {
183
- if (path.resolve(src) === path.resolve(dst)) return true;
184
- fs.rmSync(dst, { recursive: true, force: true });
185
- fs.mkdirSync(dst, { recursive: true });
186
- for (const e of fs.readdirSync(src, { withFileTypes: true })) {
187
- if (exclude.has(e.name)) continue;
188
- const s = path.join(src, e.name), d = path.join(dst, e.name);
189
- if (e.isDirectory()) { if (!copyTree(s, d, exclude)) return false; }
190
- else if (e.isFile()) fs.copyFileSync(s, d);
191
- }
192
- return true;
193
- } catch (e) { log(`copy failed ${src} → ${dst}: ${e.message}`); return false; }
194
- }
195
-
196
- function resolveTarget(opt) {
197
- if (opt) return opt;
198
- return path.join(os.homedir(), ".pi", "agent");
199
- }
200
-
201
- // Source root: bundled payload (npm tarball or dev checkout) → else git clone.
202
- function resolveSourceRoot(args) {
203
- const pkgRoot = path.resolve(__dirname, ".."); // bin/ → package root
204
- const cands = [path.join(pkgRoot, "agent"), pkgRoot, process.cwd(), path.join(process.cwd(), "agent")];
205
- for (const r of cands) {
206
- if (fs.existsSync(path.join(r, "extensions")) && fs.existsSync(path.join(r, "AGENTS.md"))) return r;
207
- }
208
- if (args.local) throw new Error(`local checkout not found (tried: ${cands.join(", ")})`);
209
- const base = path.join(os.homedir(), ".pi", "tmp");
210
- fs.mkdirSync(base, { recursive: true });
211
- const tmp = path.join(base, "kiln-" + crypto.randomBytes(4).toString("hex"));
212
- fs.mkdirSync(tmp, { recursive: true });
213
- line(`${C.dim} → cloning ${args.repo} (branch ${args.branch}) → ${tmp}${C.reset}`);
214
- const r = spawnSync("git", ["clone", "--depth", "1", "--branch", args.branch, args.repo, tmp],
215
- { stdio: ["ignore", "pipe", "pipe"], encoding: "utf8", env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GCM_INTERACTIVE: "never", GIT_ASKPASS: "echo" } });
216
- if (r.stdout) log(r.stdout.trimEnd());
217
- if (r.stderr) log(r.stderr.trimEnd());
218
- if (r.status !== 0) throw new Error(`git clone failed (exit ${r.status}). See ${tmpLog}`);
219
- clonedTmp = tmp;
220
- if (fs.existsSync(path.join(tmp, "agent"))) return path.join(tmp, "agent");
221
- return tmp;
222
- }
223
-
224
- async function main() {
225
- const args = parseArgs(process.argv.slice(2));
226
- const tmpBase = path.join(os.homedir(), ".pi", "tmp");
227
- fs.mkdirSync(tmpBase, { recursive: true });
228
- tmpLog = path.join(tmpBase, `pi-setup-${crypto.randomBytes(4).toString("hex")}.log`);
229
- fs.writeFileSync(tmpLog, "");
230
- process.on("exit", () => { if (clonedTmp && clonedTmp.startsWith(path.join(tmpBase, "kiln-"))) fs.rmSync(clonedTmp, { recursive: true, force: true }); });
231
-
232
- title();
233
- const targetDir = resolveTarget(args.target);
234
- line(`${C.dim} target ${C.reset}${targetDir}`);
235
- line(`${C.dim} repo ${C.reset}${args.repo} ${C.dim}(${args.branch})${C.reset}`);
236
- line(`${C.dim} log ${C.reset}${tmpLog}\n`);
237
-
238
- // ── preflight ──
239
- const nodeVer = process.versions.node.split("-")[0];
240
- const nv = nodeVer.split(".").map(Number);
241
- const nodeOk = nv[0] > NODE_MIN[0] || (nv[0] === NODE_MIN[0] && (nv[1] > NODE_MIN[1] || (nv[1] === NODE_MIN[1] && nv[2] >= NODE_MIN[2])));
242
- if (!nodeOk) {
243
- line(`${C.yellow} ⚠ Node ${nodeVer} < ${NODE_MIN.join(".")} — please upgrade to Node ${NODE_MIN.join(".")}+${C.reset}`);
244
- if (!args.yes) { line(" continue anyway in 3s…"); await sleep(3000); }
245
- }
246
- if (!have("npm")) { line(`${C.red} ✖ npm not found (Node installed but npm missing)${C.reset}`); process.exit(1); }
247
- line(`${C.dim} node ${nodeVer} · npm ${npmVer} · ${cmdOut("git", ["--version"]) || "git n/a"}${C.reset}\n`);
248
-
249
- // git is only needed when we must clone (no bundled payload and not --local)
250
- const pkgAgent = path.join(path.resolve(__dirname, ".."), "agent");
251
- const haveSource = fs.existsSync(path.join(pkgAgent, "extensions")) && fs.existsSync(path.join(pkgAgent, "AGENTS.md"));
252
- if (!have("git") && !haveSource && !args.local) { line(`${C.red} ✖ git not found — required to fetch config${C.reset}`); process.exit(1); }
253
-
254
- // ── [1/4] pi ──
255
- if (args.skipPi) {
256
- line(` ${C.green}✔${C.reset} ${C.dim}[1/4]${C.reset} Install pi ${C.dim}— skipped${C.reset}`);
257
- } else if (have("pi")) {
258
- const installed = globalPkgVersion(PI_PACKAGE) || cmdOut("pi", ["--version"]).replace(/^[vV]/, "").trim();
259
- const latest = latestPkgVersion(PI_PACKAGE);
260
- if (installed && latest && installed === latest) {
261
- line(` ${C.green}✔${C.reset} ${C.dim}[1/4]${C.reset} pi ${C.dim}— already installed (${installed}), skipping${C.reset}`);
262
- } else if (installed && !latest) {
263
- line(` ${C.green}✔${C.reset} ${C.dim}[1/4]${C.reset} pi ${C.dim}— already installed (${installed}), skipping version check (offline)${C.reset}`);
264
- } else {
265
- const label = installed ? `updating pi ${installed} → ${latest || "latest"} (${PI_PACKAGE})` : `installing pi (${PI_PACKAGE})`;
266
- const code = await npmStep("1", label, ["install", "-g", `${PI_PACKAGE}@latest`, "--no-audit", "--no-fund", "--min-release-age=0"]);
267
- if (code === 0) {
268
- line(` ${C.green}✔${C.reset} ${C.dim}[1/4]${C.reset} ${label} ${C.dim}— ${cmdOut("pi", ["--version"]) || "?"}${C.reset}`);
269
- } else {
270
- line(` ${C.red}✖${C.reset} ${C.dim}[1/4]${C.reset} ${label} ${C.red}failed (exit ${code})${C.reset} ${C.dim}see ${tmpLog}${C.reset}`);
271
- ok = false;
272
- }
273
- }
274
- } else {
275
- const label = have("pi") ? `updating pi (${PI_PACKAGE})` : `installing pi (${PI_PACKAGE})`;
276
- const code = await npmStep("1", label, ["install", "-g", `${PI_PACKAGE}@latest`, "--no-audit", "--no-fund", "--min-release-age=0"]);
277
- if (code === 0) {
278
- line(` ${C.green}✔${C.reset} ${C.dim}[1/4]${C.reset} ${label} ${C.dim}— ${cmdOut("pi", ["--version"]) || "?"}${C.reset}`);
279
- } else {
280
- line(` ${C.red}✖${C.reset} ${C.dim}[1/4]${C.reset} ${label} ${C.red}failed (exit ${code})${C.reset} ${C.dim}see ${tmpLog}${C.reset}`);
281
- ok = false;
282
- }
283
- }
284
- // ── [2/4] pi packages ──
285
- if (args.skipPackages) {
286
- line(` ${C.green}✔${C.reset} ${C.dim}[2/4]${C.reset} pi packages ${C.dim}— skipped${C.reset}`);
287
- } else {
288
- const bare = PACKAGES.map((p) => p.replace(/@latest$/, ""));
289
- const need = [];
290
- for (const name of bare) {
291
- const inst = globalPkgVersion(name);
292
- const latest = latestPkgVersion(name);
293
- if (inst && latest && inst === latest) {
294
- line(`${C.dim} · ${name}@${inst} already installed, skipping${C.reset}`);
295
- } else if (inst && !latest) {
296
- line(`${C.dim} · ${name}@${inst} already installed, skipping version check (offline)${C.reset}`);
297
- } else {
298
- if (inst) line(`${C.dim} · ${name} ${inst} → ${latest || "latest"} — will update${C.reset}`);
299
- need.push(`${name}@latest`);
300
- }
301
- }
302
- if (!need.length) {
303
- line(` ${C.green}✔${C.reset} ${C.dim}[2/4]${C.reset} pi packages ${C.dim}— already installed, skipping${C.reset}`);
304
- } else {
305
- const label = `installing ${need.join(" ")}`;
306
- const code = await npmStep("2", label, ["install", "-g", ...need, "--no-audit", "--no-fund"]);
307
- if (code === 0) line(` ${C.green}✔${C.reset} ${C.dim}[2/4]${C.reset} pi packages ${C.dim}— done${C.reset}`);
308
- else {
309
- line(` ${C.yellow}⚠${C.reset} ${C.dim}[2/4]${C.reset} ${label} ${C.yellow}exit ${code} — continuing (pi will auto-install)${C.reset}`);
310
- line(` ${C.dim}see ${tmpLog}${C.reset}`);
311
- }
312
- }
313
- }
314
- // ── [3/4] custom config ──
315
- try {
316
- const sourceRoot = resolveSourceRoot(args);
317
- line(`${C.dim} source ${sourceRoot}${C.reset}`);
318
- fs.mkdirSync(targetDir, { recursive: true });
319
- let bak = "";
320
- if (fs.existsSync(path.join(targetDir, "AGENTS.md")) || fs.existsSync(path.join(targetDir, "extensions"))) {
321
- const d = new Date(), p2 = (n) => String(n).padStart(2, "0");
322
- bak = `${targetDir}.bak.${d.getFullYear()}${p2(d.getMonth() + 1)}${p2(d.getDate())}-${p2(d.getHours())}${p2(d.getMinutes())}${p2(d.getSeconds())}`;
323
- line(`${C.dim} backup ${targetDir}/extensions → ${bak}${C.reset}`);
324
- fs.mkdirSync(bak, { recursive: true });
325
- for (const f of ["AGENTS.md", "keybindings.json", "README.md"]) {
326
- const s = path.join(targetDir, f);
327
- if (fs.existsSync(s)) try { fs.copyFileSync(s, path.join(bak, f)); } catch {}
328
- }
329
- const extSrc = path.join(targetDir, "extensions");
330
- if (fs.existsSync(extSrc)) {
331
- fs.mkdirSync(path.join(bak, "extensions"), { recursive: true });
332
- for (const e of fs.readdirSync(extSrc, { withFileTypes: true })) {
333
- if (!e.isDirectory()) continue;
334
- copyTree(path.join(extSrc, e.name), path.join(bak, "extensions", e.name));
335
- }
336
- }
337
- pruneBackups(targetDir, bak); // only one backup at a time
338
- }
339
-
340
- let copied = 0;
341
- const selfInstall = path.resolve(sourceRoot) === path.resolve(targetDir);
342
- if (selfInstall) line(`${C.dim} source == target — skipping file copy (self-install)${C.reset}`);
343
- for (const f of ["AGENTS.md", "keybindings.json", "README.md"]) {
344
- if (!fs.existsSync(path.join(sourceRoot, f))) continue;
345
- if (!selfInstall) fs.copyFileSync(path.join(sourceRoot, f), path.join(targetDir, f));
346
- copied++;
347
- }
348
- let freshSettings = false;
349
- const settingsPath = path.join(targetDir, "settings.json");
350
- const repoSettings = path.join(sourceRoot, "settings.json");
351
- if (!fs.existsSync(settingsPath) && fs.existsSync(repoSettings)) {
352
- fs.copyFileSync(repoSettings, settingsPath);
353
- line(`${C.dim} created settings.json from repo defaults${C.reset}`);
354
- freshSettings = true;
355
- } else if (fs.existsSync(settingsPath)) {
356
- let mode = args.settingsMode;
357
- if (mode === "ask" && (args.yes || !process.stdin.isTTY)) mode = "keep";
358
- if (mode === "ask") mode = askYN(` settings.json exists. Overwrite with repo defaults? [y/N] (Recommended for first-time setup) `) ? "overwrite" : "keep";
359
- if (mode === "overwrite" && fs.existsSync(repoSettings)) {
360
- if (bak) { try { fs.copyFileSync(settingsPath, path.join(bak, "settings.json")); } catch {} }
361
- fs.copyFileSync(repoSettings, settingsPath);
362
- line(`${C.dim} overwrote settings.json from repo defaults${C.reset}`);
363
- freshSettings = true;
364
- } else {
365
- line(`${C.dim} kept existing settings.json${C.reset}`);
366
- }
367
- }
368
- if (freshSettings) {
369
- try {
370
- const p = path.join(targetDir, "settings.json");
371
- const j = JSON.parse(fs.readFileSync(p, "utf8"));
372
- j.packages = j.packages || [];
373
- let changed = false;
374
- for (const pkg of SETTINGS_PACKAGES) if (!j.packages.includes(pkg)) { j.packages.push(pkg); changed = true; }
375
- if (changed) { fs.writeFileSync(p, JSON.stringify(j, null, 2) + "\n"); line(" patched settings.json packages → forge + context"); }
376
- } catch {}
377
- }
378
- for (const t of ["taste.md", "taste", "taste.json"]) {
379
- const tp = path.join(targetDir, t);
380
- if (fs.existsSync(tp)) line(`${C.dim} kept existing ${t}${C.reset}`);
381
- else if (fs.existsSync(path.join(sourceRoot, t))) {
382
- const s = path.join(sourceRoot, t);
383
- if (fs.statSync(s).isDirectory()) copyTree(s, tp, new Set());
384
- else fs.copyFileSync(s, tp);
385
- }
386
- }
387
-
388
- if (fs.existsSync(path.join(sourceRoot, "extensions"))) {
389
- fs.mkdirSync(path.join(targetDir, "extensions"), { recursive: true });
390
- for (const e of fs.readdirSync(path.join(sourceRoot, "extensions"), { withFileTypes: true })) {
391
- if (!e.isDirectory() || e.name.endsWith(".md")) continue;
392
- if (copyTree(path.join(sourceRoot, "extensions", e.name), path.join(targetDir, "extensions", e.name))) copied++;
393
- else line(`${C.yellow} ⚠ copy extensions/${e.name} failed${C.reset}`);
394
- }
395
- }
396
-
397
- if (!selfInstall && fs.existsSync(path.join(sourceRoot, "themes"))) {
398
- fs.rmSync(path.join(targetDir, "themes"), { recursive: true, force: true });
399
- fs.mkdirSync(path.join(targetDir, "themes"), { recursive: true });
400
- for (const e of fs.readdirSync(path.join(sourceRoot, "themes"), { withFileTypes: true })) {
401
- if (e.name === ".pi" || e.name === "nul") continue;
402
- const s = path.join(sourceRoot, "themes", e.name), dd = path.join(targetDir, "themes", e.name);
403
- if (e.isDirectory()) copyTree(s, dd, new Set());
404
- else if (e.isFile()) try { fs.copyFileSync(s, dd); } catch {}
405
- }
406
- }
407
-
408
- line(` ${C.green}✔${C.reset} ${C.dim}[3/4]${C.reset} custom config ${C.dim}— ${copied} items → ${targetDir}${C.reset}`);
409
- if (clonedTmp) { fs.rmSync(clonedTmp, { recursive: true, force: true }); clonedTmp = ""; }
410
- sweepStaleCloneTmp(tmpBase, ""); // delete any leftover kiln-* temp dirs
411
- const repoReadme = path.resolve(sourceRoot, "..", "README.md");
412
- const parentReadme = path.resolve(targetDir, "..", "README.md");
413
- if (repoReadme !== parentReadme && fs.existsSync(repoReadme)) {
414
- try { fs.copyFileSync(repoReadme, parentReadme); } catch {}
415
- }
416
- } catch (e) {
417
- line(` ${C.red}✖${C.reset} ${C.dim}[3/4]${C.reset} custom config ${C.red}failed:${C.reset} ${e.message}`);
418
- ok = false;
419
- }
420
- // ── [4/4] per-extension deps ──
421
- const extRoot = path.join(targetDir, "extensions");
422
- if (!fs.existsSync(extRoot)) {
423
- line(` ${C.yellow}⚠${C.reset} ${C.dim}[4/4]${C.reset} no extensions found at ${extRoot}`);
424
- } else {
425
- const failed = [];
426
- let installed = 0, skipped = 0;
427
- for (const e of fs.readdirSync(extRoot, { withFileTypes: true })) {
428
- if (!e.isDirectory() || e.name.endsWith(".md")) continue;
429
- const dir = path.join(extRoot, e.name), label = `extensions/${e.name}`;
430
- if (fs.existsSync(path.join(dir, "node_modules"))) {
431
- line(` ${C.dim} · ${label} — already installed, skipping${C.reset}`);
432
- skipped++;
433
- continue;
434
- }
435
- const hasPkg = fs.existsSync(path.join(dir, "package.json"));
436
- const sh = path.join(dir, "install.sh"), ps1 = path.join(dir, "install.ps1");
437
- let code;
438
- if (process.platform === "win32" && fs.existsSync(ps1)) {
439
- const shell = have("pwsh") ? "pwsh" : "powershell";
440
- code = await runStep("4", label, shell, ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", ps1]);
441
- } else if (process.platform !== "win32" && fs.existsSync(sh)) {
442
- code = await runStep("4", label, "sh", [sh]);
443
- } else if (hasPkg) {
444
- const a = ["--prefix", dir, fs.existsSync(path.join(dir, "package-lock.json")) ? "ci" : "install", "--no-audit", "--no-fund"];
445
- code = await npmStep("4", label, a);
446
- } else {
447
- line(` ${C.dim} · ${label} — no package.json, skipping${C.reset}`);
448
- continue;
449
- }
450
- if (code === 0) { line(` ${C.green}✔${C.reset} ${C.dim}[4/4]${C.reset} ${label}`); installed++; }
451
- else { line(` ${C.red}✖${C.reset} ${C.dim}[4/4]${C.reset} ${label} ${C.red}failed (exit ${code})${C.reset}`); failed.push(e.name); }
452
- }
453
- if (!failed.length) line(` ${C.green}✔${C.reset} ${C.dim}[4/4]${C.reset} extension deps ${C.dim}— ${installed} installed, ${skipped} skipped${C.reset}`);
454
- else line(` ${C.yellow}⚠${C.reset} ${C.dim}[4/4]${C.reset} extension deps ${C.yellow}${failed.length} failed:${C.reset} ${failed.join(", ")} ${C.dim}see ${tmpLog}${C.reset}`);
455
- }
456
-
457
- // ── summary ──
458
- const secs = Math.round((Date.now() - startTime) / 1000);
459
- const elapsed = secs >= 60 ? `${Math.floor(secs / 60)}m ${secs % 60}s` : `${secs}s`;
460
- line("");
461
- if (ok) {
462
- line(`${C.green}${C.bold} ✓ Setup complete${C.reset} ${C.dim}→ ${targetDir} · Kiln installed in ${elapsed}${C.reset}`);
463
- const pv = cmdOut("pi", ["--version"]);
464
- if (pv) line(`${C.dim} pi ${pv} · run: pi${C.reset}`);
465
- } else {
466
- line(`${C.yellow}${C.bold} ⚠ Setup finished with warnings${C.reset} ${C.dim}in ${elapsed} · see ${tmpLog}${C.reset}`);
467
- }
468
- line(`${C.dim} log: ${tmpLog}${C.reset}\n`);
469
- process.exitCode = ok ? 0 : 1;
470
- }
471
-
1
+ #!/usr/bin/env node
2
+ // kiln installer (npm) — installs pi, the forge + context packages, and the curated agent config.
3
+ // 1) Install/update pi (@earendil-works/pi-coding-agent)
4
+ // 2) Install pi packages (pi-context-usage, @baretread/pi-forge)
5
+ // 3) Install custom config (AGENTS.md, keybindings, extensions …)
6
+ // 4) Install per-extension npm deps via each extension's own installer
7
+ //
8
+ // Usage:
9
+ // npx @asterxsk/kiln --yes
10
+ // npm i -g @asterxsk/kiln && kiln --yes
11
+ // bunx @asterxsk/kiln --yes
12
+ // node bin/kiln.js --target /tmp/pi-test --skip-pi --skip-packages --yes
13
+ // node bin/kiln.js --overwrite-settings # replace settings.json (asks by default)
14
+ //
15
+ // Already-installed items are skipped; outdated global packages are updated.
16
+ // Only one timestamped backup (<target>.bak.*) is kept; the clone temp dir
17
+ // under ~/.pi/tmp/kiln-* is always removed.
18
+ //
19
+ // Safe to re-run. Managed files are force-overwritten; per-user files
20
+ // (settings.json, taste.md, auth.json, sessions, etc.) are preserved.
21
+ "use strict";
22
+
23
+ const { spawn, spawnSync } = require("node:child_process");
24
+ const fs = require("node:fs");
25
+ const os = require("node:os");
26
+ const path = require("node:path");
27
+ const crypto = require("node:crypto");
28
+
29
+ const PI_PACKAGE = "@earendil-works/pi-coding-agent";
30
+ const NODE_MIN = [22, 19, 0];
31
+ const DEFAULT_REPO = "https://github.com/asterxsk/kiln";
32
+ const DEFAULT_BRANCH = "main";
33
+ const PACKAGES = ["pi-context-usage@latest", "@baretread/pi-forge@latest"];
34
+ const SETTINGS_PACKAGES = ["npm:pi-context-usage", "npm:@baretread/pi-forge"];
35
+
36
+ const startTime = Date.now();
37
+ let clonedTmp = "";
38
+ let tmpLog = "";
39
+ let ok = true; // hard failures flip this; warnings don't
40
+
41
+ // npm >=11 rejects `npm_config_allow_scripts` inherited from a parent npm/npx
42
+ // process ("--allow-scripts is not allowed in project-scoped installs"). This
43
+ // happens when kiln itself runs under npx/npm exec and the user has
44
+ // `allow-scripts` in ~/.npmrc: npm forwards it as env to every child, so each
45
+ // per-extension `npm ci` fails with EALLOWSCRIPTS. Drop it; file-based config
46
+ // (~/.npmrc, per-package `allowScripts`) still applies to child npm processes.
47
+ for (const k of ["npm_config_allow_scripts", "NPM_CONFIG_ALLOW_SCRIPTS"]) delete process.env[k];
48
+
49
+ // ── summary buckets ─────────────────────────────────────────────────────
50
+ // Every completed item is recorded here; the final Summary prints them all.
51
+ const buckets = { skipped: [], updated: [], installed: [], "npm modules": [] };
52
+ const FRIENDLY = { "pi-context-usage": "context extension", "@baretread/pi-forge": "forge extension" };
53
+ function note(bucket, name) { buckets[bucket].push(name); }
54
+ function prettyPkg(spec) { return FRIENDLY[spec.replace(/^npm:/, "")] || spec; }
55
+
56
+ // ── args ────────────────────────────────────────────────────────────────
57
+ function parseArgs(argv) {
58
+ const o = {
59
+ repo: process.env.PI_CONFIG_REPO || "",
60
+ branch: process.env.PI_CONFIG_BRANCH || DEFAULT_BRANCH,
61
+ target: process.env.PI_AGENT_DIR || "",
62
+ skipPi: false, skipPackages: false, yes: false, local: false,
63
+ settingsMode: "ask", // ask | overwrite | keep
64
+ };
65
+ for (let i = 0; i < argv.length; i++) {
66
+ const a = argv[i];
67
+ if (a === "--repo") o.repo = argv[++i] ?? "";
68
+ else if (a === "--branch") o.branch = argv[++i] ?? "";
69
+ else if (a === "--target") o.target = argv[++i] ?? "";
70
+ else if (a === "--skip-pi") o.skipPi = true;
71
+ else if (a === "--skip-packages") o.skipPackages = true;
72
+ else if (a === "--local") o.local = true;
73
+ else if (a === "--overwrite-settings") o.settingsMode = "overwrite";
74
+ else if (a === "--keep-settings") o.settingsMode = "keep";
75
+ else if (a === "--yes" || a === "-y") o.yes = true;
76
+ else if (a === "--help" || a === "-h") {
77
+ console.log("Usage: kiln [--repo URL] [--branch BRANCH] [--target DIR] [--local] [--skip-pi] [--skip-packages] [--overwrite-settings|--keep-settings] [--yes]");
78
+ process.exit(0);
79
+ } else if (a === "--") break;
80
+ else if (a.startsWith("-")) { console.error(`unknown arg: ${a}`); process.exit(1); }
81
+ }
82
+ if (!o.repo) o.repo = DEFAULT_REPO;
83
+ return o;
84
+ }
85
+
86
+ // ── output ──────────────────────────────────────────────────────────────
87
+ const isTTY = !!process.stdout.isTTY && process.env.TERM !== "dumb";
88
+ const C = isTTY
89
+ ? { reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m", green: "\x1b[32m", yellow: "\x1b[33m", red: "\x1b[31m", cyan: "\x1b[36m", gray: "\x1b[90m" }
90
+ : { reset: "", bold: "", dim: "", green: "", yellow: "", red: "", cyan: "", gray: "" };
91
+ const line = (s) => process.stdout.write(s + "\n");
92
+ const log = (s) => { try { fs.appendFileSync(tmpLog, s + "\n"); } catch {} };
93
+ // Mid-run output: plain lines when piped, swallowed (→ log + summary) when live.
94
+ function detail(s) { try { log(String(s).replace(/\x1b\[[0-9;]*m/g, "")); } catch {} if (!isTTY) line(s); }
95
+
96
+ // ── forge art + live status ─────────────────────────────────────────────
97
+ const ART = [
98
+ " █▌ █▌",
99
+ " ██████████████",
100
+ " ██ ██",
101
+ " ██ ██",
102
+ " ██ ██",
103
+ " ██ ██ ",
104
+ ];
105
+ // Phase 0 = ember (dim), phase 1 = stoked (bright). Non-TTY → plain art.
106
+ function artLines(phase) {
107
+ if (!isTTY) return ART.slice();
108
+ const m = phase ? 1 : 2; // bold vs dim
109
+ const cream = `\x1b[${m};38;5;230m`, orange = `\x1b[${m};38;5;208m`, brown = `\x1b[${m};38;5;94m`, R = "\x1b[0m";
110
+ return [
111
+ ` ${cream}█▌${R} ${cream}█▌${R}`,
112
+ ` ${orange}██████████████${R}`,
113
+ ` ${orange}██${R} ${orange}██${R}`,
114
+ ` ${orange}██${R} ${orange}██${R}`,
115
+ ` ${orange}██${R} ${orange}██${R}`,
116
+ ` ${brown}██ ██${R} `,
117
+ ];
118
+ }
119
+ const SPIN = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠰", "⠠", "⠦", "⠧", "⠇", "⠏"];
120
+ let liveLabel = "", liveTimer = null, liveTickN = 0;
121
+ function liveDraw() {
122
+ if (!isTTY) return;
123
+ const rows = [...artLines(liveTickN % 10 < 5 ? 0 : 1), ` ${C.yellow}${SPIN[liveTickN % SPIN.length]}${C.reset} ${C.dim}${liveLabel}${C.reset}`];
124
+ process.stdout.write("\x1b[7A");
125
+ for (const r of rows) process.stdout.write(`\r\x1b[K${r}\n`);
126
+ }
127
+ function liveStart() {
128
+ if (!isTTY) return;
129
+ process.stdout.write("\n".repeat(7));
130
+ liveTickN = 0;
131
+ liveDraw();
132
+ liveTimer = setInterval(() => { liveTickN++; liveDraw(); }, 100);
133
+ }
134
+ function liveSet(label) { liveLabel = label; }
135
+ function liveStop() {
136
+ if (!isTTY) return;
137
+ if (liveTimer) { clearInterval(liveTimer); liveTimer = null; }
138
+ liveTickN = 5; // stoked final frame
139
+ liveDraw();
140
+ }
141
+
142
+ function title() {
143
+ line("");
144
+ line(` ${C.bold}◆ Pi Setup${C.reset}`);
145
+ line(` ${C.dim}custom agent config · pi + forge + extensions${C.reset}`);
146
+ if (!isTTY) for (const r of artLines(0)) line(` ${r}`);
147
+ line("");
148
+ }
149
+
150
+ // Run a command: the live status line shows it on TTY, plain lines when piped. Output always → log.
151
+ function runStep(step, label, cmd, args, opts = {}) {
152
+ return new Promise((resolve) => {
153
+ liveSet(`[${step}] ${label}`);
154
+ if (!isTTY) line(` · ${label}`);
155
+ const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"], shell: false, ...opts });
156
+ child.stdout.on("data", (d) => log(d.toString().trimEnd()));
157
+ child.stderr.on("data", (d) => log(d.toString().trimEnd()));
158
+ child.on("error", (e) => { log(`spawn failed: ${e.message}`); resolve(1); });
159
+ child.on("close", (code) => { resolve(code ?? 1); });
160
+ });
161
+ }
162
+
163
+ function have(cmd) {
164
+ try {
165
+ if (process.platform === "win32") return spawnSync("where", [cmd], { stdio: "ignore" }).status === 0;
166
+ return spawnSync("sh", ["-c", `command -v "${cmd}"`], { stdio: "ignore" }).status === 0;
167
+ } catch { return false; }
168
+ }
169
+ function cmdOut(cmd, args) {
170
+ try { return spawnSync(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).stdout.trim(); }
171
+ catch { return ""; }
172
+ }
173
+ function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
174
+ // Quote one shell word (cmd.exe + sh compatible for our controlled args).
175
+ const q = (a) => (/[\s"']/.test(a) ? `"${a.replace(/"/g, '\\"')}"` : a);
176
+ function cmdOutShell(command) {
177
+ try { return spawnSync(command, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], shell: true }).stdout.trim(); }
178
+ catch { return ""; }
179
+ }
180
+ // npm on Windows is npm.cmd (a batch shim) — it cannot be spawned directly,
181
+ // so run it through the shell as a single pre-quoted command string.
182
+ function npmStep(step, label, args) {
183
+ if (process.platform === "win32") return runStep(step, label, "npm.cmd " + args.map(q).join(" "), [], { shell: true });
184
+ return runStep(step, label, "npm", args);
185
+ }
186
+ // pi is also a cmd shim on Windows — same shell treatment.
187
+ function piOut(args) {
188
+ if (process.platform === "win32") return cmdOutShell("pi " + args.map(q).join(" "));
189
+ return cmdOut("pi", args);
190
+ }
191
+ function piStep(label, args) {
192
+ if (process.platform === "win32") return runStep("pkg", label, "pi " + args.map(q).join(" "), [], { shell: true });
193
+ return runStep("pkg", label, "pi", args);
194
+ }
195
+ const npmVer = process.platform === "win32" ? cmdOutShell("npm.cmd --version") : cmdOut("npm", ["--version"]);
196
+
197
+ // Installed global version of a package ("" when not installed).
198
+ function globalPkgVersion(name) {
199
+ const out = process.platform === "win32"
200
+ ? cmdOutShell(`npm.cmd ls -g "${name}" --depth=0`)
201
+ : cmdOut("npm", ["ls", "-g", name, "--depth=0"]);
202
+ const m = out.match(new RegExp(name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "@(\\S+)"));
203
+ return m ? m[1].replace(/[^0-9A-Za-z.+-].*$/, "") : "";
204
+ }
205
+ // Latest registry version ("" when offline / unknown).
206
+ function latestPkgVersion(name) {
207
+ const out = process.platform === "win32"
208
+ ? cmdOutShell(`npm.cmd view "${name}" version`)
209
+ : cmdOut("npm", ["view", name, "version"]);
210
+ const v = (out || "").split(/\s+/)[0].trim();
211
+ return /^[0-9][0-9A-Za-z.+-]*$/.test(v) ? v : "";
212
+ }
213
+ // Synchronous Y/N prompt (default N). False when non-interactive.
214
+ function askYN(question) {
215
+ try {
216
+ if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
217
+ process.stdout.write(question);
218
+ const buf = Buffer.alloc(16);
219
+ const n = fs.readSync(0, buf, 0, 16);
220
+ const ans = buf.slice(0, n).toString().trim().toLowerCase();
221
+ return ans === "y" || ans === "yes";
222
+ } catch { return false; }
223
+ }
224
+ // Keep only the newest sibling backup (<target>.bak.*); delete the rest.
225
+ function pruneBackups(targetDir, keep) {
226
+ try {
227
+ const parent = path.dirname(targetDir);
228
+ const base = path.basename(targetDir);
229
+ const keepBase = keep ? path.basename(keep) : "";
230
+ for (const b of fs.readdirSync(parent)) {
231
+ if (!b.startsWith(base + ".bak.")) continue;
232
+ if (b === keepBase) continue;
233
+ try { fs.rmSync(path.join(parent, b), { recursive: true, force: true }); } catch {}
234
+ }
235
+ } catch (e) { log(`prune backups failed: ${e.message}`); }
236
+ }
237
+ // Remove stale clone dirs (~/.pi/tmp/kiln-*) except the active one.
238
+ function sweepStaleCloneTmp(tmpBase, active) {
239
+ try {
240
+ for (const e of fs.readdirSync(tmpBase)) {
241
+ if (!e.startsWith("kiln-")) continue;
242
+ if (active && e === path.basename(active)) continue;
243
+ try { fs.rmSync(path.join(tmpBase, e), { recursive: true, force: true }); } catch {}
244
+ }
245
+ } catch {}
246
+ }
247
+
248
+ // Recursive copy, excluding names (node_modules, .git). Returns false on error.
249
+ function copyTree(src, dst, exclude = new Set(["node_modules", ".git"])) {
250
+ try {
251
+ if (path.resolve(src) === path.resolve(dst)) return true;
252
+ fs.rmSync(dst, { recursive: true, force: true });
253
+ fs.mkdirSync(dst, { recursive: true });
254
+ for (const e of fs.readdirSync(src, { withFileTypes: true })) {
255
+ if (exclude.has(e.name)) continue;
256
+ const s = path.join(src, e.name), d = path.join(dst, e.name);
257
+ if (e.isDirectory()) { if (!copyTree(s, d, exclude)) return false; }
258
+ else if (e.isFile()) fs.copyFileSync(s, d);
259
+ }
260
+ return true;
261
+ } catch (e) { log(`copy failed ${src} → ${dst}: ${e.message}`); return false; }
262
+ }
263
+
264
+ function resolveTarget(opt) {
265
+ if (opt) return opt;
266
+ return path.join(os.homedir(), ".pi", "agent");
267
+ }
268
+
269
+ // Source root: bundled payload (npm tarball or dev checkout) → else git clone.
270
+ function resolveSourceRoot(args) {
271
+ const pkgRoot = path.resolve(__dirname, ".."); // bin/ → package root
272
+ const cands = [path.join(pkgRoot, "agent"), pkgRoot, process.cwd(), path.join(process.cwd(), "agent")];
273
+ for (const r of cands) {
274
+ if (fs.existsSync(path.join(r, "extensions")) && fs.existsSync(path.join(r, "AGENTS.md"))) return r;
275
+ }
276
+ if (args.local) throw new Error(`local checkout not found (tried: ${cands.join(", ")})`);
277
+ const base = path.join(os.homedir(), ".pi", "tmp");
278
+ fs.mkdirSync(base, { recursive: true });
279
+ const tmp = path.join(base, "kiln-" + crypto.randomBytes(4).toString("hex"));
280
+ fs.mkdirSync(tmp, { recursive: true });
281
+ liveSet(`cloning repo…`);
282
+ detail(` · cloning ${args.repo} (branch ${args.branch})`);
283
+ const r = spawnSync("git", ["clone", "--depth", "1", "--branch", args.branch, args.repo, tmp],
284
+ { stdio: ["ignore", "pipe", "pipe"], encoding: "utf8", env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GCM_INTERACTIVE: "never", GIT_ASKPASS: "echo" } });
285
+ if (r.stdout) log(r.stdout.trimEnd());
286
+ if (r.stderr) log(r.stderr.trimEnd());
287
+ if (r.status !== 0) throw new Error(`git clone failed (exit ${r.status}). See ${tmpLog}`);
288
+ clonedTmp = tmp;
289
+ if (fs.existsSync(path.join(tmp, "agent"))) return path.join(tmp, "agent");
290
+ return tmp;
291
+ }
292
+
293
+ async function main() {
294
+ const args = parseArgs(process.argv.slice(2));
295
+ const tmpBase = path.join(os.homedir(), ".pi", "tmp");
296
+ fs.mkdirSync(tmpBase, { recursive: true });
297
+ tmpLog = path.join(tmpBase, `pi-setup-${crypto.randomBytes(4).toString("hex")}.log`);
298
+ fs.writeFileSync(tmpLog, "");
299
+ process.on("exit", () => { if (clonedTmp && clonedTmp.startsWith(path.join(tmpBase, "kiln-"))) fs.rmSync(clonedTmp, { recursive: true, force: true }); });
300
+
301
+ title();
302
+ const targetDir = resolveTarget(args.target);
303
+ // Settings decision up front (only when interactive with no explicit flag).
304
+ if (args.settingsMode === "ask" && !args.yes && process.stdin.isTTY && process.stdout.isTTY
305
+ && fs.existsSync(path.join(targetDir, "settings.json"))) {
306
+ args.settingsMode = askYN(" Overwrite settings (y/n): ") ? "overwrite" : "keep";
307
+ } else if (args.settingsMode === "ask" && (args.yes || !process.stdin.isTTY)) {
308
+ args.settingsMode = "keep";
309
+ }
310
+ line(`${C.dim} target ${C.reset}${targetDir}`);
311
+ line(`${C.dim} repo ${C.reset}${args.repo} ${C.dim}(${args.branch})${C.reset}`);
312
+ line(`${C.dim} log ${C.reset}${tmpLog}\n`);
313
+
314
+ // ── preflight ──
315
+ const nodeVer = process.versions.node.split("-")[0];
316
+ const nv = nodeVer.split(".").map(Number);
317
+ const nodeOk = nv[0] > NODE_MIN[0] || (nv[0] === NODE_MIN[0] && (nv[1] > NODE_MIN[1] || (nv[1] === NODE_MIN[1] && nv[2] >= NODE_MIN[2])));
318
+ if (!nodeOk) {
319
+ line(`${C.yellow} ⚠ Node ${nodeVer} < ${NODE_MIN.join(".")} — please upgrade to Node ${NODE_MIN.join(".")}+${C.reset}`);
320
+ if (!args.yes) { line(" continue anyway in 3s…"); await sleep(3000); }
321
+ }
322
+ if (!have("npm")) { line(`${C.red} ✖ npm not found (Node installed but npm missing)${C.reset}`); process.exit(1); }
323
+ line(`${C.dim} node ${nodeVer} · npm ${npmVer} · ${cmdOut("git", ["--version"]) || "git n/a"}${C.reset}\n`);
324
+
325
+ // git is only needed when we must clone (no bundled payload and not --local)
326
+ const pkgAgent = path.join(path.resolve(__dirname, ".."), "agent");
327
+ const haveSource = fs.existsSync(path.join(pkgAgent, "extensions")) && fs.existsSync(path.join(pkgAgent, "AGENTS.md"));
328
+ if (!have("git") && !haveSource && !args.local) { line(`${C.red} ✖ git not found — required to fetch config${C.reset}`); process.exit(1); }
329
+ liveStart();
330
+
331
+ // ── [1/4] pi ──
332
+ if (args.skipPi) {
333
+ detail(` · Install pi — skipped`);
334
+ note("skipped", "pi");
335
+ } else if (have("pi")) {
336
+ const installed = globalPkgVersion(PI_PACKAGE) || cmdOut("pi", ["--version"]).replace(/^[vV]/, "").trim();
337
+ const latest = latestPkgVersion(PI_PACKAGE);
338
+ if (installed && latest && installed === latest) {
339
+ detail(` · pi ${installed} — already installed, skipping`);
340
+ note("skipped", "pi");
341
+ } else if (installed && !latest) {
342
+ detail(` · pi ${installed} — already installed, skipping version check (offline)`);
343
+ note("skipped", "pi");
344
+ } else {
345
+ const label = installed ? `updating pi ${installed} → ${latest || "latest"}` : `installing pi`;
346
+ const code = await npmStep("1/4", label, ["install", "-g", `${PI_PACKAGE}@latest`, "--no-audit", "--no-fund", "--min-release-age=0"]);
347
+ if (code === 0) {
348
+ detail(` · ${label} — ${cmdOut("pi", ["--version"]) || "?"}`);
349
+ note(installed ? "updated" : "installed", "pi");
350
+ } else {
351
+ detail(` ✖ ${label} failed (exit ${code}) — see log`);
352
+ note("updated", "pi (failed — see log)");
353
+ ok = false;
354
+ }
355
+ }
356
+ } else {
357
+ const label = `installing pi`;
358
+ const code = await npmStep("1/4", label, ["install", "-g", `${PI_PACKAGE}@latest`, "--no-audit", "--no-fund", "--min-release-age=0"]);
359
+ if (code === 0) {
360
+ detail(` · ${label} — ${cmdOut("pi", ["--version"]) || "?"}`);
361
+ note("installed", "pi");
362
+ } else {
363
+ detail(` ✖ ${label} failed (exit ${code}) — see log`);
364
+ note("installed", "pi (failed — see log)");
365
+ ok = false;
366
+ }
367
+ }
368
+ // ── [2/4] pi packages (global best-effort; pi-side ensure runs after step 3) ──
369
+ if (args.skipPackages) {
370
+ detail(` · pi packages — skipped`);
371
+ } else {
372
+ const bare = PACKAGES.map((p) => p.replace(/@latest$/, ""));
373
+ const need = [];
374
+ for (const name of bare) {
375
+ const inst = globalPkgVersion(name);
376
+ const latest = latestPkgVersion(name);
377
+ if (inst && latest && inst === latest) {
378
+ detail(` · ${name}@${inst} already installed globally, skipping`);
379
+ } else if (inst && !latest) {
380
+ detail(` · ${name}@${inst} already installed globally, skipping version check (offline)`);
381
+ } else {
382
+ if (inst) detail(` · ${name} ${inst} → ${latest || "latest"} — will update globally`);
383
+ need.push(`${name}@latest`);
384
+ }
385
+ }
386
+ if (!need.length) {
387
+ detail(` · pi packages — already installed globally, skipping`);
388
+ } else {
389
+ const label = `installing ${need.join(" ")} (-g)`;
390
+ const code = await npmStep("2/4", label, ["install", "-g", ...need, "--no-audit", "--no-fund"]);
391
+ if (code === 0) detail(` · pi packages — global install done`);
392
+ else detail(` ⚠ ${label} exit ${code} — continuing (pi-side ensure runs later)`);
393
+ }
394
+ }
395
+ // ── [3/4] custom config ──
396
+ try {
397
+ const sourceRoot = resolveSourceRoot(args);
398
+ detail(` · source ${sourceRoot}`);
399
+ fs.mkdirSync(targetDir, { recursive: true });
400
+ let bak = "";
401
+ if (fs.existsSync(path.join(targetDir, "AGENTS.md")) || fs.existsSync(path.join(targetDir, "extensions"))) {
402
+ const d = new Date(), p2 = (n) => String(n).padStart(2, "0");
403
+ bak = `${targetDir}.bak.${d.getFullYear()}${p2(d.getMonth() + 1)}${p2(d.getDate())}-${p2(d.getHours())}${p2(d.getMinutes())}${p2(d.getSeconds())}`;
404
+ detail(` · backup ${targetDir}/extensions → ${bak}`);
405
+ fs.mkdirSync(bak, { recursive: true });
406
+ for (const f of ["AGENTS.md", "keybindings.json", "README.md"]) {
407
+ const s = path.join(targetDir, f);
408
+ if (fs.existsSync(s)) try { fs.copyFileSync(s, path.join(bak, f)); } catch {}
409
+ }
410
+ const extSrc = path.join(targetDir, "extensions");
411
+ if (fs.existsSync(extSrc)) {
412
+ fs.mkdirSync(path.join(bak, "extensions"), { recursive: true });
413
+ for (const e of fs.readdirSync(extSrc, { withFileTypes: true })) {
414
+ if (!e.isDirectory()) continue;
415
+ copyTree(path.join(extSrc, e.name), path.join(bak, "extensions", e.name));
416
+ }
417
+ }
418
+ pruneBackups(targetDir, bak); // only one backup at a time
419
+ }
420
+
421
+ let copied = 0;
422
+ const selfInstall = path.resolve(sourceRoot) === path.resolve(targetDir);
423
+ if (selfInstall) detail(` · source == target — skipping file copy (self-install)`);
424
+ for (const f of ["AGENTS.md", "keybindings.json", "README.md"]) {
425
+ if (!fs.existsSync(path.join(sourceRoot, f))) continue;
426
+ if (!selfInstall) fs.copyFileSync(path.join(sourceRoot, f), path.join(targetDir, f));
427
+ copied++;
428
+ }
429
+ let freshSettings = false;
430
+ const settingsPath = path.join(targetDir, "settings.json");
431
+ const repoSettings = path.join(sourceRoot, "settings.json");
432
+ if (!fs.existsSync(settingsPath) && fs.existsSync(repoSettings)) {
433
+ fs.copyFileSync(repoSettings, settingsPath);
434
+ detail(` · created settings.json from repo defaults`);
435
+ note("installed", "settings.json");
436
+ freshSettings = true;
437
+ } else if (fs.existsSync(settingsPath)) {
438
+ const mode = args.settingsMode === "ask" ? "keep" : args.settingsMode; // decided up front
439
+ if (mode === "overwrite" && fs.existsSync(repoSettings)) {
440
+ if (bak) { try { fs.copyFileSync(settingsPath, path.join(bak, "settings.json")); } catch {} }
441
+ fs.copyFileSync(repoSettings, settingsPath);
442
+ detail(` · overwrote settings.json from repo defaults`);
443
+ note("updated", "settings.json");
444
+ freshSettings = true;
445
+ } else {
446
+ detail(` · kept existing settings.json`);
447
+ note("skipped", "settings.json");
448
+ }
449
+ }
450
+ if (freshSettings) {
451
+ try {
452
+ const p = path.join(targetDir, "settings.json");
453
+ const j = JSON.parse(fs.readFileSync(p, "utf8"));
454
+ j.packages = j.packages || [];
455
+ let changed = false;
456
+ for (const pkg of SETTINGS_PACKAGES) if (!j.packages.includes(pkg)) { j.packages.push(pkg); changed = true; }
457
+ if (changed) { fs.writeFileSync(p, JSON.stringify(j, null, 2) + "\n"); detail(" · patched settings.json packages → forge + context"); }
458
+ } catch {}
459
+ }
460
+ for (const t of ["taste.md", "taste", "taste.json"]) {
461
+ const tp = path.join(targetDir, t);
462
+ if (fs.existsSync(tp)) detail(` · kept existing ${t}`);
463
+ else if (fs.existsSync(path.join(sourceRoot, t))) {
464
+ const s = path.join(sourceRoot, t);
465
+ if (fs.statSync(s).isDirectory()) copyTree(s, tp, new Set());
466
+ else fs.copyFileSync(s, tp);
467
+ }
468
+ }
469
+
470
+ if (fs.existsSync(path.join(sourceRoot, "extensions"))) {
471
+ fs.mkdirSync(path.join(targetDir, "extensions"), { recursive: true });
472
+ for (const e of fs.readdirSync(path.join(sourceRoot, "extensions"), { withFileTypes: true })) {
473
+ if (!e.isDirectory() || e.name.endsWith(".md")) continue;
474
+ const dst = path.join(targetDir, "extensions", e.name);
475
+ const existed = fs.existsSync(dst);
476
+ if (copyTree(path.join(sourceRoot, "extensions", e.name), dst)) { copied++; note(existed ? "updated" : "installed", e.name); }
477
+ else detail(` ⚠ copy extensions/${e.name} failed`);
478
+ }
479
+ }
480
+
481
+ if (!selfInstall && fs.existsSync(path.join(sourceRoot, "themes"))) {
482
+ fs.rmSync(path.join(targetDir, "themes"), { recursive: true, force: true });
483
+ fs.mkdirSync(path.join(targetDir, "themes"), { recursive: true });
484
+ for (const e of fs.readdirSync(path.join(sourceRoot, "themes"), { withFileTypes: true })) {
485
+ if (e.name === ".pi" || e.name === "nul") continue;
486
+ const s = path.join(sourceRoot, "themes", e.name), dd = path.join(targetDir, "themes", e.name);
487
+ if (e.isDirectory()) copyTree(s, dd, new Set());
488
+ else if (e.isFile()) try { fs.copyFileSync(s, dd); } catch {}
489
+ }
490
+ }
491
+
492
+ detail(` · custom config — ${copied} items → ${targetDir}`);
493
+ if (clonedTmp) { fs.rmSync(clonedTmp, { recursive: true, force: true }); clonedTmp = ""; }
494
+ sweepStaleCloneTmp(tmpBase, ""); // delete any leftover kiln-* temp dirs
495
+ const repoReadme = path.resolve(sourceRoot, "..", "README.md");
496
+ const parentReadme = path.resolve(targetDir, "..", "README.md");
497
+ if (repoReadme !== parentReadme && fs.existsSync(repoReadme)) {
498
+ try { fs.copyFileSync(repoReadme, parentReadme); } catch {}
499
+ }
500
+ } catch (e) {
501
+ detail(` ✖ custom config failed: ${e.message}`);
502
+ ok = false;
503
+ }
504
+ // ── [4/4] per-extension deps ──
505
+ const extRoot = path.join(targetDir, "extensions");
506
+ if (!fs.existsSync(extRoot)) {
507
+ detail(` ⚠ no extensions found at ${extRoot}`);
508
+ } else {
509
+ const failed = [];
510
+ let installed = 0, skipped = 0;
511
+ for (const e of fs.readdirSync(extRoot, { withFileTypes: true })) {
512
+ if (!e.isDirectory() || e.name.endsWith(".md")) continue;
513
+ const dir = path.join(extRoot, e.name), label = `extensions/${e.name}`;
514
+ if (fs.existsSync(path.join(dir, "node_modules"))) {
515
+ detail(` · ${label} — already installed, skipping`);
516
+ note("skipped", e.name);
517
+ skipped++;
518
+ continue;
519
+ }
520
+ const hasPkg = fs.existsSync(path.join(dir, "package.json"));
521
+ const sh = path.join(dir, "install.sh"), ps1 = path.join(dir, "install.ps1");
522
+ let code;
523
+ if (process.platform === "win32" && fs.existsSync(ps1)) {
524
+ const shell = have("pwsh") ? "pwsh" : "powershell";
525
+ code = await runStep("4/4", label, shell, ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", ps1]);
526
+ } else if (process.platform !== "win32" && fs.existsSync(sh)) {
527
+ code = await runStep("4/4", label, "sh", [sh]);
528
+ } else if (hasPkg) {
529
+ const a = ["--prefix", dir, fs.existsSync(path.join(dir, "package-lock.json")) ? "ci" : "install", "--no-audit", "--no-fund"];
530
+ code = await npmStep("4/4", label, a);
531
+ } else {
532
+ detail(` · ${label} — no package.json, skipping`);
533
+ continue;
534
+ }
535
+ if (code === 0) { detail(` · ${label} — done`); if (hasPkg) note("npm modules", e.name); installed++; }
536
+ else { detail(` ✖ ${label} failed (exit ${code}) — see log`); note("npm modules", `${e.name} (failed — see log)`); failed.push(e.name); }
537
+ }
538
+ if (!failed.length) detail(` · extension deps — ${installed} installed, ${skipped} skipped`);
539
+ else detail(` ⚠ extension deps ${failed.length} failed: ${failed.join(", ")} — see log`);
540
+ }
541
+
542
+ // ── pi package ensure (forge + context actually registered with pi) ──
543
+ // `pi list` is truth: global installs mean nothing to pi (it uses its own dir).
544
+ const defaultDir = path.join(os.homedir(), ".pi", "agent");
545
+ if (args.skipPackages) {
546
+ for (const s of SETTINGS_PACKAGES) note("skipped", prettyPkg(s));
547
+ } else if (!have("pi")) {
548
+ detail(` ⚠ pi binary not found — packages registered for next launch`);
549
+ for (const s of SETTINGS_PACKAGES) note("skipped", prettyPkg(s));
550
+ } else if (path.resolve(targetDir) !== path.resolve(defaultDir)) {
551
+ detail(` · custom target — packages registered for next launch`);
552
+ for (const s of SETTINGS_PACKAGES) note("skipped", prettyPkg(s));
553
+ } else {
554
+ let list = "";
555
+ try { list = piOut(["list"]); } catch { list = ""; }
556
+ for (const spec of SETTINGS_PACKAGES) {
557
+ const pretty = prettyPkg(spec);
558
+ if (list.includes(spec)) { note("skipped", pretty); continue; }
559
+ const code = await piStep(`pi install ${spec}`, ["install", spec]);
560
+ let now = "";
561
+ try { now = piOut(["list"]); } catch { now = ""; }
562
+ if (code === 0 && now.includes(spec)) { detail(` · ${pretty} — registered with pi`); note("installed", pretty); }
563
+ else { detail(` ✖ ${pretty} not registered — see log (pi retries on next launch)`); note("updated", `${pretty} (failed — see log)`); }
564
+ }
565
+ }
566
+
567
+ // ── summary ──
568
+ liveStop();
569
+ const secs = Math.round((Date.now() - startTime) / 1000);
570
+ const elapsed = secs >= 60 ? `${Math.floor(secs / 60)}m ${secs % 60}s` : `${secs}s`;
571
+ line("");
572
+ if (ok) line(`${C.green}${C.bold} ✓ Setup complete${C.reset} ${C.dim}→ ${targetDir}${C.reset}`);
573
+ else line(`${C.yellow}${C.bold} ⚠ Setup finished with warnings${C.reset} ${C.dim}see ${tmpLog}${C.reset}`);
574
+ line("");
575
+ line("Summary:");
576
+ line("");
577
+ for (const name of ["skipped", "updated", "installed", "npm modules"]) {
578
+ line(`[${name}]`);
579
+ const items = buckets[name];
580
+ if (!items.length) line("(none)");
581
+ else for (const item of items) line(item);
582
+ line("");
583
+ }
584
+ line(`Done in ${elapsed}`);
585
+ line(`${C.dim} log: ${tmpLog}${C.reset}\n`);
586
+ process.exitCode = ok ? 0 : 1;
587
+ }
588
+
472
589
  main().catch((e) => { console.error(`\n ✖ ${e.message}`); process.exit(1); });