@brandsystem/mcp 0.3.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 (223) hide show
  1. package/README.md +515 -0
  2. package/bin/brandsystem-mcp.mjs +2 -0
  3. package/dist/index.d.ts +2 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +20 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/lib/brand-dir.d.ts +56 -0
  8. package/dist/lib/brand-dir.d.ts.map +1 -0
  9. package/dist/lib/brand-dir.js +270 -0
  10. package/dist/lib/brand-dir.js.map +1 -0
  11. package/dist/lib/color-namer.d.ts +28 -0
  12. package/dist/lib/color-namer.d.ts.map +1 -0
  13. package/dist/lib/color-namer.js +155 -0
  14. package/dist/lib/color-namer.js.map +1 -0
  15. package/dist/lib/confidence.d.ts +19 -0
  16. package/dist/lib/confidence.d.ts.map +1 -0
  17. package/dist/lib/confidence.js +66 -0
  18. package/dist/lib/confidence.js.map +1 -0
  19. package/dist/lib/content-scorer.d.ts +38 -0
  20. package/dist/lib/content-scorer.d.ts.map +1 -0
  21. package/dist/lib/content-scorer.js +571 -0
  22. package/dist/lib/content-scorer.js.map +1 -0
  23. package/dist/lib/css-parser.d.ts +45 -0
  24. package/dist/lib/css-parser.d.ts.map +1 -0
  25. package/dist/lib/css-parser.js +330 -0
  26. package/dist/lib/css-parser.js.map +1 -0
  27. package/dist/lib/dtcg-compiler.d.ts +7 -0
  28. package/dist/lib/dtcg-compiler.d.ts.map +1 -0
  29. package/dist/lib/dtcg-compiler.js +89 -0
  30. package/dist/lib/dtcg-compiler.js.map +1 -0
  31. package/dist/lib/interaction-policy-compiler.d.ts +40 -0
  32. package/dist/lib/interaction-policy-compiler.d.ts.map +1 -0
  33. package/dist/lib/interaction-policy-compiler.js +60 -0
  34. package/dist/lib/interaction-policy-compiler.js.map +1 -0
  35. package/dist/lib/logo-extractor.d.ts +49 -0
  36. package/dist/lib/logo-extractor.d.ts.map +1 -0
  37. package/dist/lib/logo-extractor.js +384 -0
  38. package/dist/lib/logo-extractor.js.map +1 -0
  39. package/dist/lib/report-html.d.ts +20 -0
  40. package/dist/lib/report-html.d.ts.map +1 -0
  41. package/dist/lib/report-html.js +938 -0
  42. package/dist/lib/report-html.js.map +1 -0
  43. package/dist/lib/response.d.ts +20 -0
  44. package/dist/lib/response.d.ts.map +1 -0
  45. package/dist/lib/response.js +54 -0
  46. package/dist/lib/response.js.map +1 -0
  47. package/dist/lib/runtime-compiler.d.ts +60 -0
  48. package/dist/lib/runtime-compiler.d.ts.map +1 -0
  49. package/dist/lib/runtime-compiler.js +96 -0
  50. package/dist/lib/runtime-compiler.js.map +1 -0
  51. package/dist/lib/svg-resolver.d.ts +21 -0
  52. package/dist/lib/svg-resolver.d.ts.map +1 -0
  53. package/dist/lib/svg-resolver.js +115 -0
  54. package/dist/lib/svg-resolver.js.map +1 -0
  55. package/dist/lib/url-validator.d.ts +11 -0
  56. package/dist/lib/url-validator.d.ts.map +1 -0
  57. package/dist/lib/url-validator.js +93 -0
  58. package/dist/lib/url-validator.js.map +1 -0
  59. package/dist/lib/version.d.ts +2 -0
  60. package/dist/lib/version.d.ts.map +1 -0
  61. package/dist/lib/version.js +19 -0
  62. package/dist/lib/version.js.map +1 -0
  63. package/dist/lib/vim-generator.d.ts +13 -0
  64. package/dist/lib/vim-generator.d.ts.map +1 -0
  65. package/dist/lib/vim-generator.js +718 -0
  66. package/dist/lib/vim-generator.js.map +1 -0
  67. package/dist/resources/brand-resources.d.ts +4 -0
  68. package/dist/resources/brand-resources.d.ts.map +1 -0
  69. package/dist/resources/brand-resources.js +34 -0
  70. package/dist/resources/brand-resources.js.map +1 -0
  71. package/dist/schemas/brand-config.d.ts +28 -0
  72. package/dist/schemas/brand-config.d.ts.map +1 -0
  73. package/dist/schemas/brand-config.js +11 -0
  74. package/dist/schemas/brand-config.js.map +1 -0
  75. package/dist/schemas/brand-runtime.d.ts +251 -0
  76. package/dist/schemas/brand-runtime.d.ts.map +1 -0
  77. package/dist/schemas/brand-runtime.js +54 -0
  78. package/dist/schemas/brand-runtime.js.map +1 -0
  79. package/dist/schemas/core-identity.d.ts +302 -0
  80. package/dist/schemas/core-identity.d.ts.map +1 -0
  81. package/dist/schemas/core-identity.js +51 -0
  82. package/dist/schemas/core-identity.js.map +1 -0
  83. package/dist/schemas/index.d.ts +11 -0
  84. package/dist/schemas/index.d.ts.map +1 -0
  85. package/dist/schemas/index.js +11 -0
  86. package/dist/schemas/index.js.map +1 -0
  87. package/dist/schemas/interaction-policy.d.ts +150 -0
  88. package/dist/schemas/interaction-policy.d.ts.map +1 -0
  89. package/dist/schemas/interaction-policy.js +34 -0
  90. package/dist/schemas/interaction-policy.js.map +1 -0
  91. package/dist/schemas/messaging.d.ts +776 -0
  92. package/dist/schemas/messaging.d.ts.map +1 -0
  93. package/dist/schemas/messaging.js +68 -0
  94. package/dist/schemas/messaging.js.map +1 -0
  95. package/dist/schemas/needs-clarification.d.ts +62 -0
  96. package/dist/schemas/needs-clarification.d.ts.map +1 -0
  97. package/dist/schemas/needs-clarification.js +13 -0
  98. package/dist/schemas/needs-clarification.js.map +1 -0
  99. package/dist/schemas/strategy.d.ts +537 -0
  100. package/dist/schemas/strategy.d.ts.map +1 -0
  101. package/dist/schemas/strategy.js +71 -0
  102. package/dist/schemas/strategy.js.map +1 -0
  103. package/dist/schemas/tokens.d.ts +35 -0
  104. package/dist/schemas/tokens.d.ts.map +1 -0
  105. package/dist/schemas/tokens.js +15 -0
  106. package/dist/schemas/tokens.js.map +1 -0
  107. package/dist/schemas/visual-identity.d.ts +224 -0
  108. package/dist/schemas/visual-identity.d.ts.map +1 -0
  109. package/dist/schemas/visual-identity.js +42 -0
  110. package/dist/schemas/visual-identity.js.map +1 -0
  111. package/dist/server.d.ts +3 -0
  112. package/dist/server.d.ts.map +1 -0
  113. package/dist/server.js +75 -0
  114. package/dist/server.js.map +1 -0
  115. package/dist/tools/brand-audit-content.d.ts +3 -0
  116. package/dist/tools/brand-audit-content.d.ts.map +1 -0
  117. package/dist/tools/brand-audit-content.js +116 -0
  118. package/dist/tools/brand-audit-content.js.map +1 -0
  119. package/dist/tools/brand-audit-drift.d.ts +3 -0
  120. package/dist/tools/brand-audit-drift.d.ts.map +1 -0
  121. package/dist/tools/brand-audit-drift.js +301 -0
  122. package/dist/tools/brand-audit-drift.js.map +1 -0
  123. package/dist/tools/brand-audit.d.ts +3 -0
  124. package/dist/tools/brand-audit.d.ts.map +1 -0
  125. package/dist/tools/brand-audit.js +129 -0
  126. package/dist/tools/brand-audit.js.map +1 -0
  127. package/dist/tools/brand-build-journey.d.ts +3 -0
  128. package/dist/tools/brand-build-journey.d.ts.map +1 -0
  129. package/dist/tools/brand-build-journey.js +312 -0
  130. package/dist/tools/brand-build-journey.js.map +1 -0
  131. package/dist/tools/brand-build-matrix.d.ts +3 -0
  132. package/dist/tools/brand-build-matrix.d.ts.map +1 -0
  133. package/dist/tools/brand-build-matrix.js +525 -0
  134. package/dist/tools/brand-build-matrix.js.map +1 -0
  135. package/dist/tools/brand-build-personas.d.ts +3 -0
  136. package/dist/tools/brand-build-personas.d.ts.map +1 -0
  137. package/dist/tools/brand-build-personas.js +436 -0
  138. package/dist/tools/brand-build-personas.js.map +1 -0
  139. package/dist/tools/brand-build-themes.d.ts +3 -0
  140. package/dist/tools/brand-build-themes.d.ts.map +1 -0
  141. package/dist/tools/brand-build-themes.js +476 -0
  142. package/dist/tools/brand-build-themes.js.map +1 -0
  143. package/dist/tools/brand-check-compliance.d.ts +3 -0
  144. package/dist/tools/brand-check-compliance.d.ts.map +1 -0
  145. package/dist/tools/brand-check-compliance.js +243 -0
  146. package/dist/tools/brand-check-compliance.js.map +1 -0
  147. package/dist/tools/brand-clarify.d.ts +21 -0
  148. package/dist/tools/brand-clarify.d.ts.map +1 -0
  149. package/dist/tools/brand-clarify.js +497 -0
  150. package/dist/tools/brand-clarify.js.map +1 -0
  151. package/dist/tools/brand-compile-messaging.d.ts +3 -0
  152. package/dist/tools/brand-compile-messaging.d.ts.map +1 -0
  153. package/dist/tools/brand-compile-messaging.js +759 -0
  154. package/dist/tools/brand-compile-messaging.js.map +1 -0
  155. package/dist/tools/brand-compile.d.ts +3 -0
  156. package/dist/tools/brand-compile.d.ts.map +1 -0
  157. package/dist/tools/brand-compile.js +182 -0
  158. package/dist/tools/brand-compile.js.map +1 -0
  159. package/dist/tools/brand-deepen-identity.d.ts +3 -0
  160. package/dist/tools/brand-deepen-identity.d.ts.map +1 -0
  161. package/dist/tools/brand-deepen-identity.js +483 -0
  162. package/dist/tools/brand-deepen-identity.js.map +1 -0
  163. package/dist/tools/brand-export.d.ts +17 -0
  164. package/dist/tools/brand-export.d.ts.map +1 -0
  165. package/dist/tools/brand-export.js +730 -0
  166. package/dist/tools/brand-export.js.map +1 -0
  167. package/dist/tools/brand-extract-figma.d.ts +3 -0
  168. package/dist/tools/brand-extract-figma.d.ts.map +1 -0
  169. package/dist/tools/brand-extract-figma.js +174 -0
  170. package/dist/tools/brand-extract-figma.js.map +1 -0
  171. package/dist/tools/brand-extract-messaging.d.ts +3 -0
  172. package/dist/tools/brand-extract-messaging.d.ts.map +1 -0
  173. package/dist/tools/brand-extract-messaging.js +620 -0
  174. package/dist/tools/brand-extract-messaging.js.map +1 -0
  175. package/dist/tools/brand-extract-web.d.ts +3 -0
  176. package/dist/tools/brand-extract-web.d.ts.map +1 -0
  177. package/dist/tools/brand-extract-web.js +477 -0
  178. package/dist/tools/brand-extract-web.js.map +1 -0
  179. package/dist/tools/brand-feedback.d.ts +3 -0
  180. package/dist/tools/brand-feedback.d.ts.map +1 -0
  181. package/dist/tools/brand-feedback.js +366 -0
  182. package/dist/tools/brand-feedback.js.map +1 -0
  183. package/dist/tools/brand-ingest-assets.d.ts +3 -0
  184. package/dist/tools/brand-ingest-assets.d.ts.map +1 -0
  185. package/dist/tools/brand-ingest-assets.js +233 -0
  186. package/dist/tools/brand-ingest-assets.js.map +1 -0
  187. package/dist/tools/brand-init.d.ts +3 -0
  188. package/dist/tools/brand-init.d.ts.map +1 -0
  189. package/dist/tools/brand-init.js +66 -0
  190. package/dist/tools/brand-init.js.map +1 -0
  191. package/dist/tools/brand-preflight.d.ts +3 -0
  192. package/dist/tools/brand-preflight.d.ts.map +1 -0
  193. package/dist/tools/brand-preflight.js +608 -0
  194. package/dist/tools/brand-preflight.js.map +1 -0
  195. package/dist/tools/brand-report.d.ts +3 -0
  196. package/dist/tools/brand-report.d.ts.map +1 -0
  197. package/dist/tools/brand-report.js +154 -0
  198. package/dist/tools/brand-report.js.map +1 -0
  199. package/dist/tools/brand-runtime.d.ts +3 -0
  200. package/dist/tools/brand-runtime.d.ts.map +1 -0
  201. package/dist/tools/brand-runtime.js +37 -0
  202. package/dist/tools/brand-runtime.js.map +1 -0
  203. package/dist/tools/brand-set-logo.d.ts +3 -0
  204. package/dist/tools/brand-set-logo.d.ts.map +1 -0
  205. package/dist/tools/brand-set-logo.js +170 -0
  206. package/dist/tools/brand-set-logo.js.map +1 -0
  207. package/dist/tools/brand-start.d.ts +3 -0
  208. package/dist/tools/brand-start.d.ts.map +1 -0
  209. package/dist/tools/brand-start.js +686 -0
  210. package/dist/tools/brand-start.js.map +1 -0
  211. package/dist/tools/brand-status.d.ts +3 -0
  212. package/dist/tools/brand-status.d.ts.map +1 -0
  213. package/dist/tools/brand-status.js +175 -0
  214. package/dist/tools/brand-status.js.map +1 -0
  215. package/dist/tools/brand-write.d.ts +3 -0
  216. package/dist/tools/brand-write.d.ts.map +1 -0
  217. package/dist/tools/brand-write.js +442 -0
  218. package/dist/tools/brand-write.js.map +1 -0
  219. package/dist/types/index.d.ts +331 -0
  220. package/dist/types/index.d.ts.map +1 -0
  221. package/dist/types/index.js +52 -0
  222. package/dist/types/index.js.map +1 -0
  223. package/package.json +60 -0
@@ -0,0 +1,759 @@
1
+ import { z } from "zod";
2
+ import { BrandDir } from "../lib/brand-dir.js";
3
+ import { buildResponse, safeParseParams } from "../lib/response.js";
4
+ import { SCHEMA_VERSION } from "../schemas/index.js";
5
+ import { ERROR_CODES, } from "../types/index.js";
6
+ const SECTIONS = ["perspective", "voice", "brand_story"];
7
+ const paramsShape = {
8
+ mode: z
9
+ .enum(["interview", "record"])
10
+ .default("interview")
11
+ .describe("'interview' returns questions for missing sections; 'record' writes answers to messaging.yaml"),
12
+ section: z
13
+ .enum(SECTIONS)
14
+ .optional()
15
+ .describe("Which section to record (required when mode='record')"),
16
+ answers: z
17
+ .string()
18
+ .optional()
19
+ .describe("JSON string with structured answers for the section (required when mode='record')"),
20
+ };
21
+ const ParamsSchema = z.object(paramsShape);
22
+ // --- Default AI-ism patterns ---
23
+ const DEFAULT_AI_ISM_PATTERNS = [
24
+ "In today's [landscape/world/era]",
25
+ "It's worth noting that",
26
+ "Let's dive in",
27
+ "In conclusion",
28
+ "Unlock [your/the] potential",
29
+ "Navigate the [landscape/complexities]",
30
+ "At the end of the day",
31
+ "It goes without saying",
32
+ "Revolutionize/Transform [your/the]",
33
+ "Cutting-edge/Best-in-class",
34
+ "Leverage [our/your/the]",
35
+ "Seamless/Seamlessly",
36
+ "Empower/Empowering",
37
+ "Delve into",
38
+ "Holistic approach",
39
+ "Synergy/Synergize",
40
+ "Game-changer/Game-changing",
41
+ "Robust [solution/platform]",
42
+ "Elevate [your/the]",
43
+ "Furthermore/Moreover",
44
+ ];
45
+ const PERSPECTIVE_QUESTIONS = [
46
+ {
47
+ key: "worldview",
48
+ question: "What do you believe about your industry that most people get wrong?",
49
+ follow_up: "This is your worldview — the lens through which everything you say is filtered.",
50
+ },
51
+ {
52
+ key: "tension",
53
+ question: "What's broken about how things work today? What's the status quo you reject?",
54
+ follow_up: "Name the specific friction or dysfunction your brand exists to fix.",
55
+ },
56
+ {
57
+ key: "resolution",
58
+ question: "If everyone adopted your worldview, what would change?",
59
+ follow_up: "Paint the picture — what does the world look like when this tension is resolved?",
60
+ },
61
+ {
62
+ key: "audience",
63
+ question: "Who's this message for? Whose problem are you solving?",
64
+ follow_up: "Be specific — not 'everyone' but the person who feels that tension most acutely.",
65
+ },
66
+ {
67
+ key: "positioning",
68
+ question: "In one sentence: what do you do that nobody else does?",
69
+ follow_up: "Not your tagline — your actual competitive differentiation.",
70
+ },
71
+ {
72
+ key: "one_liner",
73
+ question: "If you had to capture it in a tagline — 3 to 5 words?",
74
+ follow_up: "Short, punchy, memorable. This becomes your one-liner.",
75
+ },
76
+ ];
77
+ const VOICE_INTERVIEW_PARTS = [
78
+ {
79
+ part: "1",
80
+ label: "Tone",
81
+ questions: [
82
+ {
83
+ key: "descriptors",
84
+ question: "Describe your brand's voice in exactly three words.",
85
+ follow_up: "These three words define your tonal range. Everything you write should fit within them.",
86
+ },
87
+ {
88
+ key: "register",
89
+ question: "Your brand sounds like a _____ talking to a _____.",
90
+ follow_up: "Example: 'a sharp colleague talking to a peer' or 'an expert talking to a curious beginner.'",
91
+ },
92
+ {
93
+ key: "never_sounds_like",
94
+ question: "What's the one thing your brand never sounds like?",
95
+ follow_up: "This is your negative constraint — the voice you'd fire someone for using.",
96
+ },
97
+ ],
98
+ },
99
+ {
100
+ part: "2",
101
+ label: "Vocabulary",
102
+ questions: [
103
+ {
104
+ key: "anchor_terms",
105
+ question: "What words should your brand ALWAYS use instead of generic alternatives? Format: use X not Y because Z.",
106
+ follow_up: "These are your anchor terms — words that signal your brand's specific worldview.",
107
+ },
108
+ {
109
+ key: "never_say",
110
+ question: "What words should your brand NEVER use?",
111
+ follow_up: "Format: word — reason. Example: 'synergy — corporate cliche that signals hollow thinking.'",
112
+ },
113
+ ],
114
+ note: "If messaging-audit.md exists, the audit's top vocabulary will be presented for keep/replace/ban triage before these questions.",
115
+ },
116
+ {
117
+ part: "3",
118
+ label: "Sentence Rules",
119
+ questions: [
120
+ {
121
+ key: "exclamation_marks",
122
+ question: "Exclamation marks: never, rarely, or freely?",
123
+ },
124
+ {
125
+ key: "hedging",
126
+ question: "Hedging language (can, may, might): ban, minimize, or allow?",
127
+ },
128
+ {
129
+ key: "person",
130
+ question: "Person: we, I, or you-focused?",
131
+ follow_up: "Most brands use 'we' for company voice. Some founders use 'I'. Which is yours?",
132
+ },
133
+ {
134
+ key: "oxford_comma",
135
+ question: "Oxford comma: yes or no?",
136
+ },
137
+ ],
138
+ },
139
+ ];
140
+ const BRAND_STORY_QUESTIONS = [
141
+ {
142
+ key: "origin",
143
+ question: "How did this company start? What was the founding insight or frustration? If you don't know the full answer, that's OK — give me what you can. We can refine later.",
144
+ follow_up: "Not the press release version — the real story.",
145
+ guidance: "Check your About page, LinkedIn company page, or ask the founder. If you're a team member (not the founder), share what you know and mark it for founder review.",
146
+ },
147
+ {
148
+ key: "tension",
149
+ question: "What obstacle or broken system did you run into? If you don't know the full answer, that's OK — give me what you can. We can refine later.",
150
+ follow_up: "Every good story has tension. What was the wall you hit?",
151
+ guidance: "Check your About page, LinkedIn company page, or ask the founder. If you're a team member (not the founder), share what you know and mark it for founder review.",
152
+ },
153
+ {
154
+ key: "resolution",
155
+ question: "What did you build or discover that resolved it? If you don't know the full answer, that's OK — give me what you can. We can refine later.",
156
+ follow_up: "This is the turning point — how did you overcome the obstacle?",
157
+ guidance: "Check your About page, LinkedIn company page, or ask the founder. If you're a team member (not the founder), share what you know and mark it for founder review.",
158
+ },
159
+ {
160
+ key: "vision",
161
+ question: "Where are you going? What does the next chapter look like? If you don't know the full answer, that's OK — give me what you can. We can refine later.",
162
+ follow_up: "Not a 5-year plan — the narrative arc. What are you building toward?",
163
+ guidance: "Check your About page, LinkedIn company page, or ask the founder. If you're a team member (not the founder), share what you know and mark it for founder review.",
164
+ },
165
+ {
166
+ key: "tagline",
167
+ question: "If you could put the whole thing in one sentence? If you don't know the full answer, that's OK — give me what you can. We can refine later.",
168
+ follow_up: "The origin, the tension, the resolution — compressed into a single line.",
169
+ guidance: "Check your About page, LinkedIn company page, or ask the founder. If you're a team member (not the founder), share what you know and mark it for founder review.",
170
+ },
171
+ ];
172
+ // --- Helpers ---
173
+ function getEmptyMessaging() {
174
+ return {
175
+ schema_version: SCHEMA_VERSION,
176
+ session: 3,
177
+ perspective: null,
178
+ voice: null,
179
+ brand_story: null,
180
+ };
181
+ }
182
+ function getMissingSections(messaging) {
183
+ if (!messaging)
184
+ return [...SECTIONS];
185
+ const missing = [];
186
+ if (!messaging.perspective)
187
+ missing.push("perspective");
188
+ if (!messaging.voice)
189
+ missing.push("voice");
190
+ if (!messaging.brand_story)
191
+ missing.push("brand_story");
192
+ return missing;
193
+ }
194
+ /** Safely parse a value as a string array — handles string, string[], and comma-separated text */
195
+ function parseStringArray(val) {
196
+ if (Array.isArray(val)) {
197
+ return val.map((v) => String(v).trim()).filter((v) => v.length > 0);
198
+ }
199
+ if (typeof val === "string" && val.trim()) {
200
+ // Split on commas, newlines, or semicolons
201
+ return val
202
+ .split(/[,;\n]+/)
203
+ .map((s) => s.trim())
204
+ .filter((s) => s.length > 0);
205
+ }
206
+ return [];
207
+ }
208
+ /** Parse anchor terms from various formats */
209
+ function parseAnchorTerms(val) {
210
+ if (Array.isArray(val)) {
211
+ return val.map((item) => {
212
+ if (typeof item === "object" && item !== null && "use" in item) {
213
+ return {
214
+ use: String(item.use ?? ""),
215
+ not: String(item.not ?? ""),
216
+ reason: String(item.reason ?? ""),
217
+ };
218
+ }
219
+ // Freeform string: "use X not Y because Z" or "X instead of Y — Z"
220
+ return parseAnchorTermFromString(String(item));
221
+ });
222
+ }
223
+ if (typeof val === "string" && val.trim()) {
224
+ // Split on newlines or semicolons to get individual terms
225
+ const lines = val
226
+ .split(/[;\n]+/)
227
+ .map((s) => s.trim())
228
+ .filter((s) => s.length > 0);
229
+ return lines.map(parseAnchorTermFromString);
230
+ }
231
+ return [];
232
+ }
233
+ function parseAnchorTermFromString(raw) {
234
+ // Pattern: "use X not Y because Z"
235
+ const useNotMatch = raw.match(/^(?:use\s+)?["']?(.+?)["']?\s+(?:not|instead of)\s+["']?(.+?)["']?\s*(?:because|—|--|:)\s*(.+)$/i);
236
+ if (useNotMatch) {
237
+ return { use: useNotMatch[1].trim(), not: useNotMatch[2].trim(), reason: useNotMatch[3].trim() };
238
+ }
239
+ // Pattern: "X → Y (reason)" or "X -> Y (reason)"
240
+ const arrowMatch = raw.match(/^["']?(.+?)["']?\s*(?:→|->)\s*["']?(.+?)["']?\s*(?:\((.+?)\))?$/);
241
+ if (arrowMatch) {
242
+ return { use: arrowMatch[1].trim(), not: arrowMatch[2].trim(), reason: arrowMatch[3]?.trim() ?? "" };
243
+ }
244
+ // Fallback: treat the whole string as the "use" term
245
+ return { use: raw.trim(), not: "", reason: "" };
246
+ }
247
+ /** Parse never-say terms from various formats */
248
+ function parseNeverSayTerms(val) {
249
+ if (Array.isArray(val)) {
250
+ return val.map((item) => {
251
+ if (typeof item === "object" && item !== null && "word" in item) {
252
+ return {
253
+ word: String(item.word ?? ""),
254
+ reason: String(item.reason ?? ""),
255
+ };
256
+ }
257
+ return parseNeverSayFromString(String(item));
258
+ });
259
+ }
260
+ if (typeof val === "string" && val.trim()) {
261
+ const lines = val
262
+ .split(/[;\n]+/)
263
+ .map((s) => s.trim())
264
+ .filter((s) => s.length > 0);
265
+ return lines.map(parseNeverSayFromString);
266
+ }
267
+ return [];
268
+ }
269
+ function parseNeverSayFromString(raw) {
270
+ // Pattern: "ban: word — reason" or "word — reason" or "word: reason"
271
+ const banMatch = raw.match(/^(?:ban:\s*)?["']?(.+?)["']?\s*(?:—|--|:)\s*(.+)$/i);
272
+ if (banMatch) {
273
+ return { word: banMatch[1].trim(), reason: banMatch[2].trim() };
274
+ }
275
+ return { word: raw.trim(), reason: "" };
276
+ }
277
+ // --- Interview mode ---
278
+ async function handleInterview(brandDir) {
279
+ const hasMessaging = await brandDir.hasMessaging();
280
+ let messaging = null;
281
+ if (hasMessaging) {
282
+ messaging = await brandDir.readMessaging();
283
+ }
284
+ // Read client name for context
285
+ let clientName = "this brand";
286
+ try {
287
+ const config = await brandDir.readConfig();
288
+ clientName = config.client_name;
289
+ }
290
+ catch {
291
+ // no config available
292
+ }
293
+ // Check for messaging-audit.md
294
+ let auditVocabulary = null;
295
+ try {
296
+ const auditContent = await brandDir.readMarkdown("messaging-audit.md");
297
+ // Extract vocabulary frequency section if it exists
298
+ const vocabMatch = auditContent.match(/vocabulary.frequency[\s\S]*?(?=##|$)/i);
299
+ if (vocabMatch) {
300
+ auditVocabulary = vocabMatch[0].trim();
301
+ }
302
+ }
303
+ catch {
304
+ // no audit file
305
+ }
306
+ const missing = getMissingSections(messaging);
307
+ if (missing.length === 0) {
308
+ return buildResponse({
309
+ what_happened: `Messaging architecture for "${clientName}" is fully populated — all 3 sections have data.`,
310
+ next_steps: [
311
+ "Run brand_compile to regenerate system-integration.md with voice rules included",
312
+ "Run brand_compile_messaging with mode='record' to update any section if needed",
313
+ "Try generating content with brand_write to test the full system",
314
+ ],
315
+ data: {
316
+ complete: true,
317
+ sections_populated: SECTIONS.map((s) => s),
318
+ },
319
+ });
320
+ }
321
+ // Build interview agenda for missing sections
322
+ const agenda = [];
323
+ if (missing.includes("perspective")) {
324
+ agenda.push({
325
+ section: "perspective",
326
+ questions: PERSPECTIVE_QUESTIONS,
327
+ });
328
+ }
329
+ if (missing.includes("voice")) {
330
+ const voiceParts = VOICE_INTERVIEW_PARTS.map((part) => ({
331
+ ...part,
332
+ ...(part.part === "2" && auditVocabulary
333
+ ? {
334
+ audit_vocabulary: auditVocabulary,
335
+ pre_question: "Before we create new vocabulary rules, here's what I found in your existing content. For each term, tell me: keep (distinctly yours), replace (generic), or ban (harmful).",
336
+ }
337
+ : {}),
338
+ }));
339
+ agenda.push({
340
+ section: "voice",
341
+ parts: voiceParts,
342
+ ai_ism_defaults: DEFAULT_AI_ISM_PATTERNS,
343
+ ai_ism_note: "These are the default AI-ism patterns that will be flagged in generated content. Ask if they want to add, remove, or adjust any.",
344
+ });
345
+ }
346
+ if (missing.includes("brand_story")) {
347
+ agenda.push({
348
+ section: "brand_story",
349
+ questions: BRAND_STORY_QUESTIONS,
350
+ });
351
+ }
352
+ const populatedSections = SECTIONS.filter((s) => !missing.includes(s));
353
+ return buildResponse({
354
+ what_happened: hasMessaging
355
+ ? `Messaging exists but ${missing.length} section(s) still need data: ${missing.join(", ")}`
356
+ : `No messaging.yaml yet. All 3 sections need data.`,
357
+ next_steps: [
358
+ "Present the interview questions below — start with the first missing section",
359
+ "After gathering answers for a section, call brand_compile_messaging with mode='record', section=<name>, answers=<JSON>",
360
+ "Repeat for each section until all are populated",
361
+ ],
362
+ data: {
363
+ client_name: clientName,
364
+ missing_sections: missing,
365
+ populated_sections: populatedSections,
366
+ interview: agenda,
367
+ conversation_guide: {
368
+ instruction: [
369
+ `You are building the messaging architecture for "${clientName}". This is Session 3 — perspective, voice, and brand story.`,
370
+ "",
371
+ "HOW TO RUN THIS INTERVIEW:",
372
+ "1. Work through ONE section at a time in order: perspective → voice → brand story.",
373
+ "2. Ask the questions conversationally — do NOT dump all questions at once.",
374
+ "3. Listen for the answer, ask the follow-up if provided, then move to the next question.",
375
+ "4. When you have enough answers for a section, call brand_compile_messaging with mode='record' to save.",
376
+ "5. Then move to the next missing section.",
377
+ "",
378
+ "SECTION INTROS (use these to transition):",
379
+ "",
380
+ "PERSPECTIVE:",
381
+ `"Let's define what your brand actually believes. Not your mission statement — your *worldview*."`,
382
+ "",
383
+ "VOICE:",
384
+ `"Now that we know what you believe, let's define how it sounds."`,
385
+ "Voice has 3 parts: Tone (3 descriptors + register), Vocabulary (anchor terms + never-say), and Sentence Rules.",
386
+ "Work through each part sequentially.",
387
+ ...(auditVocabulary
388
+ ? [
389
+ "",
390
+ "VOCABULARY TRIAGE (voice part 2):",
391
+ "A messaging audit exists. Before asking for new vocabulary, present the audit's top terms and ask: keep, replace, or ban.",
392
+ ]
393
+ : []),
394
+ "",
395
+ "BRAND STORY:",
396
+ `"Last part — your origin story. Not a mission statement, but an actual story with tension and stakes. If you don't know all the details, that's completely fine — give me what you know and we'll flag the rest for the founder or leadership team to fill in."`,
397
+ "",
398
+ "AFTER ALL SECTIONS ARE RECORDED:",
399
+ `"Your messaging architecture is set. Want to test it? Give me a content type and a topic and I'll generate something using your full brand system."`,
400
+ "Suggest running brand_write to test the full system.",
401
+ "",
402
+ "TONE: Collaborative strategist — curious, direct, non-judgmental.",
403
+ "GOAL: Get specific, deployable language — not corporate mush.",
404
+ ].join("\n"),
405
+ },
406
+ },
407
+ });
408
+ }
409
+ // --- Record mode ---
410
+ async function handleRecord(brandDir, section, answersRaw) {
411
+ let answers;
412
+ try {
413
+ answers = JSON.parse(answersRaw);
414
+ }
415
+ catch {
416
+ return buildResponse({
417
+ what_happened: "Failed to parse answers — invalid JSON",
418
+ next_steps: [
419
+ "Provide answers as a valid JSON string",
420
+ "If this keeps happening, run brand_feedback to report the issue.",
421
+ ],
422
+ data: { error: ERROR_CODES.INVALID_JSON, raw: answersRaw },
423
+ });
424
+ }
425
+ // Read or create messaging
426
+ let messaging;
427
+ if (await brandDir.hasMessaging()) {
428
+ messaging = await brandDir.readMessaging();
429
+ }
430
+ else {
431
+ messaging = getEmptyMessaging();
432
+ }
433
+ const changes = [];
434
+ switch (section) {
435
+ case "perspective": {
436
+ messaging.perspective = parsePerspective(answers);
437
+ changes.push("Set perspective (worldview, tension, resolution, audience, positioning, one_liner)");
438
+ break;
439
+ }
440
+ case "voice": {
441
+ messaging.voice = parseVoice(answers);
442
+ changes.push("Set voice codex (tone, vocabulary, ai-ism detection)");
443
+ break;
444
+ }
445
+ case "brand_story": {
446
+ messaging.brand_story = parseBrandStory(answers);
447
+ changes.push("Set brand story (origin, tension, resolution, vision, tagline)");
448
+ // Detect thin or empty fields and flag for refinement
449
+ const storyFields = [
450
+ { key: "origin", label: "Origin" },
451
+ { key: "tension", label: "Tension" },
452
+ { key: "resolution", label: "Resolution" },
453
+ { key: "vision", label: "Vision" },
454
+ { key: "tagline", label: "Tagline" },
455
+ ];
456
+ const thinFields = storyFields.filter((f) => {
457
+ const val = messaging.brand_story?.[f.key] ?? "";
458
+ return val.length < 20;
459
+ });
460
+ if (thinFields.length > 0) {
461
+ const fieldNames = thinFields.map((f) => f.label).join(", ");
462
+ changes.push(`Note: ${fieldNames} ${thinFields.length === 1 ? "is" : "are"} thin or empty — consider revisiting with the founder or leadership team to add more detail.`);
463
+ }
464
+ // Generate brand-story.md as human-readable markdown
465
+ const storyMd = generateBrandStoryMarkdown(messaging.brand_story);
466
+ await brandDir.writeMarkdown("brand-story.md", storyMd);
467
+ changes.push("Generated brand-story.md (human-readable narrative)");
468
+ break;
469
+ }
470
+ }
471
+ // Write messaging.yaml
472
+ await brandDir.writeMessaging(messaging);
473
+ // Check remaining gaps
474
+ const missing = getMissingSections(messaging);
475
+ const nextSteps = [];
476
+ if (missing.length > 0) {
477
+ nextSteps.push(`${missing.length} section(s) remaining: ${missing.join(", ")}. Continue the interview or call brand_compile_messaging mode='interview' to get questions.`);
478
+ }
479
+ else {
480
+ // All 3 sections complete — bump session and generate system integration
481
+ try {
482
+ const config = await brandDir.readConfig();
483
+ if (config.session < 3) {
484
+ config.session = 3;
485
+ await brandDir.writeConfig(config);
486
+ changes.push("Bumped config.session to 3");
487
+ }
488
+ // Regenerate system-integration.md with voice rules inline
489
+ if (await brandDir.hasVisualIdentity()) {
490
+ const identity = await brandDir.readCoreIdentity();
491
+ const visual = await brandDir.readVisualIdentity();
492
+ const { generateSystemIntegration } = await import("../lib/vim-generator.js");
493
+ const integrationMd = generateSystemIntegration(config, identity, visual, messaging);
494
+ await brandDir.writeMarkdown("system-integration.md", integrationMd);
495
+ changes.push("Updated system-integration.md with voice rules");
496
+ }
497
+ }
498
+ catch {
499
+ // Config or visual identity may not exist; non-fatal
500
+ }
501
+ nextSteps.push("All 3 messaging sections are now populated. Your messaging architecture is complete.", "Run brand_compile to regenerate the full brand system output with messaging integrated.", "Try running brand_write to test content generation with your full brand system.");
502
+ }
503
+ return buildResponse({
504
+ what_happened: `Recorded messaging section "${section}" to messaging.yaml`,
505
+ next_steps: nextSteps,
506
+ data: {
507
+ section_recorded: section,
508
+ changes,
509
+ missing_sections: missing,
510
+ all_complete: missing.length === 0,
511
+ },
512
+ });
513
+ }
514
+ // --- Parsers with freeform text fallbacks ---
515
+ function parsePerspective(answers) {
516
+ return {
517
+ worldview: String(answers.worldview ?? ""),
518
+ tension: String(answers.tension ?? ""),
519
+ resolution: String(answers.resolution ?? ""),
520
+ audience: String(answers.audience ?? ""),
521
+ positioning: String(answers.positioning ?? ""),
522
+ one_liner: String(answers.one_liner ?? ""),
523
+ };
524
+ }
525
+ function parseVoice(answers) {
526
+ // --- Tone ---
527
+ const tone = answers.tone ?? answers;
528
+ // Descriptors: accept array, string of 3 words, or comma-separated
529
+ const rawDescriptors = tone.descriptors ?? answers.descriptors ?? "";
530
+ const descriptors = parseStringArray(rawDescriptors);
531
+ const register = String(tone.register ?? answers.register ?? "");
532
+ const neverSoundsLike = String(tone.never_sounds_like ?? answers.never_sounds_like ?? "");
533
+ // --- Sentence rules ---
534
+ const sentenceRules = answers.sentence_rules ??
535
+ answers.conventions ??
536
+ answers;
537
+ const exclamation = String(sentenceRules.exclamation_marks ??
538
+ answers.exclamation_marks ??
539
+ "rarely");
540
+ const hedging = String(sentenceRules.hedging ?? answers.hedging ?? "minimize");
541
+ const person = String(sentenceRules.person ?? answers.person ?? "we");
542
+ const oxfordCommaRaw = sentenceRules.oxford_comma ?? answers.oxford_comma ?? true;
543
+ const oxfordComma = typeof oxfordCommaRaw === "boolean"
544
+ ? oxfordCommaRaw
545
+ : String(oxfordCommaRaw).toLowerCase().startsWith("y") ||
546
+ String(oxfordCommaRaw).toLowerCase() === "true";
547
+ // Build sentence_patterns from exclamation/hedging preferences
548
+ const preferPatterns = [];
549
+ const avoidPatterns = [];
550
+ if (exclamation === "never") {
551
+ avoidPatterns.push("Exclamation marks in any context");
552
+ }
553
+ else if (exclamation === "rarely") {
554
+ avoidPatterns.push("Exclamation marks except in genuinely exceptional moments");
555
+ }
556
+ if (hedging === "ban") {
557
+ avoidPatterns.push("Hedging language: can, may, might, perhaps, possibly");
558
+ }
559
+ else if (hedging === "minimize") {
560
+ avoidPatterns.push("Excessive hedging (can, may, might) — use direct statements");
561
+ }
562
+ preferPatterns.push(`${person}-focused voice`);
563
+ // --- Vocabulary ---
564
+ const vocab = answers.vocabulary ?? answers;
565
+ const anchorTerms = parseAnchorTerms(vocab.anchor ?? vocab.anchor_terms ?? answers.anchor_terms ?? []);
566
+ const neverSayTerms = parseNeverSayTerms(vocab.never_say ?? answers.never_say ?? []);
567
+ const jargonPolicy = String(vocab.jargon_policy ?? answers.jargon_policy ?? "define on first use");
568
+ // Placeholder defaults
569
+ const placeholderDefaults = vocab.placeholder_defaults ??
570
+ answers.placeholder_defaults ??
571
+ {};
572
+ // --- AI-ism detection ---
573
+ const aiIsm = answers.ai_ism_detection ?? answers;
574
+ const rawPatterns = aiIsm.patterns ?? answers.ai_ism_patterns ?? DEFAULT_AI_ISM_PATTERNS;
575
+ const aiPatterns = parseStringArray(rawPatterns);
576
+ const aiInstruction = String(aiIsm.instruction ??
577
+ answers.ai_ism_instruction ??
578
+ "Flag and rewrite any sentence matching these patterns. Replace with specific, concrete language.");
579
+ return {
580
+ tone: {
581
+ descriptors: descriptors.length > 0 ? descriptors : [],
582
+ register,
583
+ never_sounds_like: neverSoundsLike,
584
+ sentence_patterns: {
585
+ prefer: preferPatterns,
586
+ avoid: avoidPatterns,
587
+ },
588
+ conventions: {
589
+ person,
590
+ reader_address: "you",
591
+ oxford_comma: oxfordComma,
592
+ sentence_length: 18,
593
+ paragraph_length: 3,
594
+ },
595
+ },
596
+ vocabulary: {
597
+ anchor: anchorTerms,
598
+ never_say: neverSayTerms,
599
+ jargon_policy: jargonPolicy,
600
+ placeholder_defaults: {
601
+ headline: String(placeholderDefaults.headline ?? ""),
602
+ subhead: String(placeholderDefaults.subhead ?? ""),
603
+ cta: String(placeholderDefaults.cta ?? ""),
604
+ body_paragraph: String(placeholderDefaults.body_paragraph ?? ""),
605
+ },
606
+ },
607
+ ai_ism_detection: {
608
+ patterns: aiPatterns.length > 0 ? aiPatterns : DEFAULT_AI_ISM_PATTERNS,
609
+ instruction: aiInstruction,
610
+ },
611
+ };
612
+ }
613
+ function parseBrandStory(answers) {
614
+ return {
615
+ origin: String(answers.origin ?? ""),
616
+ tension: String(answers.tension ?? ""),
617
+ resolution: String(answers.resolution ?? ""),
618
+ vision: String(answers.vision ?? ""),
619
+ tagline: String(answers.tagline ?? ""),
620
+ };
621
+ }
622
+ // --- Markdown generators ---
623
+ function generateBrandStoryMarkdown(story) {
624
+ const lines = [];
625
+ lines.push("# Brand Story");
626
+ lines.push("");
627
+ lines.push(`> ${story.tagline}`);
628
+ lines.push("");
629
+ lines.push("## Origin");
630
+ lines.push("");
631
+ lines.push(story.origin);
632
+ lines.push("");
633
+ lines.push("## Tension");
634
+ lines.push("");
635
+ lines.push(story.tension);
636
+ lines.push("");
637
+ lines.push("## Resolution");
638
+ lines.push("");
639
+ lines.push(story.resolution);
640
+ lines.push("");
641
+ lines.push("## Vision");
642
+ lines.push("");
643
+ lines.push(story.vision);
644
+ lines.push("");
645
+ return lines.join("\n");
646
+ }
647
+ function appendVoiceRulesToIntegration(existingMd, messaging) {
648
+ const lines = [existingMd.trimEnd()];
649
+ lines.push("");
650
+ lines.push("");
651
+ // --- Voice Rules Section ---
652
+ lines.push("## Voice & Messaging");
653
+ lines.push("");
654
+ if (messaging.perspective) {
655
+ lines.push("### Perspective");
656
+ lines.push(`- **Worldview:** ${messaging.perspective.worldview}`);
657
+ lines.push(`- **Tension:** ${messaging.perspective.tension}`);
658
+ lines.push(`- **Positioning:** ${messaging.perspective.positioning}`);
659
+ if (messaging.perspective.one_liner) {
660
+ lines.push(`- **One-liner:** ${messaging.perspective.one_liner}`);
661
+ }
662
+ lines.push("");
663
+ }
664
+ if (messaging.voice) {
665
+ const v = messaging.voice;
666
+ lines.push("### Voice Rules");
667
+ lines.push("");
668
+ if (v.tone.descriptors.length > 0) {
669
+ lines.push(`**Tone:** ${v.tone.descriptors.join(", ")}`);
670
+ }
671
+ if (v.tone.register) {
672
+ lines.push(`**Register:** ${v.tone.register}`);
673
+ }
674
+ if (v.tone.never_sounds_like) {
675
+ lines.push(`**Never sounds like:** ${v.tone.never_sounds_like}`);
676
+ }
677
+ lines.push("");
678
+ if (v.vocabulary.anchor.length > 0) {
679
+ lines.push("**Anchor Terms (ALWAYS use these):**");
680
+ for (const t of v.vocabulary.anchor) {
681
+ const reason = t.reason ? ` — ${t.reason}` : "";
682
+ lines.push(`- Use "${t.use}" not "${t.not}"${reason}`);
683
+ }
684
+ lines.push("");
685
+ }
686
+ if (v.vocabulary.never_say.length > 0) {
687
+ lines.push("**Never Say:**");
688
+ for (const t of v.vocabulary.never_say) {
689
+ const reason = t.reason ? ` — ${t.reason}` : "";
690
+ lines.push(`- ${t.word}${reason}`);
691
+ }
692
+ lines.push("");
693
+ }
694
+ lines.push("**Conventions:**");
695
+ lines.push(`- Person: ${v.tone.conventions.person}`);
696
+ lines.push(`- Oxford comma: ${v.tone.conventions.oxford_comma ? "yes" : "no"}`);
697
+ lines.push(`- Target sentence length: ${v.tone.conventions.sentence_length} words`);
698
+ lines.push("");
699
+ if (v.ai_ism_detection.patterns.length > 0) {
700
+ lines.push("**AI-ism Detection (flag and rewrite):**");
701
+ for (const p of v.ai_ism_detection.patterns.slice(0, 10)) {
702
+ lines.push(`- "${p}"`);
703
+ }
704
+ if (v.ai_ism_detection.patterns.length > 10) {
705
+ lines.push(`- ... and ${v.ai_ism_detection.patterns.length - 10} more patterns`);
706
+ }
707
+ lines.push("");
708
+ }
709
+ }
710
+ if (messaging.brand_story) {
711
+ lines.push("### Brand Story");
712
+ lines.push(`> ${messaging.brand_story.tagline}`);
713
+ lines.push("");
714
+ }
715
+ return lines.join("\n");
716
+ }
717
+ // --- Main handler ---
718
+ async function handler(input) {
719
+ const brandDir = new BrandDir(process.cwd());
720
+ if (!(await brandDir.exists())) {
721
+ return buildResponse({
722
+ what_happened: "No .brand/ directory found",
723
+ next_steps: ["Run brand_start first to create a brand system"],
724
+ data: { error: ERROR_CODES.NOT_INITIALIZED },
725
+ });
726
+ }
727
+ if (input.mode === "interview") {
728
+ return handleInterview(brandDir);
729
+ }
730
+ // Record mode — validate required params
731
+ if (!input.section) {
732
+ return buildResponse({
733
+ what_happened: "Missing required parameter: section",
734
+ next_steps: [
735
+ `Provide section as one of: ${SECTIONS.join(", ")}`,
736
+ ],
737
+ data: { error: ERROR_CODES.MISSING_SECTION },
738
+ });
739
+ }
740
+ if (!input.answers) {
741
+ return buildResponse({
742
+ what_happened: "Missing required parameter: answers",
743
+ next_steps: [
744
+ "Provide answers as a JSON string with keys matching the section's question keys",
745
+ ],
746
+ data: { error: ERROR_CODES.MISSING_ANSWERS },
747
+ });
748
+ }
749
+ return handleRecord(brandDir, input.section, input.answers);
750
+ }
751
+ export function register(server) {
752
+ server.tool("brand_compile_messaging", "Define how a brand should sound — perspective (worldview, positioning), voice codex (tone descriptors, anchor vocabulary, never-say list, AI-ism detection), and brand story (origin, tension, resolution). Session 3 guided interview with 3 sections. Mode 'interview' returns structured questions. Mode 'record' saves answers to messaging.yaml. Completing all 3 sections generates brand-story.md and updates system-integration.md with voice rules. Use after Session 2 (visual identity). Returns section status and remaining gaps.", paramsShape, async (args) => {
753
+ const parsed = safeParseParams(ParamsSchema, args);
754
+ if (!parsed.success)
755
+ return parsed.response;
756
+ return handler(parsed.data);
757
+ });
758
+ }
759
+ //# sourceMappingURL=brand-compile-messaging.js.map