@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,443 +0,0 @@
1
- /**
2
- * pi-taste — local "taste" learning, no npx / no network.
3
- *
4
- * Mirrors Command Code's Taste but entirely on-disk:
5
- * - Learns preferences from each completed request via the active local model.
6
- * - Stores them as markdown at two scopes:
7
- * Global : ~/.pi/agent/extensions/taste/taste.md (applies to ALL projects)
8
- * Project: <cwd>/.pi/taste/taste.md (this codebase only)
9
- * - Feeds high-confidence learnings back into the system prompt each turn.
10
- *
11
- * Confidence system: new learning = newConfidence (0.40); a repeat (same
12
- * category + normalized text) raises confidence by repeatIncrement (0.10),
13
- * capped at 0.95. Only learnings >= applyThreshold (0.60) are injected.
14
- */
15
-
16
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
17
- import { CONFIG_DIR_NAME } from "@earendil-works/pi-coding-agent";
18
- import { uuidv7 } from "@earendil-works/pi-ai";
19
- import * as fs from "node:fs";
20
- import * as path from "node:path";
21
- import * as os from "node:os";
22
-
23
- // ---- Paths -------------------------------------------------------------
24
- // Global taste lives INSIDE this extension's folder (per user request).
25
- const EXT_DIR = path.join(os.homedir(), ".pi", "agent", "extensions", "taste");
26
- const GLOBAL_TASTE = path.join(EXT_DIR, "taste.md");
27
- const GLOBAL_SETTINGS = path.join(EXT_DIR, "settings.json");
28
-
29
- const projectTasteFile = (cwd: string) =>
30
- path.join(cwd, CONFIG_DIR_NAME, "taste", "taste.md");
31
- const projectSettingsFile = (cwd: string) =>
32
- path.join(cwd, CONFIG_DIR_NAME, "taste", "settings.json");
33
-
34
- // ---- Settings ----------------------------------------------------------
35
- interface Settings {
36
- learning: boolean;
37
- applyThreshold: number;
38
- newConfidence: number;
39
- repeatIncrement: number;
40
- minPromptLength: number;
41
- minIntervalMs: number;
42
- }
43
-
44
- const DEFAULTS: Settings = {
45
- learning: true,
46
- applyThreshold: 0.6,
47
- newConfidence: 0.4,
48
- repeatIncrement: 0.1,
49
- minPromptLength: 20,
50
- minIntervalMs: 15000,
51
- };
52
-
53
- function loadSettings(file: string): Settings {
54
- try {
55
- if (fs.existsSync(file)) {
56
- return { ...DEFAULTS, ...JSON.parse(fs.readFileSync(file, "utf8")) };
57
- }
58
- } catch {
59
- /* corrupt settings -> defaults */
60
- }
61
- return { ...DEFAULTS };
62
- }
63
-
64
- // ---- Taste data model --------------------------------------------------
65
- interface Item {
66
- text: string;
67
- confidence: number;
68
- }
69
- type Taste = Map<string, Item[]>;
70
-
71
- const normalize = (s: string) =>
72
- s.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
73
-
74
- function parseTaste(text: string): Taste {
75
- const taste: Taste = new Map();
76
- let category = "Other";
77
- for (const line of text.split(/\r?\n/)) {
78
- const h = line.match(/^#\s+(.+?)\s*$/);
79
- if (h) {
80
- category = h[1].trim() || "Other";
81
- if (!taste.has(category)) taste.set(category, []);
82
- continue;
83
- }
84
- const b = line.match(
85
- /^-\s+(.*?)\s*\.\s*Confidence:\s*([0-9]+(?:\.[0-9]+)?)\s*$/i,
86
- );
87
- if (b) {
88
- const items = taste.get(category) ?? [];
89
- items.push({ text: b[1].trim(), confidence: parseFloat(b[2]) });
90
- taste.set(category, items);
91
- }
92
- }
93
- return taste;
94
- }
95
-
96
- function serializeTaste(taste: Taste): string {
97
- const lines: string[] = [];
98
- for (const [category, items] of taste) {
99
- if (items.length === 0) continue;
100
- lines.push(`# ${category}`);
101
- for (const it of [...items].sort((a, b) => b.confidence - a.confidence)) {
102
- lines.push(`- ${it.text}. Confidence: ${it.confidence.toFixed(2)}`);
103
- }
104
- lines.push("");
105
- }
106
- return lines.join("\n").trimEnd() + "\n";
107
- }
108
-
109
- /** Merge one learning; returns true if the file should be rewritten. */
110
- function mergeLearning(
111
- taste: Taste,
112
- category: string,
113
- preference: string,
114
- s: Settings,
115
- ): boolean {
116
- const cat = category.trim() || "Other";
117
- const norm = normalize(preference);
118
- if (!norm) return false;
119
-
120
- const items = taste.get(cat) ?? [];
121
- const existing = items.find((i) => normalize(i.text) === norm);
122
- if (existing) {
123
- const before = existing.confidence;
124
- existing.confidence = Math.min(
125
- 0.95,
126
- +(existing.confidence + s.repeatIncrement).toFixed(2),
127
- );
128
- return existing.confidence !== before;
129
- }
130
- items.push({ text: preference.trim(), confidence: s.newConfidence });
131
- taste.set(cat, items);
132
- return true;
133
- }
134
-
135
- function readTaste(file: string): Taste {
136
- try {
137
- if (fs.existsSync(file)) return parseTaste(fs.readFileSync(file, "utf8"));
138
- } catch {
139
- /* unreadable -> empty */
140
- }
141
- return new Map();
142
- }
143
-
144
- function writeTaste(file: string, taste: Taste): void {
145
- fs.mkdirSync(path.dirname(file), { recursive: true });
146
- fs.writeFileSync(file, serializeTaste(taste), "utf8");
147
- }
148
-
149
- function safeDelete(file: string): void {
150
- try {
151
- if (fs.existsSync(file)) fs.unlinkSync(file);
152
- } catch {
153
- /* ignore */
154
- }
155
- }
156
-
157
- // ---- Apply: build the system-prompt section ----------------------------
158
- function tasteToLines(taste: Taste, threshold: number): string[] {
159
- const out: string[] = [];
160
- for (const [cat, items] of taste) {
161
- const applied = items.filter((i) => i.confidence >= threshold);
162
- if (applied.length === 0) continue;
163
- out.push(`**${cat}**`);
164
- for (const it of [...applied].sort((a, b) => b.confidence - a.confidence)) {
165
- out.push(`- ${it.text}. (conf ${it.confidence.toFixed(2)})`);
166
- }
167
- }
168
- return out;
169
- }
170
-
171
- function buildTasteSection(
172
- gTaste: Taste,
173
- pTaste: Taste,
174
- gThresh: number,
175
- pThresh: number,
176
- ): string {
177
- const gLines = tasteToLines(gTaste, gThresh);
178
- const pLines = tasteToLines(pTaste, pThresh);
179
- if (gLines.length === 0 && pLines.length === 0) return "";
180
-
181
- const parts = [
182
- "## Your learned taste (pi-taste)",
183
- "",
184
- "Preferences learned from your past requests. Follow them unless the current request clearly overrides.",
185
- "",
186
- ];
187
- if (gLines.length) {
188
- parts.push("### Global preferences (all projects)");
189
- parts.push(...gLines, "");
190
- }
191
- if (pLines.length) {
192
- parts.push("### This project");
193
- parts.push(...pLines, "");
194
- }
195
- return parts.join("\n").trimEnd();
196
- }
197
-
198
- // ---- Learn: extract preferences via the active local model ------------
199
- interface RawLearning {
200
- category?: string;
201
- preference?: string;
202
- scope?: "global" | "project";
203
- }
204
-
205
- const EXTRACT_PROMPT = `You are a coding-preference extractor. Given a user's coding request and the assistant's outcome, extract DURABLE preferences about how this user likes to work.
206
-
207
- Rules:
208
- - Extract only preferences clearly evidenced by the request or the resulting choices: tooling, libraries, language style, architecture, testing, git/commit style, documentation, or communication/format.
209
- - Do NOT extract task-specific facts (file names, bug locations, one-off decisions).
210
- - For each, set scope:
211
- - "global" if it applies to ANY software project regardless of language/stack (e.g. "prefer pnpm", "write concise commit messages").
212
- - "project" if it is specific to THIS codebase (its languages, frameworks, conventions, file layout).
213
- - Choose a short category from: CLI, TypeScript, JavaScript, Python, Rust, Go, Architecture, Testing, Git, Documentation, Style, Tooling, Communication, Other.
214
-
215
- Return ONLY minified JSON: an array of {"category":string,"preference":string,"scope":"global"|"project"}. No markdown fences, no commentary. If nothing durable, return [].`;
216
-
217
- function parseLearnings(text: string): RawLearning[] {
218
- let t = text.trim();
219
- const fence = t.match(/```(?:json)?\s*([\s\S]*?)```/i);
220
- if (fence) t = fence[1].trim();
221
- try {
222
- const parsed = JSON.parse(t);
223
- if (!Array.isArray(parsed)) return [];
224
- return parsed
225
- .filter(
226
- (x) =>
227
- x &&
228
- typeof x.preference === "string" &&
229
- x.preference.trim().length > 0,
230
- )
231
- .map((x) => ({
232
- category: typeof x.category === "string" ? x.category : "Other",
233
- preference: x.preference,
234
- scope: x.scope === "global" ? "global" : "project",
235
- }));
236
- } catch {
237
- return [];
238
- }
239
- }
240
-
241
- function lastAssistantText(ctx: ExtensionContext): string {
242
- const entries = ctx.sessionManager.getEntries();
243
- for (let i = entries.length - 1; i >= 0; i--) {
244
- const e = entries[i];
245
- if (e.type === "message" && e.message?.role === "assistant") {
246
- const c = e.message.content;
247
- if (typeof c === "string") return c;
248
- if (Array.isArray(c)) {
249
- return c
250
- .filter((p: any) => p?.type === "text")
251
- .map((p: any) => p.text)
252
- .join("\n");
253
- }
254
- return "";
255
- }
256
- }
257
- return "";
258
- }
259
-
260
- async function extractLearnings(
261
- ctx: ExtensionContext,
262
- userPrompt: string,
263
- assistantText: string,
264
- ): Promise<RawLearning[]> {
265
- if (!ctx.model) return [];
266
- if (!ctx.modelRegistry.hasConfiguredAuth(ctx.model)) return [];
267
-
268
- const context = {
269
- messages: [
270
- {
271
- role: "user" as const,
272
- content: [
273
- {
274
- type: "text" as const,
275
- text:
276
- EXTRACT_PROMPT +
277
- `\n\nUSER REQUEST:\n${userPrompt}\n\nASSISTANT OUTCOME:\n${assistantText}`,
278
- },
279
- ],
280
- timestamp: Date.now(),
281
- },
282
- ],
283
- } as any;
284
-
285
- const res = await ctx.modelRegistry.complete(ctx.model, context, {
286
- reasoningEffort: "low",
287
- cacheRetention: "none",
288
- sessionId: uuidv7(),
289
- });
290
-
291
- const text = res.content
292
- .filter((c: any) => c.type === "text")
293
- .map((c: any) => c.text)
294
- .join("");
295
- return parseLearnings(text);
296
- }
297
-
298
- // ---- Wiring ------------------------------------------------------------
299
- export default function (pi: ExtensionAPI) {
300
- let lastUserPrompt = "";
301
- let lastLearnAt = 0;
302
- let pendingLearn: Promise<void> | null = null;
303
- // True only between a genuine user `input` and that turn's `agent_settled`.
304
- // Synthetic re-runs (auto-compaction, retries) emit no `input`, so this
305
- // stays false and we never learn preferences from a system-generated prompt.
306
- let freshInput = false;
307
-
308
- // A genuine user submission arms learning. Extension-injected messages
309
- // (source "extension") are excluded so we don't learn the taste of our own
310
- // meta-prompts (e.g. review-subagent instructions).
311
- pi.on("input", (event) => {
312
- if (event.source !== "extension") freshInput = true;
313
- });
314
-
315
- // Apply: inject learned taste before each agent run.
316
- pi.on("before_agent_start", (event, ctx) => {
317
- if (freshInput) lastUserPrompt = event.prompt ?? "";
318
-
319
-
320
- const gSettings = loadSettings(GLOBAL_SETTINGS);
321
- const pSettings = loadSettings(projectSettingsFile(ctx.cwd));
322
- const gTaste = readTaste(GLOBAL_TASTE);
323
- const pTaste = readTaste(projectTasteFile(ctx.cwd));
324
-
325
- const section = buildTasteSection(
326
- gTaste,
327
- pTaste,
328
- gSettings.applyThreshold,
329
- pSettings.applyThreshold,
330
- );
331
- if (!section) return;
332
- return { systemPrompt: event.systemPrompt + "\n\n" + section };
333
- });
334
-
335
- // Learn: after a request fully settles (no retry/compact/follow-up left).
336
- pi.on("agent_settled", async (_event, ctx) => {
337
- const gSettings = loadSettings(GLOBAL_SETTINGS);
338
- const pSettings = loadSettings(projectSettingsFile(ctx.cwd));
339
- if (!gSettings.learning && !pSettings.learning) return;
340
- if (!freshInput) return; // synthetic re-run (e.g. auto-compaction) — skip
341
- freshInput = false; // this genuine turn is now being consumed
342
-
343
- const prompt = lastUserPrompt.trim();
344
- if (!prompt || prompt.length < gSettings.minPromptLength) return;
345
- if (prompt.startsWith("/taste")) return; // don't learn from our own command
346
-
347
- // Rate-limit window follows whichever scope is actually learning.
348
- const interval = gSettings.learning
349
- ? gSettings.minIntervalMs
350
- : pSettings.minIntervalMs;
351
- if (Date.now() - lastLearnAt < interval) return;
352
-
353
- const assistantText = lastAssistantText(ctx);
354
- if (!assistantText) return;
355
-
356
- const learnings = await extractLearnings(ctx, prompt, assistantText);
357
- if (learnings.length === 0) return;
358
-
359
- // Only consume the cooldown once we actually learned something.
360
- lastLearnAt = Date.now();
361
-
362
- const task = (async () => {
363
- let wrote = false;
364
- for (const l of learnings) {
365
- const isGlobal = l.scope === "global";
366
- const settings = isGlobal ? gSettings : pSettings;
367
- if (!settings.learning) continue;
368
- const file = isGlobal ? GLOBAL_TASTE : projectTasteFile(ctx.cwd);
369
- const taste = readTaste(file);
370
- if (mergeLearning(taste, l.category ?? "Other", l.preference ?? "", settings)) {
371
- writeTaste(file, taste);
372
- wrote = true;
373
- }
374
- }
375
- if (wrote && ctx.hasUI) {
376
- ctx.ui.notify(
377
- `TASTE learned ${learnings.length} preference(s)`,
378
- "info",
379
- );
380
- }
381
- })();
382
- pendingLearn = task;
383
- await task;
384
- });
385
-
386
- // Flush any in-flight learning so it isn't lost on exit.
387
- pi.on("session_shutdown", async () => {
388
- if (pendingLearn) {
389
- try {
390
- await pendingLearn;
391
- } catch {
392
- /* ignore */
393
- }
394
- }
395
- });
396
-
397
- // /taste — view or reset learnings.
398
- pi.registerCommand("taste", {
399
- description:
400
- "Show or reset learned taste. Usage: /taste [reset [global|project]]",
401
- handler: async (args, ctx) => {
402
- const parts = args.trim().split(/\s+/).filter(Boolean);
403
- if (parts[0] === "reset") {
404
- const scope = parts[1];
405
- if (scope === "global") {
406
- safeDelete(GLOBAL_TASTE);
407
- ctx.ui.notify("Cleared global taste.", "info");
408
- } else if (scope === "project") {
409
- safeDelete(projectTasteFile(ctx.cwd));
410
- ctx.ui.notify("Cleared project taste.", "info");
411
- } else if (!scope) {
412
- safeDelete(GLOBAL_TASTE);
413
- safeDelete(projectTasteFile(ctx.cwd));
414
- ctx.ui.notify("Cleared global + project taste.", "info");
415
- } else {
416
- ctx.ui.notify(
417
- `Unknown scope '${scope}'. Use 'global' or 'project'.`,
418
- "warning",
419
- );
420
- }
421
- return;
422
- }
423
-
424
- const g = readTaste(GLOBAL_TASTE);
425
- const p = readTaste(projectTasteFile(ctx.cwd));
426
- const gSettings = loadSettings(GLOBAL_SETTINGS);
427
- const pSettings = loadSettings(projectSettingsFile(ctx.cwd));
428
- const count = (t: Taste) =>
429
- [...t.values()].reduce((n, a) => n + a.length, 0);
430
- const applied = (t: Taste, th: number) =>
431
- [...t.values()].flat().filter((i) => i.confidence >= th).length;
432
-
433
- ctx.ui.notify(
434
- [
435
- `TASTE — global : ${count(g)} prefs (${applied(g, gSettings.applyThreshold)} applied) → ${GLOBAL_TASTE}`,
436
- `TASTE — project: ${count(p)} prefs (${applied(p, pSettings.applyThreshold)} applied) → ${projectTasteFile(ctx.cwd)}`,
437
- `Run '/taste reset [global|project]' to clear.`,
438
- ].join("\n"),
439
- "info",
440
- );
441
- },
442
- });
443
- }
@@ -1,23 +0,0 @@
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
@@ -1,21 +0,0 @@
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"