@sdods/mcp 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/LICENSE +17 -0
  2. package/dist/.tsbuildinfo +1 -0
  3. package/dist/cli.d.ts +50 -0
  4. package/dist/cli.js +171 -0
  5. package/dist/fs.d.ts +45 -0
  6. package/dist/fs.js +184 -0
  7. package/dist/index.d.ts +15 -0
  8. package/dist/index.js +15 -0
  9. package/dist/install/index.d.ts +46 -0
  10. package/dist/install/index.js +146 -0
  11. package/dist/prompts/index.d.ts +33 -0
  12. package/dist/prompts/index.js +151 -0
  13. package/dist/proposals.d.ts +71 -0
  14. package/dist/proposals.js +188 -0
  15. package/dist/registry/adapters.d.ts +46 -0
  16. package/dist/registry/adapters.js +76 -0
  17. package/dist/registry/registry.d.ts +96 -0
  18. package/dist/registry/registry.js +141 -0
  19. package/dist/resources.d.ts +5 -0
  20. package/dist/resources.js +130 -0
  21. package/dist/server.d.ts +29 -0
  22. package/dist/server.js +143 -0
  23. package/dist/tools/analyze.d.ts +87 -0
  24. package/dist/tools/analyze.js +820 -0
  25. package/dist/tools/data.d.ts +18 -0
  26. package/dist/tools/data.js +153 -0
  27. package/dist/tools/feature.d.ts +53 -0
  28. package/dist/tools/feature.js +340 -0
  29. package/dist/tools/index.d.ts +12 -0
  30. package/dist/tools/index.js +23 -0
  31. package/dist/tools/issue-schedule.d.ts +22 -0
  32. package/dist/tools/issue-schedule.js +193 -0
  33. package/dist/tools/project.d.ts +11 -0
  34. package/dist/tools/project.js +144 -0
  35. package/dist/tools/proposal.d.ts +19 -0
  36. package/dist/tools/proposal.js +89 -0
  37. package/dist/tools/run.d.ts +49 -0
  38. package/dist/tools/run.js +387 -0
  39. package/dist/transports/http.d.ts +40 -0
  40. package/dist/transports/http.js +151 -0
  41. package/dist/transports/stdio.d.ts +6 -0
  42. package/dist/transports/stdio.js +15 -0
  43. package/package.json +57 -0
@@ -0,0 +1,18 @@
1
+ import { z } from 'zod';
2
+ export declare function maskRows(rows: Array<Record<string, unknown>>): Array<Record<string, unknown>>;
3
+ export declare function loadRows(file: string): Array<Record<string, unknown>>;
4
+ /** Resolve a dataset file for an env with the SDODS fallback chain. */
5
+ export declare function resolveDatasetFile(root: string, spec: {
6
+ path?: string;
7
+ fallback?: string;
8
+ }, env?: string): string | undefined;
9
+ export declare const dataTools: (import("../registry/registry.js").SdodsTool<{
10
+ project: z.ZodString;
11
+ env: z.ZodOptional<z.ZodString>;
12
+ }> | import("../registry/registry.js").SdodsTool<{
13
+ project: z.ZodString;
14
+ dataset: z.ZodString;
15
+ env: z.ZodOptional<z.ZodString>;
16
+ limit: z.ZodOptional<z.ZodNumber>;
17
+ }>)[];
18
+ //# sourceMappingURL=data.d.ts.map
@@ -0,0 +1,153 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { z } from 'zod';
4
+ import { parse as parseCsv } from 'csv-parse/sync';
5
+ import { parse as parseYaml } from 'yaml';
6
+ import { projectRoot, readYaml, safeJoin } from '../fs.js';
7
+ import { defineTool, summarize } from '../registry/registry.js';
8
+ const SECRET_COL = /(password|secret|token|apikey|api_key)/i;
9
+ export function maskRows(rows) {
10
+ return rows.map((r) => Object.fromEntries(Object.entries(r).map(([k, v]) => [k, SECRET_COL.test(k) && v ? '***' : v])));
11
+ }
12
+ export function loadRows(file) {
13
+ const text = readFileSync(file, 'utf8');
14
+ if (file.endsWith('.csv'))
15
+ return parseCsv(text, {
16
+ columns: true,
17
+ skip_empty_lines: true,
18
+ trim: true,
19
+ bom: true,
20
+ });
21
+ if (file.endsWith('.json')) {
22
+ const j = JSON.parse(text);
23
+ return Array.isArray(j)
24
+ ? j
25
+ : (j.rows ?? []);
26
+ }
27
+ const y = parseYaml(text);
28
+ return Array.isArray(y)
29
+ ? y
30
+ : (y?.rows ?? []);
31
+ }
32
+ /** Resolve a dataset file for an env with the SDODS fallback chain. */
33
+ export function resolveDatasetFile(root, spec, env) {
34
+ const candidates = [];
35
+ if (spec.path) {
36
+ if (env)
37
+ candidates.push(spec.path.replace('{env}', env));
38
+ if (!spec.path.includes('{env}'))
39
+ candidates.push(spec.path);
40
+ }
41
+ if (spec.fallback)
42
+ candidates.push(spec.fallback);
43
+ if (spec.path) {
44
+ const base = spec.path.split('/').pop();
45
+ candidates.push(`data/common/${base}`);
46
+ }
47
+ for (const c of candidates) {
48
+ const f = safeJoin(root, c);
49
+ if (existsSync(f))
50
+ return f;
51
+ }
52
+ return undefined;
53
+ }
54
+ export const dataTools = [
55
+ defineTool({
56
+ name: 'data_list_datasets',
57
+ title: 'List datasets',
58
+ description: 'Datasets declared in a project yaml (type, path, fallback) and which file resolves for an environment.',
59
+ shape: { project: z.string(), env: z.string().optional() },
60
+ access: 'read',
61
+ domain: 'datasets',
62
+ capability: 'data',
63
+ handler: async (args, ctx) => {
64
+ const root = projectRoot(ctx.rootDir, args.project);
65
+ const yaml = readYaml(join(root, 'sdods.project.yaml'));
66
+ const sources = yaml?.data?.sources ?? {};
67
+ const rows = Object.entries(sources).map(([name, spec]) => ({
68
+ name,
69
+ ...spec,
70
+ resolvedFile: spec.type === 'db' || spec.type === 'openapi'
71
+ ? undefined
72
+ : resolveDatasetFile(root, spec, args.env)?.replace(root + '/', ''),
73
+ }));
74
+ return {
75
+ text: summarize(`Datasets of ${args.project}`, {
76
+ sources: rows,
77
+ userPool: yaml?.data?.userPool,
78
+ }),
79
+ data: { sources: rows, userPool: yaml?.data?.userPool },
80
+ };
81
+ },
82
+ }),
83
+ defineTool({
84
+ name: 'data_preview',
85
+ title: 'Preview dataset',
86
+ description: 'First N rows of a dataset (csv/json/yaml) for an environment; password-like columns are masked.',
87
+ shape: {
88
+ project: z.string(),
89
+ dataset: z.string(),
90
+ env: z.string().optional(),
91
+ limit: z.number().int().positive().max(200).optional(),
92
+ },
93
+ access: 'read',
94
+ domain: 'datasets',
95
+ capability: 'data',
96
+ handler: async (args, ctx) => {
97
+ const root = projectRoot(ctx.rootDir, args.project);
98
+ const yaml = readYaml(join(root, 'sdods.project.yaml'));
99
+ const spec = yaml?.data?.sources?.[args.dataset];
100
+ if (!spec)
101
+ throw Object.assign(new Error(`Unknown dataset ${args.dataset}`), {
102
+ error: { code: 'DATASET_NOT_FOUND' },
103
+ });
104
+ if (spec.type === 'db' || spec.type === 'openapi')
105
+ return {
106
+ text: `Dataset ${args.dataset} is of type ${spec.type}; preview needs a database/spec (not available here).`,
107
+ data: { note: `type ${spec.type}` },
108
+ };
109
+ const file = resolveDatasetFile(root, spec, args.env);
110
+ if (!file)
111
+ return {
112
+ text: `No file resolves for ${args.dataset} (env ${args.env ?? 'default'}).`,
113
+ data: { rows: [] },
114
+ isError: true,
115
+ };
116
+ const rows = maskRows(loadRows(file)).slice(0, args.limit ?? 20);
117
+ return {
118
+ text: summarize(`${args.dataset} from ${file.replace(root + '/', '')} (${rows.length} rows)`, rows),
119
+ data: { file: file.replace(root + '/', ''), rows },
120
+ };
121
+ },
122
+ }),
123
+ defineTool({
124
+ name: 'data_pool_status',
125
+ title: 'User pool status',
126
+ description: 'Pool users by role for an environment (from the pool dataset) and current lease lock files.',
127
+ shape: { project: z.string(), env: z.string().optional() },
128
+ access: 'read',
129
+ domain: 'datasets',
130
+ capability: 'data',
131
+ handler: async (args, ctx) => {
132
+ const root = projectRoot(ctx.rootDir, args.project);
133
+ const yaml = readYaml(join(root, 'sdods.project.yaml'));
134
+ const pool = yaml?.data?.userPool;
135
+ if (!pool)
136
+ return { text: 'No user pool configured.', data: { roles: {} } };
137
+ const spec = yaml?.data?.sources?.[pool.dataset];
138
+ const file = spec ? resolveDatasetFile(root, spec, args.env) : undefined;
139
+ const rows = file ? loadRows(file) : [];
140
+ const roleCol = pool.roleColumn ?? 'role';
141
+ const roles = {};
142
+ for (const r of rows)
143
+ roles[String(r[roleCol] ?? 'unknown')] = (roles[String(r[roleCol] ?? 'unknown')] ?? 0) + 1;
144
+ const leaseDir = join(ctx.rootDir, '.sdods', 'leases', args.project, args.env ?? '');
145
+ const leases = existsSync(leaseDir) ? (await import('node:fs')).readdirSync(leaseDir) : [];
146
+ return {
147
+ text: summarize(`Pool for ${args.project}`, { roles, leases }),
148
+ data: { dataset: pool.dataset, file: file?.replace(root + '/', ''), roles, leases },
149
+ };
150
+ },
151
+ }),
152
+ ];
153
+ //# sourceMappingURL=data.js.map
@@ -0,0 +1,53 @@
1
+ import { z } from 'zod';
2
+ export interface ParsedScenario {
3
+ name: string;
4
+ line: number;
5
+ keyword: string;
6
+ tags: string[];
7
+ steps: Array<{
8
+ keyword: string;
9
+ text: string;
10
+ line: number;
11
+ hasDocString: boolean;
12
+ hasDataTable: boolean;
13
+ }>;
14
+ examplesRows?: number;
15
+ }
16
+ export interface ParsedFeature {
17
+ path: string;
18
+ name: string;
19
+ tags: string[];
20
+ background?: {
21
+ steps: string[];
22
+ };
23
+ scenarios: ParsedScenario[];
24
+ errors: string[];
25
+ }
26
+ export declare function parseGherkin(text: string, path?: string): ParsedFeature;
27
+ export declare function basicTagCheck(feature: ParsedFeature, suites: string[]): string[];
28
+ export declare const featureTools: (import("../registry/registry.js").SdodsTool<{
29
+ project: z.ZodString;
30
+ }> | import("../registry/registry.js").SdodsTool<{
31
+ project: z.ZodString;
32
+ path: z.ZodString;
33
+ }> | import("../registry/registry.js").SdodsTool<{
34
+ text: z.ZodOptional<z.ZodString>;
35
+ project: z.ZodOptional<z.ZodString>;
36
+ path: z.ZodOptional<z.ZodString>;
37
+ }> | import("../registry/registry.js").SdodsTool<{
38
+ project: z.ZodString;
39
+ path: z.ZodString;
40
+ text: z.ZodString;
41
+ summary: z.ZodOptional<z.ZodString>;
42
+ extraFiles: z.ZodOptional<z.ZodArray<z.ZodObject<{
43
+ path: z.ZodString;
44
+ content: z.ZodString;
45
+ }, z.core.$strip>>>;
46
+ }> | import("../registry/registry.js").SdodsTool<{
47
+ project: z.ZodString;
48
+ phrase: z.ZodString;
49
+ limit: z.ZodOptional<z.ZodNumber>;
50
+ }>)[];
51
+ /** Token overlap similarity ignoring Cucumber parameter placeholders. */
52
+ export declare function similarity(a: string, b: string): number;
53
+ //# sourceMappingURL=feature.d.ts.map
@@ -0,0 +1,340 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join, relative } from 'node:path';
3
+ import { z } from 'zod';
4
+ import { AstBuilder, GherkinClassicTokenMatcher, Parser } from '@cucumber/gherkin';
5
+ import { IdGenerator } from '@cucumber/messages';
6
+ import { SdodsCliError, sdodsCli, cliOrNote } from '../cli.js';
7
+ import { projectRoot, readYaml, safeJoin, walk } from '../fs.js';
8
+ import { ProposalStore } from '../proposals.js';
9
+ import { defineTool, summarize } from '../registry/registry.js';
10
+ export function parseGherkin(text, path = 'feature') {
11
+ const parser = new Parser(new AstBuilder(IdGenerator.uuid()), new GherkinClassicTokenMatcher());
12
+ let doc;
13
+ try {
14
+ doc = parser.parse(text);
15
+ }
16
+ catch (e) {
17
+ return { path, name: '', tags: [], scenarios: [], errors: [e.message] };
18
+ }
19
+ const feature = doc.feature;
20
+ if (!feature)
21
+ return { path, name: '', tags: [], scenarios: [], errors: ['No Feature keyword found'] };
22
+ const featureTags = feature.tags.map((t) => t.name);
23
+ const scenarios = [];
24
+ let background;
25
+ const collect = (children, inherited) => {
26
+ for (const child of children) {
27
+ if (child.background)
28
+ background = { steps: child.background.steps.map((s) => `${s.keyword.trim()} ${s.text}`) };
29
+ if (child.rule)
30
+ collect(child.rule.children, [...inherited, ...child.rule.tags.map((t) => t.name)]);
31
+ const sc = child.scenario;
32
+ if (!sc)
33
+ continue;
34
+ scenarios.push({
35
+ name: sc.name,
36
+ line: sc.location.line,
37
+ keyword: sc.keyword,
38
+ tags: [...inherited, ...sc.tags.map((t) => t.name)],
39
+ steps: sc.steps.map((s) => ({
40
+ keyword: s.keyword.trim(),
41
+ text: s.text,
42
+ line: s.location.line,
43
+ hasDocString: Boolean(s.docString),
44
+ hasDataTable: Boolean(s.dataTable),
45
+ })),
46
+ examplesRows: sc.examples.length
47
+ ? sc.examples.reduce((n, ex) => n + ex.tableBody.length, 0)
48
+ : undefined,
49
+ });
50
+ }
51
+ };
52
+ collect(feature.children, featureTags);
53
+ return { path, name: feature.name, tags: featureTags, background, scenarios, errors: [] };
54
+ }
55
+ const LAYER_TAGS = ['@ui', '@api', '@hybrid'];
56
+ export function basicTagCheck(feature, suites) {
57
+ const problems = [];
58
+ for (const sc of feature.scenarios) {
59
+ const layers = sc.tags.filter((t) => LAYER_TAGS.includes(t));
60
+ if (layers.length !== 1)
61
+ problems.push(`${feature.path}:${sc.line} "${sc.name}": expected exactly one layer tag (@ui|@api|@hybrid), found ${layers.join(' ') || 'none'}`);
62
+ const suiteTags = sc.tags.filter((t) => suites.includes(t));
63
+ if (suiteTags.length !== 1)
64
+ problems.push(`${feature.path}:${sc.line} "${sc.name}": expected exactly one suite tag (${suites.join('|')}), found ${suiteTags.join(' ') || 'none'}`);
65
+ }
66
+ return problems;
67
+ }
68
+ function suitesOf(root) {
69
+ const yaml = readYaml(join(root, 'sdods.project.yaml'));
70
+ return (yaml?.tags?.suites ?? ['smoke', 'regression', 'sanity']).map((s) => `@${s}`);
71
+ }
72
+ function featureFiles(root) {
73
+ const dir = join(root, 'features');
74
+ return walk(dir, (f) => f.endsWith('.feature')).map((f) => relative(root, f).replace(/\\/g, '/'));
75
+ }
76
+ export const featureTools = [
77
+ defineTool({
78
+ name: 'feature_list',
79
+ title: 'List features',
80
+ description: 'Feature files of a project with their tags, scenario counts and module directory.',
81
+ shape: { project: z.string() },
82
+ access: 'read',
83
+ domain: 'features',
84
+ capability: 'core',
85
+ handler: async (args, ctx) => {
86
+ const root = projectRoot(ctx.rootDir, args.project);
87
+ const files = featureFiles(root).map((rel) => {
88
+ const parsed = parseGherkin(readFileSync(join(root, rel), 'utf8'), rel);
89
+ const module = rel.split('/')[1] && rel.split('/').length > 2 ? rel.split('/')[1] : undefined;
90
+ return {
91
+ path: rel,
92
+ module,
93
+ name: parsed.name,
94
+ tags: parsed.tags,
95
+ scenarios: parsed.scenarios.length,
96
+ errors: parsed.errors,
97
+ };
98
+ });
99
+ return {
100
+ text: summarize(`Features of ${args.project} (${files.length})`, files),
101
+ data: files,
102
+ };
103
+ },
104
+ }),
105
+ defineTool({
106
+ name: 'feature_read',
107
+ title: 'Read feature',
108
+ description: 'Read a feature file (path relative to the project, e.g. features/auth/login.feature).',
109
+ shape: { project: z.string(), path: z.string() },
110
+ access: 'read',
111
+ domain: 'features',
112
+ capability: 'core',
113
+ handler: async (args, ctx) => {
114
+ const root = projectRoot(ctx.rootDir, args.project);
115
+ const file = safeJoin(join(root, 'features'), args.path.replace(/^features\//, ''));
116
+ if (!existsSync(file))
117
+ throw Object.assign(new Error(`No such feature: ${args.path}`), {
118
+ error: { code: 'NOT_FOUND' },
119
+ });
120
+ const text = readFileSync(file, 'utf8');
121
+ return { text: `\`\`\`gherkin\n${text}\n\`\`\``, data: { path: args.path, text } };
122
+ },
123
+ }),
124
+ defineTool({
125
+ name: 'feature_parse',
126
+ title: 'Parse feature',
127
+ description: 'Parse Gherkin text (or a project feature path) into tags, scenarios and steps; reports syntax errors.',
128
+ shape: {
129
+ text: z.string().optional().describe('Gherkin source'),
130
+ project: z.string().optional(),
131
+ path: z
132
+ .string()
133
+ .optional()
134
+ .describe('feature path relative to the project (alternative to text)'),
135
+ },
136
+ access: 'read',
137
+ domain: 'features',
138
+ capability: 'core',
139
+ handler: async (args, ctx) => {
140
+ let text = args.text;
141
+ let path = args.path ?? 'inline.feature';
142
+ if (!text && args.project && args.path) {
143
+ const root = projectRoot(ctx.rootDir, args.project);
144
+ text = readFileSync(safeJoin(join(root, 'features'), args.path.replace(/^features\//, '')), 'utf8');
145
+ path = args.path;
146
+ }
147
+ if (!text)
148
+ throw Object.assign(new Error('Pass text, or project + path'), {
149
+ error: { code: 'INVALID_ARGS' },
150
+ });
151
+ const parsed = parseGherkin(text, path);
152
+ const suites = args.project
153
+ ? suitesOf(projectRoot(ctx.rootDir, args.project))
154
+ : ['@smoke', '@regression', '@sanity'];
155
+ const problems = parsed.errors.length ? [] : basicTagCheck(parsed, suites);
156
+ return {
157
+ text: summarize(`Parsed ${path}`, { ...parsed, tagProblems: problems }),
158
+ data: { ...parsed, tagProblems: problems },
159
+ };
160
+ },
161
+ }),
162
+ defineTool({
163
+ name: 'feature_lint',
164
+ title: 'Lint features',
165
+ description: 'Run sdods lint for a project: Gherkin syntax, tag taxonomy, module rules, undefined steps.',
166
+ shape: { project: z.string() },
167
+ access: 'read',
168
+ domain: 'features',
169
+ capability: 'core',
170
+ handler: async (args, ctx) => {
171
+ try {
172
+ const r = await sdodsCli(['lint', '-p', args.project], {
173
+ cwd: ctx.rootDir,
174
+ signal: ctx.signal,
175
+ timeoutMs: 120_000,
176
+ });
177
+ return {
178
+ text: summarize(`Lint ${args.project}: clean`, r.json),
179
+ data: r.json ?? { errors: [], warnings: [] },
180
+ };
181
+ }
182
+ catch (e) {
183
+ if (e instanceof SdodsCliError) {
184
+ if (e.notSupported)
185
+ return {
186
+ text: 'sdods lint is not available in this build; falling back to basic checks.',
187
+ data: await basicLint(ctx.rootDir, args.project),
188
+ };
189
+ const m = /\{[\s\S]*\}/.exec(e.stderr) ?? /\{[\s\S]*\}/.exec(e.message);
190
+ const data = m ? safeJson(m[0]) : { error: e.error };
191
+ return {
192
+ text: summarize(`Lint ${args.project}: problems found`, data),
193
+ data,
194
+ isError: true,
195
+ };
196
+ }
197
+ throw e;
198
+ }
199
+ },
200
+ }),
201
+ defineTool({
202
+ name: 'feature_write',
203
+ title: 'Propose a feature file',
204
+ description: 'Validate Gherkin and stage it as a proposal (proposals/<id>/files/projects/<slug>/features/<path>). Never writes to the working tree; a person accepts the proposal.',
205
+ shape: {
206
+ project: z.string(),
207
+ path: z.string().describe('relative to the project features dir, e.g. auth/login.feature'),
208
+ text: z.string(),
209
+ summary: z.string().optional(),
210
+ extraFiles: z
211
+ .array(z.object({
212
+ path: z.string().describe('relative to the project root, e.g. steps/auth.steps.ts'),
213
+ content: z.string(),
214
+ }))
215
+ .optional()
216
+ .describe('companion step definitions or page objects'),
217
+ },
218
+ access: 'write',
219
+ domain: 'features',
220
+ capability: 'core',
221
+ annotations: { destructiveHint: false },
222
+ handler: async (args, ctx) => {
223
+ const root = projectRoot(ctx.rootDir, args.project);
224
+ const parsed = parseGherkin(args.text, args.path);
225
+ if (parsed.errors.length)
226
+ return {
227
+ text: `Gherkin errors:\n${parsed.errors.join('\n')}`,
228
+ data: { errors: parsed.errors },
229
+ isError: true,
230
+ };
231
+ const problems = basicTagCheck(parsed, suitesOf(root));
232
+ if (problems.length)
233
+ return {
234
+ text: `Tag policy violations:\n${problems.join('\n')}`,
235
+ data: { errors: problems },
236
+ isError: true,
237
+ };
238
+ const projectRel = relative(ctx.rootDir, root).replace(/\\/g, '/');
239
+ const files = [
240
+ {
241
+ path: `${projectRel}/features/${args.path.replace(/^features\//, '')}`,
242
+ content: args.text,
243
+ },
244
+ ...(args.extraFiles ?? []).map((f) => ({
245
+ path: `${projectRel}/${f.path}`,
246
+ content: f.content,
247
+ })),
248
+ ];
249
+ const store = new ProposalStore(ctx.rootDir);
250
+ const manifest = store.create({
251
+ role: 'mcp',
252
+ project: args.project,
253
+ summary: args.summary ?? `Feature ${args.path} (${parsed.scenarios.length} scenarios)`,
254
+ files,
255
+ });
256
+ return {
257
+ text: `Proposal ${manifest.id} created with ${files.length} file(s). Review with proposal_get and apply with proposal_accept.`,
258
+ data: manifest,
259
+ };
260
+ },
261
+ }),
262
+ defineTool({
263
+ name: 'step_list',
264
+ title: 'List step definitions',
265
+ description: 'Step patterns available to a project (core library + project steps).',
266
+ shape: { project: z.string() },
267
+ access: 'read',
268
+ domain: 'features',
269
+ capability: 'core',
270
+ handler: async (args, ctx) => {
271
+ const data = await cliOrNote(['steps', 'list', '-p', args.project], {
272
+ cwd: ctx.rootDir,
273
+ signal: ctx.signal,
274
+ timeoutMs: 120_000,
275
+ });
276
+ return { text: summarize(`Steps for ${args.project}`, data), data };
277
+ },
278
+ }),
279
+ defineTool({
280
+ name: 'step_find',
281
+ title: 'Find matching steps',
282
+ description: 'Fuzzy-match a phrase against the step patterns of a project to reuse existing steps.',
283
+ shape: {
284
+ project: z.string(),
285
+ phrase: z.string(),
286
+ limit: z.number().int().positive().max(50).optional(),
287
+ },
288
+ access: 'read',
289
+ domain: 'features',
290
+ capability: 'core',
291
+ handler: async (args, ctx) => {
292
+ const data = await cliOrNote(['steps', 'list', '-p', args.project], { cwd: ctx.rootDir, signal: ctx.signal, timeoutMs: 120_000 });
293
+ if (!Array.isArray(data))
294
+ return { text: 'Step list unavailable in this build.', data };
295
+ const scored = data
296
+ .map((s) => ({ ...s, score: similarity(args.phrase, s.pattern) }))
297
+ .sort((a, b) => b.score - a.score)
298
+ .slice(0, args.limit ?? 10);
299
+ return { text: summarize(`Closest steps to "${args.phrase}"`, scored), data: scored };
300
+ },
301
+ }),
302
+ ];
303
+ async function basicLint(rootDir, project) {
304
+ const root = projectRoot(rootDir, project);
305
+ const suites = suitesOf(root);
306
+ const errors = [];
307
+ for (const rel of featureFiles(root)) {
308
+ const parsed = parseGherkin(readFileSync(join(root, rel), 'utf8'), rel);
309
+ errors.push(...parsed.errors.map((e) => `${rel}: ${e}`), ...basicTagCheck(parsed, suites));
310
+ }
311
+ return { errors, warnings: [] };
312
+ }
313
+ function safeJson(s) {
314
+ try {
315
+ return JSON.parse(s);
316
+ }
317
+ catch {
318
+ return { raw: s };
319
+ }
320
+ }
321
+ /** Token overlap similarity ignoring Cucumber parameter placeholders. */
322
+ export function similarity(a, b) {
323
+ const norm = (s) => s
324
+ .toLowerCase()
325
+ .replace(/\{[a-z]+\}/g, ' ')
326
+ .replace(/"[^"]*"/g, ' ')
327
+ .replace(/[^a-z0-9 ]/g, ' ')
328
+ .split(/\s+/)
329
+ .filter(Boolean);
330
+ const ta = new Set(norm(a));
331
+ const tb = new Set(norm(b));
332
+ if (!ta.size || !tb.size)
333
+ return 0;
334
+ let inter = 0;
335
+ for (const t of ta)
336
+ if (tb.has(t))
337
+ inter++;
338
+ return inter / Math.sqrt(ta.size * tb.size);
339
+ }
340
+ //# sourceMappingURL=feature.js.map
@@ -0,0 +1,12 @@
1
+ import { ToolRegistry, type SdodsTool } from '../registry/registry.js';
2
+ import { analyzeTools } from './analyze.js';
3
+ import { dataTools } from './data.js';
4
+ import { featureTools } from './feature.js';
5
+ import { issueTools, scheduleTools } from './issue-schedule.js';
6
+ import { projectTools } from './project.js';
7
+ import { proposalTools } from './proposal.js';
8
+ import { runTools } from './run.js';
9
+ export declare const ALL_TOOLS: Array<SdodsTool<any>>;
10
+ export declare function createRegistry(extra?: Array<SdodsTool<any>>): ToolRegistry;
11
+ export { analyzeTools, dataTools, featureTools, issueTools, projectTools, proposalTools, runTools, scheduleTools, };
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,23 @@
1
+ import { ToolRegistry } from '../registry/registry.js';
2
+ import { analyzeTools } from './analyze.js';
3
+ import { dataTools } from './data.js';
4
+ import { featureTools } from './feature.js';
5
+ import { issueTools, scheduleTools } from './issue-schedule.js';
6
+ import { projectTools } from './project.js';
7
+ import { proposalTools } from './proposal.js';
8
+ import { runTools } from './run.js';
9
+ export const ALL_TOOLS = [
10
+ ...projectTools,
11
+ ...featureTools,
12
+ ...runTools,
13
+ ...dataTools,
14
+ ...analyzeTools,
15
+ ...issueTools,
16
+ ...scheduleTools,
17
+ ...proposalTools,
18
+ ];
19
+ export function createRegistry(extra = []) {
20
+ return new ToolRegistry().registerAll(ALL_TOOLS).registerAll(extra);
21
+ }
22
+ export { analyzeTools, dataTools, featureTools, issueTools, projectTools, proposalTools, runTools, scheduleTools, };
23
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,22 @@
1
+ import { z } from 'zod';
2
+ export declare const issueTools: (import("../registry/registry.js").SdodsTool<{
3
+ runId: z.ZodString;
4
+ project: z.ZodOptional<z.ZodString>;
5
+ provider: z.ZodOptional<z.ZodEnum<{
6
+ github: "github";
7
+ jira: "jira";
8
+ }>>;
9
+ dryRun: z.ZodOptional<z.ZodBoolean>;
10
+ }> | import("../registry/registry.js").SdodsTool<{
11
+ project: z.ZodString;
12
+ }>)[];
13
+ export declare const scheduleTools: (import("../registry/registry.js").SdodsTool<{
14
+ project: z.ZodString;
15
+ }> | import("../registry/registry.js").SdodsTool<{
16
+ cron: z.ZodString;
17
+ timezone: z.ZodOptional<z.ZodString>;
18
+ count: z.ZodOptional<z.ZodNumber>;
19
+ }>)[];
20
+ /** Minimal 5-field cron evaluator (minute hour dom month dow) — good enough for previews. */
21
+ export declare function nextCronTimes(expr: string, count: number, timeZone: string, from?: Date): string[];
22
+ //# sourceMappingURL=issue-schedule.d.ts.map