@teamix-evo/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.
- package/LICENSE +21 -0
- package/README.md +105 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +1130 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +257 -0
- package/dist/index.js +1141 -0
- package/dist/index.js.map +1 -0
- package/package.json +44 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1130 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
|
|
6
|
+
// src/server.ts
|
|
7
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
8
|
+
import {
|
|
9
|
+
CallToolRequestSchema,
|
|
10
|
+
ListToolsRequestSchema
|
|
11
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
12
|
+
|
|
13
|
+
// src/groups/registry.ts
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
|
|
16
|
+
// src/manifest-loader.ts
|
|
17
|
+
import { readFileSync, existsSync } from "fs";
|
|
18
|
+
import { dirname, join, resolve } from "path";
|
|
19
|
+
import { UiPackageManifestSchema } from "@teamix-evo/registry";
|
|
20
|
+
function resolveManifestPath(startDir = process.cwd()) {
|
|
21
|
+
const envPath = process.env.TEAMIX_EVO_UI_MANIFEST;
|
|
22
|
+
if (envPath && existsSync(envPath)) {
|
|
23
|
+
return envPath;
|
|
24
|
+
}
|
|
25
|
+
let dir = resolve(startDir);
|
|
26
|
+
for (let i = 0; i < 16; i++) {
|
|
27
|
+
const candidate = join(
|
|
28
|
+
dir,
|
|
29
|
+
"node_modules",
|
|
30
|
+
"@teamix-evo",
|
|
31
|
+
"ui",
|
|
32
|
+
"manifest.json"
|
|
33
|
+
);
|
|
34
|
+
if (existsSync(candidate)) {
|
|
35
|
+
return candidate;
|
|
36
|
+
}
|
|
37
|
+
const parent = dirname(dir);
|
|
38
|
+
if (parent === dir) break;
|
|
39
|
+
dir = parent;
|
|
40
|
+
}
|
|
41
|
+
throw new Error(
|
|
42
|
+
[
|
|
43
|
+
"Could not locate @teamix-evo/ui/manifest.json.",
|
|
44
|
+
`Tried env TEAMIX_EVO_UI_MANIFEST + walking up from ${startDir}.`,
|
|
45
|
+
"Either install @teamix-evo/ui in the consumer project, or set TEAMIX_EVO_UI_MANIFEST to an absolute path."
|
|
46
|
+
].join(" ")
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
function loadManifest(path) {
|
|
50
|
+
const manifestPath = path ?? resolveManifestPath();
|
|
51
|
+
const raw = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
52
|
+
const result = UiPackageManifestSchema.safeParse(raw);
|
|
53
|
+
if (!result.success) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`Invalid manifest at ${manifestPath}: ${result.error.message}`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
manifest: result.data,
|
|
60
|
+
rootDir: dirname(manifestPath)
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function loadMeta(entry, rootDir) {
|
|
64
|
+
if (!entry.meta) return null;
|
|
65
|
+
const path = join(rootDir, entry.meta);
|
|
66
|
+
if (!existsSync(path)) return null;
|
|
67
|
+
const text = readFileSync(path, "utf-8");
|
|
68
|
+
return parseFrontmatter(text);
|
|
69
|
+
}
|
|
70
|
+
function parseFrontmatter(text) {
|
|
71
|
+
const FM_RE2 = /^---\s*\n([\s\S]*?)\n---\s*\n?/;
|
|
72
|
+
const match = text.match(FM_RE2);
|
|
73
|
+
if (!match) {
|
|
74
|
+
return { frontmatter: {}, body: text };
|
|
75
|
+
}
|
|
76
|
+
const fm = {};
|
|
77
|
+
for (const line of match[1].split("\n")) {
|
|
78
|
+
const m = line.match(/^([A-Za-z_][\w-]*)\s*:\s*(.+?)\s*$/);
|
|
79
|
+
if (m) fm[m[1]] = m[2].replace(/^['"]|['"]$/g, "");
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
frontmatter: fm,
|
|
83
|
+
body: text.slice(match[0].length)
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/groups/registry.ts
|
|
88
|
+
var ListComponentsInput = z.object({
|
|
89
|
+
status: z.enum(["stable", "experimental", "deprecated"]).optional()
|
|
90
|
+
});
|
|
91
|
+
var GetComponentMetaInput = z.object({
|
|
92
|
+
id: z.string().min(1)
|
|
93
|
+
});
|
|
94
|
+
var FindComponentsInput = z.object({
|
|
95
|
+
query: z.string().min(1),
|
|
96
|
+
limit: z.number().int().positive().max(100).optional()
|
|
97
|
+
});
|
|
98
|
+
var TOOLS = [
|
|
99
|
+
{
|
|
100
|
+
name: "list_components",
|
|
101
|
+
description: "List all UI components in the @teamix-evo/ui registry. Optionally filter by status (stable / experimental / deprecated). Returns id, name, description, status, registryDependencies \u2014 small enough for the model to scan whole.",
|
|
102
|
+
inputSchema: {
|
|
103
|
+
type: "object",
|
|
104
|
+
properties: {
|
|
105
|
+
status: {
|
|
106
|
+
type: "string",
|
|
107
|
+
enum: ["stable", "experimental", "deprecated"],
|
|
108
|
+
description: "Filter by maturity status."
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: "get_component_meta",
|
|
115
|
+
description: "Fetch the full registry entry + parsed meta.md for a single component by id. Returns props schema reference, registryDependencies, npm dependencies, AI generation rules, and the component description.",
|
|
116
|
+
inputSchema: {
|
|
117
|
+
type: "object",
|
|
118
|
+
properties: {
|
|
119
|
+
id: {
|
|
120
|
+
type: "string",
|
|
121
|
+
description: 'Component id (e.g. "button", "data-table").'
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
required: ["id"]
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: "find_components",
|
|
129
|
+
description: 'Substring match over component id / name / description. Use when you need a component but don\'t know its exact id (e.g. "find a component supporting async search and pagination"). Returns up to `limit` matches (default 10). Note: substring match is a v0.1 implementation; semantic search is planned for v0.7 (see ADR 0009).',
|
|
130
|
+
inputSchema: {
|
|
131
|
+
type: "object",
|
|
132
|
+
properties: {
|
|
133
|
+
query: {
|
|
134
|
+
type: "string",
|
|
135
|
+
description: "Free-text query."
|
|
136
|
+
},
|
|
137
|
+
limit: {
|
|
138
|
+
type: "integer",
|
|
139
|
+
minimum: 1,
|
|
140
|
+
maximum: 100,
|
|
141
|
+
description: "Max matches to return (default 10)."
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
required: ["query"]
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
];
|
|
148
|
+
var TOOL_NAMES = new Set(TOOLS.map((t) => t.name));
|
|
149
|
+
function pickListEntry(entry) {
|
|
150
|
+
return {
|
|
151
|
+
id: entry.id,
|
|
152
|
+
name: entry.name,
|
|
153
|
+
type: entry.type,
|
|
154
|
+
description: entry.description,
|
|
155
|
+
status: entry.status,
|
|
156
|
+
deprecatedReason: entry.deprecatedReason,
|
|
157
|
+
replacedBy: entry.replacedBy,
|
|
158
|
+
registryDependencies: entry.registryDependencies ?? []
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function createRegistryGroup(opts = {}) {
|
|
162
|
+
let cache = opts.loaded ?? null;
|
|
163
|
+
function getManifest() {
|
|
164
|
+
if (!cache) cache = loadManifest();
|
|
165
|
+
return cache;
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
name: "registry",
|
|
169
|
+
tools: TOOLS,
|
|
170
|
+
async handle(name, args) {
|
|
171
|
+
if (!TOOL_NAMES.has(name)) return void 0;
|
|
172
|
+
if (name === "list_components") {
|
|
173
|
+
const input = ListComponentsInput.parse(args ?? {});
|
|
174
|
+
const { manifest } = getManifest();
|
|
175
|
+
const entries = manifest.entries.filter((e) => e.type === "component").filter((e) => input.status ? e.status === input.status : true).map(pickListEntry);
|
|
176
|
+
return {
|
|
177
|
+
content: [{ type: "text", text: JSON.stringify(entries, null, 2) }]
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
if (name === "get_component_meta") {
|
|
181
|
+
const input = GetComponentMetaInput.parse(args);
|
|
182
|
+
const { manifest, rootDir } = getManifest();
|
|
183
|
+
const entry = manifest.entries.find((e) => e.id === input.id);
|
|
184
|
+
if (!entry) {
|
|
185
|
+
return {
|
|
186
|
+
content: [
|
|
187
|
+
{
|
|
188
|
+
type: "text",
|
|
189
|
+
text: `Component not found: ${input.id}. Use list_components to discover ids.`
|
|
190
|
+
}
|
|
191
|
+
],
|
|
192
|
+
isError: true
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
const meta = loadMeta(entry, rootDir);
|
|
196
|
+
const payload = {
|
|
197
|
+
entry,
|
|
198
|
+
meta: meta ?? null
|
|
199
|
+
};
|
|
200
|
+
return {
|
|
201
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }]
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
if (name === "find_components") {
|
|
205
|
+
const input = FindComponentsInput.parse(args);
|
|
206
|
+
const limit = input.limit ?? 10;
|
|
207
|
+
const q = input.query.toLowerCase();
|
|
208
|
+
const { manifest } = getManifest();
|
|
209
|
+
const matches = manifest.entries.filter((e) => e.type === "component").filter((e) => {
|
|
210
|
+
return e.id.toLowerCase().includes(q) || e.name.toLowerCase().includes(q) || e.description.toLowerCase().includes(q);
|
|
211
|
+
}).slice(0, limit).map(pickListEntry);
|
|
212
|
+
return {
|
|
213
|
+
content: [
|
|
214
|
+
{
|
|
215
|
+
type: "text",
|
|
216
|
+
text: JSON.stringify(
|
|
217
|
+
{
|
|
218
|
+
query: input.query,
|
|
219
|
+
limit,
|
|
220
|
+
count: matches.length,
|
|
221
|
+
results: matches
|
|
222
|
+
},
|
|
223
|
+
null,
|
|
224
|
+
2
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
]
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
return void 0;
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// src/groups/adr.ts
|
|
236
|
+
import { z as z2 } from "zod";
|
|
237
|
+
|
|
238
|
+
// src/adr-loader.ts
|
|
239
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync } from "fs";
|
|
240
|
+
import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
|
|
241
|
+
function resolveAdrRoot(startDir = process.cwd()) {
|
|
242
|
+
const envPath = process.env.TEAMIX_EVO_ADR_ROOT;
|
|
243
|
+
if (envPath && existsSync2(envPath)) {
|
|
244
|
+
return envPath;
|
|
245
|
+
}
|
|
246
|
+
let dir = resolve2(startDir);
|
|
247
|
+
for (let i = 0; i < 16; i++) {
|
|
248
|
+
const candidates = [
|
|
249
|
+
join2(dir, "docs", "adr"),
|
|
250
|
+
join2(dir, ".teamix-evo", "adr")
|
|
251
|
+
];
|
|
252
|
+
for (const c of candidates) {
|
|
253
|
+
if (existsSync2(c)) return c;
|
|
254
|
+
}
|
|
255
|
+
const parent = dirname2(dir);
|
|
256
|
+
if (parent === dir) break;
|
|
257
|
+
dir = parent;
|
|
258
|
+
}
|
|
259
|
+
throw new Error(
|
|
260
|
+
[
|
|
261
|
+
"Could not locate ADR directory.",
|
|
262
|
+
`Tried env TEAMIX_EVO_ADR_ROOT + walking up from ${startDir} for docs/adr/ or .teamix-evo/adr/.`,
|
|
263
|
+
"Either run from a teamix-evo monorepo, or set TEAMIX_EVO_ADR_ROOT to an absolute path."
|
|
264
|
+
].join(" ")
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
var SKIP_FILES = /* @__PURE__ */ new Set(["README.md", "_template.md"]);
|
|
268
|
+
var ID_RE = /^(\d{4})-/;
|
|
269
|
+
var H1_RE = /^#\s+\d{4}\.\s+(.+?)\s*$/m;
|
|
270
|
+
var STATUS_RE = /^-\s+\*\*Status\*\*:\s*(.+?)\s*$/m;
|
|
271
|
+
var DATE_RE = /^-\s+\*\*Date\*\*:\s*(.+?)\s*$/m;
|
|
272
|
+
var REGION_RE = /^-\s+\*\*Region\*\*:\s*(.+?)\s*$/m;
|
|
273
|
+
function parseAdr(slug, filePath) {
|
|
274
|
+
const idMatch = slug.match(ID_RE);
|
|
275
|
+
if (!idMatch) return null;
|
|
276
|
+
const id = idMatch[1];
|
|
277
|
+
const text = readFileSync2(filePath, "utf-8");
|
|
278
|
+
const titleMatch = text.match(H1_RE);
|
|
279
|
+
const statusMatch = text.match(STATUS_RE);
|
|
280
|
+
const dateMatch = text.match(DATE_RE);
|
|
281
|
+
const regionMatch = text.match(REGION_RE);
|
|
282
|
+
return {
|
|
283
|
+
id,
|
|
284
|
+
slug,
|
|
285
|
+
title: titleMatch?.[1] ?? slug,
|
|
286
|
+
status: statusMatch?.[1] ?? "",
|
|
287
|
+
date: dateMatch?.[1] ?? "",
|
|
288
|
+
region: regionMatch?.[1] ?? "",
|
|
289
|
+
filePath
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
function loadAdrIndex(rootDir) {
|
|
293
|
+
const root = rootDir ?? resolveAdrRoot();
|
|
294
|
+
const files = readdirSync(root).filter(
|
|
295
|
+
(f) => f.endsWith(".md") && !SKIP_FILES.has(f)
|
|
296
|
+
);
|
|
297
|
+
const entries = [];
|
|
298
|
+
for (const f of files) {
|
|
299
|
+
const slug = f.replace(/\.md$/, "");
|
|
300
|
+
const entry = parseAdr(slug, join2(root, f));
|
|
301
|
+
if (entry) entries.push(entry);
|
|
302
|
+
}
|
|
303
|
+
entries.sort((a, b) => a.id.localeCompare(b.id));
|
|
304
|
+
return { rootDir: root, entries };
|
|
305
|
+
}
|
|
306
|
+
function findAdr(loaded, idOrSlug) {
|
|
307
|
+
const needle = idOrSlug.trim();
|
|
308
|
+
if (/^\d{4}$/.test(needle)) {
|
|
309
|
+
return loaded.entries.find((e) => e.id === needle) ?? null;
|
|
310
|
+
}
|
|
311
|
+
return loaded.entries.find((e) => e.slug === needle) ?? loaded.entries.find((e) => e.id === needle) ?? null;
|
|
312
|
+
}
|
|
313
|
+
function loadAdrContent(entry) {
|
|
314
|
+
return readFileSync2(entry.filePath, "utf-8");
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// src/groups/adr.ts
|
|
318
|
+
var ListInput = z2.object({
|
|
319
|
+
/** Match `Status` line by substring (case-insensitive). */
|
|
320
|
+
status: z2.string().min(1).optional()
|
|
321
|
+
});
|
|
322
|
+
var GetInput = z2.object({
|
|
323
|
+
/** ADR id ("0011") or full slug ("0011-mcp-single-package-multi-group"). */
|
|
324
|
+
id: z2.string().min(1)
|
|
325
|
+
});
|
|
326
|
+
var FindInput = z2.object({
|
|
327
|
+
query: z2.string().min(1),
|
|
328
|
+
limit: z2.number().int().positive().max(50).optional(),
|
|
329
|
+
/**
|
|
330
|
+
* When true, returns a snippet (~200 chars) around each match so the model
|
|
331
|
+
* can decide whether to fetch the full ADR. Defaults to true.
|
|
332
|
+
*/
|
|
333
|
+
withSnippets: z2.boolean().optional()
|
|
334
|
+
});
|
|
335
|
+
var TOOLS2 = [
|
|
336
|
+
{
|
|
337
|
+
name: "adr_list",
|
|
338
|
+
description: 'List all Architecture Decision Records (ADRs) in docs/adr/. Returns id (4-digit), slug, title, status, date, region for each. Use this as the first call to discover which decisions exist before drilling into one with adr_get. Filter by status substring (e.g. "Accepted" / "Proposed" / "Superseded") to narrow down.',
|
|
339
|
+
inputSchema: {
|
|
340
|
+
type: "object",
|
|
341
|
+
properties: {
|
|
342
|
+
status: {
|
|
343
|
+
type: "string",
|
|
344
|
+
description: 'Optional case-insensitive substring filter against the ADR Status line. Examples: "Accepted", "Proposed", "Superseded".'
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
name: "adr_get",
|
|
351
|
+
description: 'Fetch the full markdown body of one ADR by id ("0011") or slug ("0011-mcp-single-package-multi-group"). Returns the entry metadata + raw markdown so the model can cite specific sections (Context / Decision / Consequences / Source). Use after adr_list / adr_find to read a specific decision in full.',
|
|
352
|
+
inputSchema: {
|
|
353
|
+
type: "object",
|
|
354
|
+
properties: {
|
|
355
|
+
id: {
|
|
356
|
+
type: "string",
|
|
357
|
+
description: 'ADR id ("0011") or full slug ("0011-mcp-single-package-multi-group").'
|
|
358
|
+
}
|
|
359
|
+
},
|
|
360
|
+
required: ["id"]
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
name: "adr_find",
|
|
365
|
+
description: 'Substring search across ADR titles and bodies (case-insensitive). Use when you remember a phrase ("source-mirror", "lint-core", "no baseline") but not which ADR it lives in. Returns up to `limit` matches (default 10) with optional snippets (~200 chars around each match). Combine with adr_get for the full text.',
|
|
366
|
+
inputSchema: {
|
|
367
|
+
type: "object",
|
|
368
|
+
properties: {
|
|
369
|
+
query: { type: "string", description: "Free-text query." },
|
|
370
|
+
limit: {
|
|
371
|
+
type: "integer",
|
|
372
|
+
minimum: 1,
|
|
373
|
+
maximum: 50,
|
|
374
|
+
description: "Max matches to return (default 10)."
|
|
375
|
+
},
|
|
376
|
+
withSnippets: {
|
|
377
|
+
type: "boolean",
|
|
378
|
+
description: "Include ~200-char snippet around each match (default true)."
|
|
379
|
+
}
|
|
380
|
+
},
|
|
381
|
+
required: ["query"]
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
];
|
|
385
|
+
var TOOL_NAMES2 = new Set(TOOLS2.map((t) => t.name));
|
|
386
|
+
function pickEntry(e) {
|
|
387
|
+
return {
|
|
388
|
+
id: e.id,
|
|
389
|
+
slug: e.slug,
|
|
390
|
+
title: e.title,
|
|
391
|
+
status: e.status,
|
|
392
|
+
date: e.date,
|
|
393
|
+
region: e.region
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
var SNIPPET_RADIUS = 100;
|
|
397
|
+
function snippet(text, index) {
|
|
398
|
+
const start = Math.max(0, index - SNIPPET_RADIUS);
|
|
399
|
+
const end = Math.min(text.length, index + SNIPPET_RADIUS);
|
|
400
|
+
const prefix = start > 0 ? "\u2026" : "";
|
|
401
|
+
const suffix = end < text.length ? "\u2026" : "";
|
|
402
|
+
return prefix + text.slice(start, end).replace(/\s+/g, " ").trim() + suffix;
|
|
403
|
+
}
|
|
404
|
+
function createAdrGroup(opts = {}) {
|
|
405
|
+
let cache = opts.loaded ?? null;
|
|
406
|
+
function getIndex() {
|
|
407
|
+
if (!cache) cache = loadAdrIndex();
|
|
408
|
+
return cache;
|
|
409
|
+
}
|
|
410
|
+
return {
|
|
411
|
+
name: "adr",
|
|
412
|
+
tools: TOOLS2,
|
|
413
|
+
async handle(name, args) {
|
|
414
|
+
if (!TOOL_NAMES2.has(name)) return void 0;
|
|
415
|
+
if (name === "adr_list") {
|
|
416
|
+
const input = ListInput.parse(args ?? {});
|
|
417
|
+
const idx = getIndex();
|
|
418
|
+
const needle = input.status?.toLowerCase();
|
|
419
|
+
const entries = idx.entries.filter(
|
|
420
|
+
(e) => needle ? e.status.toLowerCase().includes(needle) : true
|
|
421
|
+
).map(pickEntry);
|
|
422
|
+
return {
|
|
423
|
+
content: [{ type: "text", text: JSON.stringify(entries, null, 2) }]
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
if (name === "adr_get") {
|
|
427
|
+
const input = GetInput.parse(args);
|
|
428
|
+
const idx = getIndex();
|
|
429
|
+
const entry = findAdr(idx, input.id);
|
|
430
|
+
if (!entry) {
|
|
431
|
+
return {
|
|
432
|
+
content: [
|
|
433
|
+
{
|
|
434
|
+
type: "text",
|
|
435
|
+
text: `ADR not found: ${input.id}. Use adr_list to discover ids.`
|
|
436
|
+
}
|
|
437
|
+
],
|
|
438
|
+
isError: true
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
const text = loadAdrContent(entry);
|
|
442
|
+
return {
|
|
443
|
+
content: [
|
|
444
|
+
{
|
|
445
|
+
type: "text",
|
|
446
|
+
text: JSON.stringify(
|
|
447
|
+
{ entry: pickEntry(entry), content: text },
|
|
448
|
+
null,
|
|
449
|
+
2
|
|
450
|
+
)
|
|
451
|
+
}
|
|
452
|
+
]
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
if (name === "adr_find") {
|
|
456
|
+
const input = FindInput.parse(args);
|
|
457
|
+
const limit = input.limit ?? 10;
|
|
458
|
+
const withSnippets = input.withSnippets ?? true;
|
|
459
|
+
const needle = input.query.toLowerCase();
|
|
460
|
+
const idx = getIndex();
|
|
461
|
+
const matches = [];
|
|
462
|
+
for (const entry of idx.entries) {
|
|
463
|
+
const body = loadAdrContent(entry).toLowerCase();
|
|
464
|
+
const titleHit = entry.title.toLowerCase().includes(needle);
|
|
465
|
+
if (!titleHit && !body.includes(needle)) continue;
|
|
466
|
+
const snippets = [];
|
|
467
|
+
if (withSnippets) {
|
|
468
|
+
const rawBody = loadAdrContent(entry);
|
|
469
|
+
let from2 = 0;
|
|
470
|
+
for (let i = 0; i < 3; i++) {
|
|
471
|
+
const at = rawBody.toLowerCase().indexOf(needle, from2);
|
|
472
|
+
if (at < 0) break;
|
|
473
|
+
snippets.push(snippet(rawBody, at));
|
|
474
|
+
from2 = at + needle.length;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
let hits = titleHit ? 1 : 0;
|
|
478
|
+
let from = 0;
|
|
479
|
+
for (; ; ) {
|
|
480
|
+
const at = body.indexOf(needle, from);
|
|
481
|
+
if (at < 0) break;
|
|
482
|
+
hits++;
|
|
483
|
+
from = at + needle.length;
|
|
484
|
+
}
|
|
485
|
+
matches.push({
|
|
486
|
+
entry: pickEntry(entry),
|
|
487
|
+
...withSnippets ? { snippets } : {},
|
|
488
|
+
hitCount: hits
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
matches.sort((a, b) => b.hitCount - a.hitCount);
|
|
492
|
+
const sliced = matches.slice(0, limit);
|
|
493
|
+
return {
|
|
494
|
+
content: [
|
|
495
|
+
{
|
|
496
|
+
type: "text",
|
|
497
|
+
text: JSON.stringify(
|
|
498
|
+
{
|
|
499
|
+
query: input.query,
|
|
500
|
+
limit,
|
|
501
|
+
count: sliced.length,
|
|
502
|
+
totalMatchingAdrs: matches.length,
|
|
503
|
+
results: sliced
|
|
504
|
+
},
|
|
505
|
+
null,
|
|
506
|
+
2
|
|
507
|
+
)
|
|
508
|
+
}
|
|
509
|
+
]
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
return void 0;
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// src/groups/skills.ts
|
|
518
|
+
import { z as z3 } from "zod";
|
|
519
|
+
|
|
520
|
+
// src/skills-loader.ts
|
|
521
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync as readdirSync2, statSync } from "fs";
|
|
522
|
+
import { dirname as dirname3, join as join3, resolve as resolve3 } from "path";
|
|
523
|
+
import {
|
|
524
|
+
SkillsPackageManifestSchema
|
|
525
|
+
} from "@teamix-evo/registry";
|
|
526
|
+
function resolveSkillsRoot(startDir = process.cwd()) {
|
|
527
|
+
const envPath = process.env.TEAMIX_EVO_SKILLS_ROOT;
|
|
528
|
+
if (envPath && existsSync3(join3(envPath, "manifest.json"))) {
|
|
529
|
+
return envPath;
|
|
530
|
+
}
|
|
531
|
+
let dir = resolve3(startDir);
|
|
532
|
+
for (let i = 0; i < 16; i++) {
|
|
533
|
+
const candidates = [
|
|
534
|
+
join3(dir, "packages", "skills"),
|
|
535
|
+
join3(dir, "node_modules", "@teamix-evo", "skills")
|
|
536
|
+
];
|
|
537
|
+
for (const c of candidates) {
|
|
538
|
+
if (existsSync3(join3(c, "manifest.json"))) return c;
|
|
539
|
+
}
|
|
540
|
+
const parent = dirname3(dir);
|
|
541
|
+
if (parent === dir) break;
|
|
542
|
+
dir = parent;
|
|
543
|
+
}
|
|
544
|
+
throw new Error(
|
|
545
|
+
[
|
|
546
|
+
"Could not locate @teamix-evo/skills root.",
|
|
547
|
+
`Tried env TEAMIX_EVO_SKILLS_ROOT + walking up from ${startDir}.`,
|
|
548
|
+
"Either install @teamix-evo/skills or run from a monorepo, or set TEAMIX_EVO_SKILLS_ROOT to an absolute path."
|
|
549
|
+
].join(" ")
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
function loadSkillsManifest(rootDir) {
|
|
553
|
+
const root = rootDir ?? resolveSkillsRoot();
|
|
554
|
+
const manifestPath = join3(root, "manifest.json");
|
|
555
|
+
const raw = JSON.parse(readFileSync3(manifestPath, "utf-8"));
|
|
556
|
+
const parsed = SkillsPackageManifestSchema.safeParse(raw);
|
|
557
|
+
if (!parsed.success) {
|
|
558
|
+
throw new Error(
|
|
559
|
+
`Invalid skills manifest at ${manifestPath}: ${parsed.error.message}`
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
return { rootDir: root, manifest: parsed.data };
|
|
563
|
+
}
|
|
564
|
+
var FM_RE = /^---\s*\n([\s\S]*?)\n---\s*\n?/;
|
|
565
|
+
function parseFrontmatter2(text) {
|
|
566
|
+
const m = text.match(FM_RE);
|
|
567
|
+
if (!m) return { frontmatter: {}, body: text };
|
|
568
|
+
const fm = {};
|
|
569
|
+
for (const line of m[1].split("\n")) {
|
|
570
|
+
const kv = line.match(/^([A-Za-z_][\w-]*)\s*:\s*(.+?)\s*$/);
|
|
571
|
+
if (kv) fm[kv[1]] = kv[2].replace(/^['"]|['"]$/g, "");
|
|
572
|
+
}
|
|
573
|
+
return { frontmatter: fm, body: text.slice(m[0].length) };
|
|
574
|
+
}
|
|
575
|
+
function loadSkillContent(entry, rootDir) {
|
|
576
|
+
const sourcePath = join3(rootDir, entry.source);
|
|
577
|
+
if (!existsSync3(sourcePath)) return null;
|
|
578
|
+
let skillMdPath;
|
|
579
|
+
let attachments = [];
|
|
580
|
+
const dirCandidate = join3(sourcePath, "SKILL.md");
|
|
581
|
+
if (existsSync3(dirCandidate)) {
|
|
582
|
+
skillMdPath = dirCandidate;
|
|
583
|
+
try {
|
|
584
|
+
attachments = readdirSync2(sourcePath).filter((f) => f.endsWith(".md") && f !== "SKILL.md").filter((f) => {
|
|
585
|
+
try {
|
|
586
|
+
return statSync(join3(sourcePath, f)).isFile();
|
|
587
|
+
} catch {
|
|
588
|
+
return false;
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
} catch {
|
|
592
|
+
attachments = [];
|
|
593
|
+
}
|
|
594
|
+
} else {
|
|
595
|
+
skillMdPath = sourcePath;
|
|
596
|
+
}
|
|
597
|
+
const text = readFileSync3(skillMdPath, "utf-8");
|
|
598
|
+
const { frontmatter, body } = parseFrontmatter2(text);
|
|
599
|
+
return {
|
|
600
|
+
entry,
|
|
601
|
+
text,
|
|
602
|
+
body,
|
|
603
|
+
frontmatter,
|
|
604
|
+
filePath: skillMdPath,
|
|
605
|
+
attachments
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
function findSkill(loaded, idOrName) {
|
|
609
|
+
const needle = idOrName.trim();
|
|
610
|
+
return loaded.manifest.skills.find((s) => s.id === needle) ?? loaded.manifest.skills.find((s) => s.name === needle) ?? null;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// src/groups/skills.ts
|
|
614
|
+
var ListInput2 = z3.object({});
|
|
615
|
+
var GetInput2 = z3.object({
|
|
616
|
+
id: z3.string().min(1)
|
|
617
|
+
});
|
|
618
|
+
var FindInput2 = z3.object({
|
|
619
|
+
query: z3.string().min(1),
|
|
620
|
+
limit: z3.number().int().positive().max(50).optional(),
|
|
621
|
+
/**
|
|
622
|
+
* Scope: 'description' (default — fast, only matches the AI trigger contract)
|
|
623
|
+
* or 'body' (slower — matches across SKILL.md prose, useful for "where do we
|
|
624
|
+
* mention X" questions).
|
|
625
|
+
*/
|
|
626
|
+
scope: z3.enum(["description", "body", "all"]).optional()
|
|
627
|
+
});
|
|
628
|
+
var TOOLS3 = [
|
|
629
|
+
{
|
|
630
|
+
name: "skills_list",
|
|
631
|
+
description: "List all SKILL.md entries shipped by @teamix-evo/skills. Returns id, name, description (the AI trigger contract), version, source path, ides, updateStrategy. Use this to discover which skills exist before invoking one via the IDE's skill mechanism.",
|
|
632
|
+
inputSchema: { type: "object", properties: {} }
|
|
633
|
+
},
|
|
634
|
+
{
|
|
635
|
+
name: "skills_get",
|
|
636
|
+
description: "Fetch the full SKILL.md content (frontmatter + body) for one skill by id or name. Also lists attachment files (sibling .md docs the skill references). Use after skills_list / skills_find to read the actual rules / decision tree / checklist that the skill encodes.",
|
|
637
|
+
inputSchema: {
|
|
638
|
+
type: "object",
|
|
639
|
+
properties: {
|
|
640
|
+
id: {
|
|
641
|
+
type: "string",
|
|
642
|
+
description: "Skill id or name (matches manifest.skills[].id or .name)."
|
|
643
|
+
}
|
|
644
|
+
},
|
|
645
|
+
required: ["id"]
|
|
646
|
+
}
|
|
647
|
+
},
|
|
648
|
+
{
|
|
649
|
+
name: "skills_find",
|
|
650
|
+
description: 'Substring search across skill descriptions and bodies. Default scope is `description` (matches the AI trigger contract \u2014 fast). Set `scope: "body"` to search SKILL.md prose, or `scope: "all"` for both. Use when you remember a phrase ("token refresh", "design review", "drift scan") but not the skill id.',
|
|
651
|
+
inputSchema: {
|
|
652
|
+
type: "object",
|
|
653
|
+
properties: {
|
|
654
|
+
query: { type: "string", description: "Free-text query." },
|
|
655
|
+
limit: {
|
|
656
|
+
type: "integer",
|
|
657
|
+
minimum: 1,
|
|
658
|
+
maximum: 50,
|
|
659
|
+
description: "Max matches to return (default 10)."
|
|
660
|
+
},
|
|
661
|
+
scope: {
|
|
662
|
+
type: "string",
|
|
663
|
+
enum: ["description", "body", "all"],
|
|
664
|
+
description: 'Where to search: "description" (frontmatter, default), "body" (SKILL.md prose), or "all".'
|
|
665
|
+
}
|
|
666
|
+
},
|
|
667
|
+
required: ["query"]
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
];
|
|
671
|
+
var TOOL_NAMES3 = new Set(TOOLS3.map((t) => t.name));
|
|
672
|
+
function pickEntry2(s) {
|
|
673
|
+
return {
|
|
674
|
+
id: s.id,
|
|
675
|
+
name: s.name,
|
|
676
|
+
description: s.description,
|
|
677
|
+
version: s.version,
|
|
678
|
+
source: s.source,
|
|
679
|
+
ides: s.ides ?? [],
|
|
680
|
+
updateStrategy: s.updateStrategy ?? "managed",
|
|
681
|
+
template: s.template ?? false
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
function createSkillsGroup(opts = {}) {
|
|
685
|
+
let cache = opts.loaded ?? null;
|
|
686
|
+
function getManifest() {
|
|
687
|
+
if (!cache) cache = loadSkillsManifest();
|
|
688
|
+
return cache;
|
|
689
|
+
}
|
|
690
|
+
return {
|
|
691
|
+
name: "skills",
|
|
692
|
+
tools: TOOLS3,
|
|
693
|
+
async handle(name, args) {
|
|
694
|
+
if (!TOOL_NAMES3.has(name)) return void 0;
|
|
695
|
+
if (name === "skills_list") {
|
|
696
|
+
ListInput2.parse(args ?? {});
|
|
697
|
+
const { manifest } = getManifest();
|
|
698
|
+
return {
|
|
699
|
+
content: [
|
|
700
|
+
{
|
|
701
|
+
type: "text",
|
|
702
|
+
text: JSON.stringify(manifest.skills.map(pickEntry2), null, 2)
|
|
703
|
+
}
|
|
704
|
+
]
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
if (name === "skills_get") {
|
|
708
|
+
const input = GetInput2.parse(args);
|
|
709
|
+
const loaded = getManifest();
|
|
710
|
+
const entry = findSkill(loaded, input.id);
|
|
711
|
+
if (!entry) {
|
|
712
|
+
return {
|
|
713
|
+
content: [
|
|
714
|
+
{
|
|
715
|
+
type: "text",
|
|
716
|
+
text: `Skill not found: ${input.id}. Use skills_list to discover ids.`
|
|
717
|
+
}
|
|
718
|
+
],
|
|
719
|
+
isError: true
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
const content = loadSkillContent(entry, loaded.rootDir);
|
|
723
|
+
if (!content) {
|
|
724
|
+
return {
|
|
725
|
+
content: [
|
|
726
|
+
{
|
|
727
|
+
type: "text",
|
|
728
|
+
text: `Skill source missing on disk for "${entry.id}" (expected at ${entry.source}). Check packages/skills installation.`
|
|
729
|
+
}
|
|
730
|
+
],
|
|
731
|
+
isError: true
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
return {
|
|
735
|
+
content: [
|
|
736
|
+
{
|
|
737
|
+
type: "text",
|
|
738
|
+
text: JSON.stringify(
|
|
739
|
+
{
|
|
740
|
+
entry: pickEntry2(entry),
|
|
741
|
+
attachments: content.attachments,
|
|
742
|
+
content: content.text
|
|
743
|
+
},
|
|
744
|
+
null,
|
|
745
|
+
2
|
|
746
|
+
)
|
|
747
|
+
}
|
|
748
|
+
]
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
if (name === "skills_find") {
|
|
752
|
+
const input = FindInput2.parse(args);
|
|
753
|
+
const limit = input.limit ?? 10;
|
|
754
|
+
const scope = input.scope ?? "description";
|
|
755
|
+
const needle = input.query.toLowerCase();
|
|
756
|
+
const loaded = getManifest();
|
|
757
|
+
const matches = [];
|
|
758
|
+
for (const entry of loaded.manifest.skills) {
|
|
759
|
+
const descHit = scope !== "body" && entry.description.toLowerCase().includes(needle);
|
|
760
|
+
let bodyHit = false;
|
|
761
|
+
if (scope === "body" || scope === "all") {
|
|
762
|
+
const content = loadSkillContent(entry, loaded.rootDir);
|
|
763
|
+
if (content) {
|
|
764
|
+
bodyHit = content.text.toLowerCase().includes(needle);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
if (!descHit && !bodyHit) continue;
|
|
768
|
+
matches.push({
|
|
769
|
+
entry: pickEntry2(entry),
|
|
770
|
+
hitIn: descHit && bodyHit ? "both" : descHit ? "description" : "body"
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
return {
|
|
774
|
+
content: [
|
|
775
|
+
{
|
|
776
|
+
type: "text",
|
|
777
|
+
text: JSON.stringify(
|
|
778
|
+
{
|
|
779
|
+
query: input.query,
|
|
780
|
+
scope,
|
|
781
|
+
limit,
|
|
782
|
+
count: Math.min(matches.length, limit),
|
|
783
|
+
totalMatches: matches.length,
|
|
784
|
+
results: matches.slice(0, limit)
|
|
785
|
+
},
|
|
786
|
+
null,
|
|
787
|
+
2
|
|
788
|
+
)
|
|
789
|
+
}
|
|
790
|
+
]
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
return void 0;
|
|
794
|
+
}
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// src/groups/design.ts
|
|
799
|
+
import { z as z4 } from "zod";
|
|
800
|
+
|
|
801
|
+
// src/design-loader.ts
|
|
802
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync as readdirSync3, statSync as statSync2 } from "fs";
|
|
803
|
+
import { dirname as dirname4, join as join4, resolve as resolve4 } from "path";
|
|
804
|
+
function resolveDesignRoot(startDir = process.cwd()) {
|
|
805
|
+
const envPath = process.env.TEAMIX_EVO_DESIGN_ROOT;
|
|
806
|
+
if (envPath && existsSync4(envPath)) {
|
|
807
|
+
return envPath;
|
|
808
|
+
}
|
|
809
|
+
let dir = resolve4(startDir);
|
|
810
|
+
for (let i = 0; i < 16; i++) {
|
|
811
|
+
const candidate = join4(dir, ".teamix-evo", "design");
|
|
812
|
+
if (existsSync4(candidate)) return candidate;
|
|
813
|
+
const parent = dirname4(dir);
|
|
814
|
+
if (parent === dir) break;
|
|
815
|
+
dir = parent;
|
|
816
|
+
}
|
|
817
|
+
return null;
|
|
818
|
+
}
|
|
819
|
+
function loadDesign(rootDir) {
|
|
820
|
+
const root = rootDir ?? resolveDesignRoot();
|
|
821
|
+
if (!root) return null;
|
|
822
|
+
let variant = null;
|
|
823
|
+
const lockPath = join4(root, "pack.lock.json");
|
|
824
|
+
if (existsSync4(lockPath)) {
|
|
825
|
+
try {
|
|
826
|
+
const raw = JSON.parse(readFileSync4(lockPath, "utf-8"));
|
|
827
|
+
variant = typeof raw.variant === "string" ? raw.variant : null;
|
|
828
|
+
} catch {
|
|
829
|
+
variant = null;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
return { rootDir: root, variant };
|
|
833
|
+
}
|
|
834
|
+
var PRINCIPLE_HEADING_RE = /^#{2,3}\s+(P\d+)\s*[·:]\s*(.+?)\s*$/;
|
|
835
|
+
function readPrinciples(loaded) {
|
|
836
|
+
const filePath = join4(loaded.rootDir, "philosophy", "principles.md");
|
|
837
|
+
if (!existsSync4(filePath)) {
|
|
838
|
+
return {
|
|
839
|
+
principles: [],
|
|
840
|
+
sourcePath: null,
|
|
841
|
+
note: "philosophy/principles.md not found in this project. Run `teamix-evo design init <variant>` to install it."
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
const text = readFileSync4(filePath, "utf-8");
|
|
845
|
+
const lines = text.split(/\r?\n/);
|
|
846
|
+
const out = [];
|
|
847
|
+
for (let i = 0; i < lines.length; i++) {
|
|
848
|
+
const m = lines[i].match(PRINCIPLE_HEADING_RE);
|
|
849
|
+
if (!m) continue;
|
|
850
|
+
let def = "";
|
|
851
|
+
for (let j = i + 1; j < lines.length && j < i + 8; j++) {
|
|
852
|
+
const candidate = lines[j].trim();
|
|
853
|
+
if (!candidate) continue;
|
|
854
|
+
if (/^#{1,6}\s/.test(candidate)) break;
|
|
855
|
+
def = candidate.replace(/^[*_>\s-]+/, "").trim();
|
|
856
|
+
break;
|
|
857
|
+
}
|
|
858
|
+
out.push({ id: m[1], name: m[2].trim(), oneLineDef: def });
|
|
859
|
+
}
|
|
860
|
+
return { principles: out, sourcePath: filePath };
|
|
861
|
+
}
|
|
862
|
+
function resolveTokensDir(loaded) {
|
|
863
|
+
const projectRoot = dirname4(loaded.rootDir);
|
|
864
|
+
const lifted = join4(projectRoot, "tokens");
|
|
865
|
+
if (existsSync4(lifted)) return lifted;
|
|
866
|
+
const legacy = join4(loaded.rootDir, "foundations", "tokens");
|
|
867
|
+
if (existsSync4(legacy)) return legacy;
|
|
868
|
+
return null;
|
|
869
|
+
}
|
|
870
|
+
function readTokens(loaded) {
|
|
871
|
+
const tokensDir = resolveTokensDir(loaded);
|
|
872
|
+
const out = { sources: [] };
|
|
873
|
+
if (!tokensDir) {
|
|
874
|
+
out.note = ".teamix-evo/tokens/ not found in this project (and no legacy foundations/tokens/).";
|
|
875
|
+
return out;
|
|
876
|
+
}
|
|
877
|
+
const tryJson = (rel) => {
|
|
878
|
+
const fp = join4(tokensDir, rel);
|
|
879
|
+
if (!existsSync4(fp)) return void 0;
|
|
880
|
+
out.sources.push(fp);
|
|
881
|
+
try {
|
|
882
|
+
return JSON.parse(readFileSync4(fp, "utf-8"));
|
|
883
|
+
} catch {
|
|
884
|
+
return void 0;
|
|
885
|
+
}
|
|
886
|
+
};
|
|
887
|
+
const tryText = (rel) => {
|
|
888
|
+
const fp = join4(tokensDir, rel);
|
|
889
|
+
if (!existsSync4(fp)) return void 0;
|
|
890
|
+
out.sources.push(fp);
|
|
891
|
+
return readFileSync4(fp, "utf-8");
|
|
892
|
+
};
|
|
893
|
+
out.base = tryJson("base.tokens.json");
|
|
894
|
+
out.semantic = tryJson("semantic.tokens.json");
|
|
895
|
+
out.themeCss = tryText("tokens.theme.css");
|
|
896
|
+
out.overridesCss = tryText("tokens.overrides.css");
|
|
897
|
+
return out;
|
|
898
|
+
}
|
|
899
|
+
var VARIANT_PREFIX_RE = /^(cloud|opentrek|uni-manager|enterprise)-/i;
|
|
900
|
+
function readPatternIndex(loaded) {
|
|
901
|
+
const patternsDir = join4(loaded.rootDir, "patterns");
|
|
902
|
+
if (!existsSync4(patternsDir)) return [];
|
|
903
|
+
const out = [];
|
|
904
|
+
for (const name of readdirSync3(patternsDir)) {
|
|
905
|
+
if (!name.endsWith(".md")) continue;
|
|
906
|
+
const fp = join4(patternsDir, name);
|
|
907
|
+
try {
|
|
908
|
+
if (!statSync2(fp).isFile()) continue;
|
|
909
|
+
} catch {
|
|
910
|
+
continue;
|
|
911
|
+
}
|
|
912
|
+
const stem = name.replace(/\.md$/, "");
|
|
913
|
+
const text = readFileSync4(fp, "utf-8");
|
|
914
|
+
const titleMatch = text.match(/^#\s+(.+?)\s*$/m);
|
|
915
|
+
out.push({
|
|
916
|
+
id: stem,
|
|
917
|
+
title: titleMatch ? titleMatch[1] : stem,
|
|
918
|
+
sourcePath: fp,
|
|
919
|
+
variantSpecific: VARIANT_PREFIX_RE.test(stem)
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
return out.sort((a, b) => a.id.localeCompare(b.id));
|
|
923
|
+
}
|
|
924
|
+
function readPatternContent(loaded, id) {
|
|
925
|
+
const stem = id.replace(/\.md$/, "");
|
|
926
|
+
const fp = join4(loaded.rootDir, "patterns", `${stem}.md`);
|
|
927
|
+
if (!existsSync4(fp)) return null;
|
|
928
|
+
return {
|
|
929
|
+
id: stem,
|
|
930
|
+
sourcePath: fp,
|
|
931
|
+
content: readFileSync4(fp, "utf-8")
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
function readBrand(loaded) {
|
|
935
|
+
const brandDir = join4(loaded.rootDir, "brand");
|
|
936
|
+
const out = {};
|
|
937
|
+
if (!existsSync4(brandDir)) {
|
|
938
|
+
out.note = "brand/ not present \u2014 this design variant does not ship brand-specific tone / voice / examples.";
|
|
939
|
+
return out;
|
|
940
|
+
}
|
|
941
|
+
for (const key of ["tone", "voice", "examples"]) {
|
|
942
|
+
const fp = join4(brandDir, `${key}.md`);
|
|
943
|
+
if (existsSync4(fp)) {
|
|
944
|
+
out[key] = { sourcePath: fp, content: readFileSync4(fp, "utf-8") };
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
return out;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// src/groups/design.ts
|
|
951
|
+
var EmptyInput = z4.object({});
|
|
952
|
+
var GetPatternInput = z4.object({
|
|
953
|
+
id: z4.string().min(1)
|
|
954
|
+
});
|
|
955
|
+
var TOOLS4 = [
|
|
956
|
+
{
|
|
957
|
+
name: "design_list_principles",
|
|
958
|
+
description: 'List the design principles defined by the consumer project\'s installed design variant. Reads `.teamix-evo/design/philosophy/principles.md` and parses headings of the form `## P1 \xB7 Name`. Returns id, display name, and a one-line definition for each. Use this when the user asks about "the four principles", "design philosophy", or to ground a design review in the variant\'s stated values. Returns `{ principles: [], note: "..." }` if the project hasn\'t installed a design pack.',
|
|
959
|
+
inputSchema: { type: "object", properties: {} }
|
|
960
|
+
},
|
|
961
|
+
{
|
|
962
|
+
name: "design_get_tokens",
|
|
963
|
+
description: "Fetch the consumer project's design tokens \u2014 parsed JSON from `.teamix-evo/tokens/base.tokens.json` and `semantic.tokens.json`, plus raw text of `tokens.theme.css` (variant theme) and `tokens.overrides.css` (user-owned overrides) when present. Use when AI needs to know what semantic colors / spacing / radii are available before writing component styles. JSON is for introspection; CSS is for copy-paste. Each tool result lists `sources` so you can cite paths.",
|
|
964
|
+
inputSchema: { type: "object", properties: {} }
|
|
965
|
+
},
|
|
966
|
+
{
|
|
967
|
+
name: "design_list_patterns",
|
|
968
|
+
description: "Index every pattern in `.teamix-evo/design/patterns/*.md` (page types, journeys, flows, plus variant-specific markers like `cloud-*`, `opentrek-*`, `uni-manager-*`). Returns `{ id, title, sourcePath, variantSpecific }` for each. Use this before `design_get_pattern` to discover which patterns exist. When both a baseline (`page-types`) and a variant-specific (`cloud-page-types`) entry are available, prefer the variant-specific one for projects that match.",
|
|
969
|
+
inputSchema: { type: "object", properties: {} }
|
|
970
|
+
},
|
|
971
|
+
{
|
|
972
|
+
name: "design_get_pattern",
|
|
973
|
+
description: "Fetch the full markdown body of one pattern by id (filename stem, e.g. `page-types` or `cloud-page-types`). Use after `design_list_patterns` discovered the id. Returns the raw markdown plus `sourcePath`. Returns isError if the id does not exist \u2014 call `design_list_patterns` first to discover available ids.",
|
|
974
|
+
inputSchema: {
|
|
975
|
+
type: "object",
|
|
976
|
+
properties: {
|
|
977
|
+
id: {
|
|
978
|
+
type: "string",
|
|
979
|
+
description: 'Pattern id \u2014 filename stem under `patterns/`, e.g. "page-types", "cloud-page-types".'
|
|
980
|
+
}
|
|
981
|
+
},
|
|
982
|
+
required: ["id"]
|
|
983
|
+
}
|
|
984
|
+
},
|
|
985
|
+
{
|
|
986
|
+
name: "design_get_brand",
|
|
987
|
+
description: "Fetch the consumer project's variant-specific brand voice \u2014 `brand/tone.md`, `brand/voice.md`, `brand/examples.md`. Each is optional; the response contains only the files that exist. Some variants (like `_template` or `default`) ship no brand content \u2014 that's not an error, the response simply lists no entries. Use this when generating or reviewing user-facing copy, error messages, empty states, or onboarding text where brand voice matters.",
|
|
988
|
+
inputSchema: { type: "object", properties: {} }
|
|
989
|
+
}
|
|
990
|
+
];
|
|
991
|
+
var TOOL_NAMES4 = new Set(TOOLS4.map((t) => t.name));
|
|
992
|
+
var NO_DESIGN_NOTE = ".teamix-evo/design/ not found in this project. Run `npx teamix-evo design init <variant>` to install a design pack.";
|
|
993
|
+
function createDesignGroup(opts = {}) {
|
|
994
|
+
let cache = opts.loaded === void 0 ? void 0 : opts.loaded;
|
|
995
|
+
function getDesign() {
|
|
996
|
+
if (cache === void 0) cache = loadDesign(opts.rootDir);
|
|
997
|
+
return cache;
|
|
998
|
+
}
|
|
999
|
+
return {
|
|
1000
|
+
name: "design",
|
|
1001
|
+
tools: TOOLS4,
|
|
1002
|
+
async handle(name, args) {
|
|
1003
|
+
if (!TOOL_NAMES4.has(name)) return void 0;
|
|
1004
|
+
const loaded = getDesign();
|
|
1005
|
+
if (name === "design_list_principles") {
|
|
1006
|
+
EmptyInput.parse(args ?? {});
|
|
1007
|
+
if (!loaded) {
|
|
1008
|
+
return jsonResult({ principles: [], sourcePath: null, note: NO_DESIGN_NOTE });
|
|
1009
|
+
}
|
|
1010
|
+
return jsonResult(readPrinciples(loaded));
|
|
1011
|
+
}
|
|
1012
|
+
if (name === "design_get_tokens") {
|
|
1013
|
+
EmptyInput.parse(args ?? {});
|
|
1014
|
+
if (!loaded) {
|
|
1015
|
+
return jsonResult({ sources: [], note: NO_DESIGN_NOTE });
|
|
1016
|
+
}
|
|
1017
|
+
return jsonResult(readTokens(loaded));
|
|
1018
|
+
}
|
|
1019
|
+
if (name === "design_list_patterns") {
|
|
1020
|
+
EmptyInput.parse(args ?? {});
|
|
1021
|
+
if (!loaded) {
|
|
1022
|
+
return jsonResult({ patterns: [], note: NO_DESIGN_NOTE });
|
|
1023
|
+
}
|
|
1024
|
+
return jsonResult({
|
|
1025
|
+
variant: loaded.variant,
|
|
1026
|
+
patterns: readPatternIndex(loaded)
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
if (name === "design_get_pattern") {
|
|
1030
|
+
const input = GetPatternInput.parse(args);
|
|
1031
|
+
if (!loaded) {
|
|
1032
|
+
return {
|
|
1033
|
+
content: [{ type: "text", text: NO_DESIGN_NOTE }],
|
|
1034
|
+
isError: true
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
const result = readPatternContent(loaded, input.id);
|
|
1038
|
+
if (!result) {
|
|
1039
|
+
return {
|
|
1040
|
+
content: [
|
|
1041
|
+
{
|
|
1042
|
+
type: "text",
|
|
1043
|
+
text: `Pattern not found: ${input.id}. Use design_list_patterns to discover available ids.`
|
|
1044
|
+
}
|
|
1045
|
+
],
|
|
1046
|
+
isError: true
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
return jsonResult(result);
|
|
1050
|
+
}
|
|
1051
|
+
if (name === "design_get_brand") {
|
|
1052
|
+
EmptyInput.parse(args ?? {});
|
|
1053
|
+
if (!loaded) {
|
|
1054
|
+
return jsonResult({ note: NO_DESIGN_NOTE });
|
|
1055
|
+
}
|
|
1056
|
+
return jsonResult(readBrand(loaded));
|
|
1057
|
+
}
|
|
1058
|
+
return void 0;
|
|
1059
|
+
}
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
function jsonResult(payload) {
|
|
1063
|
+
return {
|
|
1064
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }]
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// src/server.ts
|
|
1069
|
+
function createServer(opts = {}) {
|
|
1070
|
+
const groups = [
|
|
1071
|
+
createRegistryGroup(opts.registry),
|
|
1072
|
+
createAdrGroup(opts.adr),
|
|
1073
|
+
createSkillsGroup(opts.skills),
|
|
1074
|
+
createDesignGroup(opts.design)
|
|
1075
|
+
// Future: createScenarioGroup(opts.scenario), // ADR 0011, v0.8
|
|
1076
|
+
];
|
|
1077
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1078
|
+
for (const group of groups) {
|
|
1079
|
+
for (const tool of group.tools) {
|
|
1080
|
+
if (seen.has(tool.name)) {
|
|
1081
|
+
throw new Error(
|
|
1082
|
+
`Duplicate MCP tool name across groups: ${tool.name}. Each group must use unique tool names \u2014 see ADR 0011 \xA75.`
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
seen.add(tool.name);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
const allTools = groups.flatMap((g) => g.tools);
|
|
1089
|
+
const server = new Server(
|
|
1090
|
+
{
|
|
1091
|
+
name: "@teamix-evo/mcp",
|
|
1092
|
+
version: "0.2.0"
|
|
1093
|
+
},
|
|
1094
|
+
{
|
|
1095
|
+
capabilities: { tools: {} }
|
|
1096
|
+
}
|
|
1097
|
+
);
|
|
1098
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
1099
|
+
tools: allTools
|
|
1100
|
+
}));
|
|
1101
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
1102
|
+
const { name, arguments: args } = req.params;
|
|
1103
|
+
for (const group of groups) {
|
|
1104
|
+
const result = await group.handle(name, args);
|
|
1105
|
+
if (result !== void 0) return result;
|
|
1106
|
+
}
|
|
1107
|
+
return {
|
|
1108
|
+
content: [
|
|
1109
|
+
{
|
|
1110
|
+
type: "text",
|
|
1111
|
+
text: `Unknown tool: ${name}. Use tools/list to discover available tools.`
|
|
1112
|
+
}
|
|
1113
|
+
],
|
|
1114
|
+
isError: true
|
|
1115
|
+
};
|
|
1116
|
+
});
|
|
1117
|
+
return server;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// src/cli.ts
|
|
1121
|
+
async function main() {
|
|
1122
|
+
const server = createServer();
|
|
1123
|
+
const transport = new StdioServerTransport();
|
|
1124
|
+
await server.connect(transport);
|
|
1125
|
+
}
|
|
1126
|
+
main().catch((err) => {
|
|
1127
|
+
console.error("[teamix-evo-mcp] fatal:", err);
|
|
1128
|
+
process.exit(1);
|
|
1129
|
+
});
|
|
1130
|
+
//# sourceMappingURL=cli.js.map
|