@agnox/mcp-server 0.0.0-stage โ†’ 1.0.1

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 (44) hide show
  1. package/README.md +179 -3
  2. package/dist/bin/agnox.d.ts +2 -0
  3. package/dist/bin/agnox.js +11 -0
  4. package/dist/bin/agnox.js.map +1 -0
  5. package/dist/bin/mcp-server.d.ts +2 -0
  6. package/dist/bin/mcp-server.js +4 -0
  7. package/dist/bin/mcp-server.js.map +1 -0
  8. package/dist/cli.d.ts +24 -0
  9. package/dist/cli.js +102 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/client.d.ts +197 -0
  12. package/dist/client.js +117 -0
  13. package/dist/client.js.map +1 -0
  14. package/dist/commands/context.d.ts +22 -0
  15. package/dist/commands/context.js +25 -0
  16. package/dist/commands/context.js.map +1 -0
  17. package/dist/commands/export-specs.d.ts +11 -0
  18. package/dist/commands/export-specs.js +85 -0
  19. package/dist/commands/export-specs.js.map +1 -0
  20. package/dist/commands/mcp.d.ts +9 -0
  21. package/dist/commands/mcp.js +29 -0
  22. package/dist/commands/mcp.js.map +1 -0
  23. package/dist/commands/runs.d.ts +6 -0
  24. package/dist/commands/runs.js +74 -0
  25. package/dist/commands/runs.js.map +1 -0
  26. package/dist/commands/trigger.d.ts +7 -0
  27. package/dist/commands/trigger.js +67 -0
  28. package/dist/commands/trigger.js.map +1 -0
  29. package/dist/config.d.ts +16 -0
  30. package/dist/config.js +44 -0
  31. package/dist/config.js.map +1 -0
  32. package/dist/format.d.ts +12 -0
  33. package/dist/format.js +45 -0
  34. package/dist/format.js.map +1 -0
  35. package/dist/mcp/server.d.ts +30 -0
  36. package/dist/mcp/server.js +144 -0
  37. package/dist/mcp/server.js.map +1 -0
  38. package/dist/mcp/tools.d.ts +32 -0
  39. package/dist/mcp/tools.js +229 -0
  40. package/dist/mcp/tools.js.map +1 -0
  41. package/dist/version.d.ts +2 -0
  42. package/dist/version.js +3 -0
  43. package/dist/version.js.map +1 -0
  44. package/package.json +48 -4
package/README.md CHANGED
@@ -1,3 +1,179 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
1
+ # @agnox/mcp-server
2
+
3
+ > **Drive [Agnox](https://agnox.dev) from your AI coding assistant and your terminal.**
4
+
5
+ [![npm](https://img.shields.io/npm/v/@agnox/mcp-server)](https://www.npmjs.com/package/@agnox/mcp-server)
6
+ [![node](https://img.shields.io/node/v/@agnox/mcp-server)](https://nodejs.org)
7
+ [![license](https://img.shields.io/npm/l/@agnox/mcp-server)](#license)
8
+
9
+ One package, two commands:
10
+
11
+ - **`mcp-server`**: an [MCP](https://modelcontextprotocol.io) server for Claude Code, Claude Desktop, Cursor and Windsurf. Your assistant can read why a run failed, including the page context at the moment of failure, quarantine a flaky test, and register the tests in your repo as Agnox test cases.
12
+ - **`agnox`**: a CLI that lists runs, prints failures, triggers a run and waits for it, and exports manual test cases as Playwright spec skeletons.
13
+
14
+ Both talk only to the Agnox public API (`/api/v1`). The package has no runtime dependencies and needs Node.js 20 or later.
15
+
16
+ ๐Ÿ“– **Full documentation:** [MCP server](https://docs.agnox.dev/docs/developer-tools/mcp-server) ยท [CLI](https://docs.agnox.dev/docs/developer-tools/cli) ยท [Public API v1](https://docs.agnox.dev/docs/api-reference/public-api-v1)
17
+
18
+ ## Quick start
19
+
20
+ ```bash
21
+ # Claude Code
22
+ claude mcp add agnox -e AGNOX_API_KEY=<your key> -- npx -y @agnox/mcp-server
23
+
24
+ # Terminal
25
+ AGNOX_API_KEY=<your key> npx -p @agnox/mcp-server agnox runs list
26
+ ```
27
+
28
+ ---
29
+
30
+ ## Configuration
31
+
32
+ | Variable | Required | Default | Purpose |
33
+ |---|---|---|---|
34
+ | `AGNOX_API_KEY` | yes | | An API key from **Settings โ†’ My Profile โ†’ API keys**. Its role decides what it can do: viewers can only read. |
35
+ | `AGNOX_API_URL` | no | `https://api.agnox.dev` | Your Agnox API, for a self-hosted install. Must be `https://`; plain `http://` is accepted only for `localhost`, `127.0.0.1` and `[::1]`. No credentials, query or fragment. Redirects are never followed, so the key cannot be sent to another host |
36
+ | `AGNOX_PROJECT_ID` | no | | The default project for tools and commands |
37
+
38
+ The key is sent only in the `x-api-key` header. It is never printed, logged or included in an error message.
39
+
40
+ ---
41
+
42
+ ## MCP server
43
+
44
+ ### Claude Code
45
+
46
+ ```bash
47
+ claude mcp add agnox \
48
+ -e AGNOX_API_KEY=<your key> \
49
+ -e AGNOX_PROJECT_ID=<project id> \
50
+ -- npx -y @agnox/mcp-server
51
+ ```
52
+
53
+ Or commit a project-scoped `.mcp.json`, and give each developer their own key through their environment:
54
+
55
+ ```json
56
+ {
57
+ "mcpServers": {
58
+ "agnox": {
59
+ "command": "npx",
60
+ "args": ["-y", "@agnox/mcp-server"],
61
+ "env": {
62
+ "AGNOX_API_URL": "https://api.agnox.dev",
63
+ "AGNOX_API_KEY": "${AGNOX_API_KEY}",
64
+ "AGNOX_PROJECT_ID": "<project id>"
65
+ }
66
+ }
67
+ }
68
+ }
69
+ ```
70
+
71
+ ### Cursor
72
+
73
+ `.cursor/mcp.json` in the project, or `~/.cursor/mcp.json` for every project:
74
+
75
+ ```json
76
+ {
77
+ "mcpServers": {
78
+ "agnox": {
79
+ "command": "npx",
80
+ "args": ["-y", "@agnox/mcp-server"],
81
+ "env": {
82
+ "AGNOX_API_URL": "https://api.agnox.dev",
83
+ "AGNOX_API_KEY": "<your key>",
84
+ "AGNOX_PROJECT_ID": "<project id>"
85
+ }
86
+ }
87
+ }
88
+ }
89
+ ```
90
+
91
+ ### Claude Desktop
92
+
93
+ **Settings โ†’ Developer โ†’ Edit Config** opens `claude_desktop_config.json` (macOS: `~/Library/Application Support/Claude/`, Windows: `%APPDATA%\Claude\`). Add the server and restart Claude Desktop:
94
+
95
+ ```json
96
+ {
97
+ "mcpServers": {
98
+ "agnox": {
99
+ "command": "npx",
100
+ "args": ["-y", "@agnox/mcp-server"],
101
+ "env": {
102
+ "AGNOX_API_KEY": "<your key>",
103
+ "AGNOX_PROJECT_ID": "<project id>"
104
+ }
105
+ }
106
+ }
107
+ }
108
+ ```
109
+
110
+ ### Windsurf
111
+
112
+ Use the same `mcpServers` block in `~/.codeium/windsurf/mcp_config.json`.
113
+
114
+ > On Windows, if the client cannot start `npx` directly, use `"command": "cmd"` with `"args": ["/c", "npx", "-y", "@agnox/mcp-server"]`.
115
+
116
+ ### Tools
117
+
118
+ | Tool | What it does |
119
+ |---|---|
120
+ | `list_recent_executions` | Recent runs, newest first, with status and passed/failed counts. Optional `projectId`, `status`, `limit` (at most 25). |
121
+ | `get_execution_failures` | Failed tests of a run (`taskId`): name, spec file, error, failure signature, test key, linked test case, and the page context at failure: URL, title, console and network errors, test ids and a DOM snapshot (cut at 4 000 characters). |
122
+ | `get_flaky_and_quarantined_tests` | Tests that are quarantined or have a stability score of D or F. |
123
+ | `toggle_test_quarantine` | Quarantine (`quarantined: true`) or release a test, by `testCaseId`, or by `testKey` from `get_execution_failures`. By key, the test case is created when it does not exist yet; give a `title`. |
124
+ | `sync_test_cases` | Register tests from the code (`tests: [{ file, titlePath }]`, at most 500) as automated test cases, so they can be tracked and quarantined before their first run. |
125
+ | `list_failure_clusters` | The most frequent failure signatures across recent runs (`days`, 1โ€“30). Needs the Root Cause Analysis feature. |
126
+
127
+ Results are a one-line summary followed by compact JSON, with lists capped at 20 items. A failing call returns a readable error to the assistant instead of breaking the session.
128
+
129
+ Things to ask:
130
+
131
+ - *"Why did the last failed Agnox run fail? Fix the broken locator."*
132
+ - *"Which tests are flaky in Agnox? Quarantine the checkout one."*
133
+ - *"Register every test in `tests/` with Agnox."*
134
+
135
+ The server speaks JSON-RPC 2.0 over stdio, one message per line. It supports MCP protocol versions `2025-11-25`, `2025-06-18`, `2025-03-26` and `2024-11-05`. `agnox mcp` starts the same server.
136
+
137
+ ---
138
+
139
+ ## CLI
140
+
141
+ Install it, or run it once through npx:
142
+
143
+ ```bash
144
+ npm i -g @agnox/mcp-server # then: agnox --help
145
+ npx -p @agnox/mcp-server agnox --help
146
+ ```
147
+
148
+ | Command | Behaviour |
149
+ |---|---|
150
+ | `agnox runs list [--project <id>] [--status <s>] [--limit <n>] [--json]` | Table of recent runs: task id, status, start, duration, passed/failed |
151
+ | `agnox runs failures <taskId> [--json]` | One block per failed test: name, file, signature and the first error lines |
152
+ | `agnox trigger --image <img> --command <cmd> --env <development\|staging\|production> [--project <id>] [--base-url <url>] [--folder <path>] [--wait] [--timeout <sec>]` | Starts a run. With `--wait`, polls every 5 s until it finishes (default timeout 1 800 s). |
153
+ | `agnox export-specs --out <dir> [--project <id>] [--folder <path>] [--force]` | Writes a Playwright skeleton per top-level folder of manual test cases. Only flat lower-case `*.spec.ts` names are written (at most 500 files, 5 MB each); symbolic links are never written through. Existing files are kept unless you pass `--force`. |
154
+ | `agnox mcp` | Runs the MCP server on stdio |
155
+ | `agnox --help`, `agnox --version` | |
156
+
157
+ `--json` prints the API data as JSON, for scripts. Exit codes: **0** success, **1** API or usage error, **2** the run finished but did not pass (`trigger --wait`).
158
+
159
+ ```yaml
160
+ # GitHub Actions: run the suite on Agnox and fail the job when it fails
161
+ - run: npx -p @agnox/mcp-server agnox trigger --image ghcr.io/acme/e2e:latest --command "npx playwright test" --env staging --wait
162
+ env:
163
+ AGNOX_API_KEY: ${{ secrets.AGNOX_API_KEY }}
164
+ AGNOX_PROJECT_ID: ${{ vars.AGNOX_PROJECT_ID }}
165
+ ```
166
+
167
+ Generated specs mark every test `test.fixme(...)`, so a skeleton never passes before someone implements it.
168
+
169
+ ---
170
+
171
+ ## Public API
172
+
173
+ The package is a thin client of the documented [Agnox Public API v1](https://docs.agnox.dev/docs/api-reference/public-api-v1). The OpenAPI 3.1 document is at `https://api.agnox.dev/api/openapi.json` (and `.yaml`).
174
+
175
+ Moving test cases in from Qase, TestRail, Xray or Zephyr Scale? See [Migrate & Export Test Cases](https://docs.agnox.dev/docs/core-features/migrate-test-cases).
176
+
177
+ ## License
178
+
179
+ MIT
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ import { runCli } from '../cli.js';
3
+ const code = await runCli(process.argv.slice(2), {
4
+ stdout: (line) => process.stdout.write(`${line}\n`),
5
+ stderr: (line) => process.stderr.write(`${line}\n`),
6
+ env: process.env,
7
+ cwd: process.cwd(),
8
+ streams: { input: process.stdin, output: process.stdout },
9
+ });
10
+ process.exitCode = code;
11
+ //# sourceMappingURL=agnox.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agnox.js","sourceRoot":"","sources":["../../src/bin/agnox.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAEnC,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;IAC7C,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,IAAI,CAAC;IACnD,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,IAAI,CAAC;IACnD,GAAG,EAAE,OAAO,CAAC,GAAG;IAChB,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE;IAClB,OAAO,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE;CAC5D,CAAC,CAAC;AACH,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { startMcpServer } from '../commands/mcp.js';
3
+ await startMcpServer(process.env, process.stdin, process.stdout, (line) => process.stderr.write(`${line}\n`));
4
+ //# sourceMappingURL=mcp-server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp-server.js","sourceRoot":"","sources":["../../src/bin/mcp-server.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEpD,MAAM,cAAc,CAAC,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC"}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The agnox command line. Results go to stdout, diagnostics to stderr.
3
+ * Exit codes: 0 success, 1 API or usage error, 2 a run finished but did not pass (trigger --wait).
4
+ */
5
+ import type { Readable, Writable } from 'node:stream';
6
+ import { AgnoxClient } from './client.js';
7
+ import { type CliFs } from './commands/context.js';
8
+ export interface ICliIo {
9
+ stdout(line: string): void;
10
+ stderr(line: string): void;
11
+ env: Record<string, string | undefined>;
12
+ cwd: string;
13
+ /** Injected in tests; otherwise built from the environment. */
14
+ client?: AgnoxClient;
15
+ fs?: CliFs;
16
+ sleep?: (ms: number) => Promise<void>;
17
+ /** stdin/stdout for `agnox mcp`. */
18
+ streams?: {
19
+ input: Readable;
20
+ output: Writable;
21
+ };
22
+ }
23
+ export declare const HELP = "agnox 1.0.1 \u2014 Agnox from the terminal\n\nUsage:\n agnox runs list [--project <id>] [--status <s>] [--limit <n>] [--json]\n agnox runs failures <taskId> [--json]\n agnox trigger --image <img> --command <cmd> --env <development|staging|production>\n [--project <id>] [--base-url <url>] [--folder <path>] [--wait] [--timeout <sec>] [--json]\n agnox export-specs --out <dir> [--project <id>] [--folder <path>] [--force]\n agnox mcp Run the MCP server on stdio (for Claude Code, Cursor, Windsurf)\n agnox --help | --version\n\nEnvironment:\n AGNOX_API_KEY API key from Settings \u2192 My Profile (required)\n AGNOX_API_URL default https://api.agnox.dev\n AGNOX_PROJECT_ID default project for --project\n\nExit codes: 0 success \u00B7 1 error \u00B7 2 run finished but did not pass (trigger --wait)";
24
+ export declare function runCli(argv: string[], io: ICliIo): Promise<number>;
package/dist/cli.js ADDED
@@ -0,0 +1,102 @@
1
+ /**
2
+ * The agnox command line. Results go to stdout, diagnostics to stderr.
3
+ * Exit codes: 0 success, 1 API or usage error, 2 a run finished but did not pass (trigger --wait).
4
+ */
5
+ import * as fsPromises from 'node:fs/promises';
6
+ import { AgnoxApiError, AgnoxClient } from './client.js';
7
+ import { ConfigError, loadConfig } from './config.js';
8
+ import { VERSION } from './version.js';
9
+ import { sanitizeForTerminal } from './format.js';
10
+ import { UsageError } from './commands/context.js';
11
+ import { runsFailures, runsList } from './commands/runs.js';
12
+ import { trigger } from './commands/trigger.js';
13
+ import { exportSpecs } from './commands/export-specs.js';
14
+ import { startMcpServer } from './commands/mcp.js';
15
+ export const HELP = `agnox ${VERSION} โ€” Agnox from the terminal
16
+
17
+ Usage:
18
+ agnox runs list [--project <id>] [--status <s>] [--limit <n>] [--json]
19
+ agnox runs failures <taskId> [--json]
20
+ agnox trigger --image <img> --command <cmd> --env <development|staging|production>
21
+ [--project <id>] [--base-url <url>] [--folder <path>] [--wait] [--timeout <sec>] [--json]
22
+ agnox export-specs --out <dir> [--project <id>] [--folder <path>] [--force]
23
+ agnox mcp Run the MCP server on stdio (for Claude Code, Cursor, Windsurf)
24
+ agnox --help | --version
25
+
26
+ Environment:
27
+ AGNOX_API_KEY API key from Settings โ†’ My Profile (required)
28
+ AGNOX_API_URL default https://api.agnox.dev
29
+ AGNOX_PROJECT_ID default project for --project
30
+
31
+ Exit codes: 0 success ยท 1 error ยท 2 run finished but did not pass (trigger --wait)`;
32
+ const COMMANDS = {
33
+ 'runs list': runsList,
34
+ 'runs failures': runsFailures,
35
+ trigger,
36
+ 'export-specs': exportSpecs,
37
+ };
38
+ function resolveCommand(argv) {
39
+ const two = argv.slice(0, 2).join(' ');
40
+ if (COMMANDS[two])
41
+ return { command: COMMANDS[two], args: argv.slice(2) };
42
+ if (argv[0] && COMMANDS[argv[0]])
43
+ return { command: COMMANDS[argv[0]], args: argv.slice(1) };
44
+ return null;
45
+ }
46
+ const realSleep = (ms) => new Promise((done) => setTimeout(done, ms));
47
+ export async function runCli(argv, io) {
48
+ if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h' || argv[0] === 'help') {
49
+ for (const line of HELP.split('\n'))
50
+ io.stdout(line);
51
+ return 0;
52
+ }
53
+ if (argv[0] === '--version' || argv[0] === '-v') {
54
+ io.stdout(VERSION);
55
+ return 0;
56
+ }
57
+ if (argv[0] === 'mcp') {
58
+ if (argv.length > 1) {
59
+ io.stderr('agnox mcp takes no arguments; configure it with AGNOX_API_KEY, AGNOX_API_URL and AGNOX_PROJECT_ID');
60
+ return 1;
61
+ }
62
+ if (!io.streams) {
63
+ io.stderr('agnox mcp needs stdin and stdout');
64
+ return 1;
65
+ }
66
+ await startMcpServer(io.env, io.streams.input, io.streams.output, io.stderr);
67
+ return 0;
68
+ }
69
+ const resolved = resolveCommand(argv);
70
+ if (!resolved) {
71
+ io.stderr(`Unknown command "${argv.join(' ')}". Run agnox --help.`);
72
+ return 1;
73
+ }
74
+ try {
75
+ const client = io.client ?? new AgnoxClient(loadConfig(io.env));
76
+ const ctx = {
77
+ client,
78
+ defaultProjectId: io.env.AGNOX_PROJECT_ID?.trim() || undefined,
79
+ stdout: io.stdout,
80
+ stderr: io.stderr,
81
+ cwd: io.cwd,
82
+ fs: io.fs ?? fsPromises,
83
+ sleep: io.sleep ?? realSleep,
84
+ };
85
+ return await resolved.command(resolved.args, ctx);
86
+ }
87
+ catch (error) {
88
+ if (error instanceof ConfigError || error instanceof UsageError || error instanceof AgnoxApiError) {
89
+ io.stderr(error instanceof AgnoxApiError ? `Error: ${sanitizeForTerminal(error.message)}` : error.message);
90
+ return 1;
91
+ }
92
+ // parseArgs rejects unknown or malformed options with a TypeError carrying an ERR_PARSE_ARGS_* code
93
+ const code = error?.code;
94
+ if (typeof code === 'string' && code.startsWith('ERR_PARSE_ARGS')) {
95
+ io.stderr(`${error.message}. Run agnox --help.`);
96
+ return 1;
97
+ }
98
+ io.stderr(`Error: ${error instanceof Error ? error.message : String(error)}`);
99
+ return 1;
100
+ }
101
+ }
102
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Small client for the Agnox public API (/api/v1). Zero dependencies: it uses
3
+ * the global fetch (Node 20+), which tests replace with a fake.
4
+ *
5
+ * The API key is sent as x-api-key and is never part of an error message.
6
+ */
7
+ import type { IAgnoxConfig } from './config.js';
8
+ export declare const REQUEST_TIMEOUT_MS = 30000;
9
+ export interface IPage<T> {
10
+ items: T[];
11
+ total: number;
12
+ limit: number;
13
+ offset: number;
14
+ hasMore: boolean;
15
+ }
16
+ export interface IProject {
17
+ id: string;
18
+ name: string;
19
+ slug: string | null;
20
+ }
21
+ export interface IExecution {
22
+ taskId: string;
23
+ projectId: string | null;
24
+ status: string;
25
+ environment: string | null;
26
+ trigger: string | null;
27
+ groupName: string | null;
28
+ startTime: string | null;
29
+ endTime: string | null;
30
+ durationMs: number | null;
31
+ total: number;
32
+ passed: number;
33
+ failed: number;
34
+ }
35
+ export interface IFailureContext {
36
+ failureUrl?: string;
37
+ pageTitle?: string;
38
+ pageErrors?: string[];
39
+ consoleErrors?: string[];
40
+ networkErrors?: Array<{
41
+ url: string;
42
+ status?: number;
43
+ method?: string;
44
+ resourceType?: string;
45
+ }>;
46
+ domSnapshot?: string;
47
+ testIds?: string[];
48
+ }
49
+ export interface IFailure {
50
+ name: string;
51
+ file?: string;
52
+ testKey?: string;
53
+ testCaseId?: string;
54
+ error?: string;
55
+ failureSignature?: string;
56
+ isQuarantined: boolean;
57
+ attemptCount?: number;
58
+ context: IFailureContext;
59
+ analysis?: {
60
+ rootCause: string;
61
+ suggestedFix: string;
62
+ generatedAt: string | null;
63
+ };
64
+ }
65
+ export interface IFailures {
66
+ taskId: string;
67
+ projectId: string | null;
68
+ items: IFailure[];
69
+ }
70
+ export interface IUnstableTest {
71
+ id: string;
72
+ tcId?: string;
73
+ title: string;
74
+ file?: string;
75
+ testKey?: string;
76
+ isQuarantined: boolean;
77
+ quarantineSource?: string;
78
+ quarantinedAt?: string;
79
+ stabilityScore?: string;
80
+ }
81
+ export interface IQuarantineResult {
82
+ id: string;
83
+ tcId?: string;
84
+ projectId: string;
85
+ title: string;
86
+ testKey?: string;
87
+ isQuarantined: boolean;
88
+ quarantineSource?: string;
89
+ stabilityScore?: string;
90
+ }
91
+ export interface ISyncResult {
92
+ received: number;
93
+ created: number;
94
+ alreadyLinked: number;
95
+ skipped: number;
96
+ }
97
+ export interface ICluster {
98
+ signature: string;
99
+ occurrences: number;
100
+ runCount: number;
101
+ testCount: number;
102
+ tests: string[];
103
+ firstSeen: string | null;
104
+ lastSeen: string | null;
105
+ sampleError: string;
106
+ lastCorrelation?: {
107
+ classification: string;
108
+ confidence: string;
109
+ createdAt: string | null;
110
+ };
111
+ }
112
+ export interface IExportFile {
113
+ path: string;
114
+ content: string;
115
+ }
116
+ export interface ITriggerRequest {
117
+ projectId: string;
118
+ image: string;
119
+ command: string;
120
+ folder?: string;
121
+ config: {
122
+ environment: 'development' | 'staging' | 'production';
123
+ baseUrl?: string;
124
+ };
125
+ ciContext: {
126
+ source: 'github' | 'gitlab' | 'azure' | 'jenkins' | 'webhook';
127
+ repository?: string;
128
+ prNumber?: number;
129
+ commitSha?: string;
130
+ };
131
+ createCycle?: boolean;
132
+ cycleId?: string;
133
+ }
134
+ export interface IQuarantineRequest {
135
+ quarantined: boolean;
136
+ testCaseId?: string;
137
+ projectId?: string;
138
+ testKey?: string;
139
+ title?: string;
140
+ file?: string;
141
+ }
142
+ export interface ISyncRequest {
143
+ projectId: string;
144
+ tests: Array<{
145
+ file: string;
146
+ titlePath: string[];
147
+ }>;
148
+ }
149
+ export interface IExportRequest {
150
+ projectId: string;
151
+ format: 'json' | 'csv' | 'markdown' | 'playwright';
152
+ folderPath?: string;
153
+ ids?: string[];
154
+ }
155
+ export declare class AgnoxApiError extends Error {
156
+ readonly status: number;
157
+ readonly code?: string | undefined;
158
+ constructor(message: string, status: number, code?: string | undefined);
159
+ }
160
+ export declare class AgnoxClient {
161
+ private readonly config;
162
+ private readonly fetchImpl;
163
+ private readonly host;
164
+ constructor(config: IAgnoxConfig, fetchImpl?: typeof fetch);
165
+ /** Remove the key from any text that might be shown to a person or a model. */
166
+ private redact;
167
+ private request;
168
+ listProjects(): Promise<IPage<IProject>>;
169
+ listExecutions(query?: {
170
+ projectId?: string;
171
+ status?: string;
172
+ limit?: number;
173
+ offset?: number;
174
+ }): Promise<IPage<IExecution>>;
175
+ getExecution(taskId: string): Promise<IExecution>;
176
+ getExecutionFailures(taskId: string): Promise<IFailures>;
177
+ triggerExecution(body: ITriggerRequest): Promise<{
178
+ taskId: string;
179
+ cycleId?: string;
180
+ }>;
181
+ listUnstableTests(projectId: string, query?: {
182
+ limit?: number;
183
+ offset?: number;
184
+ }): Promise<IPage<IUnstableTest>>;
185
+ setQuarantine(body: IQuarantineRequest): Promise<IQuarantineResult>;
186
+ syncTestCases(body: ISyncRequest): Promise<ISyncResult>;
187
+ listFailureClusters(query?: {
188
+ projectId?: string;
189
+ days?: number;
190
+ limit?: number;
191
+ }): Promise<{
192
+ items: ICluster[];
193
+ }>;
194
+ exportTestCases(body: IExportRequest): Promise<{
195
+ files: IExportFile[];
196
+ }>;
197
+ }
package/dist/client.js ADDED
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Small client for the Agnox public API (/api/v1). Zero dependencies: it uses
3
+ * the global fetch (Node 20+), which tests replace with a fake.
4
+ *
5
+ * The API key is sent as x-api-key and is never part of an error message.
6
+ */
7
+ import { VERSION } from './version.js';
8
+ export const REQUEST_TIMEOUT_MS = 30_000;
9
+ // โ”€โ”€ Errors โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
10
+ export class AgnoxApiError extends Error {
11
+ status;
12
+ code;
13
+ constructor(message, status, code) {
14
+ super(message);
15
+ this.status = status;
16
+ this.code = code;
17
+ this.name = 'AgnoxApiError';
18
+ }
19
+ }
20
+ export class AgnoxClient {
21
+ config;
22
+ fetchImpl;
23
+ host;
24
+ constructor(config, fetchImpl = globalThis.fetch) {
25
+ this.config = config;
26
+ this.fetchImpl = fetchImpl;
27
+ this.host = new URL(config.apiUrl).host;
28
+ }
29
+ /** Remove the key from any text that might be shown to a person or a model. */
30
+ redact(text) {
31
+ return this.config.apiKey ? text.split(this.config.apiKey).join('[redacted]') : text;
32
+ }
33
+ async request(method, path, options = {}) {
34
+ const url = new URL(`${this.config.apiUrl}/api/v1${path}`);
35
+ for (const [name, value] of Object.entries(options.query ?? {})) {
36
+ if (value !== undefined && value !== '')
37
+ url.searchParams.set(name, String(value));
38
+ }
39
+ const headers = {
40
+ 'x-api-key': this.config.apiKey,
41
+ 'user-agent': `agnox-cli/${VERSION}`,
42
+ accept: 'application/json',
43
+ };
44
+ if (options.body !== undefined)
45
+ headers['content-type'] = 'application/json';
46
+ let response;
47
+ try {
48
+ response = await this.fetchImpl(url.toString(), {
49
+ method,
50
+ headers,
51
+ // The key must never follow a redirect to another host.
52
+ redirect: 'manual',
53
+ body: options.body === undefined ? undefined : JSON.stringify(options.body),
54
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
55
+ });
56
+ }
57
+ catch (error) {
58
+ const name = error instanceof Error ? error.name : '';
59
+ if (name === 'TimeoutError' || name === 'AbortError') {
60
+ throw new AgnoxApiError(`Agnox at ${this.host} did not answer within ${REQUEST_TIMEOUT_MS / 1000} s`, 0, 'TIMEOUT');
61
+ }
62
+ const cause = error instanceof Error && error.cause instanceof Error ? error.cause.message : error instanceof Error ? error.message : String(error);
63
+ throw new AgnoxApiError(this.redact(`Could not reach Agnox at ${this.host}: ${cause}`), 0, 'NETWORK_ERROR');
64
+ }
65
+ if (response.status >= 300 && response.status <= 399) {
66
+ throw new AgnoxApiError(`Agnox at ${this.host} answered with a redirect; check AGNOX_API_URL`, response.status, 'REDIRECT_REFUSED');
67
+ }
68
+ let payload;
69
+ try {
70
+ payload = (await response.json());
71
+ }
72
+ catch {
73
+ payload = undefined;
74
+ }
75
+ if (!response.ok || !payload || payload.success !== true) {
76
+ const code = typeof payload?.code === 'string' ? payload.code : undefined;
77
+ let message = typeof payload?.error === 'string' && payload.error
78
+ ? payload.error
79
+ : `Unexpected response (HTTP ${response.status}) from ${this.host}`;
80
+ if (response.status === 401)
81
+ message += ' โ€” check AGNOX_API_KEY';
82
+ throw new AgnoxApiError(this.redact(message), response.status, code);
83
+ }
84
+ return payload.data;
85
+ }
86
+ listProjects() {
87
+ return this.request('GET', '/projects', { query: { limit: 100 } });
88
+ }
89
+ listExecutions(query = {}) {
90
+ return this.request('GET', '/executions', { query });
91
+ }
92
+ getExecution(taskId) {
93
+ return this.request('GET', `/executions/${encodeURIComponent(taskId)}`);
94
+ }
95
+ getExecutionFailures(taskId) {
96
+ return this.request('GET', `/executions/${encodeURIComponent(taskId)}/failures`);
97
+ }
98
+ triggerExecution(body) {
99
+ return this.request('POST', '/executions', { body });
100
+ }
101
+ listUnstableTests(projectId, query = {}) {
102
+ return this.request('GET', '/quality/unstable-tests', { query: { projectId, ...query } });
103
+ }
104
+ setQuarantine(body) {
105
+ return this.request('POST', '/test-cases/quarantine', { body });
106
+ }
107
+ syncTestCases(body) {
108
+ return this.request('POST', '/test-cases/sync', { body });
109
+ }
110
+ listFailureClusters(query = {}) {
111
+ return this.request('GET', '/failure-clusters', { query });
112
+ }
113
+ exportTestCases(body) {
114
+ return this.request('POST', '/test-cases/export', { body });
115
+ }
116
+ }
117
+ //# sourceMappingURL=client.js.map