@wrongstack/tools 0.293.0 → 0.295.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 (248) hide show
  1. package/dist/_env.d.ts +1 -1
  2. package/dist/_env.d.ts.map +1 -1
  3. package/dist/_fetch-guard.d.ts.map +1 -1
  4. package/dist/_spawn-stream.d.ts +1 -1
  5. package/dist/_spawn-stream.d.ts.map +1 -1
  6. package/dist/_util.d.ts +1 -1
  7. package/dist/_util.d.ts.map +1 -1
  8. package/dist/audit.d.ts +1 -1
  9. package/dist/audit.d.ts.map +1 -1
  10. package/dist/audit.js +81 -16
  11. package/dist/audit.js.map +2 -2
  12. package/dist/bash.d.ts +1 -1
  13. package/dist/bash.d.ts.map +1 -1
  14. package/dist/bash.js +66 -52
  15. package/dist/bash.js.map +3 -3
  16. package/dist/batch-tool-use.d.ts +1 -1
  17. package/dist/batch-tool-use.d.ts.map +1 -1
  18. package/dist/batch-tool-use.js +1 -1
  19. package/dist/batch-tool-use.js.map +1 -1
  20. package/dist/browser/index.js +10 -1
  21. package/dist/browser/index.js.map +2 -2
  22. package/dist/browser/manager.d.ts +2 -0
  23. package/dist/browser/manager.d.ts.map +1 -1
  24. package/dist/browser/tools.d.ts +1 -1
  25. package/dist/browser/tools.d.ts.map +1 -1
  26. package/dist/builtin.d.ts +1 -1
  27. package/dist/builtin.d.ts.map +1 -1
  28. package/dist/builtin.js +1394 -553
  29. package/dist/builtin.js.map +4 -4
  30. package/dist/codebase-index/background-indexer.d.ts +2 -2
  31. package/dist/codebase-index/background-indexer.d.ts.map +1 -1
  32. package/dist/codebase-index/bm25.d.ts +3 -1
  33. package/dist/codebase-index/bm25.d.ts.map +1 -1
  34. package/dist/codebase-index/codebase-index-tool.d.ts +1 -1
  35. package/dist/codebase-index/codebase-index-tool.d.ts.map +1 -1
  36. package/dist/codebase-index/codebase-search-tool.d.ts +1 -1
  37. package/dist/codebase-index/codebase-search-tool.d.ts.map +1 -1
  38. package/dist/codebase-index/codebase-stats-tool.d.ts +1 -1
  39. package/dist/codebase-index/codebase-stats-tool.d.ts.map +1 -1
  40. package/dist/codebase-index/generic-parser.d.ts +34 -0
  41. package/dist/codebase-index/generic-parser.d.ts.map +1 -0
  42. package/dist/codebase-index/go-parser.d.ts +1 -1
  43. package/dist/codebase-index/go-parser.d.ts.map +1 -1
  44. package/dist/codebase-index/index.d.ts +4 -2
  45. package/dist/codebase-index/index.d.ts.map +1 -1
  46. package/dist/codebase-index/index.js +1036 -341
  47. package/dist/codebase-index/index.js.map +4 -4
  48. package/dist/codebase-index/indexer.d.ts +6 -1
  49. package/dist/codebase-index/indexer.d.ts.map +1 -1
  50. package/dist/codebase-index/json-parser.d.ts +1 -1
  51. package/dist/codebase-index/languages.d.ts +25 -0
  52. package/dist/codebase-index/languages.d.ts.map +1 -0
  53. package/dist/codebase-index/py-parser.d.ts +9 -1
  54. package/dist/codebase-index/py-parser.d.ts.map +1 -1
  55. package/dist/codebase-index/refs-extractor.d.ts.map +1 -1
  56. package/dist/codebase-index/rs-parser.d.ts +1 -1
  57. package/dist/codebase-index/rs-parser.d.ts.map +1 -1
  58. package/dist/codebase-index/schema.d.ts +9 -2
  59. package/dist/codebase-index/schema.d.ts.map +1 -1
  60. package/dist/codebase-index/spawn-gate.d.ts +15 -0
  61. package/dist/codebase-index/spawn-gate.d.ts.map +1 -0
  62. package/dist/codebase-index/ts-parser.d.ts +2 -2
  63. package/dist/codebase-index/ts-parser.d.ts.map +1 -1
  64. package/dist/codebase-index/worker.js +961 -317
  65. package/dist/codebase-index/worker.js.map +4 -4
  66. package/dist/codebase-index/writer.d.ts +83 -4
  67. package/dist/codebase-index/writer.d.ts.map +1 -1
  68. package/dist/codebase-index/yaml-parser.d.ts +1 -1
  69. package/dist/design.d.ts +5 -2
  70. package/dist/design.d.ts.map +1 -1
  71. package/dist/design.js +36 -3
  72. package/dist/design.js.map +2 -2
  73. package/dist/diff.d.ts +1 -1
  74. package/dist/diff.d.ts.map +1 -1
  75. package/dist/diff.js +2 -2
  76. package/dist/diff.js.map +1 -1
  77. package/dist/document.d.ts +1 -1
  78. package/dist/document.d.ts.map +1 -1
  79. package/dist/document.js +1 -1
  80. package/dist/document.js.map +1 -1
  81. package/dist/e2e.d.ts +1 -1
  82. package/dist/e2e.d.ts.map +1 -1
  83. package/dist/e2e.js +1 -1
  84. package/dist/e2e.js.map +1 -1
  85. package/dist/edit.d.ts +1 -1
  86. package/dist/edit.d.ts.map +1 -1
  87. package/dist/edit.js +3 -3
  88. package/dist/edit.js.map +2 -2
  89. package/dist/exec.d.ts +1 -1
  90. package/dist/exec.d.ts.map +1 -1
  91. package/dist/exec.js +60 -21
  92. package/dist/exec.js.map +3 -3
  93. package/dist/fetch.d.ts +1 -1
  94. package/dist/fetch.d.ts.map +1 -1
  95. package/dist/fetch.js +4 -5
  96. package/dist/fetch.js.map +2 -2
  97. package/dist/format.d.ts +1 -1
  98. package/dist/format.d.ts.map +1 -1
  99. package/dist/format.js +81 -16
  100. package/dist/format.js.map +2 -2
  101. package/dist/git.d.ts +1 -1
  102. package/dist/git.d.ts.map +1 -1
  103. package/dist/git.js +3 -2
  104. package/dist/git.js.map +2 -2
  105. package/dist/glob.d.ts +1 -1
  106. package/dist/glob.d.ts.map +1 -1
  107. package/dist/glob.js +2 -2
  108. package/dist/glob.js.map +1 -1
  109. package/dist/grep.d.ts +1 -1
  110. package/dist/grep.d.ts.map +1 -1
  111. package/dist/grep.js +4 -3
  112. package/dist/grep.js.map +2 -2
  113. package/dist/index.d.ts +4 -4
  114. package/dist/index.d.ts.map +1 -1
  115. package/dist/index.js +1657 -705
  116. package/dist/index.js.map +4 -4
  117. package/dist/install.d.ts +1 -1
  118. package/dist/install.d.ts.map +1 -1
  119. package/dist/install.js +85 -17
  120. package/dist/install.js.map +2 -2
  121. package/dist/json.d.ts +1 -1
  122. package/dist/json.d.ts.map +1 -1
  123. package/dist/json.js +2 -2
  124. package/dist/json.js.map +2 -2
  125. package/dist/kanban.d.ts +4 -3
  126. package/dist/kanban.d.ts.map +1 -1
  127. package/dist/kanban.js +94 -27
  128. package/dist/kanban.js.map +2 -2
  129. package/dist/languages/execute-tool.d.ts +1 -1
  130. package/dist/languages/execute-tool.d.ts.map +1 -1
  131. package/dist/languages/execute.d.ts +1 -1
  132. package/dist/languages/execute.d.ts.map +1 -1
  133. package/dist/languages/index.d.ts +1 -1
  134. package/dist/languages/index.d.ts.map +1 -1
  135. package/dist/languages/index.js +99 -17
  136. package/dist/languages/index.js.map +2 -2
  137. package/dist/languages/package-tool.d.ts +3 -4
  138. package/dist/languages/package-tool.d.ts.map +1 -1
  139. package/dist/languages/plan.d.ts.map +1 -1
  140. package/dist/languages/profiles/additional.d.ts.map +1 -1
  141. package/dist/languages/profiles/primary.d.ts.map +1 -1
  142. package/dist/languages/tool.d.ts +1 -1
  143. package/dist/languages/tool.d.ts.map +1 -1
  144. package/dist/lint.d.ts +1 -1
  145. package/dist/lint.d.ts.map +1 -1
  146. package/dist/lint.js +81 -16
  147. package/dist/lint.js.map +2 -2
  148. package/dist/logs.d.ts +1 -1
  149. package/dist/logs.d.ts.map +1 -1
  150. package/dist/logs.js +2 -2
  151. package/dist/logs.js.map +1 -1
  152. package/dist/memory.d.ts +1 -1
  153. package/dist/memory.d.ts.map +1 -1
  154. package/dist/memory.js +1 -1
  155. package/dist/memory.js.map +1 -1
  156. package/dist/mode.d.ts +1 -1
  157. package/dist/mode.d.ts.map +1 -1
  158. package/dist/mode.js.map +1 -1
  159. package/dist/outdated.d.ts +1 -1
  160. package/dist/outdated.d.ts.map +1 -1
  161. package/dist/outdated.js +101 -19
  162. package/dist/outdated.js.map +2 -2
  163. package/dist/pack.d.ts +1 -1
  164. package/dist/pack.js +1394 -553
  165. package/dist/pack.js.map +4 -4
  166. package/dist/patch.d.ts +1 -1
  167. package/dist/patch.d.ts.map +1 -1
  168. package/dist/patch.js +2 -2
  169. package/dist/patch.js.map +1 -1
  170. package/dist/plan.d.ts +1 -1
  171. package/dist/plan.d.ts.map +1 -1
  172. package/dist/plan.js +7 -7
  173. package/dist/plan.js.map +2 -2
  174. package/dist/process-guardian.d.ts +7 -0
  175. package/dist/process-guardian.d.ts.map +1 -1
  176. package/dist/process-registry-persistent.d.ts +5 -0
  177. package/dist/process-registry-persistent.d.ts.map +1 -1
  178. package/dist/process-registry.d.ts +9 -1
  179. package/dist/process-registry.d.ts.map +1 -1
  180. package/dist/process-registry.js +18 -3
  181. package/dist/process-registry.js.map +2 -2
  182. package/dist/ps-slash.js +54 -15
  183. package/dist/ps-slash.js.map +2 -2
  184. package/dist/read.d.ts +1 -1
  185. package/dist/read.d.ts.map +1 -1
  186. package/dist/read.js +3 -2
  187. package/dist/read.js.map +2 -2
  188. package/dist/replace.d.ts +1 -1
  189. package/dist/replace.d.ts.map +1 -1
  190. package/dist/replace.js +4 -4
  191. package/dist/replace.js.map +2 -2
  192. package/dist/scaffold.d.ts +1 -1
  193. package/dist/scaffold.d.ts.map +1 -1
  194. package/dist/scaffold.js +2 -2
  195. package/dist/scaffold.js.map +1 -1
  196. package/dist/search.d.ts +1 -1
  197. package/dist/search.d.ts.map +1 -1
  198. package/dist/search.js +4 -4
  199. package/dist/search.js.map +2 -2
  200. package/dist/session-kanban.d.ts +4 -1
  201. package/dist/session-kanban.d.ts.map +1 -1
  202. package/dist/session-kanban.js +23 -15
  203. package/dist/session-kanban.js.map +2 -2
  204. package/dist/set-working-dir.d.ts +1 -1
  205. package/dist/set-working-dir.d.ts.map +1 -1
  206. package/dist/skill.d.ts +1 -1
  207. package/dist/skill.d.ts.map +1 -1
  208. package/dist/skill.js +2 -5
  209. package/dist/skill.js.map +2 -2
  210. package/dist/spawn-background.d.ts.map +1 -1
  211. package/dist/task.d.ts +3 -2
  212. package/dist/task.d.ts.map +1 -1
  213. package/dist/task.js +6 -8
  214. package/dist/task.js.map +2 -2
  215. package/dist/test.d.ts +1 -1
  216. package/dist/test.d.ts.map +1 -1
  217. package/dist/test.js +81 -16
  218. package/dist/test.js.map +2 -2
  219. package/dist/todo.d.ts +2 -1
  220. package/dist/todo.d.ts.map +1 -1
  221. package/dist/todo.js +4 -4
  222. package/dist/todo.js.map +2 -2
  223. package/dist/tool-help.d.ts +1 -1
  224. package/dist/tool-help.d.ts.map +1 -1
  225. package/dist/tool-help.js.map +1 -1
  226. package/dist/tool-search.d.ts +1 -1
  227. package/dist/tool-search.d.ts.map +1 -1
  228. package/dist/tool-search.js.map +1 -1
  229. package/dist/tool-tier.d.ts +16 -0
  230. package/dist/tool-tier.d.ts.map +1 -0
  231. package/dist/tool-tier.js +22233 -0
  232. package/dist/tool-tier.js.map +7 -0
  233. package/dist/tool-use.d.ts +1 -1
  234. package/dist/tool-use.d.ts.map +1 -1
  235. package/dist/tool-use.js.map +1 -1
  236. package/dist/tree.d.ts +1 -1
  237. package/dist/tree.d.ts.map +1 -1
  238. package/dist/tree.js +2 -2
  239. package/dist/tree.js.map +1 -1
  240. package/dist/typecheck.d.ts +1 -1
  241. package/dist/typecheck.d.ts.map +1 -1
  242. package/dist/typecheck.js +81 -16
  243. package/dist/typecheck.js.map +2 -2
  244. package/dist/write.d.ts +1 -1
  245. package/dist/write.d.ts.map +1 -1
  246. package/dist/write.js +3 -2
  247. package/dist/write.js.map +2 -2
  248. package/package.json +7 -3
@@ -66,6 +66,23 @@ export declare class IndexStore {
66
66
  * and reuse it. Cleared in {@link close} when the connection is torn down.
67
67
  */
68
68
  private readonly stmtCache;
69
+ /**
70
+ * Cached full-corpus BM25 index for the FTS5-unavailable fallback path.
71
+ * Built lazily on the first `searchRankedFallback` call and invalidated
72
+ * (via `bm25Dirty`) whenever the `symbols` table is mutated. Computing
73
+ * IDF over the full corpus is also more correct than the old per-query
74
+ * candidate-subset IDF.
75
+ *
76
+ * Cache-lifecycle invariants (single source of truth lives at the
77
+ * `invalidateBm25()` helper — see its docblock for the "every mutation
78
+ * MUST call this" contract):
79
+ * - declaration: this field + `bm25Dirty` (here)
80
+ * - invalidation: `invalidateBm25()` flips the flag and nulls the cache
81
+ * - build: `getOrBuildBm25()` rebuilds against current `symbols` rows
82
+ * - teardown: `close()` resets the flag and nulls the cache
83
+ */
84
+ private bm25Cache;
85
+ private bm25Dirty;
69
86
  /** Prepare-once helper: compile `sql` on first use, reuse thereafter. */
70
87
  private stmt;
71
88
  /**
@@ -86,12 +103,28 @@ export declare class IndexStore {
86
103
  indexDir?: string | undefined;
87
104
  });
88
105
  private initSchema;
106
+ private static readonly NEXT_SYMBOL_ID_KEY;
107
+ /** Stay under typical SQLite SQLITE_MAX_VARIABLE_NUMBER (often 999). */
108
+ private static readonly MAX_SQL_VARS;
109
+ /**
110
+ * Ensure `metadata.next_symbol_id` exists. Safe to call outside a write
111
+ * transaction on open; the first concurrent writer under BEGIN IMMEDIATE
112
+ * re-reads and advances the counter atomically.
113
+ */
114
+ private ensureNextSymbolIdSeeded;
115
+ /**
116
+ * Reserve `count` consecutive symbol ids. MUST run inside BEGIN IMMEDIATE
117
+ * so concurrent indexers cannot hand out overlapping ranges.
118
+ */
119
+ private allocateSymbolIds;
120
+ /** Multi-row INSERT for symbols — amortizes prepare/bind overhead in large batches. */
121
+ private bulkInsertSymbols;
122
+ private bulkInsertFts;
123
+ private bulkInsertRefs;
89
124
  /**
90
125
  * Insert symbols, assigning IDs atomically inside `BEGIN IMMEDIATE` /
91
- * `COMMIT`. The ID allocation (`SELECT MAX(id)`) and all `INSERT`s share
92
- * the same transaction, preventing UNIQUE constraint violations when two
93
- * processes index concurrently (each would see a different `MAX(id)` and
94
- * neither can insert with the other's IDs).
126
+ * `COMMIT`. Id ranges come from the `next_symbol_id` metadata counter
127
+ * (O(1)); multi-row INSERT amortizes bind overhead for large files.
95
128
  *
96
129
  * @returns The symbols array with `id` fields populated so the caller can
97
130
  * use them for refs without re-reading from the DB.
@@ -112,7 +145,12 @@ export declare class IndexStore {
112
145
  lang?: SymbolLang | undefined;
113
146
  file?: string | undefined;
114
147
  lspKind?: number | undefined;
148
+ }, opts?: {
149
+ limit?: number | undefined;
115
150
  }): SearchResult[];
151
+ /** Shared WHERE builder for {@link search} / empty-query ranked totals. */
152
+ private buildSearchWhere;
153
+ private countSearch;
116
154
  /**
117
155
  * Ranked search — the one-stop query the codebase-search tool and plug-lsp
118
156
  * use. With FTS5 this is a single indexed `MATCH` ranked by SQLite's native
@@ -132,6 +170,40 @@ export declare class IndexStore {
132
170
  results: SearchResult[];
133
171
  total: number;
134
172
  };
173
+ /**
174
+ * Invalidate the cached BM25 index.
175
+ *
176
+ * **Contract: every method that mutates `symbols` MUST call this before
177
+ * returning.** (`refs` mutations do not affect the BM25 fallback because
178
+ * the corpus is built from `symbols.text` via `getAllIndexable()` and the
179
+ * BM25 score is filtered by the LIKE-selected candidate set in
180
+ * `searchRankedFallback`.) Today the call sites are `repairDrift`,
181
+ * `insertSymbols`, `deleteSymbolsForFile`, `deleteFile`, `clearAll`, and
182
+ * `commitBatch`. A future mutation that adds a new write path (e.g.
183
+ * `renameFile`, `updateSignature`) MUST also call this — otherwise the
184
+ * FTS5-unavailable fallback will serve stale search results. The
185
+ * `close()` reset at L1820-1821 tears the cache down on store shutdown,
186
+ * which is the only legitimate place that flips the flag outside this
187
+ * helper.
188
+ *
189
+ * Called *before* `runWithRetry` on purpose: if the write fails all
190
+ * retries the flag stays set, forcing a rebuild on the next search rather
191
+ * than trusting a cache that may not reflect the intended mutation.
192
+ * Do not move this inside the retry closure.
193
+ */
194
+ private invalidateBm25;
195
+ /**
196
+ * Return the cached full-corpus BM25 index, rebuilding it only when the
197
+ * symbols table has been mutated since the last build. The full-corpus IDF
198
+ * is more correct than the old per-query candidate-subset IDF, and the
199
+ * amortized build cost drops from O(symbols × tokens) per search to once
200
+ * per write batch.
201
+ *
202
+ * Note: the first call after a long idle (or on a freshly opened store)
203
+ * pays the full corpus rebuild synchronously on the search path. For a
204
+ * 5 500+ symbol corpus this is a visible one-time latency spike.
205
+ */
206
+ private getOrBuildBm25;
135
207
  /** Legacy ranked path: LIKE candidates + in-process BM25 + JS snippets. */
136
208
  private searchRankedFallback;
137
209
  getAllIndexable(): Array<{
@@ -210,6 +282,13 @@ export declare class IndexStore {
210
282
  * that found a target — matching the previous per-row loop's return value.
211
283
  */
212
284
  resolveRefs(): number;
285
+ /**
286
+ * Clear symbols/refs for a file and mark it as indexed with zero symbols.
287
+ * Used by the indexer for empty-parse results so three writes share one txn.
288
+ */
289
+ replaceEmptyFile(meta: FileMeta): void;
290
+ /** Best-effort query planner refresh after a large reindex. */
291
+ optimize(): void;
213
292
  /**
214
293
  * Find all references TO a given symbol (who calls / uses this symbol?).
215
294
  */
@@ -1 +1 @@
1
- {"version":3,"file":"writer.d.ts","sourceRoot":"","sources":["../../src/codebase-index/writer.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"writer.d.ts","sourceRoot":"","sources":["../../src/codebase-index/writer.ts"],"names":[],"mappings":"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"}
@@ -4,5 +4,5 @@ export declare function parseSymbols(opts: {
4
4
  content: string;
5
5
  lang: SymbolLang;
6
6
  }): FileSymbols;
7
- export { detectLang } from './ts-parser.js';
7
+ export { detectLang } from './languages.js';
8
8
  //# sourceMappingURL=yaml-parser.d.ts.map
package/dist/design.d.ts CHANGED
@@ -1,11 +1,14 @@
1
- import type { Tool } from '@wrongstack/core';
1
+ import type { Tool } from '@wrongstack/core/types';
2
+ import { type SemanticTune } from '@wrongstack/core/design';
2
3
  type Overrides = Record<string, string>;
3
4
  interface DesignInput {
4
- action?: 'list' | 'use' | 'foundations' | 'set' | 'materialize' | 'verify' | undefined;
5
+ action?: 'list' | 'use' | 'foundations' | 'set' | 'tune' | 'materialize' | 'verify' | undefined;
5
6
  kit?: string | undefined;
6
7
  stack?: string | undefined;
7
8
  /** action "set" / "use": token overrides, e.g. { primary: "oklch(...)", "dark.bg": "#111" }. */
8
9
  set?: Overrides | undefined;
10
+ /** action "tune": high-level knobs, e.g. { radius:"lg", density:"compact", font:"Space Grotesk" }. */
11
+ tune?: SemanticTune | undefined;
9
12
  /** action "materialize": output path (project-relative); defaults per stack. */
10
13
  out?: string | undefined;
11
14
  /** action "materialize": overwrite an existing file. */
@@ -1 +1 @@
1
- {"version":3,"file":"design.d.ts","sourceRoot":"","sources":["../src/design.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAe,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAc1D,KAAK,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AA+BxC,UAAU,WAAW;IACnB,MAAM,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,aAAa,GAAG,KAAK,GAAG,aAAa,GAAG,QAAQ,GAAG,SAAS,CAAC;IACvF,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,gGAAgG;IAChG,GAAG,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;IAC5B,gFAAgF;IAChF,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,wDAAwD;IACxD,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5B,8FAA8F;IAC9F,KAAK,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;CAC9B;AAED,UAAU,YAAY;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,4EAA4E;IAC5E,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,iEAAiE;IACjE,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACjC;AAYD;;;;;;;;GAQG;AACH,eAAO,MAAM,UAAU,EAAE,IAAI,CAAC,WAAW,EAAE,YAAY,CA8QtD,CAAC"}
1
+ {"version":3,"file":"design.d.ts","sourceRoot":"","sources":["../src/design.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAUL,KAAK,YAAY,EAGlB,MAAM,yBAAyB,CAAC;AAEjC,KAAK,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AA+BxC,UAAU,WAAW;IACnB,MAAM,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,aAAa,GAAG,KAAK,GAAG,MAAM,GAAG,aAAa,GAAG,QAAQ,GAAG,SAAS,CAAC;IAChG,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,gGAAgG;IAChG,GAAG,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;IAC5B,sGAAsG;IACtG,IAAI,CAAC,EAAE,YAAY,GAAG,SAAS,CAAC;IAChC,gFAAgF;IAChF,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,wDAAwD;IACxD,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5B,8FAA8F;IAC9F,KAAK,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;CAC9B;AAED,UAAU,YAAY;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,4EAA4E;IAC5E,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,iEAAiE;IACjE,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACjC;AAYD;;;;;;;;GAQG;AACH,eAAO,MAAM,UAAU,EAAE,IAAI,CAAC,WAAW,EAAE,YAAY,CAuTtD,CAAC"}
package/dist/design.js CHANGED
@@ -9,10 +9,11 @@ import {
9
9
  materializeTokens,
10
10
  recordKitChoice,
11
11
  recordOverrides,
12
+ resolveSemanticTune,
12
13
  runDesignVerify,
13
14
  setActiveKit,
14
15
  setDesignOverrides
15
- } from "@wrongstack/core";
16
+ } from "@wrongstack/core/design";
16
17
  async function resolveReal(p) {
17
18
  const resolved = path.resolve(p);
18
19
  let probe = resolved;
@@ -55,8 +56,8 @@ var designTool = {
55
56
  properties: {
56
57
  action: {
57
58
  type: "string",
58
- enum: ["list", "use", "foundations", "set", "materialize", "verify"],
59
- description: "list = menu; use = load+pin a kit; foundations = baseline; set = override colors/tokens; materialize = write tokens to a theme file; verify = scan UI for off-palette colors. Default: list."
59
+ enum: ["list", "use", "foundations", "set", "tune", "materialize", "verify"],
60
+ description: "list = menu; use = load+pin a kit; foundations = baseline; set = override colors/tokens; tune = high-level knobs (radius/density/font/motion); materialize = write tokens to a theme file; verify = scan UI for token drift. Default: list."
60
61
  },
61
62
  kit: {
62
63
  type: "string",
@@ -72,6 +73,16 @@ var designTool = {
72
73
  description: 'Token overrides for "set"/"use": { "primary": "oklch(\u2026)", "dark.bg": "#111" }. Bare key = both themes; "light."/"dark." prefix = that theme only. Empty value clears an override.',
73
74
  additionalProperties: { type: "string" }
74
75
  },
76
+ tune: {
77
+ type: "object",
78
+ description: 'High-level knobs for "tune": { "radius": "lg", "density": "compact", "font": "Space Grotesk", "motion": "snappy" }. radius = none|sm|md|lg|xl|full or a base length; density = compact|cozy|comfortable; motion = snappy|smooth|none. Resolved to concrete token overrides.',
79
+ properties: {
80
+ radius: { type: "string" },
81
+ density: { type: "string" },
82
+ font: { type: "string" },
83
+ motion: { type: "string" }
84
+ }
85
+ },
75
86
  out: {
76
87
  type: "string",
77
88
  description: "Materialize output path (project-relative). Defaults to a per-stack convention."
@@ -169,6 +180,28 @@ ${body}`
169
180
  These win over kit tokens. Run \`design {action:"materialize"}\` to write them to a theme file.`
170
181
  };
171
182
  }
183
+ if (action === "tune") {
184
+ const patch = resolveSemanticTune(input.tune ?? {});
185
+ if (Object.keys(patch).length === 0) {
186
+ return {
187
+ action,
188
+ output: 'No recognized knobs. Pass tune:{ radius:"lg", density:"compact", font:"\u2026", motion:"snappy" }.'
189
+ };
190
+ }
191
+ const merged = await recordOverrides(ctx.projectRoot, patch, (/* @__PURE__ */ new Date()).toISOString());
192
+ if (!merged) {
193
+ return {
194
+ action,
195
+ output: 'No active kit. Pick one first: `design {action:"use", kit:"<id>"}`.'
196
+ };
197
+ }
198
+ setDesignOverrides(ctx, merged);
199
+ return {
200
+ action,
201
+ output: `Tuned. Resolved ${Object.keys(patch).length} token override(s): ${Object.entries(patch).map(([k, v]) => `${k}=${v}`).join(", ")}
202
+ Run \`design {action:"materialize"}\` to write the tuned tokens to your theme file.`
203
+ };
204
+ }
172
205
  if (action === "materialize") {
173
206
  const active = await loadActiveKit(ctx.projectRoot);
174
207
  if (!active) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/design.ts"],
4
- "sourcesContent": ["import * as fs from 'node:fs/promises';\nimport * as path from 'node:path';\nimport type { DesignStack, Tool } from '@wrongstack/core';\nimport {\n applyTokenOverrides,\n getDesignKitLoader,\n isDesignStack,\n loadActiveKit,\n materializeTokens,\n recordKitChoice,\n recordOverrides,\n runDesignVerify,\n setActiveKit,\n setDesignOverrides,\n} from '@wrongstack/core';\n\ntype Overrides = Record<string, string>;\n\n/**\n * Canonicalize `p` through `fs.realpath` so symlinks / bind mounts don't\n * hide out-of-root writes. Falls back to `path.resolve(p)` when the path\n * (or any of its parents) doesn't exist yet \u2014 callers frequently pass a\n * destination that the tool is about to write.\n */\nasync function resolveReal(p: string): Promise<string> {\n const resolved = path.resolve(p);\n let probe = resolved;\n const missing: string[] = [];\n for (;;) {\n try {\n return path.resolve(await fs.realpath(probe), ...missing);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n const parent = path.dirname(probe);\n if (parent === probe) return resolved; // reached fs root\n missing.unshift(path.basename(probe));\n probe = parent;\n continue;\n }\n // Any other error (EACCES, EBUSY, \u2026) \u2014 fall back to the lexical\n // resolve rather than throwing, so the caller can still surface a\n // meaningful error from the path.relative check below.\n return resolved;\n }\n }\n}\n\ninterface DesignInput {\n action?: 'list' | 'use' | 'foundations' | 'set' | 'materialize' | 'verify' | undefined;\n kit?: string | undefined;\n stack?: string | undefined;\n /** action \"set\" / \"use\": token overrides, e.g. { primary: \"oklch(...)\", \"dark.bg\": \"#111\" }. */\n set?: Overrides | undefined;\n /** action \"materialize\": output path (project-relative); defaults per stack. */\n out?: string | undefined;\n /** action \"materialize\": overwrite an existing file. */\n force?: boolean | undefined;\n /** action \"verify\": explicit files to scan (project-relative). Defaults to a UI-file walk. */\n files?: string[] | undefined;\n}\n\ninterface DesignOutput {\n action: string;\n kit?: string | undefined;\n stack?: string | undefined;\n output: string;\n /** action \"materialize\": where the theme file was (or would be) written. */\n path?: string | undefined;\n /** action \"verify\": adherence score 0..1 and violation count. */\n score?: number | undefined;\n violations?: number | undefined;\n}\n\nfunction normalizeOverrides(set: unknown): Overrides {\n const out: Overrides = {};\n if (set && typeof set === 'object') {\n for (const [k, v] of Object.entries(set as Record<string, unknown>)) {\n if (typeof v === 'string') out[k] = v;\n }\n }\n return out;\n}\n\n/**\n * Design Studio tool \u2014 progressive disclosure + token enforcement.\n *\n * `list`/`use`/`foundations` browse and pin kits. `set` records structured color\n * overrides (these win over kit tokens). `materialize` writes the active kit's\n * (override-applied) tokens to a real, stack-appropriate theme file so the\n * palette becomes the codebase's source of truth \u2014 not just a prompt hint.\n * `verify` scans UI files for off-palette drift.\n */\nexport const designTool: Tool<DesignInput, DesignOutput> = {\n name: 'design',\n category: 'Design',\n description:\n 'Browse, load, customize, and enforce curated frontend/mobile UI design kits. Use BEFORE writing ' +\n 'UI code to commit to one coherent, modern, responsive, dark/light, accessible design. Actions: ' +\n '\"list\" (menu), \"use\" (load+pin a kit for a stack), \"foundations\" (baseline), \"set\" (override kit ' +\n 'colors/tokens), \"materialize\" (write the tokens to a real theme file \u2014 CSS @theme/OKLCH or native), ' +\n '\"verify\" (scan UI files for off-palette colors).',\n usageHint:\n 'Flow: `design {action:\"use\", kit:\"minimal-clarity\", stack:\"web\"}` \u2192 optionally ' +\n '`design {action:\"set\", set:{primary:\"oklch(62% 0.2 25)\"}}` \u2192 `design {action:\"materialize\"}` ' +\n 'to write tokens to disk \u2192 implement against them \u2192 `design {action:\"verify\"}`.',\n permission: 'confirm',\n mutating: true,\n capabilities: ['fs.write'],\n timeoutMs: 15_000,\n inputSchema: {\n type: 'object',\n properties: {\n action: {\n type: 'string',\n enum: ['list', 'use', 'foundations', 'set', 'materialize', 'verify'],\n description:\n 'list = menu; use = load+pin a kit; foundations = baseline; set = override colors/tokens; ' +\n 'materialize = write tokens to a theme file; verify = scan UI for off-palette colors. Default: list.',\n },\n kit: {\n type: 'string',\n description: 'Kit id (required for \"use\"), e.g. \"minimal-clarity\", \"neo-brutalist\".',\n },\n stack: {\n type: 'string',\n enum: ['web', 'react-native', 'flutter', 'swiftui', 'compose'],\n description: 'Target stack \u2014 narrows guidance + materialize format. Default: web.',\n },\n set: {\n type: 'object',\n description:\n 'Token overrides for \"set\"/\"use\": { \"primary\": \"oklch(\u2026)\", \"dark.bg\": \"#111\" }. Bare key = ' +\n 'both themes; \"light.\"/\"dark.\" prefix = that theme only. Empty value clears an override.',\n additionalProperties: { type: 'string' },\n },\n out: {\n type: 'string',\n description: 'Materialize output path (project-relative). Defaults to a per-stack convention.',\n },\n force: {\n type: 'boolean',\n description: 'Materialize: overwrite an existing file (default false \u2014 refuses to clobber).',\n },\n files: {\n type: 'array',\n items: { type: 'string' },\n description: 'Verify: explicit project-relative files to scan. Default: a bounded UI-file walk.',\n },\n },\n required: [],\n },\n async execute(input, ctx): Promise<DesignOutput> {\n const loader = getDesignKitLoader(ctx.projectRoot);\n const action = input.action ?? 'list';\n const stack: DesignStack | undefined =\n input.stack && isDesignStack(input.stack) ? input.stack : undefined;\n\n if (action === 'foundations') {\n const text = await loader.foundationsText(stack);\n return { action, stack, output: text || 'No foundations document is installed.' };\n }\n\n if (action === 'use') {\n const kitId = input.kit?.trim();\n if (!kitId) {\n const menu = await loader.menuText();\n return { action, output: `No kit id provided.\\n\\n${menu}` };\n }\n const manifest = await loader.find(kitId);\n if (!manifest) {\n const menu = await loader.menuText();\n return { action, kit: kitId, output: `Kit \"${kitId}\" not found.\\n\\n${menu}` };\n }\n const resolvedStack = stack ?? manifest.stacks[0] ?? 'web';\n const body = await loader.readBody(manifest.id, resolvedStack);\n const rawTokens = await loader.readTokens(manifest.id);\n // Preserve any persisted overrides; merge in any passed with `use`.\n const persisted = await loadActiveKit(ctx.projectRoot);\n const keepOverrides =\n persisted?.kit === manifest.id ? (persisted.overrides ?? {}) : {};\n const overrides: Overrides = { ...keepOverrides, ...normalizeOverrides(input.set) };\n const tokens = rawTokens ? applyTokenOverrides(rawTokens, overrides) : rawTokens;\n\n setActiveKit(ctx, manifest.id, resolvedStack, overrides);\n await recordKitChoice(\n ctx.projectRoot,\n manifest.id,\n resolvedStack,\n 'design-tool',\n new Date().toISOString(),\n Object.keys(overrides).length ? overrides : undefined,\n );\n\n const ovLine = Object.keys(overrides).length\n ? `\\nActive color overrides: ${Object.entries(overrides)\n .map(([k, v]) => `${k}=${v}`)\n .join(', ')}\\n`\n : '';\n const header =\n `# Active design kit: ${manifest.name} (${manifest.id}) \u2014 stack: ${resolvedStack}\\n` +\n `${manifest.aesthetic}\\n${ovLine}\\n` +\n 'Implement the UI faithfully to this spec. Keep light/dark, responsive, and WCAG AA.\\n';\n const tokenBlock = tokens\n ? `\\n## Token snapshot (overrides applied)\\n\\`\\`\\`json\\n${JSON.stringify(tokens, null, 2)}\\n\\`\\`\\`\\n` +\n 'Tip: run `design {action:\"materialize\"}` to write these tokens to a real theme file.\\n'\n : '';\n return {\n action,\n kit: manifest.id,\n stack: resolvedStack,\n output: `${header}${tokenBlock}\\n${body}`,\n };\n }\n\n if (action === 'set') {\n const patch = normalizeOverrides(input.set);\n if (Object.keys(patch).length === 0) {\n return { action, output: 'No overrides given. Pass set:{ \"primary\": \"oklch(\u2026)\" }.' };\n }\n const merged = await recordOverrides(ctx.projectRoot, patch, new Date().toISOString());\n if (!merged) {\n return {\n action,\n output: 'No active kit. Pick one first: `design {action:\"use\", kit:\"<id>\"}`.',\n };\n }\n setDesignOverrides(ctx, merged);\n return {\n action,\n output:\n `Overrides updated. Active overrides: ${Object.entries(merged)\n .map(([k, v]) => `${k}=${v}`)\n .join(', ')}\\n` +\n 'These win over kit tokens. Run `design {action:\"materialize\"}` to write them to a theme file.',\n };\n }\n\n if (action === 'materialize') {\n const active = await loadActiveKit(ctx.projectRoot);\n if (!active) {\n return {\n action,\n output: 'No active kit. Pick one first: `design {action:\"use\", kit:\"<id>\"}`.',\n };\n }\n const resolvedStack: DesignStack =\n stack ?? (active.stack && isDesignStack(active.stack) ? active.stack : 'web');\n const rawTokens = await loader.readTokens(active.kit);\n if (!rawTokens) {\n return { action, kit: active.kit, output: `Kit \"${active.kit}\" has no tokens.json.` };\n }\n const tokens = applyTokenOverrides(rawTokens, active.overrides);\n const result = materializeTokens({\n tokens,\n stack: resolvedStack,\n kitId: active.kit,\n outPath: input.out,\n });\n // Containment: result.path derives from caller-supplied input.out and\n // may be either relative (joined under projectRoot) or absolute\n // (used as-is). Pin the write inside the project root, mirroring the\n // scaffold tool.\n //\n // #249 (design.ts escape check): resolve both sides through fs.realpath\n // before the relative check, so symlinks / bind mounts (e.g. /tmp on\n // CI runners is often a symlink to /private/tmp) can't smuggle an\n // out-of-root path past the prefix-startsWith-`..` guard. The\n // destination file does not exist yet, so realpath the parent\n // directory and re-join the basename.\n //\n // Note: on Windows, path.join(absoluteA, absoluteB) concatenates\n // literally (path.join('C:\\\\A\\\\B', 'C:\\\\A\\\\C.css') === 'C:\\\\A\\\\B\\\\C:\\\\A\\\\C.css')\n // rather than collapsing absoluteB onto the drive root. Treating\n // absolute result.path as absolute (not as a join-input) keeps the\n // behavior consistent across platforms and is what the test asserts.\n const root = await resolveReal(ctx.projectRoot);\n const absResolved = path.isAbsolute(result.path)\n ? path.resolve(result.path)\n : path.resolve(path.join(ctx.projectRoot, result.path));\n const absParent = await resolveReal(path.dirname(absResolved));\n const abs = path.join(absParent, path.basename(absResolved));\n const rel = path.relative(root, abs);\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\n throw new Error(\n `design: materialize path \"${result.path}\" would escape the project root`,\n );\n }\n let exists = false;\n try {\n await fs.access(abs);\n exists = true;\n } catch {\n // does not exist \u2014 safe to write\n }\n if (exists && !input.force) {\n return {\n action,\n kit: active.kit,\n stack: resolvedStack,\n path: result.path,\n output:\n `${result.path} already exists. Re-run with force:true to overwrite, or write this ` +\n `${result.format} yourself:\\n\\n\\`\\`\\`\\n${result.content}\\n\\`\\`\\``,\n };\n }\n await fs.mkdir(path.dirname(abs), { recursive: true });\n await fs.writeFile(abs, result.content);\n return {\n action,\n kit: active.kit,\n stack: resolvedStack,\n path: result.path,\n output:\n `Wrote ${result.format} to ${result.path}. Import these tokens in your UI so the kit ` +\n `palette is the source of truth. ${exists ? '(overwrote existing file)' : ''}`,\n };\n }\n\n if (action === 'verify') {\n const active = await loadActiveKit(ctx.projectRoot);\n if (!active) {\n return {\n action,\n output: 'No active kit to verify against. Pick one: `design {action:\"use\", kit:\"<id>\"}`.',\n };\n }\n const rawTokens = await loader.readTokens(active.kit);\n if (!rawTokens) {\n return { action, kit: active.kit, output: `Kit \"${active.kit}\" has no tokens.json.` };\n }\n const tokens = applyTokenOverrides(rawTokens, active.overrides);\n const report = await runDesignVerify(ctx.projectRoot, tokens, input.files);\n const pct = Math.round(report.score * 100);\n const top = report.violations\n .slice(0, 25)\n .map((v) => ` ${v.file}:${v.line} \u2014 ${v.reason}: ${v.snippet}`)\n .join('\\n');\n const summary =\n `Adherence: ${pct}% on-palette across ${report.filesScanned} file(s). ` +\n `${report.violations.length} violation(s).` +\n (report.violations.length\n ? `\\n${top}${report.violations.length > 25 ? `\\n \u2026and ${report.violations.length - 25} more` : ''}` +\n `\\n\\nReplace off-palette colors with kit tokens (or the materialized CSS vars / token utilities).`\n : '\\nNo off-palette colors found \u2014 UI adheres to the kit palette.');\n return {\n action,\n kit: active.kit,\n output: summary,\n score: report.score,\n violations: report.violations.length,\n };\n }\n\n // Default: list\n const menu = await loader.menuText();\n return {\n action: 'list',\n output:\n (menu || 'No design kits are installed.') +\n '\\n\\nLoad one with `design {action:\"use\", kit:\"<id>\", stack:\"<stack>\"}`.',\n };\n },\n};\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import * as fs from 'node:fs/promises';\nimport * as path from 'node:path';\nimport type { DesignStack } from '@wrongstack/core/design';\nimport type { Tool } from '@wrongstack/core/types';\nimport {\n applyTokenOverrides,\n getDesignKitLoader,\n isDesignStack,\n loadActiveKit,\n materializeTokens,\n recordKitChoice,\n recordOverrides,\n resolveSemanticTune,\n runDesignVerify,\n type SemanticTune,\n setActiveKit,\n setDesignOverrides,\n} from '@wrongstack/core/design';\n\ntype Overrides = Record<string, string>;\n\n/**\n * Canonicalize `p` through `fs.realpath` so symlinks / bind mounts don't\n * hide out-of-root writes. Falls back to `path.resolve(p)` when the path\n * (or any of its parents) doesn't exist yet \u2014 callers frequently pass a\n * destination that the tool is about to write.\n */\nasync function resolveReal(p: string): Promise<string> {\n const resolved = path.resolve(p);\n let probe = resolved;\n const missing: string[] = [];\n for (;;) {\n try {\n return path.resolve(await fs.realpath(probe), ...missing);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n const parent = path.dirname(probe);\n if (parent === probe) return resolved; // reached fs root\n missing.unshift(path.basename(probe));\n probe = parent;\n continue;\n }\n // Any other error (EACCES, EBUSY, \u2026) \u2014 fall back to the lexical\n // resolve rather than throwing, so the caller can still surface a\n // meaningful error from the path.relative check below.\n return resolved;\n }\n }\n}\n\ninterface DesignInput {\n action?: 'list' | 'use' | 'foundations' | 'set' | 'tune' | 'materialize' | 'verify' | undefined;\n kit?: string | undefined;\n stack?: string | undefined;\n /** action \"set\" / \"use\": token overrides, e.g. { primary: \"oklch(...)\", \"dark.bg\": \"#111\" }. */\n set?: Overrides | undefined;\n /** action \"tune\": high-level knobs, e.g. { radius:\"lg\", density:\"compact\", font:\"Space Grotesk\" }. */\n tune?: SemanticTune | undefined;\n /** action \"materialize\": output path (project-relative); defaults per stack. */\n out?: string | undefined;\n /** action \"materialize\": overwrite an existing file. */\n force?: boolean | undefined;\n /** action \"verify\": explicit files to scan (project-relative). Defaults to a UI-file walk. */\n files?: string[] | undefined;\n}\n\ninterface DesignOutput {\n action: string;\n kit?: string | undefined;\n stack?: string | undefined;\n output: string;\n /** action \"materialize\": where the theme file was (or would be) written. */\n path?: string | undefined;\n /** action \"verify\": adherence score 0..1 and violation count. */\n score?: number | undefined;\n violations?: number | undefined;\n}\n\nfunction normalizeOverrides(set: unknown): Overrides {\n const out: Overrides = {};\n if (set && typeof set === 'object') {\n for (const [k, v] of Object.entries(set as Record<string, unknown>)) {\n if (typeof v === 'string') out[k] = v;\n }\n }\n return out;\n}\n\n/**\n * Design Studio tool \u2014 progressive disclosure + token enforcement.\n *\n * `list`/`use`/`foundations` browse and pin kits. `set` records structured color\n * overrides (these win over kit tokens). `materialize` writes the active kit's\n * (override-applied) tokens to a real, stack-appropriate theme file so the\n * palette becomes the codebase's source of truth \u2014 not just a prompt hint.\n * `verify` scans UI files for off-palette drift.\n */\nexport const designTool: Tool<DesignInput, DesignOutput> = {\n name: 'design',\n category: 'Design',\n description:\n 'Browse, load, customize, and enforce curated frontend/mobile UI design kits. Use BEFORE writing ' +\n 'UI code to commit to one coherent, modern, responsive, dark/light, accessible design. Actions: ' +\n '\"list\" (menu), \"use\" (load+pin a kit for a stack), \"foundations\" (baseline), \"set\" (override kit ' +\n 'colors/tokens), \"materialize\" (write the tokens to a real theme file \u2014 CSS @theme/OKLCH or native), ' +\n '\"verify\" (scan UI files for off-palette colors).',\n usageHint:\n 'Flow: `design {action:\"use\", kit:\"minimal-clarity\", stack:\"web\"}` \u2192 optionally ' +\n '`design {action:\"set\", set:{primary:\"oklch(62% 0.2 25)\"}}` \u2192 `design {action:\"materialize\"}` ' +\n 'to write tokens to disk \u2192 implement against them \u2192 `design {action:\"verify\"}`.',\n permission: 'confirm',\n mutating: true,\n capabilities: ['fs.write'],\n timeoutMs: 15_000,\n inputSchema: {\n type: 'object',\n properties: {\n action: {\n type: 'string',\n enum: ['list', 'use', 'foundations', 'set', 'tune', 'materialize', 'verify'],\n description:\n 'list = menu; use = load+pin a kit; foundations = baseline; set = override colors/tokens; ' +\n 'tune = high-level knobs (radius/density/font/motion); materialize = write tokens to a theme ' +\n 'file; verify = scan UI for token drift. Default: list.',\n },\n kit: {\n type: 'string',\n description: 'Kit id (required for \"use\"), e.g. \"minimal-clarity\", \"neo-brutalist\".',\n },\n stack: {\n type: 'string',\n enum: ['web', 'react-native', 'flutter', 'swiftui', 'compose'],\n description: 'Target stack \u2014 narrows guidance + materialize format. Default: web.',\n },\n set: {\n type: 'object',\n description:\n 'Token overrides for \"set\"/\"use\": { \"primary\": \"oklch(\u2026)\", \"dark.bg\": \"#111\" }. Bare key = ' +\n 'both themes; \"light.\"/\"dark.\" prefix = that theme only. Empty value clears an override.',\n additionalProperties: { type: 'string' },\n },\n tune: {\n type: 'object',\n description:\n 'High-level knobs for \"tune\": { \"radius\": \"lg\", \"density\": \"compact\", \"font\": \"Space Grotesk\", ' +\n '\"motion\": \"snappy\" }. radius = none|sm|md|lg|xl|full or a base length; density = ' +\n 'compact|cozy|comfortable; motion = snappy|smooth|none. Resolved to concrete token overrides.',\n properties: {\n radius: { type: 'string' },\n density: { type: 'string' },\n font: { type: 'string' },\n motion: { type: 'string' },\n },\n },\n out: {\n type: 'string',\n description: 'Materialize output path (project-relative). Defaults to a per-stack convention.',\n },\n force: {\n type: 'boolean',\n description: 'Materialize: overwrite an existing file (default false \u2014 refuses to clobber).',\n },\n files: {\n type: 'array',\n items: { type: 'string' },\n description: 'Verify: explicit project-relative files to scan. Default: a bounded UI-file walk.',\n },\n },\n required: [],\n },\n async execute(input, ctx): Promise<DesignOutput> {\n const loader = getDesignKitLoader(ctx.projectRoot);\n const action = input.action ?? 'list';\n const stack: DesignStack | undefined =\n input.stack && isDesignStack(input.stack) ? input.stack : undefined;\n\n if (action === 'foundations') {\n const text = await loader.foundationsText(stack);\n return { action, stack, output: text || 'No foundations document is installed.' };\n }\n\n if (action === 'use') {\n const kitId = input.kit?.trim();\n if (!kitId) {\n const menu = await loader.menuText();\n return { action, output: `No kit id provided.\\n\\n${menu}` };\n }\n const manifest = await loader.find(kitId);\n if (!manifest) {\n const menu = await loader.menuText();\n return { action, kit: kitId, output: `Kit \"${kitId}\" not found.\\n\\n${menu}` };\n }\n const resolvedStack = stack ?? manifest.stacks[0] ?? 'web';\n const body = await loader.readBody(manifest.id, resolvedStack);\n const rawTokens = await loader.readTokens(manifest.id);\n // Preserve any persisted overrides; merge in any passed with `use`.\n const persisted = await loadActiveKit(ctx.projectRoot);\n const keepOverrides =\n persisted?.kit === manifest.id ? (persisted.overrides ?? {}) : {};\n const overrides: Overrides = { ...keepOverrides, ...normalizeOverrides(input.set) };\n const tokens = rawTokens ? applyTokenOverrides(rawTokens, overrides) : rawTokens;\n\n setActiveKit(ctx, manifest.id, resolvedStack, overrides);\n await recordKitChoice(\n ctx.projectRoot,\n manifest.id,\n resolvedStack,\n 'design-tool',\n new Date().toISOString(),\n Object.keys(overrides).length ? overrides : undefined,\n );\n\n const ovLine = Object.keys(overrides).length\n ? `\\nActive color overrides: ${Object.entries(overrides)\n .map(([k, v]) => `${k}=${v}`)\n .join(', ')}\\n`\n : '';\n const header =\n `# Active design kit: ${manifest.name} (${manifest.id}) \u2014 stack: ${resolvedStack}\\n` +\n `${manifest.aesthetic}\\n${ovLine}\\n` +\n 'Implement the UI faithfully to this spec. Keep light/dark, responsive, and WCAG AA.\\n';\n const tokenBlock = tokens\n ? `\\n## Token snapshot (overrides applied)\\n\\`\\`\\`json\\n${JSON.stringify(tokens, null, 2)}\\n\\`\\`\\`\\n` +\n 'Tip: run `design {action:\"materialize\"}` to write these tokens to a real theme file.\\n'\n : '';\n return {\n action,\n kit: manifest.id,\n stack: resolvedStack,\n output: `${header}${tokenBlock}\\n${body}`,\n };\n }\n\n if (action === 'set') {\n const patch = normalizeOverrides(input.set);\n if (Object.keys(patch).length === 0) {\n return { action, output: 'No overrides given. Pass set:{ \"primary\": \"oklch(\u2026)\" }.' };\n }\n const merged = await recordOverrides(ctx.projectRoot, patch, new Date().toISOString());\n if (!merged) {\n return {\n action,\n output: 'No active kit. Pick one first: `design {action:\"use\", kit:\"<id>\"}`.',\n };\n }\n setDesignOverrides(ctx, merged);\n return {\n action,\n output:\n `Overrides updated. Active overrides: ${Object.entries(merged)\n .map(([k, v]) => `${k}=${v}`)\n .join(', ')}\\n` +\n 'These win over kit tokens. Run `design {action:\"materialize\"}` to write them to a theme file.',\n };\n }\n\n if (action === 'tune') {\n const patch = resolveSemanticTune(input.tune ?? {});\n if (Object.keys(patch).length === 0) {\n return {\n action,\n output:\n 'No recognized knobs. Pass tune:{ radius:\"lg\", density:\"compact\", font:\"\u2026\", motion:\"snappy\" }.',\n };\n }\n const merged = await recordOverrides(ctx.projectRoot, patch, new Date().toISOString());\n if (!merged) {\n return {\n action,\n output: 'No active kit. Pick one first: `design {action:\"use\", kit:\"<id>\"}`.',\n };\n }\n setDesignOverrides(ctx, merged);\n return {\n action,\n output:\n `Tuned. Resolved ${Object.keys(patch).length} token override(s): ${Object.entries(patch)\n .map(([k, v]) => `${k}=${v}`)\n .join(', ')}\\n` +\n 'Run `design {action:\"materialize\"}` to write the tuned tokens to your theme file.',\n };\n }\n\n if (action === 'materialize') {\n const active = await loadActiveKit(ctx.projectRoot);\n if (!active) {\n return {\n action,\n output: 'No active kit. Pick one first: `design {action:\"use\", kit:\"<id>\"}`.',\n };\n }\n const resolvedStack: DesignStack =\n stack ?? (active.stack && isDesignStack(active.stack) ? active.stack : 'web');\n const rawTokens = await loader.readTokens(active.kit);\n if (!rawTokens) {\n return { action, kit: active.kit, output: `Kit \"${active.kit}\" has no tokens.json.` };\n }\n const tokens = applyTokenOverrides(rawTokens, active.overrides);\n const result = materializeTokens({\n tokens,\n stack: resolvedStack,\n kitId: active.kit,\n outPath: input.out,\n });\n // Containment: result.path derives from caller-supplied input.out and\n // may be either relative (joined under projectRoot) or absolute\n // (used as-is). Pin the write inside the project root, mirroring the\n // scaffold tool.\n //\n // #249 (design.ts escape check): resolve both sides through fs.realpath\n // before the relative check, so symlinks / bind mounts (e.g. /tmp on\n // CI runners is often a symlink to /private/tmp) can't smuggle an\n // out-of-root path past the prefix-startsWith-`..` guard. The\n // destination file does not exist yet, so realpath the parent\n // directory and re-join the basename.\n //\n // Note: on Windows, path.join(absoluteA, absoluteB) concatenates\n // literally (path.join('C:\\\\A\\\\B', 'C:\\\\A\\\\C.css') === 'C:\\\\A\\\\B\\\\C:\\\\A\\\\C.css')\n // rather than collapsing absoluteB onto the drive root. Treating\n // absolute result.path as absolute (not as a join-input) keeps the\n // behavior consistent across platforms and is what the test asserts.\n const root = await resolveReal(ctx.projectRoot);\n const absResolved = path.isAbsolute(result.path)\n ? path.resolve(result.path)\n : path.resolve(path.join(ctx.projectRoot, result.path));\n const absParent = await resolveReal(path.dirname(absResolved));\n const abs = path.join(absParent, path.basename(absResolved));\n const rel = path.relative(root, abs);\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\n throw new Error(\n `design: materialize path \"${result.path}\" would escape the project root`,\n );\n }\n let exists = false;\n try {\n await fs.access(abs);\n exists = true;\n } catch {\n // does not exist \u2014 safe to write\n }\n if (exists && !input.force) {\n return {\n action,\n kit: active.kit,\n stack: resolvedStack,\n path: result.path,\n output:\n `${result.path} already exists. Re-run with force:true to overwrite, or write this ` +\n `${result.format} yourself:\\n\\n\\`\\`\\`\\n${result.content}\\n\\`\\`\\``,\n };\n }\n await fs.mkdir(path.dirname(abs), { recursive: true });\n await fs.writeFile(abs, result.content);\n return {\n action,\n kit: active.kit,\n stack: resolvedStack,\n path: result.path,\n output:\n `Wrote ${result.format} to ${result.path}. Import these tokens in your UI so the kit ` +\n `palette is the source of truth. ${exists ? '(overwrote existing file)' : ''}`,\n };\n }\n\n if (action === 'verify') {\n const active = await loadActiveKit(ctx.projectRoot);\n if (!active) {\n return {\n action,\n output: 'No active kit to verify against. Pick one: `design {action:\"use\", kit:\"<id>\"}`.',\n };\n }\n const rawTokens = await loader.readTokens(active.kit);\n if (!rawTokens) {\n return { action, kit: active.kit, output: `Kit \"${active.kit}\" has no tokens.json.` };\n }\n const tokens = applyTokenOverrides(rawTokens, active.overrides);\n const report = await runDesignVerify(ctx.projectRoot, tokens, input.files);\n const pct = Math.round(report.score * 100);\n const top = report.violations\n .slice(0, 25)\n .map((v) => ` ${v.file}:${v.line} \u2014 ${v.reason}: ${v.snippet}`)\n .join('\\n');\n const summary =\n `Adherence: ${pct}% on-palette across ${report.filesScanned} file(s). ` +\n `${report.violations.length} violation(s).` +\n (report.violations.length\n ? `\\n${top}${report.violations.length > 25 ? `\\n \u2026and ${report.violations.length - 25} more` : ''}` +\n `\\n\\nReplace off-palette colors with kit tokens (or the materialized CSS vars / token utilities).`\n : '\\nNo off-palette colors found \u2014 UI adheres to the kit palette.');\n return {\n action,\n kit: active.kit,\n output: summary,\n score: report.score,\n violations: report.violations.length,\n };\n }\n\n // Default: list\n const menu = await loader.menuText();\n return {\n action: 'list',\n output:\n (menu || 'No design kits are installed.') +\n '\\n\\nLoad one with `design {action:\"use\", kit:\"<id>\", stack:\"<stack>\"}`.',\n };\n },\n};\n"],
5
+ "mappings": "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",
6
6
  "names": ["menu"]
7
7
  }
package/dist/diff.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Tool } from '@wrongstack/core';
1
+ import type { Tool } from '@wrongstack/core/types';
2
2
  interface DiffInput {
3
3
  path?: string | undefined;
4
4
  files?: string | string[] | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"diff.d.ts","sourceRoot":"","sources":["../src/diff.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAG7C,UAAU,SAAS;IACjB,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC;IACtC,CAAC,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACvB,CAAC,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACvB,MAAM,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC7B,IAAI,CAAC,EAAE,SAAS,GAAG,cAAc,GAAG,MAAM,GAAG,SAAS,CAAC;IACvD,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC9B;AAED,UAAU,UAAU;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,SAAS,EAAE,OAAO,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,eAAO,MAAM,QAAQ,EAAE,IAAI,CAAC,SAAS,EAAE,UAAU,CA8DhD,CAAC"}
1
+ {"version":3,"file":"diff.d.ts","sourceRoot":"","sources":["../src/diff.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAGnD,UAAU,SAAS;IACjB,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC;IACtC,CAAC,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACvB,CAAC,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACvB,MAAM,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC7B,IAAI,CAAC,EAAE,SAAS,GAAG,cAAc,GAAG,MAAM,GAAG,SAAS,CAAC;IACvD,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC9B;AAED,UAAU,UAAU;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,SAAS,EAAE,OAAO,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,eAAO,MAAM,QAAQ,EAAE,IAAI,CAAC,SAAS,EAAE,UAAU,CA8DhD,CAAC"}
package/dist/diff.js CHANGED
@@ -3,11 +3,11 @@ import { spawn } from "node:child_process";
3
3
  import { statSync } from "node:fs";
4
4
  import * as fs from "node:fs/promises";
5
5
  import * as path2 from "node:path";
6
- import { buildChildEnv } from "@wrongstack/core";
6
+ import { buildChildEnv } from "@wrongstack/core/utils";
7
7
 
8
8
  // src/_util.ts
9
9
  import * as path from "node:path";
10
- import * as Core from "@wrongstack/core";
10
+ import * as Core from "@wrongstack/core/utils";
11
11
  function resolvePath(input, ctx) {
12
12
  return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.workingDir ?? ctx.cwd, input);
13
13
  }
package/dist/diff.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/diff.ts", "../src/_util.ts"],
4
- "sourcesContent": ["import { spawn } from 'node:child_process';\nimport { statSync } from 'node:fs';\nimport * as fs from 'node:fs/promises';\nimport * as path from 'node:path';\nimport { buildChildEnv } from '@wrongstack/core';\nimport type { Tool } from '@wrongstack/core';\nimport { safeResolve } from './_util.js';\n\ninterface DiffInput {\n path?: string | undefined;\n files?: string | string[] | undefined;\n a?: string | undefined;\n b?: string | undefined;\n staged?: boolean | undefined;\n mode?: 'unified' | 'side-by-side' | 'stat' | undefined;\n context?: number | undefined;\n}\n\ninterface DiffOutput {\n diff: string;\n files: string[];\n truncated: boolean;\n mode: string;\n}\n\nexport const diffTool: Tool<DiffInput, DiffOutput> = {\n name: 'diff',\n category: 'Filesystem',\n description:\n 'Show file content with line numbers, staged/working-tree diffs via git, or commit/branch diffs. A safer and more structured alternative to raw `git diff` via shell.',\n usageHint:\n 'USE FOR CODE REVIEW AND CHANGE INSPECTION:\\n\\n' +\n '- `files` + no `a`/`b` \u2192 show file content with line numbers (NOT a unified diff; no +/- prefixes).\\n' +\n '- `a` and/or `b` \u2192 git-style commit/branch diff (unified format, real +/- prefixes).\\n' +\n '- `staged: true` \u2192 only show staged changes.\\n' +\n '- `mode` can be \"unified\", \"stat\", or \"side-by-side\" (only affects the git-diff path).\\n' +\n '\\n' +\n 'NOTE: For a true file-vs-file unified diff, supply `a` and `b` so the tool ' +\n 'delegates to `git diff`. The `files`-only path is a line-numbered dump, not a diff.\\n' +\n '\\n' +\n 'This tool has important safety guards against flag injection (see previous security findings).',\n permission: 'auto',\n mutating: false,\n capabilities: ['fs.read'],\n icon: 'diff',\n timeoutMs: 10_000,\n inputSchema: {\n type: 'object',\n properties: {\n path: {\n type: 'string',\n description: 'Working directory for the diff operation (defaults to project root).',\n },\n files: {\n type: 'string',\n description: 'Files or globs to diff (e.g. \"src/**/*.ts\" or comma-separated list).',\n },\n a: {\n type: 'string',\n description: 'First ref/commit/branch for git diff (e.g. HEAD, main, a commit hash).',\n },\n b: {\n type: 'string',\n description: 'Second ref/commit/branch for git diff.',\n },\n staged: {\n type: 'boolean',\n description: 'If true, only show changes that are staged in git.',\n },\n mode: {\n type: 'string',\n enum: ['unified', 'side-by-side', 'stat'],\n description: 'Output format. \"unified\" is default, \"stat\" shows summary only.',\n },\n context: {\n type: 'integer',\n description: 'Number of context lines for unified diffs (default: 3).',\n },\n },\n },\n async execute(input, ctx, opts) {\n if (input.a !== undefined || input.b !== undefined) {\n return await gitDiff(input, ctx, opts.signal);\n }\n\n return await fileDiff(input, ctx, opts.signal);\n },\n};\n\nasync function gitDiff(\n input: DiffInput,\n ctx: import('@wrongstack/core').Context,\n signal: AbortSignal,\n): Promise<DiffOutput> {\n // Flag injection: a/b are passed as positional args BEFORE the `--`\n // separator, so a leading '-' would be parsed as a git option. The most\n // dangerous is `--output=<path>`, which makes `git diff` write to an\n // arbitrary path (outside the project root, with no confirmation since this\n // tool is permission:'auto'). Reject leading-dash refs unconditionally \u2014\n // mirrors the guard in git.ts (validateWorktreeInput) and install.ts.\n if (input.a?.startsWith('-')) {\n throw new Error(`diff: unsafe ref \"${input.a}\" \u2014 refs may not begin with '-' (flag injection)`);\n }\n if (input.b?.startsWith('-')) {\n throw new Error(`diff: unsafe ref \"${input.b}\" \u2014 refs may not begin with '-' (flag injection)`);\n }\n\n const gitDir = findGitDir(ctx.cwd);\n if (!gitDir) {\n return { diff: '', files: [], truncated: false, mode: 'unified' };\n }\n\n const args: string[] = ['diff', '--no-color'];\n if (input.staged) args.push('--staged');\n if (input.a) args.push(input.a);\n if (input.b) args.push(input.b);\n if (input.files) {\n const files = Array.isArray(input.files) ? input.files : input.files.split(',');\n args.push('--', ...files.map((f) => f.trim()));\n }\n\n const result = await runGit(args, gitDir, signal);\n return {\n diff: result.stdout,\n files: [],\n truncated: result.stdout.length > 100_000,\n mode: 'unified',\n };\n}\n\nfunction findGitDir(cwd: string): string | null {\n let dir = cwd;\n for (let i = 0; i < 20; i++) {\n try {\n const stat = statSync(path.join(dir, '.git'));\n if (stat.isDirectory()) return dir;\n } catch {\n // continue\n }\n const parent = path.dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n return null;\n}\n\nfunction runGit(\n args: string[],\n cwd: string,\n signal: AbortSignal,\n): Promise<{ stdout: string; stderr: string; exitCode: number }> {\n return new Promise((resolve) => {\n let stdout = '';\n let stderr = '';\n\n const child = spawn('git', args, {\n cwd,\n signal,\n env: buildChildEnv(),\n stdio: ['ignore', 'pipe', 'pipe'],\n windowsHide: true,\n });\n child.stdout?.on('data', (c) => {\n stdout += c.toString();\n });\n child.stderr?.on('data', (c) => {\n stderr += c.toString();\n });\n child.on('close', (code) => resolve({ stdout, stderr, exitCode: code ?? 0 }));\n /* v8 ignore next -- spawn 'error' only fires when the git binary is missing; defensive. */\n child.on('error', (e) => resolve({ stdout: '', stderr: e.message, exitCode: 1 }));\n });\n}\n\nasync function fileDiff(\n input: DiffInput,\n ctx: import('@wrongstack/core').Context,\n _signal: AbortSignal,\n): Promise<DiffOutput> {\n // `context` is accepted on the input schema for API stability but is\n // unused in the line-dump path \u2014 there is no notion of \"context lines\"\n // when there is no real diff. The git-diff path (`a`/`b`) ignores it too.\n void input.context;\n\n const files = input.files\n ? (Array.isArray(input.files) ? input.files : input.files.split(','))\n .map((f) => f.trim())\n .filter(Boolean)\n : [];\n\n if (files.length === 0) {\n return {\n diff: 'No files specified',\n files: [],\n truncated: false,\n mode: input.mode ?? 'unified',\n };\n }\n\n const results: string[] = [];\n\n for (const file of files) {\n const absPath = safeResolve(file, ctx);\n const stat = await fs.stat(absPath).catch(() => null);\n if (!stat?.isFile()) continue;\n\n const content = await fs.readFile(absPath, 'utf8');\n const lines = content.split(/\\r?\\n/);\n results.push(formatWithLineNumbers(file, lines));\n }\n\n return {\n diff: results.join('\\n\\n'),\n files,\n truncated: false,\n mode: input.mode ?? 'unified',\n };\n}\n\n/**\n * Render a file's content as a line-numbered dump. This is intentionally\n * NOT a unified diff \u2014 it has no `-`/`+` prefixes. For a real diff\n * between two revisions, use the `a`/`b` params (delegates to `git diff`).\n *\n * Format: ` N | content` with the line number right-aligned. The\n * `context` parameter is accepted for API compatibility but unused \u2014\n * a line dump has no notion of \"context lines\".\n */\nfunction formatWithLineNumbers(file: string, lines: string[]): string {\n const width = String(lines.length).length;\n const numbered = lines.map((line, i) => `${String(i + 1).padStart(width)} | ${line}`).join('\\n');\n return `--- ${file} (line-numbered dump, not a unified diff) ---\\n${numbered}`;\n}\n", "import { createHash } from 'node:crypto';\nimport * as fsp from 'node:fs/promises';\nimport * as path from 'node:path';\nimport * as Core from '@wrongstack/core';\nimport type { Context } from '@wrongstack/core';\n\n/**\n * sha-256 hex of a UTF-8 string. Used by the file tools to record a content\n * hash alongside the mtime in `ctx.recordRead` \u2014 the hash is the authoritative\n * staleness arbiter for `edit` (mtime has a 2 s tolerance window on Windows).\n */\nexport function sha256hex(content: string): string {\n return createHash('sha256').update(content, 'utf8').digest('hex');\n}\n/** Detected package manager for a project directory. */\nexport type PackageManager = 'pnpm' | 'yarn' | 'npm';\n\n/**\n * Detect the project's package manager by inspecting lockfiles in `cwd`.\n * Order: pnpm \u2192 yarn \u2192 npm (default). Missing or unreadable directories fall\n * back to `npm` rather than throwing, so a `safeResolve`-checked cwd that\n * happens to be empty never aborts the tool.\n */\nexport async function detectPackageManager(cwd: string): Promise<PackageManager> {\n const { stat } = await import('node:fs/promises');\n try {\n await stat(`${cwd}/pnpm-lock.yaml`);\n return 'pnpm';\n } catch {\n /* not pnpm */\n }\n try {\n await stat(`${cwd}/yarn.lock`);\n return 'yarn';\n } catch {\n /* not yarn */\n }\n return 'npm';\n}\n\nexport function resolvePath(input: string, ctx: Context): string {\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.workingDir ?? ctx.cwd, input);\n}\n\n/**\n * Roots every file tool may always reach, even in restricted mode: the\n * project root and the user-global `~/.wrongstack` directory (config, memory,\n * sessions, skills). `~/.wrongstack` honors the `WRONGSTACK_HOME` override.\n */\nfunction allowedRoots(ctx: Context): string[] {\n return [path.resolve(ctx.projectRoot), path.resolve(Core.wstackGlobalRoot())];\n}\n\n/** True if `target` is `root` itself or nested inside any of `roots`. */\nfunction isInsideAny(target: string, roots: string[]): boolean {\n return roots.some((root) => {\n const rel = path.relative(root, target);\n return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));\n });\n}\n\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\n const target = path.resolve(absPath);\n // Unrestricted filesystem access: skip the project-root containment check.\n if (ctx.allowOutsideProjectRoot) return target;\n if (isInsideAny(target, allowedRoots(ctx))) return target;\n throw new Error(`Path \"${absPath}\" is outside project root \"${path.resolve(ctx.projectRoot)}\"`);\n}\n\nexport function safeResolve(input: string, ctx: Context): string {\n return ensureInsideRoot(resolvePath(input, ctx), ctx);\n}\n\n/**\n * Defense against in-root\u2192out-of-root symlink escape (CWE-59). `safeResolve`\n * only does a syntactic `../` check, so a symlink that lives *inside* the\n * project root but points outside still passes it. This resolves the path\n * through `fs.realpath` and re-verifies containment against the realpath of\n * the project root (comparing like-for-like, since the root itself may be a\n * symlink \u2014 macOS `/var`\u2192`/private/var`, Windows 8.3 short names). For a path\n * that does not exist yet (e.g. a `write` to a new file) the nearest existing\n * ancestor directory is checked instead. Throws if the real target escapes.\n *\n * Mirrors the per-file guard already used in `replace.ts`/`grep.ts`; applied\n * to single-file `read`/`edit`/`write` it throws (rather than skips) because\n * the caller named exactly one file.\n */\nexport async function assertRealInsideRoot(absPath: string, ctx: Context): Promise<void> {\n // Unrestricted filesystem access: no symlink-escape check to perform.\n if (ctx.allowOutsideProjectRoot) return;\n // Compare like-for-like against the realpath of each always-allowed root\n // (project root + ~/.wrongstack), since a root may itself be a symlink.\n const realRoots = await Promise.all(\n allowedRoots(ctx).map((r) => fsp.realpath(r).catch(() => path.resolve(r))),\n );\n let probe = absPath;\n for (;;) {\n let real: string;\n try {\n real = await fsp.realpath(probe);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n const parent = path.dirname(probe);\n if (parent === probe) return; // reached fs root without escaping\n probe = parent;\n continue;\n }\n throw err;\n }\n if (isInsideAny(real, realRoots)) return;\n throw new Error(\n `Path \"${absPath}\" resolves through a symlink outside project root \"${realRoots[0]}\"`,\n );\n }\n}\n\n/** `safeResolve` + symlink realpath containment check. Async. */\nexport async function safeResolveReal(input: string, ctx: Context): Promise<string> {\n const abs = safeResolve(input, ctx);\n await assertRealInsideRoot(abs, ctx);\n return abs;\n}\n\nexport function truncateMiddle(s: string, max: number): string {\n if (Buffer.byteLength(s, 'utf8') <= max) return s;\n const half = Math.floor(max / 2);\n return (\n s.slice(0, half) +\n `\\n\u2026[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]\u2026\\n` +\n s.slice(-half)\n );\n}\n\nexport function isBinaryBuffer(buf: Buffer): boolean {\n const len = Math.min(buf.length, 8192);\n for (let i = 0; i < len; i++) {\n if (buf[i] === 0) return true;\n }\n return false;\n}\n\n// \u2500\u2500\u2500 Command-output normalization (token-saving) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// Raw process output is full of tokens the model gains nothing from: ANSI\n// escapes, carriage-return progress spam, runs of identical warning lines, and\n// huge tails of build noise. These helpers strip that noise before the output\n// reaches the LLM. They are scoped to COMMAND tools (bash/git/exec and the\n// _spawn-stream consumers) \u2014 never applied to structured/code outputs.\n\n/** Unified byte cap for all command tool output fed to the model. */\nexport const COMMAND_OUTPUT_MAX_BYTES = 32_768;\n\n/** Runs of >= this many identical consecutive lines are collapsed. */\nconst REPEAT_RUN_THRESHOLD = 3;\n\n/**\n * Collapse carriage-return overwrites the way a terminal would: `\\r\\n` becomes\n * `\\n`, and a bare `\\r` (progress redraw) keeps only the text after the LAST\n * `\\r` on its physical line. Without this, a single progress bar that redraws\n * 200 times explodes into 200 lines.\n */\nexport function collapseCarriageReturns(text: string): string {\n const lf = text.replace(/\\r\\n/g, '\\n');\n if (!lf.includes('\\r')) return lf;\n return lf\n .split('\\n')\n .map((line) => (line.includes('\\r') ? line.slice(line.lastIndexOf('\\r') + 1) : line))\n .join('\\n');\n}\n\n/**\n * Collapse a run of `minRun`+ identical consecutive lines into the line once\n * plus a marker. Consecutive-only \u2014 it never reorders or dedups non-adjacent\n * lines, so diffs/source stay intact.\n */\nexport function collapseConsecutiveDuplicates(text: string, minRun = REPEAT_RUN_THRESHOLD): string {\n const lines = text.split('\\n');\n const out: string[] = [];\n let i = 0;\n while (i < lines.length) {\n let j = i + 1;\n while (j < lines.length && lines[j] === lines[i]) j++;\n const run = j - i;\n if (run >= minRun) {\n out.push(lines[i]!, `\u2026 \u27E8repeated ${run}\u00D7\u27E9`);\n } else {\n for (let k = i; k < j; k++) out.push(lines[k]!);\n }\n i = j;\n }\n return out.join('\\n');\n}\n\n/** Largest prefix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeHeadBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n /* v8 ignore next -- only caller (truncateHeadTail) passes a budget smaller than s; defensive. */\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(0, mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(0, lo);\n}\n\n/** Largest suffix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeTailBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n /* v8 ignore next -- only caller (truncateHeadTail) passes a budget smaller than s; defensive. */\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(s.length - mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(s.length - lo);\n}\n\n/**\n * Truncate to `maxBytes` keeping BOTH ends \u2014 the head (what ran / early context)\n * and the tail (errors and summaries usually land last), biased ~45/55 toward\n * the tail. The result never exceeds `maxBytes`.\n */\nexport function truncateHeadTail(s: string, maxBytes: number): string {\n const total = Buffer.byteLength(s, 'utf8');\n if (total <= maxBytes) return s;\n // Reserve a fixed allowance for the marker so the final string can't exceed\n // the cap even though the dropped-byte count's digit width varies.\n const MARKER_RESERVE = 64;\n const avail = Math.max(0, maxBytes - MARKER_RESERVE);\n const headBudget = Math.floor(avail * 0.45);\n const head = takeHeadBytes(s, headBudget);\n const tail = takeTailBytes(s, avail - Buffer.byteLength(head, 'utf8'));\n const kept = Buffer.byteLength(head, 'utf8') + Buffer.byteLength(tail, 'utf8');\n return `${head}\\n\u2026[truncated ${total - kept} bytes]\u2026\\n${tail}`;\n}\n\n/**\n * Full token-saving pipeline for command tool output: strip ANSI \u2192 collapse\n * carriage-return progress \u2192 trim trailing whitespace \u2192 collapse identical\n * consecutive lines \u2192 squeeze blank-line runs \u2192 head+tail truncate to the cap.\n */\nexport function normalizeCommandOutput(\n raw: string,\n opts: { maxBytes?: number | undefined } = {},\n): string {\n if (!raw) return raw;\n let text = Core.stripAnsi(raw);\n text = collapseCarriageReturns(text);\n text = text.replace(/[ \\t]+$/gm, ''); // trailing whitespace per line\n text = collapseConsecutiveDuplicates(text);\n text = text.replace(/\\n{3,}/g, '\\n\\n'); // >=2 blank lines \u2192 1\n return truncateHeadTail(text, opts.maxBytes ?? COMMAND_OUTPUT_MAX_BYTES);\n}\n"],
4
+ "sourcesContent": ["import { spawn } from 'node:child_process';\nimport { statSync } from 'node:fs';\nimport * as fs from 'node:fs/promises';\nimport * as path from 'node:path';\nimport { buildChildEnv } from '@wrongstack/core/utils';\nimport type { Tool } from '@wrongstack/core/types';\nimport { safeResolve } from './_util.js';\n\ninterface DiffInput {\n path?: string | undefined;\n files?: string | string[] | undefined;\n a?: string | undefined;\n b?: string | undefined;\n staged?: boolean | undefined;\n mode?: 'unified' | 'side-by-side' | 'stat' | undefined;\n context?: number | undefined;\n}\n\ninterface DiffOutput {\n diff: string;\n files: string[];\n truncated: boolean;\n mode: string;\n}\n\nexport const diffTool: Tool<DiffInput, DiffOutput> = {\n name: 'diff',\n category: 'Filesystem',\n description:\n 'Show file content with line numbers, staged/working-tree diffs via git, or commit/branch diffs. A safer and more structured alternative to raw `git diff` via shell.',\n usageHint:\n 'USE FOR CODE REVIEW AND CHANGE INSPECTION:\\n\\n' +\n '- `files` + no `a`/`b` \u2192 show file content with line numbers (NOT a unified diff; no +/- prefixes).\\n' +\n '- `a` and/or `b` \u2192 git-style commit/branch diff (unified format, real +/- prefixes).\\n' +\n '- `staged: true` \u2192 only show staged changes.\\n' +\n '- `mode` can be \"unified\", \"stat\", or \"side-by-side\" (only affects the git-diff path).\\n' +\n '\\n' +\n 'NOTE: For a true file-vs-file unified diff, supply `a` and `b` so the tool ' +\n 'delegates to `git diff`. The `files`-only path is a line-numbered dump, not a diff.\\n' +\n '\\n' +\n 'This tool has important safety guards against flag injection (see previous security findings).',\n permission: 'auto',\n mutating: false,\n capabilities: ['fs.read'],\n icon: 'diff',\n timeoutMs: 10_000,\n inputSchema: {\n type: 'object',\n properties: {\n path: {\n type: 'string',\n description: 'Working directory for the diff operation (defaults to project root).',\n },\n files: {\n type: 'string',\n description: 'Files or globs to diff (e.g. \"src/**/*.ts\" or comma-separated list).',\n },\n a: {\n type: 'string',\n description: 'First ref/commit/branch for git diff (e.g. HEAD, main, a commit hash).',\n },\n b: {\n type: 'string',\n description: 'Second ref/commit/branch for git diff.',\n },\n staged: {\n type: 'boolean',\n description: 'If true, only show changes that are staged in git.',\n },\n mode: {\n type: 'string',\n enum: ['unified', 'side-by-side', 'stat'],\n description: 'Output format. \"unified\" is default, \"stat\" shows summary only.',\n },\n context: {\n type: 'integer',\n description: 'Number of context lines for unified diffs (default: 3).',\n },\n },\n },\n async execute(input, ctx, opts) {\n if (input.a !== undefined || input.b !== undefined) {\n return await gitDiff(input, ctx, opts.signal);\n }\n\n return await fileDiff(input, ctx, opts.signal);\n },\n};\n\nasync function gitDiff(\n input: DiffInput,\n ctx: import('@wrongstack/core/agent').Context,\n signal: AbortSignal,\n): Promise<DiffOutput> {\n // Flag injection: a/b are passed as positional args BEFORE the `--`\n // separator, so a leading '-' would be parsed as a git option. The most\n // dangerous is `--output=<path>`, which makes `git diff` write to an\n // arbitrary path (outside the project root, with no confirmation since this\n // tool is permission:'auto'). Reject leading-dash refs unconditionally \u2014\n // mirrors the guard in git.ts (validateWorktreeInput) and install.ts.\n if (input.a?.startsWith('-')) {\n throw new Error(`diff: unsafe ref \"${input.a}\" \u2014 refs may not begin with '-' (flag injection)`);\n }\n if (input.b?.startsWith('-')) {\n throw new Error(`diff: unsafe ref \"${input.b}\" \u2014 refs may not begin with '-' (flag injection)`);\n }\n\n const gitDir = findGitDir(ctx.cwd);\n if (!gitDir) {\n return { diff: '', files: [], truncated: false, mode: 'unified' };\n }\n\n const args: string[] = ['diff', '--no-color'];\n if (input.staged) args.push('--staged');\n if (input.a) args.push(input.a);\n if (input.b) args.push(input.b);\n if (input.files) {\n const files = Array.isArray(input.files) ? input.files : input.files.split(',');\n args.push('--', ...files.map((f) => f.trim()));\n }\n\n const result = await runGit(args, gitDir, signal);\n return {\n diff: result.stdout,\n files: [],\n truncated: result.stdout.length > 100_000,\n mode: 'unified',\n };\n}\n\nfunction findGitDir(cwd: string): string | null {\n let dir = cwd;\n for (let i = 0; i < 20; i++) {\n try {\n const stat = statSync(path.join(dir, '.git'));\n if (stat.isDirectory()) return dir;\n } catch {\n // continue\n }\n const parent = path.dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n return null;\n}\n\nfunction runGit(\n args: string[],\n cwd: string,\n signal: AbortSignal,\n): Promise<{ stdout: string; stderr: string; exitCode: number }> {\n return new Promise((resolve) => {\n let stdout = '';\n let stderr = '';\n\n const child = spawn('git', args, {\n cwd,\n signal,\n env: buildChildEnv(),\n stdio: ['ignore', 'pipe', 'pipe'],\n windowsHide: true,\n });\n child.stdout?.on('data', (c) => {\n stdout += c.toString();\n });\n child.stderr?.on('data', (c) => {\n stderr += c.toString();\n });\n child.on('close', (code) => resolve({ stdout, stderr, exitCode: code ?? 0 }));\n /* v8 ignore next -- spawn 'error' only fires when the git binary is missing; defensive. */\n child.on('error', (e) => resolve({ stdout: '', stderr: e.message, exitCode: 1 }));\n });\n}\n\nasync function fileDiff(\n input: DiffInput,\n ctx: import('@wrongstack/core/agent').Context,\n _signal: AbortSignal,\n): Promise<DiffOutput> {\n // `context` is accepted on the input schema for API stability but is\n // unused in the line-dump path \u2014 there is no notion of \"context lines\"\n // when there is no real diff. The git-diff path (`a`/`b`) ignores it too.\n void input.context;\n\n const files = input.files\n ? (Array.isArray(input.files) ? input.files : input.files.split(','))\n .map((f) => f.trim())\n .filter(Boolean)\n : [];\n\n if (files.length === 0) {\n return {\n diff: 'No files specified',\n files: [],\n truncated: false,\n mode: input.mode ?? 'unified',\n };\n }\n\n const results: string[] = [];\n\n for (const file of files) {\n const absPath = safeResolve(file, ctx);\n const stat = await fs.stat(absPath).catch(() => null);\n if (!stat?.isFile()) continue;\n\n const content = await fs.readFile(absPath, 'utf8');\n const lines = content.split(/\\r?\\n/);\n results.push(formatWithLineNumbers(file, lines));\n }\n\n return {\n diff: results.join('\\n\\n'),\n files,\n truncated: false,\n mode: input.mode ?? 'unified',\n };\n}\n\n/**\n * Render a file's content as a line-numbered dump. This is intentionally\n * NOT a unified diff \u2014 it has no `-`/`+` prefixes. For a real diff\n * between two revisions, use the `a`/`b` params (delegates to `git diff`).\n *\n * Format: ` N | content` with the line number right-aligned. The\n * `context` parameter is accepted for API compatibility but unused \u2014\n * a line dump has no notion of \"context lines\".\n */\nfunction formatWithLineNumbers(file: string, lines: string[]): string {\n const width = String(lines.length).length;\n const numbered = lines.map((line, i) => `${String(i + 1).padStart(width)} | ${line}`).join('\\n');\n return `--- ${file} (line-numbered dump, not a unified diff) ---\\n${numbered}`;\n}\n", "import { createHash } from 'node:crypto';\nimport * as fsp from 'node:fs/promises';\nimport * as path from 'node:path';\nimport * as Core from '@wrongstack/core/utils';\nimport type { Context } from '@wrongstack/core/agent';\n\n/**\n * sha-256 hex of a UTF-8 string. Used by the file tools to record a content\n * hash alongside the mtime in `ctx.recordRead` \u2014 the hash is the authoritative\n * staleness arbiter for `edit` (mtime has a 2 s tolerance window on Windows).\n */\nexport function sha256hex(content: string): string {\n return createHash('sha256').update(content, 'utf8').digest('hex');\n}\n/** Detected package manager for a project directory. */\nexport type PackageManager = 'pnpm' | 'yarn' | 'npm';\n\n/**\n * Detect the project's package manager by inspecting lockfiles in `cwd`.\n * Order: pnpm \u2192 yarn \u2192 npm (default). Missing or unreadable directories fall\n * back to `npm` rather than throwing, so a `safeResolve`-checked cwd that\n * happens to be empty never aborts the tool.\n */\nexport async function detectPackageManager(cwd: string): Promise<PackageManager> {\n const { stat } = await import('node:fs/promises');\n try {\n await stat(`${cwd}/pnpm-lock.yaml`);\n return 'pnpm';\n } catch {\n /* not pnpm */\n }\n try {\n await stat(`${cwd}/yarn.lock`);\n return 'yarn';\n } catch {\n /* not yarn */\n }\n return 'npm';\n}\n\nexport function resolvePath(input: string, ctx: Context): string {\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.workingDir ?? ctx.cwd, input);\n}\n\n/**\n * Roots every file tool may always reach, even in restricted mode: the\n * project root and the user-global `~/.wrongstack` directory (config, memory,\n * sessions, skills). `~/.wrongstack` honors the `WRONGSTACK_HOME` override.\n */\nfunction allowedRoots(ctx: Context): string[] {\n return [path.resolve(ctx.projectRoot), path.resolve(Core.wstackGlobalRoot())];\n}\n\n/** True if `target` is `root` itself or nested inside any of `roots`. */\nfunction isInsideAny(target: string, roots: string[]): boolean {\n return roots.some((root) => {\n const rel = path.relative(root, target);\n return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));\n });\n}\n\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\n const target = path.resolve(absPath);\n // Unrestricted filesystem access: skip the project-root containment check.\n if (ctx.allowOutsideProjectRoot) return target;\n if (isInsideAny(target, allowedRoots(ctx))) return target;\n throw new Error(`Path \"${absPath}\" is outside project root \"${path.resolve(ctx.projectRoot)}\"`);\n}\n\nexport function safeResolve(input: string, ctx: Context): string {\n return ensureInsideRoot(resolvePath(input, ctx), ctx);\n}\n\n/**\n * Defense against in-root\u2192out-of-root symlink escape (CWE-59). `safeResolve`\n * only does a syntactic `../` check, so a symlink that lives *inside* the\n * project root but points outside still passes it. This resolves the path\n * through `fs.realpath` and re-verifies containment against the realpath of\n * the project root (comparing like-for-like, since the root itself may be a\n * symlink \u2014 macOS `/var`\u2192`/private/var`, Windows 8.3 short names). For a path\n * that does not exist yet (e.g. a `write` to a new file) the nearest existing\n * ancestor directory is checked instead. Throws if the real target escapes.\n *\n * Mirrors the per-file guard already used in `replace.ts`/`grep.ts`; applied\n * to single-file `read`/`edit`/`write` it throws (rather than skips) because\n * the caller named exactly one file.\n */\nexport async function assertRealInsideRoot(absPath: string, ctx: Context): Promise<void> {\n // Unrestricted filesystem access: no symlink-escape check to perform.\n if (ctx.allowOutsideProjectRoot) return;\n // Compare like-for-like against the realpath of each always-allowed root\n // (project root + ~/.wrongstack), since a root may itself be a symlink.\n const realRoots = await Promise.all(\n allowedRoots(ctx).map((r) => fsp.realpath(r).catch(() => path.resolve(r))),\n );\n let probe = absPath;\n for (;;) {\n let real: string;\n try {\n real = await fsp.realpath(probe);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n const parent = path.dirname(probe);\n if (parent === probe) return; // reached fs root without escaping\n probe = parent;\n continue;\n }\n throw err;\n }\n if (isInsideAny(real, realRoots)) return;\n throw new Error(\n `Path \"${absPath}\" resolves through a symlink outside project root \"${realRoots[0]}\"`,\n );\n }\n}\n\n/** `safeResolve` + symlink realpath containment check. Async. */\nexport async function safeResolveReal(input: string, ctx: Context): Promise<string> {\n const abs = safeResolve(input, ctx);\n await assertRealInsideRoot(abs, ctx);\n return abs;\n}\n\nexport function truncateMiddle(s: string, max: number): string {\n if (Buffer.byteLength(s, 'utf8') <= max) return s;\n const half = Math.floor(max / 2);\n return (\n s.slice(0, half) +\n `\\n\u2026[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]\u2026\\n` +\n s.slice(-half)\n );\n}\n\nexport function isBinaryBuffer(buf: Buffer): boolean {\n const len = Math.min(buf.length, 8192);\n for (let i = 0; i < len; i++) {\n if (buf[i] === 0) return true;\n }\n return false;\n}\n\n// \u2500\u2500\u2500 Command-output normalization (token-saving) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// Raw process output is full of tokens the model gains nothing from: ANSI\n// escapes, carriage-return progress spam, runs of identical warning lines, and\n// huge tails of build noise. These helpers strip that noise before the output\n// reaches the LLM. They are scoped to COMMAND tools (bash/git/exec and the\n// _spawn-stream consumers) \u2014 never applied to structured/code outputs.\n\n/** Unified byte cap for all command tool output fed to the model. */\nexport const COMMAND_OUTPUT_MAX_BYTES = 32_768;\n\n/** Runs of >= this many identical consecutive lines are collapsed. */\nconst REPEAT_RUN_THRESHOLD = 3;\n\n/**\n * Collapse carriage-return overwrites the way a terminal would: `\\r\\n` becomes\n * `\\n`, and a bare `\\r` (progress redraw) keeps only the text after the LAST\n * `\\r` on its physical line. Without this, a single progress bar that redraws\n * 200 times explodes into 200 lines.\n */\nexport function collapseCarriageReturns(text: string): string {\n const lf = text.replace(/\\r\\n/g, '\\n');\n if (!lf.includes('\\r')) return lf;\n return lf\n .split('\\n')\n .map((line) => (line.includes('\\r') ? line.slice(line.lastIndexOf('\\r') + 1) : line))\n .join('\\n');\n}\n\n/**\n * Collapse a run of `minRun`+ identical consecutive lines into the line once\n * plus a marker. Consecutive-only \u2014 it never reorders or dedups non-adjacent\n * lines, so diffs/source stay intact.\n */\nexport function collapseConsecutiveDuplicates(text: string, minRun = REPEAT_RUN_THRESHOLD): string {\n const lines = text.split('\\n');\n const out: string[] = [];\n let i = 0;\n while (i < lines.length) {\n let j = i + 1;\n while (j < lines.length && lines[j] === lines[i]) j++;\n const run = j - i;\n if (run >= minRun) {\n out.push(lines[i]!, `\u2026 \u27E8repeated ${run}\u00D7\u27E9`);\n } else {\n for (let k = i; k < j; k++) out.push(lines[k]!);\n }\n i = j;\n }\n return out.join('\\n');\n}\n\n/** Largest prefix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeHeadBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n /* v8 ignore next -- only caller (truncateHeadTail) passes a budget smaller than s; defensive. */\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(0, mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(0, lo);\n}\n\n/** Largest suffix of `s` whose UTF-8 byte length is <= `maxBytes`. */\nfunction takeTailBytes(s: string, maxBytes: number): string {\n if (maxBytes <= 0) return '';\n /* v8 ignore next -- only caller (truncateHeadTail) passes a budget smaller than s; defensive. */\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n let lo = 0;\n let hi = s.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n if (Buffer.byteLength(s.slice(s.length - mid), 'utf8') <= maxBytes) lo = mid;\n else hi = mid - 1;\n }\n return s.slice(s.length - lo);\n}\n\n/**\n * Truncate to `maxBytes` keeping BOTH ends \u2014 the head (what ran / early context)\n * and the tail (errors and summaries usually land last), biased ~45/55 toward\n * the tail. The result never exceeds `maxBytes`.\n */\nexport function truncateHeadTail(s: string, maxBytes: number): string {\n const total = Buffer.byteLength(s, 'utf8');\n if (total <= maxBytes) return s;\n // Reserve a fixed allowance for the marker so the final string can't exceed\n // the cap even though the dropped-byte count's digit width varies.\n const MARKER_RESERVE = 64;\n const avail = Math.max(0, maxBytes - MARKER_RESERVE);\n const headBudget = Math.floor(avail * 0.45);\n const head = takeHeadBytes(s, headBudget);\n const tail = takeTailBytes(s, avail - Buffer.byteLength(head, 'utf8'));\n const kept = Buffer.byteLength(head, 'utf8') + Buffer.byteLength(tail, 'utf8');\n return `${head}\\n\u2026[truncated ${total - kept} bytes]\u2026\\n${tail}`;\n}\n\n/**\n * Full token-saving pipeline for command tool output: strip ANSI \u2192 collapse\n * carriage-return progress \u2192 trim trailing whitespace \u2192 collapse identical\n * consecutive lines \u2192 squeeze blank-line runs \u2192 head+tail truncate to the cap.\n */\nexport function normalizeCommandOutput(\n raw: string,\n opts: { maxBytes?: number | undefined } = {},\n): string {\n if (!raw) return raw;\n let text = Core.stripAnsi(raw);\n text = collapseCarriageReturns(text);\n text = text.replace(/[ \\t]+$/gm, ''); // trailing whitespace per line\n text = collapseConsecutiveDuplicates(text);\n text = text.replace(/\\n{3,}/g, '\\n\\n'); // >=2 blank lines \u2192 1\n return truncateHeadTail(text, opts.maxBytes ?? COMMAND_OUTPUT_MAX_BYTES);\n}\n"],
5
5
  "mappings": "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",
6
6
  "names": ["path", "stat", "resolve"]
7
7
  }
@@ -1,4 +1,4 @@
1
- import type { Tool } from '@wrongstack/core';
1
+ import type { Tool } from '@wrongstack/core/types';
2
2
  interface DocumentInput {
3
3
  target: 'file' | 'function' | 'class' | 'type' | 'all';
4
4
  path?: string | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"document.d.ts","sourceRoot":"","sources":["../src/document.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAG7C,UAAU,aAAa;IACrB,MAAM,EAAE,MAAM,GAAG,UAAU,GAAG,OAAO,GAAG,MAAM,GAAG,KAAK,CAAC;IACvD,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC;IACtC,KAAK,CAAC,EAAE,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,SAAS,CAAC;IAChD,SAAS,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAChC,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC1B;AAED,UAAU,cAAc;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,YAAY,GAAG,SAAS,GAAG,OAAO,CAAC;IAC3C,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5B;AAED,UAAU,cAAc;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,MAAM,CAAC;IACzB,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC;CACf;AAED,eAAO,MAAM,YAAY,EAAE,IAAI,CAAC,aAAa,EAAE,cAAc,CAoF5D,CAAC"}
1
+ {"version":3,"file":"document.d.ts","sourceRoot":"","sources":["../src/document.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAGnD,UAAU,aAAa;IACrB,MAAM,EAAE,MAAM,GAAG,UAAU,GAAG,OAAO,GAAG,MAAM,GAAG,KAAK,CAAC;IACvD,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC;IACtC,KAAK,CAAC,EAAE,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,SAAS,CAAC;IAChD,SAAS,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAChC,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC1B;AAED,UAAU,cAAc;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,YAAY,GAAG,SAAS,GAAG,OAAO,CAAC;IAC3C,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5B;AAED,UAAU,cAAc;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,MAAM,CAAC;IACzB,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC;CACf;AAED,eAAO,MAAM,YAAY,EAAE,IAAI,CAAC,aAAa,EAAE,cAAc,CAoF5D,CAAC"}
package/dist/document.js CHANGED
@@ -3,7 +3,7 @@ import * as fs from "node:fs/promises";
3
3
 
4
4
  // src/_util.ts
5
5
  import * as path from "node:path";
6
- import * as Core from "@wrongstack/core";
6
+ import * as Core from "@wrongstack/core/utils";
7
7
  function resolvePath(input, ctx) {
8
8
  return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.workingDir ?? ctx.cwd, input);
9
9
  }