@autifyhq/aximo-cli 0.11.1 → 0.12.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/README.md CHANGED
@@ -660,7 +660,7 @@ claude mcp add aximo -- aximo mcp
660
660
 
661
661
  VS Code (Copilot agent mode) uses the same entry under a `servers` key in `.vscode/mcp.json`.
662
662
 
663
- The server exposes 21 tools: 14 read-only ones for inspecting sessions, cases, plans, and plan runs (`aximo_get_session_timeline` is the load-bearing one for Playwright export; `aximo_list_plans` and `aximo_get_plan` cover plan discovery; `aximo_list_plan_runs` and `aximo_get_plan_run` cover run-status polling) plus 7 that author content (create and delete cases, create and update plans, save a session as a case). See the [MCP reference](./skills/aximo-cli/references/mcp.md) for per-client config paths, the full tool table, the Playwright export prompt, and a no-client smoke test.
663
+ The server exposes 22 tools: 14 read-only ones for inspecting sessions, cases, plans, and plan runs (`aximo_get_session_timeline` is the load-bearing one for Playwright export; `aximo_list_plans` and `aximo_get_plan` cover plan discovery; `aximo_list_plan_runs` and `aximo_get_plan_run` cover run-status polling) plus 8 that author content (create, update, and delete cases, create and update plans, save a session as a case). See the [MCP reference](./skills/aximo-cli/references/mcp.md) for per-client config paths, the full tool table, the Playwright export prompt, and a no-client smoke test.
664
664
 
665
665
  ## Agent Skills
666
666
 
@@ -24,7 +24,7 @@ async function connect() {
24
24
  };
25
25
  }
26
26
  describe("createMcpServer wiring", () => {
27
- it("exposes all 21 tools through tools/list with valid JSON Schema", async () => {
27
+ it("exposes all 22 tools through tools/list with valid JSON Schema", async () => {
28
28
  const { client, cleanup } = await connect();
29
29
  try {
30
30
  const { tools } = await client.listTools();
@@ -40,6 +40,7 @@ describe("createMcpServer wiring", () => {
40
40
  "aximo_list_projects",
41
41
  "aximo_list_sessions",
42
42
  "aximo_create_case",
43
+ "aximo_update_case",
43
44
  "aximo_delete_case",
44
45
  "aximo_create_plan",
45
46
  "aximo_add_case_to_plan",
@@ -186,7 +186,7 @@ export function defineTools(deps = {}) {
186
186
  {
187
187
  name: "aximo_get_case",
188
188
  title: "Get test case",
189
- description: "Get the full scenario text and metadata for a saved test case. Useful for grounding generated Playwright specs.",
189
+ description: "Get the full scenario text and metadata for a saved test case, including its per-target default configuration (mobile, web hostname overrides, desktop, multi-device) — the run trigger's per-run override fields supersede those defaults. Useful for grounding generated Playwright specs.",
190
190
  inputSchema: {
191
191
  caseId: z.string().uuid(),
192
192
  },
@@ -201,17 +201,37 @@ export function defineTools(deps = {}) {
201
201
  inputSchema: {
202
202
  projectId: z.string().uuid(),
203
203
  scenario: z.string().min(1),
204
- type: z.enum(["web", "mobile", "desktop"]).optional(),
204
+ type: z.enum(["web", "mobile", "desktop", "multi_device"]).optional(),
205
205
  title: z.string().min(1).max(500).optional(),
206
206
  description: z.string().optional(),
207
207
  format: z.enum(["gherkin", "markdown", "plain_text"]).optional(),
208
- startUrl: z.string().url().optional(),
208
+ startUrl: z.string().url().nullable().optional(),
209
209
  directoryId: z.string().uuid().optional(),
210
210
  },
211
211
  handler: async (input) => call(async () => {
212
212
  return await request("/api/cases", { method: "POST", body: input }, deps);
213
213
  }),
214
214
  },
215
+ {
216
+ name: "aximo_update_case",
217
+ title: "Update test case",
218
+ description: "Update a test case's title, description, scenario, type, format, or startUrl (pass null to clear startUrl). Omitted fields are left unchanged; at least one field is required. Per-target device configuration cannot be changed over the API, and changing type does not create or clear it (e.g. switching to mobile leaves apiMobileConfig null until set in the dashboard).",
219
+ inputSchema: {
220
+ caseId: z.string().uuid(),
221
+ title: z.string().min(1).max(500).optional(),
222
+ description: z.string().optional(),
223
+ scenario: z.string().min(1).optional(),
224
+ type: z.enum(["web", "mobile", "desktop", "multi_device"]).optional(),
225
+ format: z.enum(["gherkin", "markdown", "plain_text"]).optional(),
226
+ startUrl: z.string().url().nullable().optional(),
227
+ },
228
+ handler: async ({ caseId, ...body }) => call(async () => {
229
+ if (Object.keys(body).length === 0) {
230
+ throw new Error("at least one field to update is required");
231
+ }
232
+ return await request(`/api/cases/${encodeURIComponent(caseId)}`, { method: "PATCH", body }, deps);
233
+ }),
234
+ },
215
235
  {
216
236
  name: "aximo_delete_case",
217
237
  title: "Delete test case",
@@ -306,17 +326,39 @@ export function defineTools(deps = {}) {
306
326
  {
307
327
  name: "aximo_list_plan_runs",
308
328
  title: "List plan runs",
309
- description: "List a plan's runs, newest first, without per-case detail. Use aximo_get_plan_run for one run's per-case breakdown. Paginate by passing the previous page's nextCursor as cursor. A run's status reflects execution, not the test verdict: read passedCases/failedCases to judge outcomes.",
329
+ description: "List plan runs, newest first, without per-case detail. Pass planId for one plan's runs, or projectId for run history across every plan in a workspace (each item then carries its plan's id and title); exactly one of the two is required, and status filtering is available with projectId. Use aximo_get_plan_run for one run's per-case breakdown. Paginate by passing the previous page's nextCursor as cursor. A run's status reflects execution, not the test verdict: read passedCases/failedCases to judge outcomes.",
310
330
  inputSchema: {
311
- planId: z.string().uuid(),
331
+ planId: z
332
+ .string()
333
+ .uuid()
334
+ .optional()
335
+ .describe("Exactly one of planId or projectId is required."),
336
+ projectId: z
337
+ .string()
338
+ .uuid()
339
+ .optional()
340
+ .describe("Exactly one of planId or projectId is required."),
341
+ status: z
342
+ .enum(["created", "running", "completed", "failed", "cancelled"])
343
+ .optional()
344
+ .describe("Only with projectId: restrict to runs with this execution status."),
312
345
  limit: z.number().int().positive().max(50).optional(),
313
346
  cursor: z
314
347
  .string()
315
348
  .optional()
316
349
  .describe("nextCursor from the previous page (ISO 8601 createdAt of its last run)."),
317
350
  },
318
- handler: async ({ planId, ...rest }) => call(async () => {
319
- return await request(`/api/triggers/plans/${encodeURIComponent(planId)}/runs`, { query: buildQuery(rest) }, deps);
351
+ handler: async ({ planId, projectId, status, ...rest }) => call(async () => {
352
+ if (!planId === !projectId) {
353
+ throw new Error("pass exactly one of planId or projectId");
354
+ }
355
+ if (planId) {
356
+ if (status) {
357
+ throw new Error("the status filter requires projectId");
358
+ }
359
+ return await request(`/api/triggers/plans/${encodeURIComponent(planId)}/runs`, { query: buildQuery(rest) }, deps);
360
+ }
361
+ return await request("/api/plan-runs", { query: buildQuery({ projectId, status, ...rest }) }, deps);
320
362
  }),
321
363
  },
322
364
  {
@@ -32,6 +32,7 @@ describe("defineTools", () => {
32
32
  "aximo_list_cases",
33
33
  "aximo_get_case",
34
34
  "aximo_create_case",
35
+ "aximo_update_case",
35
36
  "aximo_delete_case",
36
37
  "aximo_create_plan",
37
38
  "aximo_add_case_to_plan",
@@ -146,6 +147,62 @@ describe("aximo_list_projects", () => {
146
147
  expect(new URL(captured.url).pathname).toBe("/api/organizations/projects");
147
148
  });
148
149
  });
150
+ describe("aximo_update_case", () => {
151
+ const caseId = "55555555-5555-4555-9555-555555555555";
152
+ it("PATCHes /api/cases/:id with only the provided fields", async () => {
153
+ let body = null;
154
+ const payload = {
155
+ success: true,
156
+ case: {
157
+ id: caseId,
158
+ projectId: "p1",
159
+ type: "web",
160
+ title: "Renamed",
161
+ description: null,
162
+ scenario: "Given a renamed page",
163
+ format: "gherkin",
164
+ startUrl: null,
165
+ updatedAt: "2026-08-14T00:00:00.000Z",
166
+ },
167
+ };
168
+ server.use(http.patch(`${TEST_BASE_URL}/api/cases/${caseId}`, async ({ request: req }) => {
169
+ body = await req.json();
170
+ return HttpResponse.json(payload);
171
+ }));
172
+ const tool = byName(defineTools(), "aximo_update_case");
173
+ const result = await tool.handler({
174
+ caseId,
175
+ title: "Renamed",
176
+ scenario: "Given a renamed page",
177
+ });
178
+ expect(result.isError).toBeUndefined();
179
+ expect(parsePayload(result)).toEqual(payload);
180
+ expect(body).toEqual({ title: "Renamed", scenario: "Given a renamed page" });
181
+ });
182
+ it("rejects an update with no fields instead of issuing an empty PATCH", async () => {
183
+ let called = false;
184
+ server.use(http.patch(`${TEST_BASE_URL}/api/cases/${caseId}`, () => {
185
+ called = true;
186
+ return HttpResponse.json({ success: true });
187
+ }));
188
+ const tool = byName(defineTools(), "aximo_update_case");
189
+ const result = await tool.handler({ caseId });
190
+ expect(result.isError).toBe(true);
191
+ expect(parsePayload(result).error).toBe("at least one field to update is required");
192
+ expect(called).toBe(false);
193
+ });
194
+ it("passes startUrl: null through as an explicit clear, not an empty update", async () => {
195
+ let body = null;
196
+ server.use(http.patch(`${TEST_BASE_URL}/api/cases/${caseId}`, async ({ request: req }) => {
197
+ body = await req.json();
198
+ return HttpResponse.json({ success: true });
199
+ }));
200
+ const tool = byName(defineTools(), "aximo_update_case");
201
+ const result = await tool.handler({ caseId, startUrl: null });
202
+ expect(result.isError).toBeUndefined();
203
+ expect(body).toEqual({ startUrl: null });
204
+ });
205
+ });
149
206
  describe("aximo_list_plans", () => {
150
207
  const projectId = "44444444-4444-4444-8444-444444444444";
151
208
  it("calls /api/plans, passing filters through as query params", async () => {
@@ -255,7 +312,7 @@ describe("aximo_list_plan_runs", () => {
255
312
  items: [
256
313
  {
257
314
  id: "88888888-8888-4888-9888-888888888888",
258
- status: "succeeded",
315
+ status: "completed",
259
316
  triggerSource: "api",
260
317
  executionMode: "parallel",
261
318
  totalCases: 3,
@@ -292,6 +349,66 @@ describe("aximo_list_plan_runs", () => {
292
349
  expect(result.isError).toBe(true);
293
350
  expect(parsePayload(result).error).toBe("Test plan not found");
294
351
  });
352
+ it("calls /api/plan-runs when given a projectId, passing status through", async () => {
353
+ const projectId = "44444444-4444-4444-8444-444444444444";
354
+ let url = null;
355
+ const payload = {
356
+ items: [
357
+ {
358
+ id: "88888888-8888-4888-9888-888888888888",
359
+ planId,
360
+ plan: { id: planId, title: "Nightly regression" },
361
+ status: "failed",
362
+ triggerSource: "api",
363
+ executionMode: "parallel",
364
+ totalCases: 3,
365
+ completedCases: 3,
366
+ passedCases: 2,
367
+ failedCases: 1,
368
+ startedAt: "2026-08-01T00:00:00.000Z",
369
+ completedAt: "2026-08-01T00:10:00.000Z",
370
+ createdAt: "2026-08-01T00:00:00.000Z",
371
+ },
372
+ ],
373
+ nextCursor: "2026-08-01T00:00:00.000Z",
374
+ };
375
+ server.use(http.get(`${TEST_BASE_URL}/api/plan-runs`, ({ request: req }) => {
376
+ url = new URL(req.url);
377
+ return HttpResponse.json(payload);
378
+ }));
379
+ const tool = byName(defineTools(), "aximo_list_plan_runs");
380
+ const result = await tool.handler({
381
+ projectId,
382
+ status: "failed",
383
+ limit: 5,
384
+ cursor: "2026-08-02T00:00:00.000Z",
385
+ });
386
+ expect(result.isError).toBeUndefined();
387
+ expect(parsePayload(result)).toEqual(payload);
388
+ expect(url).not.toBeNull();
389
+ expect(url.searchParams.get("projectId")).toBe(projectId);
390
+ expect(url.searchParams.get("status")).toBe("failed");
391
+ expect(url.searchParams.get("limit")).toBe("5");
392
+ expect(url.searchParams.get("cursor")).toBe("2026-08-02T00:00:00.000Z");
393
+ });
394
+ it("rejects calls with both, or neither, of planId and projectId", async () => {
395
+ const tool = byName(defineTools(), "aximo_list_plan_runs");
396
+ const both = await tool.handler({
397
+ planId,
398
+ projectId: "44444444-4444-4444-8444-444444444444",
399
+ });
400
+ expect(both.isError).toBe(true);
401
+ expect(parsePayload(both).error).toBe("pass exactly one of planId or projectId");
402
+ const neither = await tool.handler({});
403
+ expect(neither.isError).toBe(true);
404
+ expect(parsePayload(neither).error).toBe("pass exactly one of planId or projectId");
405
+ });
406
+ it("rejects the status filter without projectId", async () => {
407
+ const tool = byName(defineTools(), "aximo_list_plan_runs");
408
+ const result = await tool.handler({ planId, status: "failed" });
409
+ expect(result.isError).toBe(true);
410
+ expect(parsePayload(result).error).toBe("the status filter requires projectId");
411
+ });
295
412
  });
296
413
  describe("aximo_get_plan_run", () => {
297
414
  const planId = "77777777-7777-4777-9777-777777777777";
@@ -299,7 +416,7 @@ describe("aximo_get_plan_run", () => {
299
416
  it("calls /api/triggers/plans/:id/runs/:runId and returns the run with cases", async () => {
300
417
  const payload = {
301
418
  id: planRunId,
302
- status: "succeeded",
419
+ status: "completed",
303
420
  triggerSource: "api",
304
421
  executionMode: "sequential",
305
422
  totalCases: 1,
package/dist/types.d.ts CHANGED
@@ -161,7 +161,7 @@ export interface PlanDetail {
161
161
  export interface PlanRunSummary {
162
162
  id: string;
163
163
  status: string;
164
- triggerSource: string | null;
164
+ triggerSource: string;
165
165
  executionMode: string;
166
166
  totalCases: number;
167
167
  completedCases: number;
@@ -171,8 +171,15 @@ export interface PlanRunSummary {
171
171
  completedAt: string | null;
172
172
  createdAt: string;
173
173
  }
174
- export interface PlanRunList {
175
- items: PlanRunSummary[];
174
+ export interface ProjectPlanRunSummary extends PlanRunSummary {
175
+ planId: string;
176
+ plan: {
177
+ id: string;
178
+ title: string;
179
+ };
180
+ }
181
+ export interface PlanRunList<T = PlanRunSummary> {
182
+ items: T[];
176
183
  nextCursor?: string | undefined;
177
184
  }
178
185
  export interface Device {
@@ -236,11 +243,11 @@ export interface UpdateCaseResult {
236
243
  case: {
237
244
  id: string;
238
245
  projectId: string;
239
- type: string;
246
+ type: CaseType;
240
247
  title: string;
241
248
  description: string | null;
242
249
  scenario: string;
243
- format: string;
250
+ format: CaseFormat;
244
251
  startUrl: string | null;
245
252
  updatedAt: string;
246
253
  };
@@ -366,6 +373,53 @@ export interface SessionCapture {
366
373
  createdAt: string;
367
374
  updatedAt: string;
368
375
  }
376
+ export interface CaseAppInfo {
377
+ uploadArn: string;
378
+ uploadType: "own_app" | "existing" | "sample";
379
+ appName: string | null;
380
+ bundleId?: string;
381
+ }
382
+ export interface CaseMobileConfig {
383
+ deviceArn: string;
384
+ deviceName: string;
385
+ platform?: "ANDROID" | "IOS";
386
+ uploadArn?: string;
387
+ uploadType?: "own_app" | "existing" | "sample";
388
+ appName?: string;
389
+ bundleId?: string;
390
+ apps?: CaseAppInfo[];
391
+ }
392
+ export interface CaseWebConfig {
393
+ hostnameOverrideIds?: string[];
394
+ }
395
+ export interface CaseDesktopConfig {
396
+ userDeviceId: string;
397
+ name: string;
398
+ platform: "windows" | "macos" | "linux";
399
+ os: string | null;
400
+ }
401
+ export interface CaseMultiDeviceWebDevice {
402
+ kind: "web";
403
+ name: string;
404
+ startUrl?: string | null;
405
+ userDeviceId?: string;
406
+ localDeviceName?: string;
407
+ }
408
+ export interface CaseMultiDeviceMobileDevice {
409
+ kind: "mobile";
410
+ name: string;
411
+ mobilePlatform: "android" | "ios";
412
+ deviceArn?: string;
413
+ deviceName?: string;
414
+ userDeviceId?: string;
415
+ localDeviceName?: string;
416
+ uploadArn?: string;
417
+ uploadType?: "own_app" | "existing" | "sample";
418
+ appName?: string;
419
+ bundleId?: string;
420
+ apps?: CaseAppInfo[];
421
+ }
422
+ export type CaseMultiDeviceDevice = CaseMultiDeviceWebDevice | CaseMultiDeviceMobileDevice;
369
423
  export interface CaseDetail {
370
424
  id: string;
371
425
  projectId: string;
@@ -380,4 +434,8 @@ export interface CaseDetail {
380
434
  lastStatus: string | null;
381
435
  createdAt: string;
382
436
  updatedAt: string;
437
+ apiMobileConfig: CaseMobileConfig | null;
438
+ apiWebConfig: CaseWebConfig | null;
439
+ apiDesktopConfig: CaseDesktopConfig | null;
440
+ apiMultiDeviceConfig: CaseMultiDeviceDevice[] | null;
383
441
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autifyhq/aximo-cli",
3
- "version": "0.11.1",
3
+ "version": "0.12.0",
4
4
  "description": "Autify Aximo Command Line Interface (CLI)",
5
5
  "keywords": [
6
6
  "autify",
@@ -59,6 +59,7 @@
59
59
  "oxfmt": "0.45.0",
60
60
  "oxlint": "1.60.0",
61
61
  "postject": "1.0.0-alpha.6",
62
+ "resedit": "3.0.2",
62
63
  "secretlint": "11.7.1",
63
64
  "tsx": "4.21.0",
64
65
  "typescript": "6.0.2",