@asterxsk/kiln 0.1.0 → 0.2.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 (203) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +170 -170
  3. package/agent/AGENTS.md +67 -67
  4. package/agent/README.md +5 -5
  5. package/agent/extensions/AGENTS.md +68 -68
  6. package/agent/extensions/ask-user/index.ts +418 -418
  7. package/agent/extensions/ask-user/package-lock.json +769 -769
  8. package/agent/extensions/ask-user/package.json +19 -19
  9. package/agent/extensions/ask-user/prompt.ts +45 -45
  10. package/agent/extensions/ask-user/tsconfig.json +7 -7
  11. package/agent/extensions/background-terminals/docs/implementation-guide.md +942 -942
  12. package/agent/extensions/background-terminals/index.ts +627 -627
  13. package/agent/extensions/background-terminals/manager.test.ts +735 -735
  14. package/agent/extensions/background-terminals/output.test.ts +109 -109
  15. package/agent/extensions/background-terminals/package-lock.json +769 -769
  16. package/agent/extensions/background-terminals/package.json +17 -17
  17. package/agent/extensions/background-terminals/prompt.test.ts +125 -125
  18. package/agent/extensions/background-terminals/ps.test.ts +82 -82
  19. package/agent/extensions/background-terminals/result-delivery.test.ts +44 -44
  20. package/agent/extensions/background-terminals/src/domain.ts +87 -87
  21. package/agent/extensions/background-terminals/src/manager.ts +907 -907
  22. package/agent/extensions/background-terminals/src/output.ts +84 -84
  23. package/agent/extensions/background-terminals/src/prompt.ts +142 -142
  24. package/agent/extensions/background-terminals/src/result-delivery.ts +27 -27
  25. package/agent/extensions/background-terminals/src/runtime.ts +36 -36
  26. package/agent/extensions/background-terminals/src/ui/output-view.ts +79 -79
  27. package/agent/extensions/background-terminals/src/ui/ps.ts +621 -621
  28. package/agent/extensions/background-terminals/tsconfig.json +7 -7
  29. package/agent/extensions/file-search/index.spec.ts +443 -443
  30. package/agent/extensions/file-search/index.ts +459 -459
  31. package/agent/extensions/file-search/package-lock.json +2253 -2253
  32. package/agent/extensions/file-search/package.json +23 -23
  33. package/agent/extensions/file-search/src/args.ts +122 -122
  34. package/agent/extensions/file-search/src/binaries.ts +422 -422
  35. package/agent/extensions/file-search/src/output.ts +126 -126
  36. package/agent/extensions/file-search/src/process.ts +146 -146
  37. package/agent/extensions/file-search/src/prompt.ts +52 -52
  38. package/agent/extensions/file-search/tsconfig.json +7 -7
  39. package/agent/extensions/modelconf/PLAN.md +915 -915
  40. package/agent/extensions/modelconf/index.ts +296 -296
  41. package/agent/extensions/modelconf/src/ui/ModelConfView.ts +1101 -1101
  42. package/agent/extensions/pi-web-access/CHANGELOG.md +690 -690
  43. package/agent/extensions/pi-web-access/LICENSE +21 -21
  44. package/agent/extensions/pi-web-access/README.md +470 -470
  45. package/agent/extensions/pi-web-access/SECURITY.md +5 -5
  46. package/agent/extensions/pi-web-access/activity.ts +101 -101
  47. package/agent/extensions/pi-web-access/auth-fetch.ts +148 -148
  48. package/agent/extensions/pi-web-access/brightdata-unlocker.ts +272 -272
  49. package/agent/extensions/pi-web-access/chrome-cookies.ts +669 -669
  50. package/agent/extensions/pi-web-access/content-find.ts +139 -139
  51. package/agent/extensions/pi-web-access/credential-source.ts +191 -191
  52. package/agent/extensions/pi-web-access/data-uri-sanitize.ts +406 -406
  53. package/agent/extensions/pi-web-access/datalab-pdf-extract.ts +568 -568
  54. package/agent/extensions/pi-web-access/declared-web-links.ts +173 -173
  55. package/agent/extensions/pi-web-access/evidence/CONTRACT-EVIDENCE.md +496 -496
  56. package/agent/extensions/pi-web-access/evidence/contract-probe.mjs +140 -140
  57. package/agent/extensions/pi-web-access/exa.ts +526 -526
  58. package/agent/extensions/pi-web-access/extract.ts +1196 -1196
  59. package/agent/extensions/pi-web-access/feature-config.ts +29 -29
  60. package/agent/extensions/pi-web-access/fetch-params.ts +111 -111
  61. package/agent/extensions/pi-web-access/gemini-adc.ts +298 -298
  62. package/agent/extensions/pi-web-access/gemini-api.ts +353 -353
  63. package/agent/extensions/pi-web-access/gemini-pdf-extract.ts +108 -108
  64. package/agent/extensions/pi-web-access/gemini-url-context.ts +128 -128
  65. package/agent/extensions/pi-web-access/gemini-web-config.ts +101 -101
  66. package/agent/extensions/pi-web-access/gemini-web.ts +487 -487
  67. package/agent/extensions/pi-web-access/github-api.ts +197 -197
  68. package/agent/extensions/pi-web-access/github-extract.ts +746 -746
  69. package/agent/extensions/pi-web-access/github-issue-pr.ts +700 -700
  70. package/agent/extensions/pi-web-access/index.ts +1737 -1737
  71. package/agent/extensions/pi-web-access/package-lock.json +5808 -5808
  72. package/agent/extensions/pi-web-access/package.json +64 -64
  73. package/agent/extensions/pi-web-access/page-query.ts +96 -96
  74. package/agent/extensions/pi-web-access/pdf-extract.ts +409 -409
  75. package/agent/extensions/pi-web-access/promise-try.d.ts +7 -7
  76. package/agent/extensions/pi-web-access/query-rewrite.ts +51 -51
  77. package/agent/extensions/pi-web-access/render-search-error.ts +170 -170
  78. package/agent/extensions/pi-web-access/rsc-extract.ts +338 -338
  79. package/agent/extensions/pi-web-access/source-check.ts +282 -282
  80. package/agent/extensions/pi-web-access/ssrf-protection.ts +526 -526
  81. package/agent/extensions/pi-web-access/storage.ts +521 -521
  82. package/agent/extensions/pi-web-access/summary-model-scope.ts +125 -125
  83. package/agent/extensions/pi-web-access/test/auth-fetch.test.mjs +208 -208
  84. package/agent/extensions/pi-web-access/test/brightdata-unlocker.test.mjs +840 -840
  85. package/agent/extensions/pi-web-access/test/chrome-cookie-extraction.test.mjs +441 -441
  86. package/agent/extensions/pi-web-access/test/config-path.test.mjs +283 -283
  87. package/agent/extensions/pi-web-access/test/content-find.test.mjs +25 -25
  88. package/agent/extensions/pi-web-access/test/credential-source.test.mjs +118 -118
  89. package/agent/extensions/pi-web-access/test/data-uri-sanitize.test.mjs +210 -210
  90. package/agent/extensions/pi-web-access/test/datalab-pdf-extract.test.mjs +552 -552
  91. package/agent/extensions/pi-web-access/test/declared-web-links.test.mjs +212 -212
  92. package/agent/extensions/pi-web-access/test/fetch-answer-storage.test.mjs +40 -40
  93. package/agent/extensions/pi-web-access/test/fetch-cache-storage.test.mjs +334 -334
  94. package/agent/extensions/pi-web-access/test/fetch-content-domain-policy.test.mjs +95 -95
  95. package/agent/extensions/pi-web-access/test/fetch-modes.test.mjs +53 -53
  96. package/agent/extensions/pi-web-access/test/fetch-not-found-guidance.test.mjs +92 -92
  97. package/agent/extensions/pi-web-access/test/fetch-params.test.mjs +86 -86
  98. package/agent/extensions/pi-web-access/test/fetch-render-call.test.mjs +34 -34
  99. package/agent/extensions/pi-web-access/test/fetch-routing.test.mjs +173 -173
  100. package/agent/extensions/pi-web-access/test/gemini-adc-auth.test.mjs +257 -257
  101. package/agent/extensions/pi-web-access/test/gemini-api-transport.test.mjs +170 -170
  102. package/agent/extensions/pi-web-access/test/gemini-pdf-extract.test.mjs +133 -133
  103. package/agent/extensions/pi-web-access/test/gemini-web-cookie-opt-in.test.mjs +178 -178
  104. package/agent/extensions/pi-web-access/test/gemini-web-header-overflow.test.mjs +148 -148
  105. package/agent/extensions/pi-web-access/test/get-search-content.test.mjs +223 -223
  106. package/agent/extensions/pi-web-access/test/github-extract.test.mjs +378 -378
  107. package/agent/extensions/pi-web-access/test/github-issue-pr.test.mjs +565 -565
  108. package/agent/extensions/pi-web-access/test/inline-content-config.test.mjs +99 -99
  109. package/agent/extensions/pi-web-access/test/lazy-extract-load.test.mjs +118 -118
  110. package/agent/extensions/pi-web-access/test/local-video-oversize.test.mjs +52 -52
  111. package/agent/extensions/pi-web-access/test/package-typebox-dependency.test.mjs +50 -50
  112. package/agent/extensions/pi-web-access/test/page-query.test.mjs +51 -51
  113. package/agent/extensions/pi-web-access/test/pdf-config.test.mjs +140 -140
  114. package/agent/extensions/pi-web-access/test/pdf-extract.test.mjs +500 -500
  115. package/agent/extensions/pi-web-access/test/proxy-transport.test.mjs +286 -286
  116. package/agent/extensions/pi-web-access/test/query-rewrite.test.mjs +52 -52
  117. package/agent/extensions/pi-web-access/test/rsc-fallback.test.mjs +102 -102
  118. package/agent/extensions/pi-web-access/test/search-error-render.test.mjs +152 -152
  119. package/agent/extensions/pi-web-access/test/search-providers.test.mjs +274 -274
  120. package/agent/extensions/pi-web-access/test/source-check.test.mjs +179 -179
  121. package/agent/extensions/pi-web-access/test/ssrf-allow-ranges-config.test.mjs +205 -205
  122. package/agent/extensions/pi-web-access/test/ssrf-protection.test.mjs +456 -456
  123. package/agent/extensions/pi-web-access/test/tool-registration-config.test.mjs +182 -182
  124. package/agent/extensions/pi-web-access/test/youtube-extract-errors.test.mjs +64 -64
  125. package/agent/extensions/pi-web-access/tsconfig.json +11 -11
  126. package/agent/extensions/pi-web-access/utils.ts +451 -451
  127. package/agent/extensions/pi-web-access/video-extract.ts +392 -392
  128. package/agent/extensions/pi-web-access/youtube-extract.ts +328 -328
  129. package/agent/extensions/shared/activity-status.ts +31 -31
  130. package/agent/extensions/shared/child-session.test.ts +270 -270
  131. package/agent/extensions/shared/child-session.ts +148 -148
  132. package/agent/extensions/shared/context-utilization.test.ts +48 -48
  133. package/agent/extensions/shared/context-utilization.ts +47 -47
  134. package/agent/extensions/shared/dashboard-state.ts +99 -99
  135. package/agent/extensions/shared/tool-call-timeout.test.ts +117 -117
  136. package/agent/extensions/shared/tool-call-timeout.ts +104 -104
  137. package/agent/extensions/subagents/by-the-way.test.ts +29 -29
  138. package/agent/extensions/subagents/claude.test.ts +119 -119
  139. package/agent/extensions/subagents/codex.test.ts +102 -102
  140. package/agent/extensions/subagents/context-usage.test.ts +107 -107
  141. package/agent/extensions/subagents/docs/design-plan.md +568 -568
  142. package/agent/extensions/subagents/docs/effect-v4-extension-guide.md +354 -354
  143. package/agent/extensions/subagents/docs/effect-v4-notes.md +571 -571
  144. package/agent/extensions/subagents/index.ts +779 -779
  145. package/agent/extensions/subagents/manager.test.ts +276 -276
  146. package/agent/extensions/subagents/package-lock.json +2244 -2244
  147. package/agent/extensions/subagents/package.json +19 -19
  148. package/agent/extensions/subagents/result-delivery.test.ts +27 -27
  149. package/agent/extensions/subagents/src/backend.ts +73 -73
  150. package/agent/extensions/subagents/src/backends/claude.ts +701 -701
  151. package/agent/extensions/subagents/src/backends/codex.ts +1060 -1060
  152. package/agent/extensions/subagents/src/backends/pi.ts +575 -575
  153. package/agent/extensions/subagents/src/backends/stub.ts +300 -300
  154. package/agent/extensions/subagents/src/by-the-way.ts +21 -21
  155. package/agent/extensions/subagents/src/domain.ts +253 -253
  156. package/agent/extensions/subagents/src/format.ts +74 -74
  157. package/agent/extensions/subagents/src/manager.ts +736 -736
  158. package/agent/extensions/subagents/src/prompt.ts +92 -92
  159. package/agent/extensions/subagents/src/result-delivery.ts +20 -20
  160. package/agent/extensions/subagents/src/runtime.ts +53 -53
  161. package/agent/extensions/subagents/src/ui/takeover.ts +583 -583
  162. package/agent/extensions/subagents/src/ui/transcript.ts +201 -201
  163. package/agent/extensions/subagents/takeover.test.ts +29 -29
  164. package/agent/extensions/subagents/tsconfig.json +7 -7
  165. package/agent/extensions/todo/AGENTS.md +38 -38
  166. package/agent/extensions/todo/LICENSE +21 -21
  167. package/agent/extensions/todo/config.ts +55 -55
  168. package/agent/extensions/todo/index.ts +151 -151
  169. package/agent/extensions/todo/locales/de.json +17 -17
  170. package/agent/extensions/todo/locales/en.json +15 -15
  171. package/agent/extensions/todo/locales/es.json +17 -17
  172. package/agent/extensions/todo/locales/fr.json +17 -17
  173. package/agent/extensions/todo/locales/pt-BR.json +17 -17
  174. package/agent/extensions/todo/locales/pt.json +17 -17
  175. package/agent/extensions/todo/locales/ru.json +17 -17
  176. package/agent/extensions/todo/locales/uk.json +17 -17
  177. package/agent/extensions/todo/locales/zh.json +17 -17
  178. package/agent/extensions/todo/package-lock.json +3358 -3358
  179. package/agent/extensions/todo/package.json +67 -67
  180. package/agent/extensions/todo/state/i18n-bridge.ts +64 -64
  181. package/agent/extensions/todo/state/invariants.ts +20 -20
  182. package/agent/extensions/todo/state/replay.ts +38 -38
  183. package/agent/extensions/todo/state/selectors.ts +107 -107
  184. package/agent/extensions/todo/state/state-reducer.ts +326 -326
  185. package/agent/extensions/todo/state/state.ts +18 -18
  186. package/agent/extensions/todo/state/store.ts +82 -82
  187. package/agent/extensions/todo/state/task-graph.ts +57 -57
  188. package/agent/extensions/todo/todo-overlay.ts +200 -200
  189. package/agent/extensions/todo/todo.ts +155 -155
  190. package/agent/extensions/todo/tool/response-envelope.ts +109 -109
  191. package/agent/extensions/todo/tool/types.ts +206 -206
  192. package/agent/extensions/todo/view/format.ts +177 -177
  193. package/agent/install.ps1 +637 -527
  194. package/agent/install.sh +620 -511
  195. package/agent/keybindings.json +7 -7
  196. package/bin/kiln.js +124 -11
  197. package/package.json +8 -2
  198. package/agent/extensions/taste/index.ts +0 -443
  199. package/agent/extensions/taste/install.ps1 +0 -23
  200. package/agent/extensions/taste/install.sh +0 -21
  201. /package/agent/extensions/{status line → statusline}/index.ts +0 -0
  202. /package/agent/extensions/{status line → statusline}/install.ps1 +0 -0
  203. /package/agent/extensions/{status line → statusline}/install.sh +0 -0
@@ -1,296 +1,296 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import { loadHiddenModels, getVisibilityMap, saveHiddenModels } from "./src/persistence.js";
3
- import { ModelConfView } from "./src/ui/ModelConfView.js";
4
- import fs from "node:fs";
5
- import os from "node:os";
6
- import path from "node:path";
7
- import { globToRegExp } from "./src/glob.js";
8
-
9
- // ---------- helpers for filtering main /model list ----------
10
- function settingsPath(): string {
11
- return path.join(os.homedir(), ".pi", "agent", "settings.json");
12
- }
13
-
14
- function loadHiddenModelsSync(): string[] | undefined {
15
- try {
16
- const raw = fs.readFileSync(settingsPath(), "utf8");
17
- const j = JSON.parse(raw) as { hiddenModels?: string[] };
18
- return j.hiddenModels;
19
- } catch {
20
- return undefined;
21
- }
22
- }
23
- // Backward compat alias
24
- function loadEnabledModelsSync(): string[] | undefined {
25
- return loadHiddenModelsSync();
26
- }
27
-
28
- const VALID_THINKING = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
29
-
30
- function stripThinkingLevel(pattern: string): string {
31
- const idx = pattern.lastIndexOf(":");
32
- if (idx !== -1) {
33
- const suffix = pattern.slice(idx + 1);
34
- if (VALID_THINKING.has(suffix)) return pattern.slice(0, idx);
35
- }
36
- return pattern;
37
- }
38
-
39
- function matchesPattern(modelRef: string, pattern: string): boolean {
40
- const pat = stripThinkingLevel(pattern);
41
- const lowerRef = modelRef.toLowerCase();
42
- const lowerPat = pat.toLowerCase();
43
- const bareId = modelRef.split("/").slice(1).join("/")?.toLowerCase() ?? lowerRef;
44
- const hasGlob = /[*?\[\]]/.test(pat);
45
- if (hasGlob) {
46
- const rx = globToRegExp(pat);
47
- // pi matches against full ref OR bare id via minimatch
48
- if (rx.test(modelRef)) return true;
49
- if (rx.test(bareId)) return true;
50
- // also try lower variants (globToRegExp is already i-flag)
51
- return false;
52
- } else {
53
- return lowerRef === lowerPat || bareId === lowerPat;
54
- }
55
- }
56
-
57
- function modelMatchesAnyPattern(modelRef: string, patterns: string[]): boolean {
58
- for (const p of patterns) if (matchesPattern(modelRef, p)) return true;
59
- return false;
60
- }
61
-
62
- function patchRuntime(runtime: any) {
63
- if (!runtime || runtime.__modelconfPatched) return;
64
- const origSnapshot = runtime.getAvailableSnapshot?.bind(runtime);
65
- const origGetAvailable = runtime.getAvailable?.bind(runtime);
66
- const origGetModels = runtime.getModels?.bind(runtime);
67
-
68
- if (typeof origSnapshot === "function") {
69
- runtime.getAvailableSnapshot = function (...args: any[]) {
70
- const base: any[] = origSnapshot(...args);
71
- const hidden = loadHiddenModelsSync();
72
- if (!hidden || hidden.length === 0) return base;
73
- const filtered = base.filter((m: any) => {
74
- const ref = `${m.provider}/${m.id}`;
75
- // Hide if matches hidden denylist (from full list). Keep visible otherwise.
76
- const isHidden = modelMatchesAnyPattern(ref, hidden) || modelMatchesAnyPattern(m.id ?? "", hidden);
77
- return !isHidden;
78
- });
79
- return filtered;
80
- };
81
- // keep original for modelconf's own unfiltered view
82
- runtime.__originalGetAvailableSnapshot = origSnapshot;
83
- }
84
-
85
- // also patch async getAvailable(providerId, opts) if present
86
- if (typeof origGetAvailable === "function") {
87
- runtime.getAvailable = async function (providerId?: string, opts?: any) {
88
- const base: any[] = await origGetAvailable(providerId, opts);
89
- const hidden = loadHiddenModelsSync();
90
- if (!hidden || hidden.length === 0) return base;
91
- // Always filter hidden from any provider query — hidden applies to full picker list
92
- const filtered = base.filter((m: any) => {
93
- const ref = `${m.provider}/${m.id}`;
94
- const isHidden = modelMatchesAnyPattern(ref, hidden) || modelMatchesAnyPattern(m.id ?? "", hidden);
95
- return !isHidden;
96
- });
97
- return filtered;
98
- };
99
- runtime.__originalGetAvailable = origGetAvailable;
100
- }
101
-
102
- runtime.__modelconfPatched = true;
103
- }
104
-
105
- function patchRegistry(registry: any) {
106
- if (!registry) return;
107
- // ModelRegistry.getAvailable is async and delegates to runtime; patching runtime is enough.
108
- // Also patch registry.getAvailableSnapshot if it exists (some pi versions expose it)
109
- if (typeof registry.getAvailableSnapshot === "function" && !registry.__modelconfPatched) {
110
- const orig = registry.getAvailableSnapshot.bind(registry);
111
- registry.getAvailableSnapshot = function (...args: any[]) {
112
- const base: any[] = orig(...args);
113
- const hidden = loadHiddenModelsSync();
114
- if (!hidden || hidden.length === 0) return base;
115
- const filtered = base.filter((m: any) => {
116
- const ref = `${m.provider}/${m.id}`;
117
- return !modelMatchesAnyPattern(ref, hidden);
118
- });
119
- return filtered;
120
- };
121
- registry.__originalGetAvailableSnapshot = orig;
122
- registry.__modelconfPatched = true;
123
- }
124
- if (registry.runtime) patchRuntime(registry.runtime);
125
- }
126
-
127
- export default function modelconf(pi: ExtensionAPI) {
128
- // patch on session start for every new session's runtime
129
- try {
130
- (pi as any).on?.("session_start", async (_ev: any, ctx: any) => {
131
- try {
132
- patchRegistry(ctx?.modelRegistry);
133
- } catch {}
134
- });
135
- } catch {}
136
-
137
- // also attempt to patch via global require at load time (best-effort, may be different copy)
138
- try {
139
- // try to find main pi runtime via ctx-less import — no-op if not found
140
- // we rely on patchRegistry on first command invocation as fallback
141
- } catch {}
142
-
143
- async function openModelConf(ctx: any) {
144
- if (ctx.mode !== "tui") {
145
- ctx.ui.notify("/modelconf requires TUI mode", "error");
146
- return;
147
- }
148
-
149
- // Ensure main list filtering is active for this session
150
- try {
151
- patchRegistry(ctx.modelRegistry);
152
- } catch {}
153
-
154
- const rawAll = ctx.modelRegistry.getAll() as any[];
155
- const allModelsRaw = rawAll.map((m) => ({
156
- provider: (m as any).provider ?? "unknown",
157
- id: (m as any).id ?? "",
158
- name: (m as any).name ?? (m as any).id ?? "",
159
- }));
160
-
161
- const hiddenRaw = await loadHiddenModels();
162
- const allIds = allModelsRaw.map((m) => `${m.provider}/${m.id}`);
163
-
164
- // ---- Determine available models (main /model list) as auth-filtered set ----
165
- // This is the FULL list that /model shows (not the scoped subset). We use
166
- // hasConfiguredAuth / getProviderAuthStatus to replicate pi's available snapshot.
167
- const allProvidersSet = new Set<string>(allModelsRaw.map((m) => m.provider));
168
- const allProviders = [...allProvidersSet];
169
-
170
- const hasConfiguredAuthFn =
171
- typeof (ctx.modelRegistry as any).hasConfiguredAuth === "function"
172
- ? (ctx.modelRegistry as any).hasConfiguredAuth.bind(ctx.modelRegistry)
173
- : null;
174
- const getProviderAuthStatusFn =
175
- typeof (ctx.modelRegistry as any).getProviderAuthStatus === "function"
176
- ? (ctx.modelRegistry as any).getProviderAuthStatus.bind(ctx.modelRegistry)
177
- : null;
178
-
179
- function isProviderAuthConfigured(provider: string): boolean {
180
- try {
181
- if (getProviderAuthStatusFn) {
182
- const s: any = getProviderAuthStatusFn(provider);
183
- if (s && (s.ok === true || s.configured === true)) return true;
184
- }
185
- } catch {}
186
- try {
187
- if (hasConfiguredAuthFn) {
188
- for (const mm of rawAll) {
189
- if ((mm as any).provider === provider) {
190
- try {
191
- if (hasConfiguredAuthFn(mm)) return true;
192
- } catch {}
193
- }
194
- }
195
- }
196
- } catch {}
197
- return false;
198
- }
199
-
200
- // Available providers = those with configured auth. Fallback to all if none detected
201
- // (e.g. no auth yet, to avoid empty view).
202
- let availableProviders = allProviders.filter((p) => isProviderAuthConfigured(p));
203
- if (availableProviders.length === 0) availableProviders = allProviders;
204
-
205
- // Full list for modelconf = all models from available providers.
206
- // This is the same set that /model's main list shows BEFORE enabledModels filtering.
207
- // We intentionally do NOT filter by enabledModels here — we show the full available
208
- // catalogue so users can toggle visibility for the main list.
209
- // If runtime was patched, getAvailableSnapshot would already be filtered; we bypass that
210
- // by using getAll filtered by auth, not the patched snapshot.
211
- const allModels = allModelsRaw.filter((m) => availableProviders.includes(m.provider));
212
-
213
- // Visibility: hiddenRaw is denylist for full picker. Visible = not hidden.
214
- function isVisiblePat(modelRef: string, hidden: string[] | undefined): boolean {
215
- if (!hidden || hidden.length === 0) return true;
216
- return !modelMatchesAnyPattern(modelRef, hidden);
217
- }
218
-
219
- const visibleSet = new Set<string>();
220
- for (const m of allModels) {
221
- const ref = `${m.provider}/${m.id}`;
222
- if (isVisiblePat(ref, hiddenRaw)) visibleSet.add(ref);
223
- }
224
-
225
- await ctx.ui.custom((tui: any, theme: any, kb: any, done: any) => {
226
- const view = new ModelConfView({
227
- tui,
228
- theme,
229
- keybindings: kb,
230
- allModels: allModels as any,
231
- visibleSet,
232
- enabledRaw: hiddenRaw,
233
- allIds,
234
- onDone: (saved: boolean) => {
235
- done(saved);
236
- try {
237
- tui?.requestRender?.();
238
- } catch {}
239
- },
240
- onPersist: async (nextEnabled: string[] | undefined) => {
241
- let filtered = nextEnabled;
242
- if (filtered !== undefined) {
243
- const allSet = new Set(allIds);
244
- filtered = filtered.filter((id) => allSet.has(id));
245
- if (filtered.length === allIds.length) filtered = undefined;
246
- if (filtered !== undefined) filtered = [...filtered].sort();
247
- }
248
- await saveHiddenModels(filtered);
249
- const hiddenCount = filtered === undefined ? 0 : filtered.length;
250
- const visibleInAvailable = allModels.length - hiddenCount; // rough; hidden may be outside available but still filtered
251
- // More accurate: count hidden that are in available
252
- let hiddenInAvailable = 0;
253
- if (filtered) {
254
- hiddenInAvailable = allModels.filter((m) => modelMatchesAnyPattern(`${m.provider}/${m.id}`, filtered!)).length;
255
- }
256
- const actualVisible = allModels.length - hiddenInAvailable;
257
- ctx.ui.notify(
258
- `modelconf: saved ${hiddenCount} hidden models — ${actualVisible} visible in /model — reload to apply`,
259
- "info"
260
- );
261
- try {
262
- await (ctx as any).reload?.();
263
- } catch {}
264
- // re-patch after save so /model immediately reflects new setting without reload
265
- try {
266
- patchRegistry(ctx.modelRegistry);
267
- } catch {}
268
- },
269
- });
270
-
271
- return {
272
- render: view.render.bind(view),
273
- handleInput: view.handleInput.bind(view),
274
- invalidate: view.invalidate.bind(view),
275
- };
276
- });
277
- }
278
-
279
- pi.registerCommand("modelconf", {
280
- description:
281
- "Manage main /model list visibility per provider (fuzzy: /, toggle: space, bulk: x, save: enter)",
282
- handler: async (_args, ctx) => openModelConf(ctx),
283
- });
284
-
285
- // Also patch /model command to show filtered main list if pi allows overriding.
286
- // We register a wrapper that ensures filtering is active; we don't replace the UI,
287
- // filtering is done via runtime patch, so no need to override command.
288
- // The patch above ensures /model's ModelSelectorComponent automatically shows filtered list.
289
-
290
- try {
291
- (pi as any).registerShortcut?.("ctrl+alt+m" as any, {
292
- description: "Open modelconf",
293
- handler: async (ctx: any) => openModelConf(ctx),
294
- });
295
- } catch {}
296
- }
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { loadHiddenModels, getVisibilityMap, saveHiddenModels } from "./src/persistence.js";
3
+ import { ModelConfView } from "./src/ui/ModelConfView.js";
4
+ import fs from "node:fs";
5
+ import os from "node:os";
6
+ import path from "node:path";
7
+ import { globToRegExp } from "./src/glob.js";
8
+
9
+ // ---------- helpers for filtering main /model list ----------
10
+ function settingsPath(): string {
11
+ return path.join(os.homedir(), ".pi", "agent", "settings.json");
12
+ }
13
+
14
+ function loadHiddenModelsSync(): string[] | undefined {
15
+ try {
16
+ const raw = fs.readFileSync(settingsPath(), "utf8");
17
+ const j = JSON.parse(raw) as { hiddenModels?: string[] };
18
+ return j.hiddenModels;
19
+ } catch {
20
+ return undefined;
21
+ }
22
+ }
23
+ // Backward compat alias
24
+ function loadEnabledModelsSync(): string[] | undefined {
25
+ return loadHiddenModelsSync();
26
+ }
27
+
28
+ const VALID_THINKING = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
29
+
30
+ function stripThinkingLevel(pattern: string): string {
31
+ const idx = pattern.lastIndexOf(":");
32
+ if (idx !== -1) {
33
+ const suffix = pattern.slice(idx + 1);
34
+ if (VALID_THINKING.has(suffix)) return pattern.slice(0, idx);
35
+ }
36
+ return pattern;
37
+ }
38
+
39
+ function matchesPattern(modelRef: string, pattern: string): boolean {
40
+ const pat = stripThinkingLevel(pattern);
41
+ const lowerRef = modelRef.toLowerCase();
42
+ const lowerPat = pat.toLowerCase();
43
+ const bareId = modelRef.split("/").slice(1).join("/")?.toLowerCase() ?? lowerRef;
44
+ const hasGlob = /[*?\[\]]/.test(pat);
45
+ if (hasGlob) {
46
+ const rx = globToRegExp(pat);
47
+ // pi matches against full ref OR bare id via minimatch
48
+ if (rx.test(modelRef)) return true;
49
+ if (rx.test(bareId)) return true;
50
+ // also try lower variants (globToRegExp is already i-flag)
51
+ return false;
52
+ } else {
53
+ return lowerRef === lowerPat || bareId === lowerPat;
54
+ }
55
+ }
56
+
57
+ function modelMatchesAnyPattern(modelRef: string, patterns: string[]): boolean {
58
+ for (const p of patterns) if (matchesPattern(modelRef, p)) return true;
59
+ return false;
60
+ }
61
+
62
+ function patchRuntime(runtime: any) {
63
+ if (!runtime || runtime.__modelconfPatched) return;
64
+ const origSnapshot = runtime.getAvailableSnapshot?.bind(runtime);
65
+ const origGetAvailable = runtime.getAvailable?.bind(runtime);
66
+ const origGetModels = runtime.getModels?.bind(runtime);
67
+
68
+ if (typeof origSnapshot === "function") {
69
+ runtime.getAvailableSnapshot = function (...args: any[]) {
70
+ const base: any[] = origSnapshot(...args);
71
+ const hidden = loadHiddenModelsSync();
72
+ if (!hidden || hidden.length === 0) return base;
73
+ const filtered = base.filter((m: any) => {
74
+ const ref = `${m.provider}/${m.id}`;
75
+ // Hide if matches hidden denylist (from full list). Keep visible otherwise.
76
+ const isHidden = modelMatchesAnyPattern(ref, hidden) || modelMatchesAnyPattern(m.id ?? "", hidden);
77
+ return !isHidden;
78
+ });
79
+ return filtered;
80
+ };
81
+ // keep original for modelconf's own unfiltered view
82
+ runtime.__originalGetAvailableSnapshot = origSnapshot;
83
+ }
84
+
85
+ // also patch async getAvailable(providerId, opts) if present
86
+ if (typeof origGetAvailable === "function") {
87
+ runtime.getAvailable = async function (providerId?: string, opts?: any) {
88
+ const base: any[] = await origGetAvailable(providerId, opts);
89
+ const hidden = loadHiddenModelsSync();
90
+ if (!hidden || hidden.length === 0) return base;
91
+ // Always filter hidden from any provider query — hidden applies to full picker list
92
+ const filtered = base.filter((m: any) => {
93
+ const ref = `${m.provider}/${m.id}`;
94
+ const isHidden = modelMatchesAnyPattern(ref, hidden) || modelMatchesAnyPattern(m.id ?? "", hidden);
95
+ return !isHidden;
96
+ });
97
+ return filtered;
98
+ };
99
+ runtime.__originalGetAvailable = origGetAvailable;
100
+ }
101
+
102
+ runtime.__modelconfPatched = true;
103
+ }
104
+
105
+ function patchRegistry(registry: any) {
106
+ if (!registry) return;
107
+ // ModelRegistry.getAvailable is async and delegates to runtime; patching runtime is enough.
108
+ // Also patch registry.getAvailableSnapshot if it exists (some pi versions expose it)
109
+ if (typeof registry.getAvailableSnapshot === "function" && !registry.__modelconfPatched) {
110
+ const orig = registry.getAvailableSnapshot.bind(registry);
111
+ registry.getAvailableSnapshot = function (...args: any[]) {
112
+ const base: any[] = orig(...args);
113
+ const hidden = loadHiddenModelsSync();
114
+ if (!hidden || hidden.length === 0) return base;
115
+ const filtered = base.filter((m: any) => {
116
+ const ref = `${m.provider}/${m.id}`;
117
+ return !modelMatchesAnyPattern(ref, hidden);
118
+ });
119
+ return filtered;
120
+ };
121
+ registry.__originalGetAvailableSnapshot = orig;
122
+ registry.__modelconfPatched = true;
123
+ }
124
+ if (registry.runtime) patchRuntime(registry.runtime);
125
+ }
126
+
127
+ export default function modelconf(pi: ExtensionAPI) {
128
+ // patch on session start for every new session's runtime
129
+ try {
130
+ (pi as any).on?.("session_start", async (_ev: any, ctx: any) => {
131
+ try {
132
+ patchRegistry(ctx?.modelRegistry);
133
+ } catch {}
134
+ });
135
+ } catch {}
136
+
137
+ // also attempt to patch via global require at load time (best-effort, may be different copy)
138
+ try {
139
+ // try to find main pi runtime via ctx-less import — no-op if not found
140
+ // we rely on patchRegistry on first command invocation as fallback
141
+ } catch {}
142
+
143
+ async function openModelConf(ctx: any) {
144
+ if (ctx.mode !== "tui") {
145
+ ctx.ui.notify("/modelconf requires TUI mode", "error");
146
+ return;
147
+ }
148
+
149
+ // Ensure main list filtering is active for this session
150
+ try {
151
+ patchRegistry(ctx.modelRegistry);
152
+ } catch {}
153
+
154
+ const rawAll = ctx.modelRegistry.getAll() as any[];
155
+ const allModelsRaw = rawAll.map((m) => ({
156
+ provider: (m as any).provider ?? "unknown",
157
+ id: (m as any).id ?? "",
158
+ name: (m as any).name ?? (m as any).id ?? "",
159
+ }));
160
+
161
+ const hiddenRaw = await loadHiddenModels();
162
+ const allIds = allModelsRaw.map((m) => `${m.provider}/${m.id}`);
163
+
164
+ // ---- Determine available models (main /model list) as auth-filtered set ----
165
+ // This is the FULL list that /model shows (not the scoped subset). We use
166
+ // hasConfiguredAuth / getProviderAuthStatus to replicate pi's available snapshot.
167
+ const allProvidersSet = new Set<string>(allModelsRaw.map((m) => m.provider));
168
+ const allProviders = [...allProvidersSet];
169
+
170
+ const hasConfiguredAuthFn =
171
+ typeof (ctx.modelRegistry as any).hasConfiguredAuth === "function"
172
+ ? (ctx.modelRegistry as any).hasConfiguredAuth.bind(ctx.modelRegistry)
173
+ : null;
174
+ const getProviderAuthStatusFn =
175
+ typeof (ctx.modelRegistry as any).getProviderAuthStatus === "function"
176
+ ? (ctx.modelRegistry as any).getProviderAuthStatus.bind(ctx.modelRegistry)
177
+ : null;
178
+
179
+ function isProviderAuthConfigured(provider: string): boolean {
180
+ try {
181
+ if (getProviderAuthStatusFn) {
182
+ const s: any = getProviderAuthStatusFn(provider);
183
+ if (s && (s.ok === true || s.configured === true)) return true;
184
+ }
185
+ } catch {}
186
+ try {
187
+ if (hasConfiguredAuthFn) {
188
+ for (const mm of rawAll) {
189
+ if ((mm as any).provider === provider) {
190
+ try {
191
+ if (hasConfiguredAuthFn(mm)) return true;
192
+ } catch {}
193
+ }
194
+ }
195
+ }
196
+ } catch {}
197
+ return false;
198
+ }
199
+
200
+ // Available providers = those with configured auth. Fallback to all if none detected
201
+ // (e.g. no auth yet, to avoid empty view).
202
+ let availableProviders = allProviders.filter((p) => isProviderAuthConfigured(p));
203
+ if (availableProviders.length === 0) availableProviders = allProviders;
204
+
205
+ // Full list for modelconf = all models from available providers.
206
+ // This is the same set that /model's main list shows BEFORE enabledModels filtering.
207
+ // We intentionally do NOT filter by enabledModels here — we show the full available
208
+ // catalogue so users can toggle visibility for the main list.
209
+ // If runtime was patched, getAvailableSnapshot would already be filtered; we bypass that
210
+ // by using getAll filtered by auth, not the patched snapshot.
211
+ const allModels = allModelsRaw.filter((m) => availableProviders.includes(m.provider));
212
+
213
+ // Visibility: hiddenRaw is denylist for full picker. Visible = not hidden.
214
+ function isVisiblePat(modelRef: string, hidden: string[] | undefined): boolean {
215
+ if (!hidden || hidden.length === 0) return true;
216
+ return !modelMatchesAnyPattern(modelRef, hidden);
217
+ }
218
+
219
+ const visibleSet = new Set<string>();
220
+ for (const m of allModels) {
221
+ const ref = `${m.provider}/${m.id}`;
222
+ if (isVisiblePat(ref, hiddenRaw)) visibleSet.add(ref);
223
+ }
224
+
225
+ await ctx.ui.custom((tui: any, theme: any, kb: any, done: any) => {
226
+ const view = new ModelConfView({
227
+ tui,
228
+ theme,
229
+ keybindings: kb,
230
+ allModels: allModels as any,
231
+ visibleSet,
232
+ enabledRaw: hiddenRaw,
233
+ allIds,
234
+ onDone: (saved: boolean) => {
235
+ done(saved);
236
+ try {
237
+ tui?.requestRender?.();
238
+ } catch {}
239
+ },
240
+ onPersist: async (nextEnabled: string[] | undefined) => {
241
+ let filtered = nextEnabled;
242
+ if (filtered !== undefined) {
243
+ const allSet = new Set(allIds);
244
+ filtered = filtered.filter((id) => allSet.has(id));
245
+ if (filtered.length === allIds.length) filtered = undefined;
246
+ if (filtered !== undefined) filtered = [...filtered].sort();
247
+ }
248
+ await saveHiddenModels(filtered);
249
+ const hiddenCount = filtered === undefined ? 0 : filtered.length;
250
+ const visibleInAvailable = allModels.length - hiddenCount; // rough; hidden may be outside available but still filtered
251
+ // More accurate: count hidden that are in available
252
+ let hiddenInAvailable = 0;
253
+ if (filtered) {
254
+ hiddenInAvailable = allModels.filter((m) => modelMatchesAnyPattern(`${m.provider}/${m.id}`, filtered!)).length;
255
+ }
256
+ const actualVisible = allModels.length - hiddenInAvailable;
257
+ ctx.ui.notify(
258
+ `modelconf: saved ${hiddenCount} hidden models — ${actualVisible} visible in /model — reload to apply`,
259
+ "info"
260
+ );
261
+ try {
262
+ await (ctx as any).reload?.();
263
+ } catch {}
264
+ // re-patch after save so /model immediately reflects new setting without reload
265
+ try {
266
+ patchRegistry(ctx.modelRegistry);
267
+ } catch {}
268
+ },
269
+ });
270
+
271
+ return {
272
+ render: view.render.bind(view),
273
+ handleInput: view.handleInput.bind(view),
274
+ invalidate: view.invalidate.bind(view),
275
+ };
276
+ });
277
+ }
278
+
279
+ pi.registerCommand("modelconf", {
280
+ description:
281
+ "Manage main /model list visibility per provider (fuzzy: /, toggle: space, bulk: x, save: enter)",
282
+ handler: async (_args, ctx) => openModelConf(ctx),
283
+ });
284
+
285
+ // Also patch /model command to show filtered main list if pi allows overriding.
286
+ // We register a wrapper that ensures filtering is active; we don't replace the UI,
287
+ // filtering is done via runtime patch, so no need to override command.
288
+ // The patch above ensures /model's ModelSelectorComponent automatically shows filtered list.
289
+
290
+ try {
291
+ (pi as any).registerShortcut?.("ctrl+alt+m" as any, {
292
+ description: "Open modelconf",
293
+ handler: async (ctx: any) => openModelConf(ctx),
294
+ });
295
+ } catch {}
296
+ }