@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.
- package/LICENSE +17 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/cli.d.ts +50 -0
- package/dist/cli.js +171 -0
- package/dist/fs.d.ts +45 -0
- package/dist/fs.js +184 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +15 -0
- package/dist/install/index.d.ts +46 -0
- package/dist/install/index.js +146 -0
- package/dist/prompts/index.d.ts +33 -0
- package/dist/prompts/index.js +151 -0
- package/dist/proposals.d.ts +71 -0
- package/dist/proposals.js +188 -0
- package/dist/registry/adapters.d.ts +46 -0
- package/dist/registry/adapters.js +76 -0
- package/dist/registry/registry.d.ts +96 -0
- package/dist/registry/registry.js +141 -0
- package/dist/resources.d.ts +5 -0
- package/dist/resources.js +130 -0
- package/dist/server.d.ts +29 -0
- package/dist/server.js +143 -0
- package/dist/tools/analyze.d.ts +87 -0
- package/dist/tools/analyze.js +820 -0
- package/dist/tools/data.d.ts +18 -0
- package/dist/tools/data.js +153 -0
- package/dist/tools/feature.d.ts +53 -0
- package/dist/tools/feature.js +340 -0
- package/dist/tools/index.d.ts +12 -0
- package/dist/tools/index.js +23 -0
- package/dist/tools/issue-schedule.d.ts +22 -0
- package/dist/tools/issue-schedule.js +193 -0
- package/dist/tools/project.d.ts +11 -0
- package/dist/tools/project.js +144 -0
- package/dist/tools/proposal.d.ts +19 -0
- package/dist/tools/proposal.js +89 -0
- package/dist/tools/run.d.ts +49 -0
- package/dist/tools/run.js +387 -0
- package/dist/transports/http.d.ts +40 -0
- package/dist/transports/http.js +151 -0
- package/dist/transports/stdio.d.ts +6 -0
- package/dist/transports/stdio.js +15 -0
- package/package.json +57 -0
package/dist/fs.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { join, relative, resolve, sep } from 'node:path';
|
|
3
|
+
import { parse as parseYaml } from 'yaml';
|
|
4
|
+
import { runFiles, scenarioFiles, } from '@sdods/contracts';
|
|
5
|
+
export const RUNS_DIR = '.sdods/runs';
|
|
6
|
+
export const PROJECTS_DIR = 'projects';
|
|
7
|
+
export const PROPOSALS_DIR = 'proposals';
|
|
8
|
+
export function runsRoot(rootDir) {
|
|
9
|
+
return resolve(rootDir, process.env.SDODS_ARTIFACTS_DIR ?? RUNS_DIR);
|
|
10
|
+
}
|
|
11
|
+
export function projectRoot(rootDir, slug) {
|
|
12
|
+
const dir = resolve(rootDir, process.env.SDODS_PROJECTS_DIR ?? PROJECTS_DIR, slug);
|
|
13
|
+
if (!existsSync(join(dir, 'sdods.project.yaml'))) {
|
|
14
|
+
throw Object.assign(new Error(`Unknown project "${slug}" (no sdods.project.yaml under ${dir}).`), {
|
|
15
|
+
error: { code: 'PROJECT_NOT_FOUND' },
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
return dir;
|
|
19
|
+
}
|
|
20
|
+
export function readJson(file) {
|
|
21
|
+
if (!existsSync(file))
|
|
22
|
+
return undefined;
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(readFileSync(file, 'utf8'));
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export function readYaml(file) {
|
|
31
|
+
if (!existsSync(file))
|
|
32
|
+
return undefined;
|
|
33
|
+
return parseYaml(readFileSync(file, 'utf8'));
|
|
34
|
+
}
|
|
35
|
+
/** Resolve `rel` inside `base`, refusing traversal outside it. */
|
|
36
|
+
export function safeJoin(base, rel) {
|
|
37
|
+
const target = resolve(base, rel);
|
|
38
|
+
const relPath = relative(base, target);
|
|
39
|
+
if (relPath.startsWith('..') || relPath.includes(`..${sep}`) || resolve(target) !== target) {
|
|
40
|
+
throw Object.assign(new Error(`Path escapes ${base}: ${rel}`), {
|
|
41
|
+
error: { code: 'INVALID_ARGS' },
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return target;
|
|
45
|
+
}
|
|
46
|
+
export function walk(dir, filter, max = 5000) {
|
|
47
|
+
const out = [];
|
|
48
|
+
const stack = [dir];
|
|
49
|
+
while (stack.length && out.length < max) {
|
|
50
|
+
const cur = stack.pop();
|
|
51
|
+
if (!existsSync(cur))
|
|
52
|
+
continue;
|
|
53
|
+
for (const name of readdirSync(cur)) {
|
|
54
|
+
if (name === 'node_modules' || name.startsWith('.') || name === 'dist')
|
|
55
|
+
continue;
|
|
56
|
+
const full = join(cur, name);
|
|
57
|
+
const st = statSync(full);
|
|
58
|
+
if (st.isDirectory())
|
|
59
|
+
stack.push(full);
|
|
60
|
+
else if (filter(full))
|
|
61
|
+
out.push(full);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return out.sort();
|
|
65
|
+
}
|
|
66
|
+
export function listRuns(rootDir, limit = 50) {
|
|
67
|
+
const root = runsRoot(rootDir);
|
|
68
|
+
if (!existsSync(root))
|
|
69
|
+
return [];
|
|
70
|
+
const entries = [];
|
|
71
|
+
for (const runId of readdirSync(root)) {
|
|
72
|
+
const dir = join(root, runId);
|
|
73
|
+
if (!statSync(dir).isDirectory())
|
|
74
|
+
continue;
|
|
75
|
+
entries.push({
|
|
76
|
+
runId,
|
|
77
|
+
dir,
|
|
78
|
+
manifest: readJson(join(dir, runFiles.manifest)),
|
|
79
|
+
summary: readJson(join(dir, runFiles.summary)),
|
|
80
|
+
mtime: statSync(dir).mtimeMs,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
entries.sort((a, b) => b.mtime - a.mtime);
|
|
84
|
+
return entries.slice(0, limit);
|
|
85
|
+
}
|
|
86
|
+
export function getRun(rootDir, runId) {
|
|
87
|
+
const dir = join(runsRoot(rootDir), runId);
|
|
88
|
+
if (!existsSync(dir))
|
|
89
|
+
return undefined;
|
|
90
|
+
return {
|
|
91
|
+
runId,
|
|
92
|
+
dir,
|
|
93
|
+
manifest: readJson(join(dir, runFiles.manifest)),
|
|
94
|
+
summary: readJson(join(dir, runFiles.summary)),
|
|
95
|
+
mtime: statSync(dir).mtimeMs,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
export function lastRun(rootDir) {
|
|
99
|
+
return listRuns(rootDir, 1)[0];
|
|
100
|
+
}
|
|
101
|
+
/** Walk `<runDir>/<slug>/<fingerprint>/r<retry>/` directories. */
|
|
102
|
+
export function listScenarioDirs(runDir, slug) {
|
|
103
|
+
const out = [];
|
|
104
|
+
if (!existsSync(runDir))
|
|
105
|
+
return out;
|
|
106
|
+
const slugs = slug
|
|
107
|
+
? [slug]
|
|
108
|
+
: readdirSync(runDir).filter((n) => !n.startsWith('.') &&
|
|
109
|
+
statSync(join(runDir, n)).isDirectory() &&
|
|
110
|
+
!['html-report', 'dashboard', 'runner-output', 'shard-reports', 'diff'].includes(n));
|
|
111
|
+
for (const s of slugs) {
|
|
112
|
+
const sDir = join(runDir, s);
|
|
113
|
+
if (!existsSync(sDir))
|
|
114
|
+
continue;
|
|
115
|
+
for (const fp of readdirSync(sDir)) {
|
|
116
|
+
const fpDir = join(sDir, fp);
|
|
117
|
+
if (!statSync(fpDir).isDirectory())
|
|
118
|
+
continue;
|
|
119
|
+
for (const r of readdirSync(fpDir)) {
|
|
120
|
+
const m = /^r(\d+)$/.exec(r);
|
|
121
|
+
if (!m)
|
|
122
|
+
continue;
|
|
123
|
+
out.push(readScenarioDir(join(fpDir, r), fp, Number(m[1])));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
export function readScenarioDir(dir, fingerprint, retry) {
|
|
130
|
+
const files = existsSync(dir) ? readdirSync(dir) : [];
|
|
131
|
+
const screenshots = [];
|
|
132
|
+
for (const f of files) {
|
|
133
|
+
const sc = /^scenario-(start|end|failure)\.png$/.exec(f);
|
|
134
|
+
if (sc)
|
|
135
|
+
screenshots.push({ file: f, phase: sc[1] });
|
|
136
|
+
const st = /^(\d{2})-(before|after)\.png$/.exec(f);
|
|
137
|
+
if (st)
|
|
138
|
+
screenshots.push({ file: f, phase: st[2], stepIndex: Number(st[1]) });
|
|
139
|
+
}
|
|
140
|
+
const apiSnapshots = [];
|
|
141
|
+
const apiDir = join(dir, 'api');
|
|
142
|
+
if (existsSync(apiDir)) {
|
|
143
|
+
for (const f of readdirSync(apiDir)) {
|
|
144
|
+
const m = /^(\d{2})-(\d+)-(request|response)\.json$/.exec(f);
|
|
145
|
+
if (!m)
|
|
146
|
+
continue;
|
|
147
|
+
apiSnapshots.push({
|
|
148
|
+
file: `api/${f}`,
|
|
149
|
+
stepIndex: Number(m[1]),
|
|
150
|
+
callIndex: Number(m[2]),
|
|
151
|
+
part: m[3],
|
|
152
|
+
body: readJson(join(apiDir, f)),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const healFile = join(dir, scenarioFiles.healLog);
|
|
157
|
+
const healEvents = existsSync(healFile)
|
|
158
|
+
? readFileSync(healFile, 'utf8')
|
|
159
|
+
.split('\n')
|
|
160
|
+
.filter(Boolean)
|
|
161
|
+
.map((l) => {
|
|
162
|
+
try {
|
|
163
|
+
return JSON.parse(l);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
.filter(Boolean)
|
|
170
|
+
: [];
|
|
171
|
+
return {
|
|
172
|
+
fingerprint,
|
|
173
|
+
retry,
|
|
174
|
+
dir,
|
|
175
|
+
meta: readJson(join(dir, scenarioFiles.meta)),
|
|
176
|
+
screenshots: screenshots.sort((a, b) => (a.stepIndex ?? -1) - (b.stepIndex ?? -1)),
|
|
177
|
+
apiSnapshots: apiSnapshots.sort((a, b) => a.stepIndex - b.stepIndex || a.callIndex - b.callIndex),
|
|
178
|
+
healEvents,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
export function screenshotUri(runId, fingerprint, retry, file) {
|
|
182
|
+
return `sdods://screenshot/${runId}/${fingerprint}/${retry}/${file}`;
|
|
183
|
+
}
|
|
184
|
+
//# sourceMappingURL=fs.js.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export * from './registry/registry.js';
|
|
2
|
+
export * from './registry/adapters.js';
|
|
3
|
+
export * from './cli.js';
|
|
4
|
+
export * from './fs.js';
|
|
5
|
+
export * from './proposals.js';
|
|
6
|
+
export * from './prompts/index.js';
|
|
7
|
+
export * from './tools/index.js';
|
|
8
|
+
export { parseGherkin, basicTagCheck, similarity } from './tools/feature.js';
|
|
9
|
+
export { analyzeApp, analyzeCoverage, analyzeBestPractices, analyzeLocators, analyzeChangeImpact, } from './tools/analyze.js';
|
|
10
|
+
export { nextCronTimes } from './tools/issue-schedule.js';
|
|
11
|
+
export * from './server.js';
|
|
12
|
+
export * from './transports/stdio.js';
|
|
13
|
+
export * from './transports/http.js';
|
|
14
|
+
export * from './install/index.js';
|
|
15
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export * from './registry/registry.js';
|
|
2
|
+
export * from './registry/adapters.js';
|
|
3
|
+
export * from './cli.js';
|
|
4
|
+
export * from './fs.js';
|
|
5
|
+
export * from './proposals.js';
|
|
6
|
+
export * from './prompts/index.js';
|
|
7
|
+
export * from './tools/index.js';
|
|
8
|
+
export { parseGherkin, basicTagCheck, similarity } from './tools/feature.js';
|
|
9
|
+
export { analyzeApp, analyzeCoverage, analyzeBestPractices, analyzeLocators, analyzeChangeImpact, } from './tools/analyze.js';
|
|
10
|
+
export { nextCronTimes } from './tools/issue-schedule.js';
|
|
11
|
+
export * from './server.js';
|
|
12
|
+
export * from './transports/stdio.js';
|
|
13
|
+
export * from './transports/http.js';
|
|
14
|
+
export * from './install/index.js';
|
|
15
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export type McpClient = 'claude' | 'codex' | 'cursor' | 'vscode' | 'windsurf';
|
|
2
|
+
export declare const MCP_CLIENTS: McpClient[];
|
|
3
|
+
export interface SnippetOptions {
|
|
4
|
+
project?: string;
|
|
5
|
+
env?: string;
|
|
6
|
+
caps?: string;
|
|
7
|
+
/** when set, produce HTTP config instead of stdio */
|
|
8
|
+
httpUrl?: string;
|
|
9
|
+
tokenPlaceholder?: string;
|
|
10
|
+
/** command used to start the stdio server */
|
|
11
|
+
command?: string;
|
|
12
|
+
args?: string[];
|
|
13
|
+
withPlaywright?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export declare function serverEntry(o: SnippetOptions, client: McpClient): Record<string, unknown>;
|
|
16
|
+
export declare function playwrightEntry(): Record<string, unknown>;
|
|
17
|
+
/** `codex mcp add` / `claude mcp add` command lines for the same configuration. */
|
|
18
|
+
export declare function cliCommands(o: SnippetOptions): {
|
|
19
|
+
claude: string;
|
|
20
|
+
codex: string;
|
|
21
|
+
};
|
|
22
|
+
/** Codex keeps MCP servers in `~/.codex/config.toml` under `[mcp_servers.<name>]`. */
|
|
23
|
+
export declare function codexTomlEntries(o: SnippetOptions): Record<string, Record<string, unknown>>;
|
|
24
|
+
export declare function codexConfigPath(): string;
|
|
25
|
+
export declare function snippets(o: SnippetOptions): Record<McpClient, {
|
|
26
|
+
file: string;
|
|
27
|
+
json?: Record<string, unknown>;
|
|
28
|
+
toml?: string;
|
|
29
|
+
cli?: string;
|
|
30
|
+
}>;
|
|
31
|
+
/** Write (merge) the client config file; never clobbers other servers. */
|
|
32
|
+
export declare function installClientConfig(rootDir: string, client: McpClient, o: SnippetOptions): {
|
|
33
|
+
file: string;
|
|
34
|
+
merged: Record<string, unknown>;
|
|
35
|
+
created: boolean;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Merge `[mcp_servers.sdods]` (and playwright) into the Codex config, keeping every other
|
|
39
|
+
* table and server intact. Used when the `codex` CLI is not available to do it itself.
|
|
40
|
+
*/
|
|
41
|
+
export declare function installCodexConfig(o: SnippetOptions, file?: string): {
|
|
42
|
+
file: string;
|
|
43
|
+
merged: Record<string, unknown>;
|
|
44
|
+
created: boolean;
|
|
45
|
+
};
|
|
46
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { parse as parseToml, stringify as stringifyToml } from 'smol-toml';
|
|
5
|
+
export const MCP_CLIENTS = ['claude', 'codex', 'cursor', 'vscode', 'windsurf'];
|
|
6
|
+
function stdioArgs(o) {
|
|
7
|
+
const args = [...(o.args ?? ['sdods', 'mcp'])];
|
|
8
|
+
if (o.project)
|
|
9
|
+
args.push('--project', o.project);
|
|
10
|
+
if (o.env)
|
|
11
|
+
args.push('--env', o.env);
|
|
12
|
+
if (o.caps)
|
|
13
|
+
args.push('--caps', o.caps);
|
|
14
|
+
return args;
|
|
15
|
+
}
|
|
16
|
+
export function serverEntry(o, client) {
|
|
17
|
+
const token = o.tokenPlaceholder ?? (client === 'vscode' ? '${input:sdods-token}' : '<YOUR_SDODS_TOKEN>');
|
|
18
|
+
if (o.httpUrl) {
|
|
19
|
+
return { type: 'http', url: o.httpUrl, headers: { Authorization: `Bearer ${token}` } };
|
|
20
|
+
}
|
|
21
|
+
return { command: o.command ?? 'npx', args: stdioArgs(o) };
|
|
22
|
+
}
|
|
23
|
+
export function playwrightEntry() {
|
|
24
|
+
return { command: 'npx', args: ['playwright', 'mcp', '--headless'] };
|
|
25
|
+
}
|
|
26
|
+
/** `codex mcp add` / `claude mcp add` command lines for the same configuration. */
|
|
27
|
+
export function cliCommands(o) {
|
|
28
|
+
const stdio = `${o.command ?? 'npx'} ${stdioArgs(o).join(' ')}`;
|
|
29
|
+
const token = o.tokenPlaceholder ?? '$SDODS_TOKEN';
|
|
30
|
+
return {
|
|
31
|
+
claude: o.httpUrl
|
|
32
|
+
? `claude mcp add --transport http sdods ${o.httpUrl} --header "Authorization: Bearer ${token}"`
|
|
33
|
+
: `claude mcp add sdods -- ${stdio}`,
|
|
34
|
+
codex: o.httpUrl
|
|
35
|
+
? `codex mcp add sdods --url ${o.httpUrl} --bearer-token-env-var SDODS_TOKEN`
|
|
36
|
+
: `codex mcp add sdods -- ${stdio}`,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/** Codex keeps MCP servers in `~/.codex/config.toml` under `[mcp_servers.<name>]`. */
|
|
40
|
+
export function codexTomlEntries(o) {
|
|
41
|
+
const sdods = o.httpUrl
|
|
42
|
+
? { url: o.httpUrl, bearer_token_env_var: 'SDODS_TOKEN' }
|
|
43
|
+
: { command: o.command ?? 'npx', args: stdioArgs(o) };
|
|
44
|
+
const out = { sdods };
|
|
45
|
+
if (o.withPlaywright !== false)
|
|
46
|
+
out.playwright = playwrightEntry();
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
export function codexConfigPath() {
|
|
50
|
+
return join(process.env.CODEX_HOME ?? join(homedir(), '.codex'), 'config.toml');
|
|
51
|
+
}
|
|
52
|
+
export function snippets(o) {
|
|
53
|
+
const pw = o.withPlaywright === false ? {} : { playwright: playwrightEntry() };
|
|
54
|
+
const cli = cliCommands(o);
|
|
55
|
+
return {
|
|
56
|
+
claude: {
|
|
57
|
+
file: '.mcp.json',
|
|
58
|
+
json: { mcpServers: { sdods: serverEntry(o, 'claude'), ...pw } },
|
|
59
|
+
cli: cli.claude,
|
|
60
|
+
},
|
|
61
|
+
codex: {
|
|
62
|
+
file: codexConfigPath(),
|
|
63
|
+
toml: stringifyToml({ mcp_servers: codexTomlEntries(o) }),
|
|
64
|
+
cli: cli.codex,
|
|
65
|
+
},
|
|
66
|
+
cursor: {
|
|
67
|
+
file: '.cursor/mcp.json',
|
|
68
|
+
json: { mcpServers: { sdods: serverEntry(o, 'cursor'), ...pw } },
|
|
69
|
+
},
|
|
70
|
+
windsurf: {
|
|
71
|
+
file: '.windsurf/mcp.json',
|
|
72
|
+
json: { mcpServers: { sdods: serverEntry(o, 'windsurf'), ...pw } },
|
|
73
|
+
},
|
|
74
|
+
vscode: {
|
|
75
|
+
file: '.vscode/mcp.json',
|
|
76
|
+
json: {
|
|
77
|
+
servers: { sdods: serverEntry(o, 'vscode'), ...pw },
|
|
78
|
+
...(o.httpUrl
|
|
79
|
+
? {
|
|
80
|
+
inputs: [
|
|
81
|
+
{
|
|
82
|
+
id: 'sdods-token',
|
|
83
|
+
type: 'promptString',
|
|
84
|
+
description: 'SDODS API token',
|
|
85
|
+
password: true,
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
}
|
|
89
|
+
: {}),
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/** Write (merge) the client config file; never clobbers other servers. */
|
|
95
|
+
export function installClientConfig(rootDir, client, o) {
|
|
96
|
+
if (client === 'codex')
|
|
97
|
+
return installCodexConfig(o);
|
|
98
|
+
const s = snippets(o)[client];
|
|
99
|
+
const file = join(rootDir, s.file);
|
|
100
|
+
const existing = existsSync(file) ? safeParse(readFileSync(file, 'utf8')) : {};
|
|
101
|
+
const merged = { ...existing };
|
|
102
|
+
const key = client === 'vscode' ? 'servers' : 'mcpServers';
|
|
103
|
+
merged[key] = {
|
|
104
|
+
...(existing[key] ?? {}),
|
|
105
|
+
...(s.json?.[key] ?? {}),
|
|
106
|
+
};
|
|
107
|
+
if (client === 'vscode' && s.json?.inputs) {
|
|
108
|
+
const inputs = (existing.inputs ?? []).filter((i) => i.id !== 'sdods-token');
|
|
109
|
+
merged.inputs = [...inputs, ...s.json.inputs];
|
|
110
|
+
}
|
|
111
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
112
|
+
writeFileSync(file, JSON.stringify(merged, null, 2) + '\n');
|
|
113
|
+
return { file, merged, created: Object.keys(existing).length === 0 };
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Merge `[mcp_servers.sdods]` (and playwright) into the Codex config, keeping every other
|
|
117
|
+
* table and server intact. Used when the `codex` CLI is not available to do it itself.
|
|
118
|
+
*/
|
|
119
|
+
export function installCodexConfig(o, file = codexConfigPath()) {
|
|
120
|
+
const existing = existsSync(file) ? safeParseToml(readFileSync(file, 'utf8')) : {};
|
|
121
|
+
const servers = {
|
|
122
|
+
...(existing.mcp_servers ?? {}),
|
|
123
|
+
...codexTomlEntries(o),
|
|
124
|
+
};
|
|
125
|
+
const merged = { ...existing, mcp_servers: servers };
|
|
126
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
127
|
+
writeFileSync(file, stringifyToml(merged) + '\n');
|
|
128
|
+
return { file, merged, created: Object.keys(existing).length === 0 };
|
|
129
|
+
}
|
|
130
|
+
function safeParse(text) {
|
|
131
|
+
try {
|
|
132
|
+
return JSON.parse(text);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return {};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function safeParseToml(text) {
|
|
139
|
+
try {
|
|
140
|
+
return parseToml(text);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return {};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Role prompts shared by the MCP `prompts/*` surface and the SDODS agents.
|
|
3
|
+
* Kept as TypeScript strings so both packages import them without file I/O.
|
|
4
|
+
*/
|
|
5
|
+
export type RoleName = 'planner' | 'generator' | 'healer' | 'upgrader' | 'reviewer';
|
|
6
|
+
export declare const CONVENTIONS = "# SDODS conventions (read first)\n\nSDODS is an automation and orchestration platform with a reusable architecture.\n\n- One project = `projects/<slug>/sdods.project.yaml` + `envs/<env>.yaml`; features live in `features/<module>/`.\n- Every scenario carries exactly one layer tag (@ui, @api, @hybrid) and exactly one suite tag (@smoke, @regression, @sanity).\n Optional tags: @visual @a11y @perf @mock @data-driven @pool, value tags @user:<role> @data:<dataset> @har:<name> @jira:KEY @skip:<browser>.\n- Reuse existing steps first (call step_list / step_find). Add a new step only when no existing phrasing fits; name it in the same style.\n- Locator priority: role+name > label > test id (project testIdAttribute) > placeholder > text > CSS. Never XPath.\n- Page objects use step decorators (@Fixture, @Given/@When/@Then) and heal-aware locators (`this.heal.locator(primary, { role, name, testId, description })`).\n- Never write to the working tree: propose files with feature_write / proposal tools; a person reviews and accepts.\n- Keep scenarios independent, deterministic, and free of sleeps; prefer API seeding over UI setup; clean up created data.\n- Secrets are never literals; reference ${VAR}.\n- Stop when you run out of budget or turns and summarise what remains.";
|
|
7
|
+
/** The rules that still apply when there is no room for the full conventions. */
|
|
8
|
+
export declare const SMALL_CONVENTIONS = "SDODS rules:\n- Every Feature has one layer tag: @ui, @api or @hybrid.\n- Every Scenario has one suite tag: @smoke, @regression or @sanity.\n- Reuse an existing step pattern; never invent one.\n- You never edit files. feature_write creates a proposal a person reviews.\n- Call one tool per reply, with a single JSON object of arguments.";
|
|
9
|
+
export declare const ROLE_PROMPTS: Record<RoleName, {
|
|
10
|
+
title: string;
|
|
11
|
+
description: string;
|
|
12
|
+
body: string;
|
|
13
|
+
}>;
|
|
14
|
+
/**
|
|
15
|
+
* Prompts for the small profile.
|
|
16
|
+
*
|
|
17
|
+
* The full conventions are 600 tokens of dense, multi-clause prose: a frontier model reads all of
|
|
18
|
+
* it, a 7B model follows the first two bullets and forgets the rest. These say one thing per line,
|
|
19
|
+
* name the tools in the order they should be called, and show one correct call — which is what the
|
|
20
|
+
* measured difference between a usable and a useless answer looked like.
|
|
21
|
+
*/
|
|
22
|
+
export declare const SMALL_MODEL_PROMPTS: Record<RoleName, string>;
|
|
23
|
+
export interface PromptContext {
|
|
24
|
+
project?: string;
|
|
25
|
+
env?: string;
|
|
26
|
+
goal?: string;
|
|
27
|
+
steps?: string[];
|
|
28
|
+
extra?: string;
|
|
29
|
+
/** 'small' swaps the conventions and the role body for their short forms. */
|
|
30
|
+
profile?: 'full' | 'small';
|
|
31
|
+
}
|
|
32
|
+
export declare function renderPrompt(role: RoleName, ctx?: PromptContext): string;
|
|
33
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
export const CONVENTIONS = `# SDODS conventions (read first)
|
|
2
|
+
|
|
3
|
+
SDODS is an automation and orchestration platform with a reusable architecture.
|
|
4
|
+
|
|
5
|
+
- One project = \`projects/<slug>/sdods.project.yaml\` + \`envs/<env>.yaml\`; features live in \`features/<module>/\`.
|
|
6
|
+
- Every scenario carries exactly one layer tag (@ui, @api, @hybrid) and exactly one suite tag (@smoke, @regression, @sanity).
|
|
7
|
+
Optional tags: @visual @a11y @perf @mock @data-driven @pool, value tags @user:<role> @data:<dataset> @har:<name> @jira:KEY @skip:<browser>.
|
|
8
|
+
- Reuse existing steps first (call step_list / step_find). Add a new step only when no existing phrasing fits; name it in the same style.
|
|
9
|
+
- Locator priority: role+name > label > test id (project testIdAttribute) > placeholder > text > CSS. Never XPath.
|
|
10
|
+
- Page objects use step decorators (@Fixture, @Given/@When/@Then) and heal-aware locators (\`this.heal.locator(primary, { role, name, testId, description })\`).
|
|
11
|
+
- Never write to the working tree: propose files with feature_write / proposal tools; a person reviews and accepts.
|
|
12
|
+
- Keep scenarios independent, deterministic, and free of sleeps; prefer API seeding over UI setup; clean up created data.
|
|
13
|
+
- Secrets are never literals; reference \${VAR}.
|
|
14
|
+
- Stop when you run out of budget or turns and summarise what remains.`;
|
|
15
|
+
/** The rules that still apply when there is no room for the full conventions. */
|
|
16
|
+
export const SMALL_CONVENTIONS = `SDODS rules:
|
|
17
|
+
- Every Feature has one layer tag: @ui, @api or @hybrid.
|
|
18
|
+
- Every Scenario has one suite tag: @smoke, @regression or @sanity.
|
|
19
|
+
- Reuse an existing step pattern; never invent one.
|
|
20
|
+
- You never edit files. feature_write creates a proposal a person reviews.
|
|
21
|
+
- Call one tool per reply, with a single JSON object of arguments.`;
|
|
22
|
+
export const ROLE_PROMPTS = {
|
|
23
|
+
planner: {
|
|
24
|
+
title: 'SDODS planner',
|
|
25
|
+
description: 'Explore an application (or its source/OpenAPI) and produce a tagged test plan.',
|
|
26
|
+
body: `You are the SDODS PLANNER.
|
|
27
|
+
|
|
28
|
+
Goal: produce \`docs/test-plans/<name>.md\` for the requested feature area.
|
|
29
|
+
|
|
30
|
+
Method:
|
|
31
|
+
1. Read the project config (project_get_config) and existing features (feature_list) to avoid duplicates.
|
|
32
|
+
2. Explore the application: if a browser MCP is available, navigate the relevant routes and take ARIA snapshots; otherwise read source roots and the OpenAPI spec (analyze_project, analyze_routes).
|
|
33
|
+
3. Write independent scenarios grouped by module. For each: title, preconditions, Gherkin steps (reuse step phrasing from step_list), the layer and suite tag, data needs (@data:/@user:), negative cases, and which existing steps already cover it.
|
|
34
|
+
4. Include an "Out of scope / risks" section and an estimate of new steps needed.
|
|
35
|
+
5. Save the plan through the proposal tools; do not modify features directly.`,
|
|
36
|
+
},
|
|
37
|
+
generator: {
|
|
38
|
+
title: 'SDODS generator',
|
|
39
|
+
description: 'Turn a plan, a goal, or a recorded spec into feature files, steps and page objects.',
|
|
40
|
+
body: `You are the SDODS GENERATOR.
|
|
41
|
+
|
|
42
|
+
Goal: produce feature files (and only-when-needed steps and page objects) as a proposal.
|
|
43
|
+
|
|
44
|
+
Rules:
|
|
45
|
+
- Start with step_list; match every step you write to an existing pattern verbatim when one exists.
|
|
46
|
+
- Feature files go under \`projects/<slug>/features/<module>/<name>.feature\` with one layer tag and one suite tag at Feature level plus the module tag.
|
|
47
|
+
- Use Scenario Outline with a \`# title-format:\` comment for data-driven cases; prefer datasets (@data:) over inline literals for credentials.
|
|
48
|
+
- New steps go to \`projects/<slug>/steps/<module>.steps.ts\` using the project's \`steps/fixtures.ts\` exports; new page objects to \`projects/<slug>/pages/<Name>Page.ts\` with decorators and heal-aware locators.
|
|
49
|
+
- Validate with feature_parse and feature_lint before proposing. Run at most 3 scenarios with run_tests to verify; if they fail for environmental reasons, tag @fixme with a reason.
|
|
50
|
+
- Output: one proposal with all files and a summary of what was reused vs added.`,
|
|
51
|
+
},
|
|
52
|
+
healer: {
|
|
53
|
+
title: 'SDODS healer',
|
|
54
|
+
description: 'Diagnose a failing scenario and propose the smallest locator/step fix.',
|
|
55
|
+
body: `You are the SDODS HEALER.
|
|
56
|
+
|
|
57
|
+
Input: a run id and scenario fingerprint (run_get_scenario), the error, before/after screenshots, heal events and locator stats (heal_events, heal_locator_stats).
|
|
58
|
+
|
|
59
|
+
Method:
|
|
60
|
+
1. Classify the failure: locator changed, timing, data, environment, or a real product bug.
|
|
61
|
+
2. For locator failures prefer role/label/test-id locators; use heal_events candidates that succeeded. Never loosen an assertion to make a test pass.
|
|
62
|
+
3. If history shows the scenario flaky (alternating outcomes), propose a @flaky @retries:2 quarantine with a note instead of a code change.
|
|
63
|
+
4. Propose the minimal patch (page object or step) via feature_write/proposal tools, then re-run only that scenario (run_tests with tags/scenario filter) at most 3 times.
|
|
64
|
+
5. Report: root cause, change, verification result, and whether a product bug should be filed (issue_create when asked).`,
|
|
65
|
+
},
|
|
66
|
+
upgrader: {
|
|
67
|
+
title: 'SDODS upgrader',
|
|
68
|
+
description: 'Map code or API changes to affected scenarios and propose additions/updates.',
|
|
69
|
+
body: `You are the SDODS UPGRADER.
|
|
70
|
+
|
|
71
|
+
Input: a project plus a git diff range or an OpenAPI old/new pair (analyze_change_impact, analyze_coverage).
|
|
72
|
+
|
|
73
|
+
Method:
|
|
74
|
+
1. Identify changed routes, components, endpoints and schemas.
|
|
75
|
+
2. Map them to modules and existing features; list scenarios that need updating and gaps that need new scenarios.
|
|
76
|
+
3. Propose updated/new feature files and data rows (datasets) as a proposal; never delete scenarios, mark deprecated ones with @fixme and a reason.
|
|
77
|
+
4. Summarise coverage before/after and anything you could not map.`,
|
|
78
|
+
},
|
|
79
|
+
reviewer: {
|
|
80
|
+
title: 'SDODS reviewer',
|
|
81
|
+
description: 'Review feature files for tagging, reuse, data-driven refactors and best practices.',
|
|
82
|
+
body: `You are the SDODS REVIEWER.
|
|
83
|
+
|
|
84
|
+
Input: feature files (feature_read/feature_parse), the step list and analyze_best_practices output.
|
|
85
|
+
|
|
86
|
+
Produce a review with: tag-policy violations, duplicated or near-duplicate steps (suggest reuse), scenarios that should become a Scenario Outline with a dataset, missing @a11y/@visual/@perf coverage on key pages, brittle locators in page objects, hardcoded URLs or secrets, and ordering/independence problems. Offer concrete rewrites as a proposal when the change is mechanical.`,
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Prompts for the small profile.
|
|
91
|
+
*
|
|
92
|
+
* The full conventions are 600 tokens of dense, multi-clause prose: a frontier model reads all of
|
|
93
|
+
* it, a 7B model follows the first two bullets and forgets the rest. These say one thing per line,
|
|
94
|
+
* name the tools in the order they should be called, and show one correct call — which is what the
|
|
95
|
+
* measured difference between a usable and a useless answer looked like.
|
|
96
|
+
*/
|
|
97
|
+
export const SMALL_MODEL_PROMPTS = {
|
|
98
|
+
planner: `You plan tests for SDODS. One tool call per reply, in this order:
|
|
99
|
+
1. project_get_config({"project":"<slug>"}), then feature_list({"project":"<slug>"}).
|
|
100
|
+
2. analyze_routes or analyze_coverage — find what is untested.
|
|
101
|
+
3. feature_write — save the plan as docs/test-plans/<name>.md via extraFiles.
|
|
102
|
+
Write scenarios as titles plus steps chosen from the step list below. Never invent a step.`,
|
|
103
|
+
generator: `You write Gherkin for SDODS. One tool call per reply, in this order:
|
|
104
|
+
1. step_find({"project":"<slug>","phrase":"<what you are testing>"}) — pick patterns from the result.
|
|
105
|
+
2. feature_parse({"project":"<slug>","text":"<your draft>"}) — fix whatever it reports.
|
|
106
|
+
3. feature_write({"project":"<slug>","path":"<module>/<name>.feature","text":"<final text>"}).
|
|
107
|
+
Rules:
|
|
108
|
+
- The Feature line is preceded by exactly one layer tag (@ui, @api or @hybrid).
|
|
109
|
+
- Every Scenario carries exactly one suite tag (@smoke, @regression or @sanity).
|
|
110
|
+
- Only use step patterns from the list below. Never invent a step.
|
|
111
|
+
Example call:
|
|
112
|
+
feature_write({"project":"shop","path":"auth/login.feature","text":"@ui\nFeature: Login\n\n @smoke\n Scenario: Valid credentials\n Given I am on the login page\n"})`,
|
|
113
|
+
healer: `You fix one failing SDODS scenario. One tool call per reply, in this order:
|
|
114
|
+
1. analyze_failure — read the error.
|
|
115
|
+
2. run_get_scenario or heal_events — see what the locator did.
|
|
116
|
+
3. feature_read — read the file you intend to change.
|
|
117
|
+
4. feature_write — propose the smallest possible change.
|
|
118
|
+
Change one locator or one step. Do not rewrite the scenario. If the application is broken rather
|
|
119
|
+
than the test, say so in the summary and write nothing.`,
|
|
120
|
+
upgrader: `You update SDODS tests after a code change. One tool call per reply, in this order:
|
|
121
|
+
1. analyze_change_impact — which scenarios the change touches.
|
|
122
|
+
2. feature_read — read one of them.
|
|
123
|
+
3. feature_write — propose the update.
|
|
124
|
+
Only change what the diff forces. Use step patterns from the list below.`,
|
|
125
|
+
reviewer: `You review SDODS feature files. One tool call per reply, in this order:
|
|
126
|
+
1. feature_list({"project":"<slug>"}), then feature_read({"project":"<slug>","path":"<file>"}).
|
|
127
|
+
2. analyze_best_practices and analyze_locators — get the findings.
|
|
128
|
+
3. Write your review as text. Do not call feature_write unless asked to change something.
|
|
129
|
+
Report: missing or duplicated tags, steps that could be reused, CSS locators that should be role
|
|
130
|
+
or test-id locators. Be specific: name the file and the line.`,
|
|
131
|
+
};
|
|
132
|
+
export function renderPrompt(role, ctx = {}) {
|
|
133
|
+
const p = ROLE_PROMPTS[role];
|
|
134
|
+
const lines = ctx.profile === 'small'
|
|
135
|
+
? [SMALL_CONVENTIONS, '', SMALL_MODEL_PROMPTS[role], '']
|
|
136
|
+
: [CONVENTIONS, '', p.body, ''];
|
|
137
|
+
if (ctx.project)
|
|
138
|
+
lines.push(`Project: ${ctx.project}${ctx.env ? ` · environment: ${ctx.env}` : ''}`);
|
|
139
|
+
if (ctx.goal)
|
|
140
|
+
lines.push(`Goal: ${ctx.goal}`);
|
|
141
|
+
if (ctx.steps?.length) {
|
|
142
|
+
const limit = ctx.profile === 'small' ? 60 : 400;
|
|
143
|
+
lines.push('', 'Existing step patterns (reuse these first):');
|
|
144
|
+
for (const s of ctx.steps.slice(0, limit))
|
|
145
|
+
lines.push(`- ${s}`);
|
|
146
|
+
}
|
|
147
|
+
if (ctx.extra)
|
|
148
|
+
lines.push('', ctx.extra);
|
|
149
|
+
return lines.join('\n');
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proposals are the ONLY write path for agents and MCP write tools: files land under
|
|
3
|
+
* `proposals/<id>/files/<path>` with a manifest, and a person applies them.
|
|
4
|
+
*/
|
|
5
|
+
export type ProposalStatus = 'pending' | 'accepted' | 'rejected';
|
|
6
|
+
export type ProposalFileOp = 'add' | 'modify' | 'delete';
|
|
7
|
+
export interface ProposalFile {
|
|
8
|
+
path: string;
|
|
9
|
+
op: ProposalFileOp;
|
|
10
|
+
/** relative to the proposal dir; absent for delete */
|
|
11
|
+
from?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface ProposalManifest {
|
|
14
|
+
id: string;
|
|
15
|
+
role: string;
|
|
16
|
+
project?: string;
|
|
17
|
+
env?: string;
|
|
18
|
+
createdAt: string;
|
|
19
|
+
status: ProposalStatus;
|
|
20
|
+
summary: string;
|
|
21
|
+
costUsd?: number;
|
|
22
|
+
model?: string;
|
|
23
|
+
files: ProposalFile[];
|
|
24
|
+
verification?: {
|
|
25
|
+
ranScenarios?: string[];
|
|
26
|
+
status?: 'passed' | 'failed' | 'not-run';
|
|
27
|
+
notes?: string;
|
|
28
|
+
};
|
|
29
|
+
reviewedAt?: string;
|
|
30
|
+
reviewedBy?: string;
|
|
31
|
+
appliedTo?: string;
|
|
32
|
+
}
|
|
33
|
+
export interface CreateProposalInput {
|
|
34
|
+
role: string;
|
|
35
|
+
project?: string;
|
|
36
|
+
env?: string;
|
|
37
|
+
summary: string;
|
|
38
|
+
files: Array<{
|
|
39
|
+
path: string;
|
|
40
|
+
content?: string;
|
|
41
|
+
op?: ProposalFileOp;
|
|
42
|
+
}>;
|
|
43
|
+
costUsd?: number;
|
|
44
|
+
model?: string;
|
|
45
|
+
verification?: ProposalManifest['verification'];
|
|
46
|
+
}
|
|
47
|
+
export declare class ProposalStore {
|
|
48
|
+
readonly rootDir: string;
|
|
49
|
+
readonly dir: string;
|
|
50
|
+
constructor(rootDir: string);
|
|
51
|
+
private manifestPath;
|
|
52
|
+
create(input: CreateProposalInput): ProposalManifest;
|
|
53
|
+
list(filter?: {
|
|
54
|
+
status?: ProposalStatus;
|
|
55
|
+
project?: string;
|
|
56
|
+
}): ProposalManifest[];
|
|
57
|
+
get(id: string): ProposalManifest | undefined;
|
|
58
|
+
readFile(id: string, file: ProposalFile): string | undefined;
|
|
59
|
+
/** Unified-ish diff for review (no external dependency). */
|
|
60
|
+
diff(id: string): string;
|
|
61
|
+
/** Apply to `targetRoot` (default: the repo). Returns the written paths. */
|
|
62
|
+
accept(id: string, opts?: {
|
|
63
|
+
targetRoot?: string;
|
|
64
|
+
reviewedBy?: string;
|
|
65
|
+
}): string[];
|
|
66
|
+
reject(id: string, reason?: string, reviewedBy?: string): ProposalManifest;
|
|
67
|
+
sizeOf(id: string): number;
|
|
68
|
+
}
|
|
69
|
+
/** Line-based diff with +/- markers (LCS on lines, adequate for review output). */
|
|
70
|
+
export declare function simpleDiff(before: string, after: string): string;
|
|
71
|
+
//# sourceMappingURL=proposals.d.ts.map
|