@design-ai/cli 4.55.0 → 4.57.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 (301) hide show
  1. package/.claude-plugin/plugin.json +109 -25
  2. package/CHANGELOG.md +75 -0
  3. package/README.ko.md +15 -32
  4. package/README.md +18 -31
  5. package/cli/bin/design-ai.mjs +1 -0
  6. package/cli/commands/help.mjs +7 -4
  7. package/cli/commands/index.mjs +292 -0
  8. package/cli/commands/learn-help.mjs +149 -0
  9. package/cli/commands/learn-print-profile.mjs +373 -0
  10. package/cli/commands/learn-print-restore.mjs +349 -0
  11. package/cli/commands/learn-print-signals.mjs +453 -0
  12. package/cli/commands/learn.mjs +60 -1281
  13. package/cli/commands/pack.mjs +7 -3
  14. package/cli/commands/prompt.mjs +7 -3
  15. package/cli/commands/search.mjs +94 -1
  16. package/cli/lib/dispatch.mjs +3 -0
  17. package/cli/lib/embedding-index.mjs +199 -0
  18. package/cli/lib/embedding-provider.mjs +121 -0
  19. package/cli/lib/embedding-rerank.mjs +52 -0
  20. package/cli/lib/learn-args.mjs +490 -0
  21. package/cli/lib/learn-backup.mjs +748 -0
  22. package/cli/lib/learn-curation.mjs +612 -0
  23. package/cli/lib/learn-eval.mjs +459 -0
  24. package/cli/lib/learn-profile.mjs +763 -0
  25. package/cli/lib/learn-select.mjs +223 -0
  26. package/cli/lib/learn-shared.mjs +158 -0
  27. package/cli/lib/learn-test-support.mjs +218 -0
  28. package/cli/lib/learn-usage.mjs +360 -0
  29. package/cli/lib/learn.mjs +83 -3694
  30. package/cli/lib/lexical.mjs +137 -0
  31. package/cli/lib/local-config.mjs +110 -0
  32. package/cli/lib/mcp-server.mjs +221 -13
  33. package/cli/lib/pack.mjs +31 -4
  34. package/cli/lib/prompt.mjs +42 -4
  35. package/cli/lib/recall.mjs +193 -0
  36. package/cli/lib/retrieval-index.mjs +0 -0
  37. package/cli/lib/search-ranked.mjs +170 -0
  38. package/cli/lib/search.mjs +11 -1
  39. package/cli/lib/signals-backlog-commands.mjs +673 -0
  40. package/cli/lib/signals-backlog.mjs +361 -0
  41. package/cli/lib/signals-eval.mjs +176 -0
  42. package/cli/lib/signals-readiness.mjs +247 -0
  43. package/cli/lib/signals-registry.mjs +377 -0
  44. package/cli/lib/signals-render.mjs +478 -0
  45. package/cli/lib/signals-shared.mjs +75 -0
  46. package/cli/lib/signals.mjs +16 -2337
  47. package/cli/lib/site-analysis.mjs +297 -0
  48. package/cli/lib/site-args.mjs +433 -0
  49. package/cli/lib/site-bundle-build.mjs +127 -0
  50. package/cli/lib/site-bundle-check.mjs +454 -0
  51. package/cli/lib/site-bundle-commands.mjs +95 -0
  52. package/cli/lib/site-bundle-compare.mjs +157 -0
  53. package/cli/lib/site-bundle-contract.mjs +79 -0
  54. package/cli/lib/site-bundle-files.mjs +87 -0
  55. package/cli/lib/site-bundle-handoff-expected.mjs +173 -0
  56. package/cli/lib/site-bundle-handoff-runbook-action-summary.mjs +332 -0
  57. package/cli/lib/site-bundle-handoff-runbook-actions.mjs +113 -0
  58. package/cli/lib/site-bundle-handoff-runbook-evidence-fields.mjs +164 -0
  59. package/cli/lib/site-bundle-handoff-runbook-evidence.mjs +334 -0
  60. package/cli/lib/site-bundle-handoff-runbook-format.mjs +31 -0
  61. package/cli/lib/site-bundle-handoff-runbook-human-lines.mjs +167 -0
  62. package/cli/lib/site-bundle-handoff-runbook-maps.mjs +238 -0
  63. package/cli/lib/site-bundle-handoff-runbook-next-step.mjs +278 -0
  64. package/cli/lib/site-bundle-handoff-runbook-stage-metadata.mjs +84 -0
  65. package/cli/lib/site-bundle-handoff-runbook.mjs +444 -0
  66. package/cli/lib/site-bundle-handoff-summary.mjs +183 -0
  67. package/cli/lib/site-bundle-handoff.mjs +271 -0
  68. package/cli/lib/site-bundle-readme.mjs +98 -0
  69. package/cli/lib/site-bundle-repair-report.mjs +143 -0
  70. package/cli/lib/site-bundle-repair.mjs +68 -0
  71. package/cli/lib/site-content.mjs +399 -0
  72. package/cli/lib/site-evidence.mjs +35 -0
  73. package/cli/lib/site-mcp-commands.mjs +28 -0
  74. package/cli/lib/site-mcp-probes.mjs +159 -0
  75. package/cli/lib/site-mcp-readiness.mjs +157 -0
  76. package/cli/lib/site-mcp-report.mjs +324 -0
  77. package/cli/lib/site-next-actions.mjs +333 -0
  78. package/cli/lib/site-options.mjs +104 -0
  79. package/cli/lib/site-prompts.mjs +332 -0
  80. package/cli/lib/site-starter.mjs +153 -0
  81. package/cli/lib/site-strings.mjs +23 -0
  82. package/cli/lib/site-tasks.mjs +93 -0
  83. package/cli/lib/site-test-support.mjs +68 -0
  84. package/cli/lib/site-workflow-graph.mjs +309 -0
  85. package/cli/lib/site-workspace.mjs +492 -0
  86. package/cli/lib/site.mjs +108 -6617
  87. package/cli/lib/skill-proposals-apply-commands.mjs +135 -0
  88. package/cli/lib/skill-proposals-apply-contract.mjs +750 -0
  89. package/cli/lib/skill-proposals-apply-plan.mjs +118 -0
  90. package/cli/lib/skill-proposals-generate.mjs +298 -0
  91. package/cli/lib/skill-proposals-render.mjs +532 -0
  92. package/cli/lib/skill-proposals-review.mjs +262 -0
  93. package/cli/lib/skill-proposals.mjs +15 -2046
  94. package/cli/lib/workspace-args.mjs +83 -0
  95. package/cli/lib/workspace-git.mjs +169 -0
  96. package/cli/lib/workspace-learning.mjs +483 -0
  97. package/cli/lib/workspace-repo.mjs +139 -0
  98. package/cli/lib/workspace-report.mjs +283 -0
  99. package/cli/lib/workspace-test-support.mjs +99 -0
  100. package/cli/lib/workspace.mjs +30 -1056
  101. package/docs/AI-LEARNING-PHASE2.md +215 -0
  102. package/docs/DISTRIBUTION.ko.md +35 -6
  103. package/docs/DISTRIBUTION.md +35 -8
  104. package/docs/NEXT-SURFACE-DECISION.md +125 -0
  105. package/docs/PRODUCT-READINESS.md +3 -3
  106. package/docs/RELEASE-CHECKLIST.md +20 -3
  107. package/docs/RELEASE-GATES.ko.md +35 -0
  108. package/docs/RELEASE-GATES.md +234 -0
  109. package/docs/ROADMAP.md +2232 -8
  110. package/docs/external-status.md +20 -7
  111. package/docs/inspection-20260630.md +169 -0
  112. package/docs/integrations/design-ai-mcp-server.md +35 -3
  113. package/docs/integrations/vscode-walkthrough.ko.md +3 -3
  114. package/docs/integrations/vscode-walkthrough.md +3 -3
  115. package/docs/reference/ant-design.md +413 -0
  116. package/docs/reference/awesome-design-md.md +439 -0
  117. package/docs/reference/mui.md +783 -0
  118. package/docs/reference/shadcn-ui.md +298 -0
  119. package/docs/site-overrides/main.html +1 -1
  120. package/examples/component-accordion-actions.md +5 -5
  121. package/examples/component-accordion-details.md +5 -5
  122. package/examples/component-accordion-summary.md +5 -5
  123. package/examples/component-accordion.md +3 -3
  124. package/examples/component-affix.md +1 -1
  125. package/examples/component-alert-dialog.md +1 -1
  126. package/examples/component-alert-title.md +1 -1
  127. package/examples/component-alert.md +3 -3
  128. package/examples/component-anchor.md +1 -1
  129. package/examples/component-app-bar.md +1 -1
  130. package/examples/component-aspect-ratio.md +1 -1
  131. package/examples/component-auto-complete.md +2 -2
  132. package/examples/component-avatar-group.md +5 -5
  133. package/examples/component-avatar.md +3 -3
  134. package/examples/component-back-top.md +1 -1
  135. package/examples/component-backdrop.md +1 -1
  136. package/examples/component-badge.md +3 -3
  137. package/examples/component-border-beam.md +3 -3
  138. package/examples/component-bottom-navigation.md +1 -1
  139. package/examples/component-box.md +1 -1
  140. package/examples/component-breadcrumb.md +3 -3
  141. package/examples/component-button-base.md +3 -3
  142. package/examples/component-button-group.md +2 -2
  143. package/examples/component-button.md +3 -3
  144. package/examples/component-calendar.md +2 -2
  145. package/examples/component-card-actions.md +1 -1
  146. package/examples/component-card-content.md +1 -1
  147. package/examples/component-card-header.md +1 -1
  148. package/examples/component-card-media.md +1 -1
  149. package/examples/component-card.md +3 -3
  150. package/examples/component-carousel.md +2 -2
  151. package/examples/component-cascader.md +1 -1
  152. package/examples/component-chart.md +1 -1
  153. package/examples/component-checkbox.md +3 -3
  154. package/examples/component-click-away-listener.md +1 -1
  155. package/examples/component-code.md +1 -1
  156. package/examples/component-collapsible.md +1 -1
  157. package/examples/component-color-picker.md +1 -1
  158. package/examples/component-combobox.md +1 -1
  159. package/examples/component-command.md +1 -1
  160. package/examples/component-config-provider.md +3 -3
  161. package/examples/component-context-menu.md +1 -1
  162. package/examples/component-css-baseline.md +3 -3
  163. package/examples/component-date-picker.md +2 -2
  164. package/examples/component-descriptions.md +1 -1
  165. package/examples/component-dialog-actions.md +1 -1
  166. package/examples/component-dialog-content-text.md +1 -1
  167. package/examples/component-dialog-content.md +1 -1
  168. package/examples/component-dialog-title.md +1 -1
  169. package/examples/component-dialog.md +2 -2
  170. package/examples/component-divider.md +3 -3
  171. package/examples/component-drawer.md +3 -3
  172. package/examples/component-dropdown.md +3 -3
  173. package/examples/component-empty.md +2 -2
  174. package/examples/component-fade.md +1 -1
  175. package/examples/component-field.md +1 -1
  176. package/examples/component-filled-input.md +1 -1
  177. package/examples/component-flex.md +1 -1
  178. package/examples/component-float-button.md +2 -2
  179. package/examples/component-form-control-label.md +1 -1
  180. package/examples/component-form-control.md +1 -1
  181. package/examples/component-form-controls.md +1 -1
  182. package/examples/component-form-group.md +1 -1
  183. package/examples/component-form-helper-text.md +1 -1
  184. package/examples/component-form-label.md +1 -1
  185. package/examples/component-form.md +2 -2
  186. package/examples/component-grid.md +2 -2
  187. package/examples/component-grow.md +1 -1
  188. package/examples/component-hover-card.md +1 -1
  189. package/examples/component-icon-button.md +1 -1
  190. package/examples/component-icon.md +1 -1
  191. package/examples/component-image-list.md +1 -1
  192. package/examples/component-image.md +1 -1
  193. package/examples/component-input-adornment.md +1 -1
  194. package/examples/component-input-base.md +1 -1
  195. package/examples/component-input-number.md +1 -1
  196. package/examples/component-input-otp.md +2 -2
  197. package/examples/component-input.md +3 -3
  198. package/examples/component-item.md +1 -1
  199. package/examples/component-label.md +1 -1
  200. package/examples/component-layout.md +1 -1
  201. package/examples/component-link.md +1 -1
  202. package/examples/component-list-item-avatar.md +1 -1
  203. package/examples/component-list-item-button.md +1 -1
  204. package/examples/component-list-item-icon.md +1 -1
  205. package/examples/component-list-item-text.md +1 -1
  206. package/examples/component-list-item.md +1 -1
  207. package/examples/component-list-subheader.md +1 -1
  208. package/examples/component-list.md +2 -2
  209. package/examples/component-masonry.md +1 -1
  210. package/examples/component-mentions.md +1 -1
  211. package/examples/component-menu-item.md +1 -1
  212. package/examples/component-menu-list.md +1 -1
  213. package/examples/component-menu.md +1 -1
  214. package/examples/component-menubar.md +1 -1
  215. package/examples/component-message.md +1 -1
  216. package/examples/component-mobile-stepper.md +1 -1
  217. package/examples/component-modal.md +5 -5
  218. package/examples/component-navigation-menu.md +1 -1
  219. package/examples/component-notification.md +1 -1
  220. package/examples/component-outlined-input.md +1 -1
  221. package/examples/component-pagination.md +3 -3
  222. package/examples/component-paper.md +1 -1
  223. package/examples/component-popconfirm.md +1 -1
  224. package/examples/component-popover.md +3 -3
  225. package/examples/component-popper.md +1 -1
  226. package/examples/component-progress.md +2 -2
  227. package/examples/component-qr-code.md +1 -1
  228. package/examples/component-radio.md +3 -3
  229. package/examples/component-rate.md +2 -2
  230. package/examples/component-resizable.md +1 -1
  231. package/examples/component-result.md +1 -1
  232. package/examples/component-scroll-area.md +1 -1
  233. package/examples/component-segmented.md +3 -3
  234. package/examples/component-select.md +3 -3
  235. package/examples/component-separator.md +1 -1
  236. package/examples/component-sheet.md +1 -1
  237. package/examples/component-sidebar.md +1 -1
  238. package/examples/component-skeleton.md +3 -3
  239. package/examples/component-slide.md +1 -1
  240. package/examples/component-slider.md +3 -3
  241. package/examples/component-snackbar-content.md +1 -1
  242. package/examples/component-snackbar.md +1 -1
  243. package/examples/component-sonner.md +1 -1
  244. package/examples/component-space.md +1 -1
  245. package/examples/component-speed-dial-action.md +1 -1
  246. package/examples/component-speed-dial.md +1 -1
  247. package/examples/component-spin.md +2 -2
  248. package/examples/component-spinner.md +1 -1
  249. package/examples/component-splitter.md +2 -2
  250. package/examples/component-stack.md +1 -1
  251. package/examples/component-statistic.md +1 -1
  252. package/examples/component-step-button.md +4 -4
  253. package/examples/component-step-connector.md +4 -4
  254. package/examples/component-step-content.md +1 -1
  255. package/examples/component-step-icon.md +1 -1
  256. package/examples/component-step-label.md +1 -1
  257. package/examples/component-step.md +2 -2
  258. package/examples/component-steps.md +2 -2
  259. package/examples/component-swipeable-drawer.md +1 -1
  260. package/examples/component-switch.md +3 -3
  261. package/examples/component-tab-scroll-button.md +5 -5
  262. package/examples/component-tab.md +1 -1
  263. package/examples/component-table-body.md +1 -1
  264. package/examples/component-table-cell.md +1 -1
  265. package/examples/component-table-container.md +1 -1
  266. package/examples/component-table-footer.md +1 -1
  267. package/examples/component-table-head.md +1 -1
  268. package/examples/component-table-pagination.md +1 -1
  269. package/examples/component-table-row.md +1 -1
  270. package/examples/component-table-sort-label.md +1 -1
  271. package/examples/component-table.md +2 -2
  272. package/examples/component-tabs.md +3 -3
  273. package/examples/component-tag-badge.md +2 -2
  274. package/examples/component-tag.md +1 -1
  275. package/examples/component-textarea-autosize.md +1 -1
  276. package/examples/component-textarea.md +1 -1
  277. package/examples/component-time-picker.md +1 -1
  278. package/examples/component-timeline.md +1 -1
  279. package/examples/component-toast.md +4 -4
  280. package/examples/component-toggle-button.md +1 -1
  281. package/examples/component-toggle.md +1 -1
  282. package/examples/component-toolbar.md +1 -1
  283. package/examples/component-tooltip.md +3 -3
  284. package/examples/component-tour.md +1 -1
  285. package/examples/component-transfer.md +1 -1
  286. package/examples/component-tree-select.md +1 -1
  287. package/examples/component-tree.md +1 -1
  288. package/examples/component-typography.md +2 -2
  289. package/examples/component-upload.md +1 -1
  290. package/examples/component-watermark.md +1 -1
  291. package/examples/component-zoom.md +1 -1
  292. package/knowledge/COVERAGE.md +4 -3
  293. package/knowledge/components/INDEX.md +202 -202
  294. package/knowledge/patterns/brand-references.md +72 -72
  295. package/package.json +1 -1
  296. package/tools/audit/local-ci.py +16 -1
  297. package/tools/audit/package-smoke.py +717 -9
  298. package/tools/audit/registry-smoke.py +912 -10
  299. package/tools/audit/release-metadata.py +47 -1
  300. package/tools/audit/smoke_assertions.py +998 -35
  301. package/tools/migrations/refs-links-to-reference-pages.py +179 -0
@@ -0,0 +1,137 @@
1
+ // Shared deterministic lexical (BM25-style) scoring for corpus and learning retrieval.
2
+ // Phase A of docs/AI-LEARNING-PHASE2.md: zero-dependency, no randomness, no time-dependent scoring.
3
+
4
+ export const BM25_K1 = 1.2;
5
+ export const BM25_B = 0.75;
6
+
7
+ // Hangul syllable block (U+AC00–U+D7A3). Korean surface forms are written without
8
+ // spaces between stem and particle (버튼을, 접근성이), so a whitespace-delimited token
9
+ // is an atomic surface form. To let a stem query match particle-attached documents
10
+ // (and vice versa), we deterministically expand every Hangul run of length >= 2 into
11
+ // its overlapping character bigrams and emit them ALONGSIDE the original token.
12
+ // 버튼을 -> keep 버튼을, add 버튼, 튼을
13
+ // 접근성 -> keep 접근성, add 접근, 근성
14
+ // 버튼 -> the 2-char run's only bigram is itself; kept via dedupe
15
+ // Latin/number tokens and mixed tokens' non-Hangul parts are unchanged. The rule is
16
+ // pure and deterministic (no locale, no time, no randomness); order is normalized by
17
+ // the dedupe+sort in lexicalQueryTokens and by termFrequencies for stats.
18
+ const HANGUL_RUN = /[가-힣]{2,}/gu;
19
+
20
+ function hangulBigrams(token) {
21
+ const bigrams = [];
22
+ for (const run of token.match(HANGUL_RUN) || []) {
23
+ for (let i = 0; i + 2 <= run.length; i += 1) {
24
+ bigrams.push(run.slice(i, i + 2));
25
+ }
26
+ }
27
+ return bigrams;
28
+ }
29
+
30
+ // Same base token model as learn-select.mjs: Unicode letters/numbers, lowercased,
31
+ // length >= 2. Hangul tokens additionally emit character bigrams (see HANGUL_RUN).
32
+ export function lexicalTokens(text) {
33
+ const base = (String(text || "")
34
+ .toLowerCase()
35
+ .match(/[\p{L}\p{N}]+/gu) || [])
36
+ .filter((token) => token.length >= 2);
37
+
38
+ const expanded = [];
39
+ for (const token of base) {
40
+ expanded.push(token);
41
+ for (const bigram of hangulBigrams(token)) {
42
+ // A 2-char Hangul run's only bigram is the token itself; do not re-emit it,
43
+ // which would double-count its term frequency and distort BM25.
44
+ if (bigram !== token) expanded.push(bigram);
45
+ }
46
+ }
47
+ return expanded;
48
+ }
49
+
50
+ export function lexicalQueryTokens(query) {
51
+ return Array.from(new Set(lexicalTokens(query))).sort();
52
+ }
53
+
54
+ function termFrequencies(tokens) {
55
+ const terms = {};
56
+ for (const token of tokens) {
57
+ terms[token] = (terms[token] || 0) + 1;
58
+ }
59
+ const sorted = {};
60
+ for (const term of Object.keys(terms).sort()) {
61
+ sorted[term] = terms[term];
62
+ }
63
+ return sorted;
64
+ }
65
+
66
+ // documents: [{ id, text }] -> deterministic index stats with sorted ids and term keys.
67
+ export function buildLexicalStats(documents) {
68
+ const indexed = [...documents]
69
+ .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
70
+ .map((doc) => {
71
+ const tokens = lexicalTokens(doc.text);
72
+ return { id: doc.id, length: tokens.length, terms: termFrequencies(tokens) };
73
+ });
74
+
75
+ const docFrequency = {};
76
+ for (const doc of indexed) {
77
+ for (const term of Object.keys(doc.terms)) {
78
+ docFrequency[term] = (docFrequency[term] || 0) + 1;
79
+ }
80
+ }
81
+ const sortedDocFrequency = {};
82
+ for (const term of Object.keys(docFrequency).sort()) {
83
+ sortedDocFrequency[term] = docFrequency[term];
84
+ }
85
+
86
+ const totalLength = indexed.reduce((sum, doc) => sum + doc.length, 0);
87
+ return {
88
+ documentCount: indexed.length,
89
+ termCount: Object.keys(sortedDocFrequency).length,
90
+ avgDocLength: indexed.length > 0 ? totalLength / indexed.length : 0,
91
+ docFrequency: sortedDocFrequency,
92
+ documents: indexed,
93
+ };
94
+ }
95
+
96
+ function inverseDocumentFrequency(documentCount, docFrequency) {
97
+ return Math.log(1 + (documentCount - docFrequency + 0.5) / (docFrequency + 0.5));
98
+ }
99
+
100
+ export function bm25Score(queryTokens, doc, stats) {
101
+ if (stats.documentCount === 0 || doc.length === 0) return { score: 0, matchedTokens: [] };
102
+
103
+ let score = 0;
104
+ const matchedTokens = [];
105
+ for (const token of queryTokens) {
106
+ const tf = doc.terms[token] || 0;
107
+ if (tf === 0) continue;
108
+ const df = stats.docFrequency[token] || 0;
109
+ const idf = inverseDocumentFrequency(stats.documentCount, df);
110
+ const lengthNorm = 1 - BM25_B + BM25_B * (doc.length / (stats.avgDocLength || 1));
111
+ score += idf * ((tf * (BM25_K1 + 1)) / (tf + BM25_K1 * lengthNorm));
112
+ matchedTokens.push(token);
113
+ }
114
+ return { score, matchedTokens };
115
+ }
116
+
117
+ // Fully ordered ranking: score desc, then id asc. Zero-score documents are excluded.
118
+ export function rankLexical(query, stats, { limit = 20 } = {}) {
119
+ const queryTokens = lexicalQueryTokens(query);
120
+ if (queryTokens.length === 0) return [];
121
+
122
+ const ranked = [];
123
+ for (const doc of stats.documents) {
124
+ const { score, matchedTokens } = bm25Score(queryTokens, doc, stats);
125
+ if (score > 0) {
126
+ ranked.push({ id: doc.id, score, matchedTokens });
127
+ }
128
+ }
129
+ ranked.sort((a, b) => {
130
+ if (b.score !== a.score) return b.score - a.score;
131
+ return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
132
+ });
133
+ return ranked.slice(0, limit).map((hit) => ({
134
+ ...hit,
135
+ score: Number(hit.score.toFixed(6)),
136
+ }));
137
+ }
@@ -0,0 +1,110 @@
1
+ // Local, user-authored configuration for optional Phase B features
2
+ // (docs/AI-LEARNING-PHASE2.md, "Phase A implementation review", decision 5 / FU-4).
3
+ //
4
+ // design-ai never writes this file. It is durable, per-machine, user-authored
5
+ // configuration — the CLI only reads it. Presence of a configured provider is
6
+ // NOT enough to enable embeddings: every consumer still requires the explicit
7
+ // per-invocation --embeddings flag (config supplies the provider, the flag arms it).
8
+
9
+ import { existsSync, readFileSync } from "node:fs";
10
+ import { homedir } from "node:os";
11
+ import path from "node:path";
12
+
13
+ export function defaultConfigFile() {
14
+ return process.env.DESIGN_AI_CONFIG_FILE || path.join(homedir(), ".design-ai", "config.json");
15
+ }
16
+
17
+ const TOP_LEVEL_KEYS = new Set(["version", "embeddings"]);
18
+ const EMBEDDINGS_KEYS = new Set(["provider", "modelLabel"]);
19
+ const PROVIDER_KEYS = new Set(["command", "args"]);
20
+
21
+ function fail(message) {
22
+ return { present: true, config: null, error: message };
23
+ }
24
+
25
+ function isPlainObject(value) {
26
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
27
+ }
28
+
29
+ function validateProvider(provider) {
30
+ if (!isPlainObject(provider)) return "embeddings.provider must be an object";
31
+
32
+ const unknown = Object.keys(provider).filter((key) => !PROVIDER_KEYS.has(key));
33
+ if (unknown.length > 0) return `embeddings.provider has unknown key(s): ${unknown.join(", ")}`;
34
+
35
+ if (typeof provider.command !== "string" || provider.command.trim() === "") {
36
+ return "embeddings.provider.command is required and must be a non-empty string";
37
+ }
38
+
39
+ if (provider.args !== undefined) {
40
+ if (!Array.isArray(provider.args) || !provider.args.every((arg) => typeof arg === "string")) {
41
+ return "embeddings.provider.args must be an array of strings";
42
+ }
43
+ }
44
+
45
+ return "";
46
+ }
47
+
48
+ function validateEmbeddings(embeddings) {
49
+ if (!isPlainObject(embeddings)) return "embeddings must be an object";
50
+
51
+ const unknown = Object.keys(embeddings).filter((key) => !EMBEDDINGS_KEYS.has(key));
52
+ if (unknown.length > 0) return `embeddings has unknown key(s): ${unknown.join(", ")}`;
53
+
54
+ if (embeddings.provider === undefined) return "embeddings.provider is required";
55
+ const providerError = validateProvider(embeddings.provider);
56
+ if (providerError) return providerError;
57
+
58
+ if (embeddings.modelLabel !== undefined && typeof embeddings.modelLabel !== "string") {
59
+ return "embeddings.modelLabel must be a string";
60
+ }
61
+
62
+ return "";
63
+ }
64
+
65
+ function validateConfig(config) {
66
+ if (!isPlainObject(config)) return "config must be a JSON object";
67
+
68
+ const unknown = Object.keys(config).filter((key) => !TOP_LEVEL_KEYS.has(key));
69
+ if (unknown.length > 0) return `config has unknown top-level key(s): ${unknown.join(", ")}`;
70
+
71
+ if (config.version !== 1) return "config.version must be 1";
72
+
73
+ if (config.embeddings !== undefined) {
74
+ const embeddingsError = validateEmbeddings(config.embeddings);
75
+ if (embeddingsError) return embeddingsError;
76
+ }
77
+
78
+ return "";
79
+ }
80
+
81
+ // Never writes; design-ai only reads this user-authored file.
82
+ export function loadLocalConfig(filePath = defaultConfigFile()) {
83
+ if (!existsSync(filePath)) return { present: false, config: null, error: "" };
84
+
85
+ let raw;
86
+ try {
87
+ raw = readFileSync(filePath, "utf8");
88
+ } catch (error) {
89
+ return fail(`unreadable config file: ${error.message}`);
90
+ }
91
+
92
+ let parsed;
93
+ try {
94
+ parsed = JSON.parse(raw);
95
+ } catch (error) {
96
+ return fail(`config file is not valid JSON: ${error.message}`);
97
+ }
98
+
99
+ const error = validateConfig(parsed);
100
+ if (error) return fail(error);
101
+
102
+ return { present: true, config: parsed, error: "" };
103
+ }
104
+
105
+ // Convenience accessor: the configured embedding provider, or null when absent
106
+ // or the config is malformed/missing. Callers still must gate on --embeddings.
107
+ export function configuredEmbeddingProvider(filePath = defaultConfigFile()) {
108
+ const { config } = loadLocalConfig(filePath);
109
+ return config?.embeddings?.provider || null;
110
+ }
@@ -10,6 +10,14 @@ import { DESIGN_AI_HOME, PACKAGE_ROOT } from "./paths.mjs";
10
10
  const PROTOCOL_VERSION = "2025-11-25";
11
11
  const MAX_TOOL_OUTPUT_BYTES = 220_000;
12
12
  const DESIGN_AI_BIN = path.join(PACKAGE_ROOT, "cli", "bin", "design-ai.mjs");
13
+ const MCP_RESPONSE_METHODS = new Set([
14
+ "initialize",
15
+ "ping",
16
+ "tools/list",
17
+ "tools/call",
18
+ "resources/list",
19
+ "prompts/list",
20
+ ]);
13
21
 
14
22
  function optionalString(description = "") {
15
23
  return { type: "string", description };
@@ -42,7 +50,7 @@ export const MCP_TOOLS = [
42
50
  {
43
51
  name: "design_ai_prompt",
44
52
  title: "Generate an agent prompt",
45
- description: "Generate a ready-to-use design-ai agent prompt from a brief. Read-only unless withLearning is true, which records local usage metadata.",
53
+ description: "Generate a ready-to-use design-ai agent prompt from a brief. Read-only unless withLearning is true, which records local usage metadata. Supports opt-in recall augmentation via withRecall, which enriches the output with brief-relevant shipped corpus knowledge.",
46
54
  inputSchema: {
47
55
  type: "object",
48
56
  properties: {
@@ -51,6 +59,8 @@ export const MCP_TOOLS = [
51
59
  withLearning: optionalBoolean("Include local learning preferences and record local usage metadata."),
52
60
  learningCategory: optionalString("Optional learning category when withLearning is true."),
53
61
  learningLimit: optionalInteger({ description: "Maximum learning entries, 1-100.", minimum: 1, maximum: 100 }),
62
+ withRecall: optionalBoolean("Opt-in recall augmentation: enriches the output with brief-relevant shipped corpus knowledge ranked by the deterministic lexical scorer. Requires no index and makes no network call."),
63
+ recallLimit: optionalInteger({ description: "Maximum recall passages, 1-20.", minimum: 1, maximum: 20 }),
54
64
  json: optionalBoolean("Return machine-readable prompt plan JSON instead of Markdown."),
55
65
  },
56
66
  required: ["brief"],
@@ -60,7 +70,7 @@ export const MCP_TOOLS = [
60
70
  {
61
71
  name: "design_ai_pack",
62
72
  title: "Generate prompt plus bounded context",
63
- description: "Generate a prompt pack with relevant design-ai context files. Read-only unless withLearning is true, which records local usage metadata.",
73
+ description: "Generate a prompt pack with relevant design-ai context files. Read-only unless withLearning is true, which records local usage metadata. Supports opt-in recall augmentation via withRecall, which enriches the output with brief-relevant shipped corpus knowledge.",
64
74
  inputSchema: {
65
75
  type: "object",
66
76
  properties: {
@@ -70,6 +80,8 @@ export const MCP_TOOLS = [
70
80
  withLearning: optionalBoolean("Include local learning preferences and record local usage metadata."),
71
81
  learningCategory: optionalString("Optional learning category when withLearning is true."),
72
82
  learningLimit: optionalInteger({ description: "Maximum learning entries, 1-100.", minimum: 1, maximum: 100 }),
83
+ withRecall: optionalBoolean("Opt-in recall augmentation: enriches the output with brief-relevant shipped corpus knowledge ranked by the deterministic lexical scorer. Requires no index and makes no network call."),
84
+ recallLimit: optionalInteger({ description: "Maximum recall passages, 1-20.", minimum: 1, maximum: 20 }),
73
85
  json: optionalBoolean("Return machine-readable pack JSON instead of Markdown."),
74
86
  },
75
87
  required: ["brief"],
@@ -79,13 +91,14 @@ export const MCP_TOOLS = [
79
91
  {
80
92
  name: "design_ai_search",
81
93
  title: "Search design-ai corpus",
82
- description: "Search knowledge, examples, skills, docs, agents, or commands. Read-only.",
94
+ description: "Search knowledge, examples, skills, docs, agents, or commands. Read-only. Supports an opt-in ranked mode for deterministic BM25-style results.",
83
95
  inputSchema: {
84
96
  type: "object",
85
97
  properties: {
86
98
  query: { type: "string", minLength: 2, description: "Search query." },
87
99
  dir: optionalString("Optional corpus directory: knowledge, examples, skills, docs, agents, commands."),
88
100
  limit: optionalInteger({ description: "Maximum hits, 1-500.", minimum: 1, maximum: 500 }),
101
+ ranked: optionalBoolean("Opt-in deterministic BM25-style ranked results. Requires no index and never builds one; falls back to a live corpus scan when no index is present."),
89
102
  },
90
103
  required: ["query"],
91
104
  additionalProperties: false,
@@ -196,6 +209,51 @@ function assertString(value, name) {
196
209
  return value;
197
210
  }
198
211
 
212
+ function assertMcpInputValue(name, value, schema) {
213
+ if (schema.type === "string") {
214
+ if (typeof value !== "string") throw new Error(`${name} must be a string`);
215
+ if (schema.minLength && value.trim() === "") {
216
+ throw new Error(`${name} must be a non-empty string`);
217
+ }
218
+ if (schema.minLength && value.length < schema.minLength) {
219
+ throw new Error(`${name} must be at least ${schema.minLength} characters`);
220
+ }
221
+ return;
222
+ }
223
+
224
+ if (schema.type === "boolean") {
225
+ if (typeof value !== "boolean") throw new Error(`${name} must be a boolean`);
226
+ return;
227
+ }
228
+
229
+ if (schema.type === "integer") {
230
+ if (!Number.isInteger(value)) throw new Error(`${name} must be an integer`);
231
+ if (schema.minimum !== undefined && value < schema.minimum) {
232
+ throw new Error(`${name} must be at least ${schema.minimum}`);
233
+ }
234
+ if (schema.maximum !== undefined && value > schema.maximum) {
235
+ throw new Error(`${name} must be at most ${schema.maximum}`);
236
+ }
237
+ }
238
+ }
239
+
240
+ function assertMcpToolInput(tool, input) {
241
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
242
+ throw new Error(`${tool.name} arguments must be an object`);
243
+ }
244
+
245
+ const properties = tool.inputSchema.properties || {};
246
+ for (const name of tool.inputSchema.required || []) {
247
+ if (!Object.hasOwn(input, name)) throw new Error(`${tool.name}.${name} is required`);
248
+ }
249
+
250
+ for (const [name, value] of Object.entries(input)) {
251
+ const schema = properties[name];
252
+ if (!schema) throw new Error(`Unknown argument for ${tool.name}: ${name}`);
253
+ assertMcpInputValue(`${tool.name}.${name}`, value, schema);
254
+ }
255
+ }
256
+
199
257
  function maybePush(args, flag, value) {
200
258
  if (value !== undefined && value !== null && value !== "") args.push(flag, String(value));
201
259
  }
@@ -228,6 +286,8 @@ export function buildCliInvocation(toolName, input = {}) {
228
286
  maybeBool(args, "--with-learning", input.withLearning);
229
287
  maybePush(args, "--learning-category", input.learningCategory);
230
288
  maybePush(args, "--learning-limit", input.learningLimit);
289
+ maybeBool(args, "--with-recall", input.withRecall);
290
+ maybePush(args, "--recall-limit", input.recallLimit);
231
291
  maybeBool(args, "--json", input.json);
232
292
  return { args, stdin };
233
293
  }
@@ -239,6 +299,8 @@ export function buildCliInvocation(toolName, input = {}) {
239
299
  maybeBool(args, "--with-learning", input.withLearning);
240
300
  maybePush(args, "--learning-category", input.learningCategory);
241
301
  maybePush(args, "--learning-limit", input.learningLimit);
302
+ maybeBool(args, "--with-recall", input.withRecall);
303
+ maybePush(args, "--recall-limit", input.recallLimit);
242
304
  maybeBool(args, "--json", input.json);
243
305
  return { args, stdin };
244
306
  }
@@ -247,6 +309,7 @@ export function buildCliInvocation(toolName, input = {}) {
247
309
  args.push("search", assertString(input.query, "query"), "--json");
248
310
  maybePush(args, "--dir", input.dir);
249
311
  maybePush(args, "--limit", input.limit);
312
+ maybeBool(args, "--ranked", input.ranked);
250
313
  return { args, stdin };
251
314
  }
252
315
 
@@ -328,6 +391,9 @@ export function runDesignAiCli(args, { stdin = "" } = {}) {
328
391
  }
329
392
 
330
393
  export async function callMcpTool(name, input = {}, runCli = runDesignAiCli) {
394
+ const tool = MCP_TOOLS.find((item) => item.name === name);
395
+ if (tool) assertMcpToolInput(tool, input || {});
396
+
331
397
  const invocation = buildCliInvocation(name, input || {});
332
398
  const result = await runCli(invocation.args, { stdin: invocation.stdin });
333
399
  const text = [
@@ -349,14 +415,131 @@ function errorResponse(id, code, message) {
349
415
  return { jsonrpc: "2.0", id: id ?? null, error: { code, message } };
350
416
  }
351
417
 
418
+ function chooseProtocolVersion(requestedVersion) {
419
+ return requestedVersion === PROTOCOL_VERSION ? requestedVersion : PROTOCOL_VERSION;
420
+ }
421
+
422
+ function isObjectRecord(value) {
423
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
424
+ }
425
+
426
+ function validateMcpRequestEnvelope(message) {
427
+ if (!isObjectRecord(message)) {
428
+ return "Invalid MCP request: request must be an object";
429
+ }
430
+ if (message.jsonrpc !== "2.0") {
431
+ return 'Invalid MCP request: jsonrpc must be "2.0"';
432
+ }
433
+ if (hasRequestId(message) && !isValidMcpRequestId(message.id)) {
434
+ return "Invalid MCP request: id must be a string, number, or null";
435
+ }
436
+ return "";
437
+ }
438
+
439
+ function validateMcpRequestMethod(message) {
440
+ if (!Object.hasOwn(message, "method")) {
441
+ return "Invalid MCP request: missing method";
442
+ }
443
+ if (typeof message.method !== "string" || message.method.trim() === "") {
444
+ return "Invalid MCP request: method must be a non-empty string";
445
+ }
446
+ return "";
447
+ }
448
+
449
+ function isMcpNotificationMethod(method) {
450
+ return typeof method === "string" && method.startsWith("notifications/");
451
+ }
452
+
453
+ function validateMcpNotificationRequest(message) {
454
+ if (isMcpNotificationMethod(message.method) && hasRequestId(message)) {
455
+ return `Invalid MCP request: ${message.method} must not include id`;
456
+ }
457
+ return "";
458
+ }
459
+
460
+ function validateMcpRequestIdRequirement(message) {
461
+ if (MCP_RESPONSE_METHODS.has(message.method) && !hasRequestId(message)) {
462
+ return `Invalid MCP request: ${message.method} must include id`;
463
+ }
464
+ return "";
465
+ }
466
+
467
+ function validateOptionalObjectParams(method, params) {
468
+ if (params === undefined || isObjectRecord(params)) return "";
469
+ return `${method} params must be an object when provided`;
470
+ }
471
+
472
+ function validateToolCallParams(params) {
473
+ if (!isObjectRecord(params)) {
474
+ return "tools/call params must be an object";
475
+ }
476
+ if (typeof params.name !== "string" || params.name.trim() === "") {
477
+ return "tools/call params.name must be a non-empty string";
478
+ }
479
+ if (Object.hasOwn(params, "arguments") && params.arguments !== undefined && !isObjectRecord(params.arguments)) {
480
+ return "tools/call params.arguments must be an object when provided";
481
+ }
482
+ return "";
483
+ }
484
+
485
+ function validateInitializeParams(params) {
486
+ if (!isObjectRecord(params)) {
487
+ return "initialize params must be an object";
488
+ }
489
+ if (Object.hasOwn(params, "protocolVersion") && typeof params.protocolVersion !== "string") {
490
+ return "initialize params.protocolVersion must be a string";
491
+ }
492
+ if (typeof params.protocolVersion === "string" && params.protocolVersion.trim() === "") {
493
+ return "initialize params.protocolVersion must be a non-empty string";
494
+ }
495
+ return "";
496
+ }
497
+
498
+ function hasRequestId(message) {
499
+ return isObjectRecord(message) && Object.hasOwn(message, "id");
500
+ }
501
+
502
+ function isValidMcpRequestId(id) {
503
+ return id === null || typeof id === "string" || (typeof id === "number" && Number.isFinite(id));
504
+ }
505
+
506
+ function responseIdForMessage(message) {
507
+ if (!hasRequestId(message)) return undefined;
508
+ return isValidMcpRequestId(message.id) ? message.id : undefined;
509
+ }
510
+
511
+ function isInvalidRequestResponse(response) {
512
+ return response?.error?.code === -32600;
513
+ }
514
+
515
+ function shouldWriteMcpResponse(message, response) {
516
+ if (!response) return false;
517
+ return hasRequestId(message) || isInvalidRequestResponse(response);
518
+ }
519
+
352
520
  export async function handleMcpRequest(message, { runCli = runDesignAiCli } = {}) {
353
- const { id, method, params = {} } = message || {};
521
+ const id = responseIdForMessage(message);
522
+ const envelopeError = validateMcpRequestEnvelope(message);
523
+ if (envelopeError) return errorResponse(id, -32600, envelopeError);
524
+
525
+ const methodError = validateMcpRequestMethod(message);
526
+ if (methodError) return errorResponse(id, -32600, methodError);
527
+
528
+ const notificationError = validateMcpNotificationRequest(message);
529
+ if (notificationError) return errorResponse(id, -32600, notificationError);
530
+
531
+ const requestIdError = validateMcpRequestIdRequirement(message);
532
+ if (requestIdError) return errorResponse(id, -32600, requestIdError);
354
533
 
355
- if (!method) return errorResponse(id, -32600, "Invalid MCP request: missing method");
534
+ const { method, params } = message;
356
535
 
357
536
  if (method === "initialize") {
537
+ const initializeParams = params === undefined ? {} : params;
538
+ const paramsError = validateInitializeParams(initializeParams);
539
+ if (paramsError) return errorResponse(id, -32602, paramsError);
540
+
358
541
  return successResponse(id, {
359
- protocolVersion: params.protocolVersion || PROTOCOL_VERSION,
542
+ protocolVersion: chooseProtocolVersion(initializeParams.protocolVersion),
360
543
  capabilities: {
361
544
  tools: { listChanged: false },
362
545
  },
@@ -368,19 +551,36 @@ export async function handleMcpRequest(message, { runCli = runDesignAiCli } = {}
368
551
  });
369
552
  }
370
553
 
371
- if (method === "notifications/initialized") return null;
372
- if (method === "ping") return successResponse(id, {});
554
+ if (isMcpNotificationMethod(method)) return null;
555
+ if (method === "ping") {
556
+ const paramsError = validateOptionalObjectParams(method, params);
557
+ if (paramsError) return errorResponse(id, -32602, paramsError);
558
+ return successResponse(id, {});
559
+ }
373
560
 
374
561
  if (method === "tools/list") {
562
+ const paramsError = validateOptionalObjectParams(method, params);
563
+ if (paramsError) return errorResponse(id, -32602, paramsError);
375
564
  return successResponse(id, { tools: MCP_TOOLS });
376
565
  }
377
566
 
378
567
  if (method === "tools/call") {
568
+ const paramsError = validateToolCallParams(params);
569
+ if (paramsError) return errorResponse(id, -32602, paramsError);
570
+
379
571
  const name = params.name;
380
572
  const tool = MCP_TOOLS.find((item) => item.name === name);
381
573
  if (!tool) return errorResponse(id, -32602, `Unknown tool: ${name}`);
574
+
575
+ const toolArguments = params.arguments === undefined ? {} : params.arguments;
382
576
  try {
383
- const result = await callMcpTool(name, params.arguments || {}, runCli);
577
+ assertMcpToolInput(tool, toolArguments);
578
+ } catch (validationError) {
579
+ return errorResponse(id, -32602, validationError.message || String(validationError));
580
+ }
581
+
582
+ try {
583
+ const result = await callMcpTool(name, toolArguments, runCli);
384
584
  return successResponse(id, result);
385
585
  } catch (error) {
386
586
  return successResponse(id, {
@@ -390,8 +590,16 @@ export async function handleMcpRequest(message, { runCli = runDesignAiCli } = {}
390
590
  }
391
591
  }
392
592
 
393
- if (method === "resources/list") return successResponse(id, { resources: [] });
394
- if (method === "prompts/list") return successResponse(id, { prompts: [] });
593
+ if (method === "resources/list") {
594
+ const paramsError = validateOptionalObjectParams(method, params);
595
+ if (paramsError) return errorResponse(id, -32602, paramsError);
596
+ return successResponse(id, { resources: [] });
597
+ }
598
+ if (method === "prompts/list") {
599
+ const paramsError = validateOptionalObjectParams(method, params);
600
+ if (paramsError) return errorResponse(id, -32602, paramsError);
601
+ return successResponse(id, { prompts: [] });
602
+ }
395
603
 
396
604
  return errorResponse(id, -32601, `Method not found: ${method}`);
397
605
  }
@@ -411,11 +619,11 @@ export function startMcpStdioServer({ input = process.stdin, output = process.st
411
619
 
412
620
  try {
413
621
  const response = await handleMcpRequest(message);
414
- if (response && message.id !== undefined) {
622
+ if (shouldWriteMcpResponse(message, response)) {
415
623
  output.write(`${JSON.stringify(response)}\n`);
416
624
  }
417
625
  } catch (requestError) {
418
- const id = message && message.id !== undefined ? message.id : null;
626
+ const id = responseIdForMessage(message) ?? null;
419
627
  output.write(`${JSON.stringify(errorResponse(id, -32603, requestError.message || String(requestError)))}\n`);
420
628
  }
421
629
  });