@asterxsk/kiln 0.1.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 (263) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +170 -0
  3. package/agent/AGENTS.md +67 -0
  4. package/agent/README.md +5 -0
  5. package/agent/extensions/AGENTS.md +68 -0
  6. package/agent/extensions/ask-user/index.ts +418 -0
  7. package/agent/extensions/ask-user/install.ps1 +23 -0
  8. package/agent/extensions/ask-user/install.sh +21 -0
  9. package/agent/extensions/ask-user/package-lock.json +769 -0
  10. package/agent/extensions/ask-user/package.json +19 -0
  11. package/agent/extensions/ask-user/prompt.ts +45 -0
  12. package/agent/extensions/ask-user/tsconfig.json +7 -0
  13. package/agent/extensions/background-terminals/docs/implementation-guide.md +942 -0
  14. package/agent/extensions/background-terminals/index.ts +627 -0
  15. package/agent/extensions/background-terminals/install.ps1 +23 -0
  16. package/agent/extensions/background-terminals/install.sh +21 -0
  17. package/agent/extensions/background-terminals/manager.test.ts +735 -0
  18. package/agent/extensions/background-terminals/output.test.ts +109 -0
  19. package/agent/extensions/background-terminals/package-lock.json +769 -0
  20. package/agent/extensions/background-terminals/package.json +17 -0
  21. package/agent/extensions/background-terminals/prompt.test.ts +125 -0
  22. package/agent/extensions/background-terminals/ps.test.ts +82 -0
  23. package/agent/extensions/background-terminals/result-delivery.test.ts +44 -0
  24. package/agent/extensions/background-terminals/src/domain.ts +87 -0
  25. package/agent/extensions/background-terminals/src/manager.ts +907 -0
  26. package/agent/extensions/background-terminals/src/output.ts +84 -0
  27. package/agent/extensions/background-terminals/src/prompt.ts +142 -0
  28. package/agent/extensions/background-terminals/src/result-delivery.ts +27 -0
  29. package/agent/extensions/background-terminals/src/runtime.ts +36 -0
  30. package/agent/extensions/background-terminals/src/ui/output-view.ts +79 -0
  31. package/agent/extensions/background-terminals/src/ui/ps.ts +621 -0
  32. package/agent/extensions/background-terminals/tsconfig.json +7 -0
  33. package/agent/extensions/destructive/README.md +31 -0
  34. package/agent/extensions/destructive/index.ts +88 -0
  35. package/agent/extensions/destructive/install.ps1 +23 -0
  36. package/agent/extensions/destructive/install.sh +21 -0
  37. package/agent/extensions/file-search/index.spec.ts +443 -0
  38. package/agent/extensions/file-search/index.ts +459 -0
  39. package/agent/extensions/file-search/install.ps1 +23 -0
  40. package/agent/extensions/file-search/install.sh +21 -0
  41. package/agent/extensions/file-search/package-lock.json +2253 -0
  42. package/agent/extensions/file-search/package.json +23 -0
  43. package/agent/extensions/file-search/src/args.ts +122 -0
  44. package/agent/extensions/file-search/src/binaries.ts +422 -0
  45. package/agent/extensions/file-search/src/output.ts +126 -0
  46. package/agent/extensions/file-search/src/process.ts +146 -0
  47. package/agent/extensions/file-search/src/prompt.ts +52 -0
  48. package/agent/extensions/file-search/tsconfig.json +7 -0
  49. package/agent/extensions/goal/README.md +50 -0
  50. package/agent/extensions/goal/index.ts +155 -0
  51. package/agent/extensions/goal/install.ps1 +23 -0
  52. package/agent/extensions/goal/install.sh +21 -0
  53. package/agent/extensions/modelconf/PLAN.md +915 -0
  54. package/agent/extensions/modelconf/README.md +66 -0
  55. package/agent/extensions/modelconf/index.ts +296 -0
  56. package/agent/extensions/modelconf/install.ps1 +23 -0
  57. package/agent/extensions/modelconf/install.sh +21 -0
  58. package/agent/extensions/modelconf/package-lock.json +1809 -0
  59. package/agent/extensions/modelconf/package.json +13 -0
  60. package/agent/extensions/modelconf/src/fuzzy.ts +26 -0
  61. package/agent/extensions/modelconf/src/glob.ts +13 -0
  62. package/agent/extensions/modelconf/src/persistence.test.ts +46 -0
  63. package/agent/extensions/modelconf/src/persistence.ts +164 -0
  64. package/agent/extensions/modelconf/src/ui/ModelConfView.test.ts +75 -0
  65. package/agent/extensions/modelconf/src/ui/ModelConfView.ts +1101 -0
  66. package/agent/extensions/modelconf/tsconfig.json +15 -0
  67. package/agent/extensions/pi-web-access/CHANGELOG.md +690 -0
  68. package/agent/extensions/pi-web-access/LICENSE +21 -0
  69. package/agent/extensions/pi-web-access/README.md +470 -0
  70. package/agent/extensions/pi-web-access/SECURITY.md +5 -0
  71. package/agent/extensions/pi-web-access/activity.ts +101 -0
  72. package/agent/extensions/pi-web-access/auth-fetch.ts +148 -0
  73. package/agent/extensions/pi-web-access/banner.png +0 -0
  74. package/agent/extensions/pi-web-access/brightdata-unlocker.ts +272 -0
  75. package/agent/extensions/pi-web-access/chrome-cookies.ts +669 -0
  76. package/agent/extensions/pi-web-access/content-find.ts +139 -0
  77. package/agent/extensions/pi-web-access/credential-source.ts +191 -0
  78. package/agent/extensions/pi-web-access/data-uri-sanitize.ts +406 -0
  79. package/agent/extensions/pi-web-access/datalab-pdf-extract.ts +568 -0
  80. package/agent/extensions/pi-web-access/declared-web-links.ts +173 -0
  81. package/agent/extensions/pi-web-access/evidence/CONTRACT-EVIDENCE.md +496 -0
  82. package/agent/extensions/pi-web-access/evidence/contract-probe.mjs +140 -0
  83. package/agent/extensions/pi-web-access/exa.ts +526 -0
  84. package/agent/extensions/pi-web-access/extract.ts +1196 -0
  85. package/agent/extensions/pi-web-access/feature-config.ts +29 -0
  86. package/agent/extensions/pi-web-access/fetch-params.ts +111 -0
  87. package/agent/extensions/pi-web-access/gemini-adc.ts +298 -0
  88. package/agent/extensions/pi-web-access/gemini-api.ts +353 -0
  89. package/agent/extensions/pi-web-access/gemini-pdf-extract.ts +108 -0
  90. package/agent/extensions/pi-web-access/gemini-search.ts +21 -0
  91. package/agent/extensions/pi-web-access/gemini-url-context.ts +128 -0
  92. package/agent/extensions/pi-web-access/gemini-web-config.ts +101 -0
  93. package/agent/extensions/pi-web-access/gemini-web.ts +487 -0
  94. package/agent/extensions/pi-web-access/github-api.ts +197 -0
  95. package/agent/extensions/pi-web-access/github-extract.ts +746 -0
  96. package/agent/extensions/pi-web-access/github-issue-pr.ts +700 -0
  97. package/agent/extensions/pi-web-access/index.ts +1737 -0
  98. package/agent/extensions/pi-web-access/package-lock.json +5808 -0
  99. package/agent/extensions/pi-web-access/package.json +64 -0
  100. package/agent/extensions/pi-web-access/page-query.ts +96 -0
  101. package/agent/extensions/pi-web-access/pdf-extract.ts +409 -0
  102. package/agent/extensions/pi-web-access/pi-web-fetch-demo.mp4 +0 -0
  103. package/agent/extensions/pi-web-access/promise-try.d.ts +7 -0
  104. package/agent/extensions/pi-web-access/query-rewrite.ts +51 -0
  105. package/agent/extensions/pi-web-access/render-search-error.ts +170 -0
  106. package/agent/extensions/pi-web-access/rsc-extract.ts +338 -0
  107. package/agent/extensions/pi-web-access/search-types.ts +20 -0
  108. package/agent/extensions/pi-web-access/source-check.ts +282 -0
  109. package/agent/extensions/pi-web-access/ssrf-protection.ts +526 -0
  110. package/agent/extensions/pi-web-access/storage.ts +521 -0
  111. package/agent/extensions/pi-web-access/summary-model-scope.ts +125 -0
  112. package/agent/extensions/pi-web-access/test/auth-fetch.test.mjs +208 -0
  113. package/agent/extensions/pi-web-access/test/brightdata-unlocker.test.mjs +840 -0
  114. package/agent/extensions/pi-web-access/test/chrome-cookie-extraction.test.mjs +441 -0
  115. package/agent/extensions/pi-web-access/test/config-path.test.mjs +283 -0
  116. package/agent/extensions/pi-web-access/test/content-find.test.mjs +25 -0
  117. package/agent/extensions/pi-web-access/test/credential-source.test.mjs +118 -0
  118. package/agent/extensions/pi-web-access/test/data-uri-sanitize.test.mjs +210 -0
  119. package/agent/extensions/pi-web-access/test/datalab-pdf-extract.test.mjs +552 -0
  120. package/agent/extensions/pi-web-access/test/declared-web-links.test.mjs +212 -0
  121. package/agent/extensions/pi-web-access/test/fetch-answer-storage.test.mjs +40 -0
  122. package/agent/extensions/pi-web-access/test/fetch-cache-storage.test.mjs +334 -0
  123. package/agent/extensions/pi-web-access/test/fetch-content-domain-policy.test.mjs +95 -0
  124. package/agent/extensions/pi-web-access/test/fetch-modes.test.mjs +53 -0
  125. package/agent/extensions/pi-web-access/test/fetch-not-found-guidance.test.mjs +92 -0
  126. package/agent/extensions/pi-web-access/test/fetch-params.test.mjs +86 -0
  127. package/agent/extensions/pi-web-access/test/fetch-render-call.test.mjs +34 -0
  128. package/agent/extensions/pi-web-access/test/fetch-routing.test.mjs +173 -0
  129. package/agent/extensions/pi-web-access/test/gemini-adc-auth.test.mjs +257 -0
  130. package/agent/extensions/pi-web-access/test/gemini-api-transport.test.mjs +170 -0
  131. package/agent/extensions/pi-web-access/test/gemini-pdf-extract.test.mjs +133 -0
  132. package/agent/extensions/pi-web-access/test/gemini-web-cookie-opt-in.test.mjs +178 -0
  133. package/agent/extensions/pi-web-access/test/gemini-web-header-overflow.test.mjs +148 -0
  134. package/agent/extensions/pi-web-access/test/get-search-content.test.mjs +223 -0
  135. package/agent/extensions/pi-web-access/test/github-extract.test.mjs +378 -0
  136. package/agent/extensions/pi-web-access/test/github-issue-pr.test.mjs +565 -0
  137. package/agent/extensions/pi-web-access/test/inline-content-config.test.mjs +99 -0
  138. package/agent/extensions/pi-web-access/test/lazy-extract-load.test.mjs +118 -0
  139. package/agent/extensions/pi-web-access/test/local-video-oversize.test.mjs +52 -0
  140. package/agent/extensions/pi-web-access/test/package-typebox-dependency.test.mjs +50 -0
  141. package/agent/extensions/pi-web-access/test/page-query.test.mjs +51 -0
  142. package/agent/extensions/pi-web-access/test/pdf-config.test.mjs +140 -0
  143. package/agent/extensions/pi-web-access/test/pdf-extract.test.mjs +500 -0
  144. package/agent/extensions/pi-web-access/test/proxy-transport.test.mjs +286 -0
  145. package/agent/extensions/pi-web-access/test/query-rewrite.test.mjs +52 -0
  146. package/agent/extensions/pi-web-access/test/rsc-fallback.test.mjs +102 -0
  147. package/agent/extensions/pi-web-access/test/search-error-render.test.mjs +152 -0
  148. package/agent/extensions/pi-web-access/test/search-providers.test.mjs +274 -0
  149. package/agent/extensions/pi-web-access/test/source-check.test.mjs +179 -0
  150. package/agent/extensions/pi-web-access/test/ssrf-allow-ranges-config.test.mjs +205 -0
  151. package/agent/extensions/pi-web-access/test/ssrf-protection.test.mjs +456 -0
  152. package/agent/extensions/pi-web-access/test/summary-model-scope.test.mjs +106 -0
  153. package/agent/extensions/pi-web-access/test/tool-registration-config.test.mjs +182 -0
  154. package/agent/extensions/pi-web-access/test/web-search-answer-render.test.mjs +66 -0
  155. package/agent/extensions/pi-web-access/test/youtube-extract-errors.test.mjs +64 -0
  156. package/agent/extensions/pi-web-access/tsconfig.json +11 -0
  157. package/agent/extensions/pi-web-access/utils.ts +451 -0
  158. package/agent/extensions/pi-web-access/video-extract.ts +392 -0
  159. package/agent/extensions/pi-web-access/youtube-extract.ts +328 -0
  160. package/agent/extensions/shared/activity-status.ts +31 -0
  161. package/agent/extensions/shared/child-session.test.ts +270 -0
  162. package/agent/extensions/shared/child-session.ts +148 -0
  163. package/agent/extensions/shared/context-utilization.test.ts +48 -0
  164. package/agent/extensions/shared/context-utilization.ts +47 -0
  165. package/agent/extensions/shared/dashboard-state.ts +99 -0
  166. package/agent/extensions/shared/install.ps1 +23 -0
  167. package/agent/extensions/shared/install.sh +21 -0
  168. package/agent/extensions/shared/tool-call-timeout.test.ts +117 -0
  169. package/agent/extensions/shared/tool-call-timeout.ts +104 -0
  170. package/agent/extensions/skillsconf/README.md +74 -0
  171. package/agent/extensions/skillsconf/index.ts +92 -0
  172. package/agent/extensions/skillsconf/install.ps1 +23 -0
  173. package/agent/extensions/skillsconf/install.sh +21 -0
  174. package/agent/extensions/skillsconf/package-lock.json +1809 -0
  175. package/agent/extensions/skillsconf/package.json +18 -0
  176. package/agent/extensions/skillsconf/src/delete-skill.test.ts +64 -0
  177. package/agent/extensions/skillsconf/src/delete-skill.ts +45 -0
  178. package/agent/extensions/skillsconf/src/filter.test.ts +80 -0
  179. package/agent/extensions/skillsconf/src/filter.ts +74 -0
  180. package/agent/extensions/skillsconf/src/fuzzy.ts +26 -0
  181. package/agent/extensions/skillsconf/src/persistence.test.ts +72 -0
  182. package/agent/extensions/skillsconf/src/persistence.ts +169 -0
  183. package/agent/extensions/skillsconf/src/ui/SkillConfView.test.ts +337 -0
  184. package/agent/extensions/skillsconf/src/ui/SkillConfView.ts +758 -0
  185. package/agent/extensions/skillsconf/src/ui/text-input.ts +91 -0
  186. package/agent/extensions/skillsconf/src/ui/tui-helpers.ts +144 -0
  187. package/agent/extensions/skillsconf/tsconfig.json +15 -0
  188. package/agent/extensions/status line/index.ts +282 -0
  189. package/agent/extensions/status line/install.ps1 +23 -0
  190. package/agent/extensions/status line/install.sh +21 -0
  191. package/agent/extensions/subagents/by-the-way.test.ts +29 -0
  192. package/agent/extensions/subagents/claude.test.ts +119 -0
  193. package/agent/extensions/subagents/codex.test.ts +102 -0
  194. package/agent/extensions/subagents/context-usage.test.ts +107 -0
  195. package/agent/extensions/subagents/docs/design-plan.md +568 -0
  196. package/agent/extensions/subagents/docs/effect-v4-extension-guide.md +354 -0
  197. package/agent/extensions/subagents/docs/effect-v4-notes.md +571 -0
  198. package/agent/extensions/subagents/index.ts +779 -0
  199. package/agent/extensions/subagents/install.ps1 +23 -0
  200. package/agent/extensions/subagents/install.sh +21 -0
  201. package/agent/extensions/subagents/manager.test.ts +276 -0
  202. package/agent/extensions/subagents/package-lock.json +2244 -0
  203. package/agent/extensions/subagents/package.json +19 -0
  204. package/agent/extensions/subagents/result-delivery.test.ts +27 -0
  205. package/agent/extensions/subagents/src/backend.ts +73 -0
  206. package/agent/extensions/subagents/src/backends/claude.ts +701 -0
  207. package/agent/extensions/subagents/src/backends/codex.ts +1060 -0
  208. package/agent/extensions/subagents/src/backends/pi.ts +575 -0
  209. package/agent/extensions/subagents/src/backends/stub.ts +300 -0
  210. package/agent/extensions/subagents/src/by-the-way.ts +21 -0
  211. package/agent/extensions/subagents/src/domain.ts +253 -0
  212. package/agent/extensions/subagents/src/format.ts +74 -0
  213. package/agent/extensions/subagents/src/manager.ts +736 -0
  214. package/agent/extensions/subagents/src/prompt.ts +92 -0
  215. package/agent/extensions/subagents/src/result-delivery.ts +20 -0
  216. package/agent/extensions/subagents/src/runtime.ts +53 -0
  217. package/agent/extensions/subagents/src/ui/takeover.ts +583 -0
  218. package/agent/extensions/subagents/src/ui/transcript.ts +201 -0
  219. package/agent/extensions/subagents/takeover.test.ts +29 -0
  220. package/agent/extensions/subagents/tsconfig.json +7 -0
  221. package/agent/extensions/taste/index.ts +443 -0
  222. package/agent/extensions/taste/install.ps1 +23 -0
  223. package/agent/extensions/taste/install.sh +21 -0
  224. package/agent/extensions/todo/AGENTS.md +38 -0
  225. package/agent/extensions/todo/LICENSE +21 -0
  226. package/agent/extensions/todo/config.ts +55 -0
  227. package/agent/extensions/todo/index.ts +151 -0
  228. package/agent/extensions/todo/install.ps1 +23 -0
  229. package/agent/extensions/todo/install.sh +21 -0
  230. package/agent/extensions/todo/locales/de.json +17 -0
  231. package/agent/extensions/todo/locales/en.json +15 -0
  232. package/agent/extensions/todo/locales/es.json +17 -0
  233. package/agent/extensions/todo/locales/fr.json +17 -0
  234. package/agent/extensions/todo/locales/pt-BR.json +17 -0
  235. package/agent/extensions/todo/locales/pt.json +17 -0
  236. package/agent/extensions/todo/locales/ru.json +17 -0
  237. package/agent/extensions/todo/locales/uk.json +17 -0
  238. package/agent/extensions/todo/locales/zh.json +17 -0
  239. package/agent/extensions/todo/package-lock.json +3358 -0
  240. package/agent/extensions/todo/package.json +67 -0
  241. package/agent/extensions/todo/state/i18n-bridge.ts +64 -0
  242. package/agent/extensions/todo/state/invariants.ts +20 -0
  243. package/agent/extensions/todo/state/replay.ts +38 -0
  244. package/agent/extensions/todo/state/selectors.ts +107 -0
  245. package/agent/extensions/todo/state/state-reducer.ts +326 -0
  246. package/agent/extensions/todo/state/state.ts +18 -0
  247. package/agent/extensions/todo/state/store.ts +82 -0
  248. package/agent/extensions/todo/state/task-graph.ts +57 -0
  249. package/agent/extensions/todo/todo-overlay.ts +200 -0
  250. package/agent/extensions/todo/todo.ts +155 -0
  251. package/agent/extensions/todo/tool/response-envelope.ts +109 -0
  252. package/agent/extensions/todo/tool/types.ts +206 -0
  253. package/agent/extensions/todo/verify-ref-system.js +0 -0
  254. package/agent/extensions/todo/view/format.ts +177 -0
  255. package/agent/extensions/trim-context/README.md +54 -0
  256. package/agent/extensions/trim-context/index.ts +487 -0
  257. package/agent/extensions/trim-context/install.ps1 +23 -0
  258. package/agent/extensions/trim-context/install.sh +21 -0
  259. package/agent/install.ps1 +527 -0
  260. package/agent/install.sh +511 -0
  261. package/agent/keybindings.json +7 -0
  262. package/bin/kiln.js +359 -0
  263. package/package.json +21 -0
@@ -0,0 +1,148 @@
1
+ import * as path from "node:path";
2
+ import {
3
+ DefaultResourceLoader,
4
+ getAgentDir,
5
+ ProjectTrustStore,
6
+ SettingsManager,
7
+ type AgentSession,
8
+ type SessionShutdownEvent,
9
+ } from "@earendil-works/pi-coding-agent";
10
+
11
+ const CHILD_SHUTDOWN_TIMEOUT_MS = 5_000;
12
+
13
+ /** Tools that headless children must not receive. Everything else stays enabled. */
14
+ export const CHILD_EXCLUDED_TOOL_NAMES = [
15
+ "subagent_spawn",
16
+ "subagent_wait",
17
+ "subagent_cancel",
18
+ "subagent_check",
19
+ "subagent_list",
20
+ "workflow",
21
+ "ask_user",
22
+ ] as const;
23
+
24
+ /** Fresh SDK options avoid turning the denylist into an accidental allowlist. */
25
+ export function childToolPolicy() {
26
+ return { excludeTools: [...CHILD_EXCLUDED_TOOL_NAMES] };
27
+ }
28
+
29
+ export interface ChildResourceOptions {
30
+ cwd: string;
31
+ projectTrusted: boolean;
32
+ appendSystemPrompt?: string[];
33
+ agentDir?: string;
34
+ }
35
+
36
+ /** Load normal global/package resources and trust-gated project resources. */
37
+ export async function createChildResources(options: ChildResourceOptions) {
38
+ const agentDir = options.agentDir ?? getAgentDir();
39
+ const settingsManager = SettingsManager.create(options.cwd, agentDir, {
40
+ projectTrusted: options.projectTrusted,
41
+ });
42
+ const loader = new DefaultResourceLoader({
43
+ cwd: options.cwd,
44
+ agentDir,
45
+ settingsManager,
46
+ ...(options.appendSystemPrompt
47
+ ? { appendSystemPrompt: options.appendSystemPrompt }
48
+ : {}),
49
+ });
50
+ await loader.reload();
51
+ return { loader, settingsManager };
52
+ }
53
+
54
+ /**
55
+ * Same-directory children inherit the live parent decision. An alternate cwd
56
+ * is trusted only when Pi's persisted trust store explicitly trusts it (or a
57
+ * containing directory); unreadable/invalid trust data fails closed.
58
+ */
59
+ export function resolveStandaloneChildProjectTrust(options: {
60
+ parentCwd: string;
61
+ childCwd: string;
62
+ parentTrusted: boolean;
63
+ agentDir?: string;
64
+ }) {
65
+ if (path.resolve(options.childCwd) === path.resolve(options.parentCwd)) {
66
+ return options.parentTrusted;
67
+ }
68
+ try {
69
+ const trustStore = new ProjectTrustStore(options.agentDir ?? getAgentDir());
70
+ return trustStore.get(options.childCwd) === true;
71
+ } catch {
72
+ return false;
73
+ }
74
+ }
75
+
76
+ /** Start child extension session hooks/resources in headless print mode. */
77
+ export async function bindChildSessionExtensions(
78
+ session: Pick<AgentSession, "bindExtensions">,
79
+ ) {
80
+ await session.bindExtensions({ mode: "print" });
81
+ }
82
+
83
+ interface ChildExtensionRunner {
84
+ hasHandlers(eventType: string): boolean;
85
+ emit(event: SessionShutdownEvent): Promise<unknown>;
86
+ }
87
+
88
+ export interface DisposableChildSession {
89
+ readonly extensionRunner: ChildExtensionRunner;
90
+ dispose(): void;
91
+ }
92
+
93
+ const childShutdowns = new WeakMap<object, Promise<void>>();
94
+
95
+ function waitBounded(operation: Promise<unknown>, timeoutMs: number) {
96
+ let timer: ReturnType<typeof setTimeout> | undefined;
97
+ const timeout = new Promise<void>((resolve) => {
98
+ timer = setTimeout(resolve, timeoutMs);
99
+ });
100
+ return Promise.race([
101
+ operation.then(
102
+ () => undefined,
103
+ () => undefined,
104
+ ),
105
+ timeout,
106
+ ])
107
+ .catch(() => {})
108
+ .finally(() => {
109
+ if (timer) clearTimeout(timer);
110
+ });
111
+ }
112
+
113
+ /**
114
+ * Emit child session_shutdown once, then dispose once. Hook failures and a
115
+ * bounded hook deadline never prevent disposal.
116
+ */
117
+ export function shutdownAndDisposeChildSession(
118
+ session: DisposableChildSession,
119
+ options: { timeoutMs?: number } = {},
120
+ ) {
121
+ const existing = childShutdowns.get(session);
122
+ if (existing) return existing;
123
+
124
+ const shutdown = (async () => {
125
+ try {
126
+ if (session.extensionRunner.hasHandlers("session_shutdown")) {
127
+ await waitBounded(
128
+ session.extensionRunner.emit({
129
+ type: "session_shutdown",
130
+ reason: "quit",
131
+ }),
132
+ options.timeoutMs ?? CHILD_SHUTDOWN_TIMEOUT_MS,
133
+ );
134
+ }
135
+ } catch {
136
+ // Extension runner inspection/emission is best-effort during teardown.
137
+ } finally {
138
+ try {
139
+ session.dispose();
140
+ } catch {
141
+ // Disposal is terminal and must remain idempotent for callers.
142
+ }
143
+ }
144
+ })();
145
+
146
+ childShutdowns.set(session, shutdown);
147
+ return shutdown;
148
+ }
@@ -0,0 +1,48 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import {
4
+ contextPercent,
5
+ formatContextUtilization,
6
+ } from "./context-utilization.ts";
7
+
8
+ test("formats current context occupancy against model capacity", () => {
9
+ assert.equal(
10
+ formatContextUtilization({ tokens: 26_040, contextWindow: 372_000 }),
11
+ "7%/372k",
12
+ );
13
+ });
14
+
15
+ test("formats the latest post-compaction usage rather than prior cumulative usage", () => {
16
+ const latestUsage = { tokens: 18_000, contextWindow: 200_000 };
17
+ assert.equal(formatContextUtilization(latestUsage), "9%/200k");
18
+ });
19
+
20
+ test("clamps over-capacity and nonsensical token values", () => {
21
+ assert.equal(
22
+ contextPercent({ tokens: 500_000, contextWindow: 200_000 }),
23
+ 100,
24
+ );
25
+ assert.equal(
26
+ formatContextUtilization({
27
+ tokens: Number.POSITIVE_INFINITY,
28
+ contextWindow: 200_000,
29
+ }),
30
+ "?%/200k",
31
+ );
32
+ assert.equal(
33
+ formatContextUtilization({ tokens: -1, contextWindow: 200_000 }),
34
+ "?%/200k",
35
+ );
36
+ });
37
+
38
+ test("handles missing usage or capacity without NaN or Infinity", () => {
39
+ assert.equal(
40
+ formatContextUtilization({ tokens: null, contextWindow: 372_000 }),
41
+ "?%/372k",
42
+ );
43
+ assert.equal(formatContextUtilization({ tokens: 12_000 }), "");
44
+ assert.equal(
45
+ formatContextUtilization({ tokens: 12_000, contextWindow: 0 }),
46
+ "",
47
+ );
48
+ });
@@ -0,0 +1,47 @@
1
+ /** Compact, defensive context-window utilization formatting for child agents. */
2
+
3
+ export interface ContextUtilization {
4
+ /** Current conversation context occupancy; null while it is unknown after compaction. */
5
+ tokens?: number | null;
6
+ /** Capacity of the model currently serving the conversation. */
7
+ contextWindow?: number | null;
8
+ }
9
+
10
+ function usableTokens(value: number | null | undefined) {
11
+ return typeof value === "number" && Number.isFinite(value) && value >= 0
12
+ ? value
13
+ : undefined;
14
+ }
15
+
16
+ function usableCapacity(value: number | null | undefined) {
17
+ return typeof value === "number" && Number.isFinite(value) && value > 0
18
+ ? value
19
+ : undefined;
20
+ }
21
+
22
+ export function contextPercent(usage: ContextUtilization) {
23
+ const tokens = usableTokens(usage.tokens);
24
+ const capacity = usableCapacity(usage.contextWindow);
25
+ if (tokens === undefined || capacity === undefined) return undefined;
26
+ return Math.round(Math.min(100, Math.max(0, (tokens / capacity) * 100)));
27
+ }
28
+
29
+ export function formatCompactTokens(count: number) {
30
+ if (count < 1000) return Math.round(count).toString();
31
+ if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
32
+ if (count < 1000000) return `${Math.round(count / 1000)}k`;
33
+ return `${(count / 1000000).toFixed(1)}M`;
34
+ }
35
+
36
+ /**
37
+ * Render `%/capacity`. If occupancy is temporarily unknown (notably directly
38
+ * after compaction), retain the useful model capacity as `?%/capacity`. If no
39
+ * valid capacity is available, omit the statistic because no percentage can
40
+ * be computed.
41
+ */
42
+ export function formatContextUtilization(usage: ContextUtilization) {
43
+ const capacity = usableCapacity(usage.contextWindow);
44
+ if (capacity === undefined) return "";
45
+ const percent = contextPercent(usage);
46
+ return `${percent === undefined ? "?" : percent}%/${formatCompactTokens(capacity)}`;
47
+ }
@@ -0,0 +1,99 @@
1
+ export const MODEL_INFO_CHANNEL = "dashboard:model-info";
2
+ export const GIT_INFO_CHANNEL = "dashboard:git-info";
3
+ export const REFRESH_CHANNEL = "dashboard:refresh";
4
+
5
+ export interface ModelInfoState {
6
+ provider: string;
7
+ modelId: string;
8
+ modelName: string;
9
+ thinking: string;
10
+ contextTokens: number | null;
11
+ contextWindow: number;
12
+ contextPercent: number | null;
13
+ cost: number;
14
+ tokensPerSecond: number | null;
15
+ generating: boolean;
16
+ }
17
+
18
+ export interface PullRequestInfo {
19
+ number: number;
20
+ url: string;
21
+ isDraft: boolean;
22
+ }
23
+
24
+ export interface GitInfoState {
25
+ isRepository: boolean;
26
+ branch: string | null;
27
+ changedFiles: number;
28
+ pullRequest: PullRequestInfo | null;
29
+ }
30
+
31
+ export function emptyModelInfoState(): ModelInfoState {
32
+ return {
33
+ provider: "",
34
+ modelId: "no-model",
35
+ modelName: "No model",
36
+ thinking: "off",
37
+ contextTokens: null,
38
+ contextWindow: 0,
39
+ contextPercent: null,
40
+ cost: 0,
41
+ tokensPerSecond: null,
42
+ generating: false,
43
+ };
44
+ }
45
+
46
+ export function emptyGitInfoState(): GitInfoState {
47
+ return {
48
+ isRepository: false,
49
+ branch: null,
50
+ changedFiles: 0,
51
+ pullRequest: null,
52
+ };
53
+ }
54
+
55
+ function isRecord(value: unknown): value is Record<string, unknown> {
56
+ return typeof value === "object" && value !== null;
57
+ }
58
+
59
+ function isNullableNumber(value: unknown) {
60
+ return value === null || typeof value === "number";
61
+ }
62
+
63
+ export function isModelInfoState(value: unknown): value is ModelInfoState {
64
+ if (!isRecord(value)) return false;
65
+
66
+ return (
67
+ typeof value.provider === "string" &&
68
+ typeof value.modelId === "string" &&
69
+ typeof value.modelName === "string" &&
70
+ typeof value.thinking === "string" &&
71
+ isNullableNumber(value.contextTokens) &&
72
+ typeof value.contextWindow === "number" &&
73
+ isNullableNumber(value.contextPercent) &&
74
+ typeof value.cost === "number" &&
75
+ isNullableNumber(value.tokensPerSecond) &&
76
+ typeof value.generating === "boolean"
77
+ );
78
+ }
79
+
80
+ function isPullRequestInfo(value: unknown): value is PullRequestInfo {
81
+ if (!isRecord(value)) return false;
82
+
83
+ return (
84
+ typeof value.number === "number" &&
85
+ typeof value.url === "string" &&
86
+ typeof value.isDraft === "boolean"
87
+ );
88
+ }
89
+
90
+ export function isGitInfoState(value: unknown): value is GitInfoState {
91
+ if (!isRecord(value)) return false;
92
+
93
+ return (
94
+ typeof value.isRepository === "boolean" &&
95
+ (value.branch === null || typeof value.branch === "string") &&
96
+ typeof value.changedFiles === "number" &&
97
+ (value.pullRequest === null || isPullRequestInfo(value.pullRequest))
98
+ );
99
+ }
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env pwsh
2
+ # Per-extension installer — Windows / PowerShell
3
+ # Installs npm dependencies for this extension. No-op if no package.json.
4
+ $ErrorActionPreference = "Stop"
5
+ $extDir = $PSScriptRoot
6
+ if (-not $extDir) { $extDir = Split-Path -Parent $MyInvocation.MyCommand.Path }
7
+ if (-not (Test-Path "$extDir/package.json")) {
8
+ Write-Host " - no package.json — skipping" -ForegroundColor DarkGray
9
+ exit 0
10
+ }
11
+ if (-not (Get-Command npm -ErrorAction SilentlyContinue)) {
12
+ Write-Host " x npm not found — install Node.js 22+" -ForegroundColor Red
13
+ exit 1
14
+ }
15
+ if (Test-Path "$extDir/package-lock.json") {
16
+ Write-Host " - npm ci" -ForegroundColor DarkGray
17
+ npm --prefix $extDir ci --no-audit --no-fund
18
+ } else {
19
+ Write-Host " - npm install" -ForegroundColor DarkGray
20
+ npm --prefix $extDir install --no-audit --no-fund
21
+ }
22
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
23
+ Write-Host " + done" -ForegroundColor Green
@@ -0,0 +1,21 @@
1
+ #!/bin/sh
2
+ # Per-extension installer — macOS / Linux
3
+ # Installs npm dependencies for this extension. No-op if no package.json.
4
+ set -eu
5
+ EXT_DIR="$(cd "$(dirname "$0")" && pwd)"
6
+ if [ ! -f "$EXT_DIR/package.json" ]; then
7
+ echo " · no package.json — skipping"
8
+ exit 0
9
+ fi
10
+ if ! command -v npm >/dev/null 2>&1; then
11
+ echo " ✖ npm not found — install Node.js 22+" >&2
12
+ exit 1
13
+ fi
14
+ if [ -f "$EXT_DIR/package-lock.json" ]; then
15
+ echo " · npm ci"
16
+ npm --prefix "$EXT_DIR" ci --no-audit --no-fund
17
+ else
18
+ echo " · npm install"
19
+ npm --prefix "$EXT_DIR" install --no-audit --no-fund
20
+ fi
21
+ echo " ✓ done"
@@ -0,0 +1,117 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
4
+ import { Type } from "typebox";
5
+ import {
6
+ createToolCallTimeoutGuard,
7
+ runWithToolCallTimeout,
8
+ CHILD_TOOL_CALL_TIMEOUT_MS,
9
+ ToolCallTimeoutError,
10
+ } from "./tool-call-timeout.ts";
11
+
12
+ test("the production timeout error names the tool and three-minute limit", () => {
13
+ assert.equal(
14
+ new ToolCallTimeoutError("fixture_tool", CHILD_TOOL_CALL_TIMEOUT_MS)
15
+ .message,
16
+ 'Tool call "fixture_tool" timed out after 3 minutes.',
17
+ );
18
+ });
19
+
20
+ test("a hung tool call fails clearly and receives an abort signal", async () => {
21
+ let executionSignal: AbortSignal | undefined;
22
+
23
+ await assert.rejects(
24
+ runWithToolCallTimeout("hung_fixture", 10, undefined, (signal) => {
25
+ executionSignal = signal;
26
+ return new Promise(() => {});
27
+ }),
28
+ (error: unknown) => {
29
+ assert.equal(error instanceof ToolCallTimeoutError, true);
30
+ assert.equal(
31
+ error instanceof Error ? error.message : "",
32
+ 'Tool call "hung_fixture" timed out after 10 ms.',
33
+ );
34
+ return true;
35
+ },
36
+ );
37
+
38
+ assert.equal(executionSignal?.aborted, true);
39
+ assert.equal(executionSignal?.reason instanceof ToolCallTimeoutError, true);
40
+ });
41
+
42
+ test("parent cancellation still stops the timeout wrapper immediately", async () => {
43
+ const controller = new AbortController();
44
+ const reason = new Error("cancelled fixture");
45
+ const pending = runWithToolCallTimeout(
46
+ "hung_fixture",
47
+ 60_000,
48
+ controller.signal,
49
+ () => new Promise(() => {}),
50
+ );
51
+
52
+ controller.abort(reason);
53
+
54
+ await assert.rejects(pending, (error: unknown) => error === reason);
55
+ });
56
+
57
+ test("the guard wraps each definition once and can discover later tools", () => {
58
+ const definitions = new Map<string, ToolDefinition>();
59
+ const createDefinition = (name: string): ToolDefinition => ({
60
+ name,
61
+ label: name,
62
+ description: name,
63
+ parameters: Type.Object({}),
64
+ async execute() {
65
+ return { content: [{ type: "text", text: "done" }], details: {} };
66
+ },
67
+ });
68
+ const first = createDefinition("first");
69
+ definitions.set(first.name, first);
70
+ const registry = {
71
+ getAllTools: () => [...definitions.keys()].map((name) => ({ name })),
72
+ getToolDefinition: (name: string) => definitions.get(name),
73
+ };
74
+ const guard = createToolCallTimeoutGuard(10);
75
+
76
+ const firstExecute = first.execute;
77
+ guard.apply(registry);
78
+ const firstWrappedExecute = first.execute;
79
+ assert.notEqual(firstWrappedExecute, firstExecute);
80
+
81
+ const second = createDefinition("second");
82
+ const secondExecute = second.execute;
83
+ definitions.set(second.name, second);
84
+ guard.apply(registry);
85
+
86
+ assert.equal(first.execute, firstWrappedExecute);
87
+ assert.notEqual(second.execute, secondExecute);
88
+ });
89
+
90
+ test("successful and terminating tool results pass through unchanged", async () => {
91
+ const result = {
92
+ content: [{ type: "text" as const, text: "recorded" }],
93
+ details: { value: "fixture" },
94
+ terminate: true,
95
+ };
96
+
97
+ assert.equal(
98
+ await runWithToolCallTimeout(
99
+ "structured_output",
100
+ 10,
101
+ undefined,
102
+ async () => result,
103
+ ),
104
+ result,
105
+ );
106
+ });
107
+
108
+ test("the timeout is fresh for each tool call, not shared across calls", async () => {
109
+ const execute = () =>
110
+ runWithToolCallTimeout("slow_fixture", 100, undefined, async () => {
111
+ await new Promise((resolve) => setTimeout(resolve, 60));
112
+ return "done";
113
+ });
114
+
115
+ assert.equal(await execute(), "done");
116
+ assert.equal(await execute(), "done");
117
+ });
@@ -0,0 +1,104 @@
1
+ import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
2
+
3
+ export const CHILD_TOOL_CALL_TIMEOUT_MS = 3 * 60 * 1_000;
4
+
5
+ interface ToolRegistry {
6
+ getAllTools(): Array<{ name: string }>;
7
+ getToolDefinition(name: string): ToolDefinition | undefined;
8
+ }
9
+
10
+ function formatTimeout(timeoutMs: number) {
11
+ if (timeoutMs % 60_000 === 0) {
12
+ const minutes = timeoutMs / 60_000;
13
+ return `${minutes} minute${minutes === 1 ? "" : "s"}`;
14
+ }
15
+ if (timeoutMs % 1_000 === 0) {
16
+ const seconds = timeoutMs / 1_000;
17
+ return `${seconds} second${seconds === 1 ? "" : "s"}`;
18
+ }
19
+ return `${timeoutMs} ms`;
20
+ }
21
+
22
+ export class ToolCallTimeoutError extends Error {
23
+ constructor(toolName: string, timeoutMs: number) {
24
+ super(
25
+ `Tool call "${toolName}" timed out after ${formatTimeout(timeoutMs)}.`,
26
+ );
27
+ this.name = "ToolCallTimeoutError";
28
+ }
29
+ }
30
+
31
+ export async function runWithToolCallTimeout<T>(
32
+ toolName: string,
33
+ timeoutMs: number,
34
+ signal: AbortSignal | undefined,
35
+ execute: (signal: AbortSignal) => Promise<T>,
36
+ ) {
37
+ const timeoutController = new AbortController();
38
+ const executionSignal = signal
39
+ ? AbortSignal.any([signal, timeoutController.signal])
40
+ : timeoutController.signal;
41
+ const timeoutError = new ToolCallTimeoutError(toolName, timeoutMs);
42
+ let timer: ReturnType<typeof setTimeout> | undefined;
43
+ const timeout = new Promise<never>((_resolve, reject) => {
44
+ timer = setTimeout(() => {
45
+ reject(timeoutError);
46
+ timeoutController.abort(timeoutError);
47
+ }, timeoutMs);
48
+ });
49
+
50
+ let removeAbortListener: (() => void) | undefined;
51
+ const aborted = new Promise<never>((_resolve, reject) => {
52
+ if (!signal) return;
53
+ const onAbort = () => {
54
+ reject(
55
+ signal.reason instanceof Error
56
+ ? signal.reason
57
+ : new Error(`Tool call "${toolName}" was aborted.`),
58
+ );
59
+ };
60
+ if (signal.aborted) {
61
+ onAbort();
62
+ return;
63
+ }
64
+ signal.addEventListener("abort", onAbort, { once: true });
65
+ removeAbortListener = () => signal.removeEventListener("abort", onAbort);
66
+ });
67
+
68
+ try {
69
+ return await Promise.race([execute(executionSignal), timeout, aborted]);
70
+ } finally {
71
+ if (timer) clearTimeout(timer);
72
+ removeAbortListener?.();
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Wrap every currently registered child tool with an independent execution
78
+ * timeout. Calling apply() again is safe and picks up tools registered later.
79
+ */
80
+ export function createToolCallTimeoutGuard(
81
+ timeoutMs = CHILD_TOOL_CALL_TIMEOUT_MS,
82
+ ) {
83
+ const wrapped = new WeakSet<ToolDefinition>();
84
+
85
+ const wrap = (definition: ToolDefinition) => {
86
+ if (wrapped.has(definition)) return;
87
+ wrapped.add(definition);
88
+
89
+ const execute = definition.execute;
90
+ definition.execute = async (toolCallId, params, signal, onUpdate, ctx) =>
91
+ runWithToolCallTimeout(definition.name, timeoutMs, signal, (signal) =>
92
+ execute.call(definition, toolCallId, params, signal, onUpdate, ctx),
93
+ );
94
+ };
95
+
96
+ return {
97
+ apply(session: ToolRegistry) {
98
+ for (const { name } of session.getAllTools()) {
99
+ const definition = session.getToolDefinition(name);
100
+ if (definition) wrap(definition);
101
+ }
102
+ },
103
+ };
104
+ }
@@ -0,0 +1,74 @@
1
+ # skillsconf
2
+
3
+ Per-skill visibility manager for pi, with **packages** (named groups) for mass enable/disable.
4
+
5
+ Disabled skills are stripped from the system prompt every turn (via `before_agent_start`),
6
+ so they cost zero context. Nothing is moved on disk — packages are just named lists
7
+ stored in settings.
8
+
9
+ ```
10
+ /skillsconf
11
+ ```
12
+
13
+ ## Keybindings
14
+
15
+ - `↑` / `↓` or `j` / `k` — navigate; `Home`/`End` (`g`/`G`) jump
16
+ - `Space` — toggle the selected skill **or** the selected package (whole group on/off)
17
+ - `/` — fuzzy filter by name/description; `↑`/`↓` exit typing (filter kept) and
18
+ move; `←`/`→` edit the query; `Esc` clears, `Enter` keeps filter
19
+ - `e` — toggle grouped view (packages + ungrouped skills) vs flat list of all skills
20
+ - `n` — new package (type a name, `Enter` creates, `Esc` cancels)
21
+ - `a` — on a package: enter assign mode (cursor jumps to skills, `Space` toggles
22
+ membership, `Enter` done, `Esc` cancel without changing the package)
23
+ - `d` — delete what's under the cursor (package group, or skill files from disk)
24
+ with a `y`/`n` confirm (`Esc` cancels)
25
+ - `s` — save to settings, stay open (`● unsaved — press s to save` clears; takes
26
+ effect next turn)
27
+ - `Esc` / `q` — quit (`s` first if you want to keep changes); quitting with pending
28
+ changes shows `changes discarded (not saved)`
29
+ Package rows show `(enabled/total)`: `◉` all on, `◍` partial, `○` all off / empty.
30
+ Each package row leads with an accent `▸` marker and bold name so it stands out
31
+ from skills.
32
+
33
+ Skills that belong to a package are hidden from the main list and managed via
34
+ their package row (grouped view). `e` switches to a flat list showing every
35
+ skill; `/` search and assign mode always see every skill, with a `⟨package⟩`
36
+ tag showing membership.
37
+
38
+ ## Persistence
39
+
40
+ Writes `disabledSkills` and `skillPackages` in `~/.pi/agent/settings.json`:
41
+
42
+ - Nothing disabled + no packages → both keys removed to keep settings clean
43
+ - `skillPackages` maps package name → sorted member skill names (empty packages kept)
44
+ - Stale names (skills no longer on disk) are filtered on the next save
45
+
46
+ No reload happens on save — the prompt filter re-reads settings from disk every
47
+ turn, so changes apply from the next agent turn on. Physically deleted skills
48
+ vanish from the loaded list after `/reload`.
49
+
50
+ ## Keyboard shortcut
51
+
52
+ `Ctrl+Alt+S` opens skillsconf when supported by the host.
53
+
54
+ ## How filtering works
55
+
56
+ pi renders loaded skills as `<skill><name>…` blocks inside `<available_skills>`.
57
+ The extension removes the blocks whose `<name>` is in `disabledSkills` (verbatim block
58
+ surgery — kept blocks are byte-identical). When all skills are disabled, the whole
59
+ section including its intro is removed.
60
+
61
+ ## Development
62
+
63
+ ```bash
64
+ npx tsc --noEmit --project ./tsconfig.json
65
+ ```
66
+
67
+ Tests are plain `node:assert` files (same style as `modelconf`):
68
+
69
+ ```bash
70
+ npx tsc --project ./tsconfig.json --outDir /tmp/skillsconf-test
71
+ node --test /tmp/skillsconf-test/src/filter.test.js \
72
+ /tmp/skillsconf-test/src/persistence.test.js \
73
+ /tmp/skillsconf-test/src/ui/SkillConfView.test.js
74
+ ```