@autifyhq/aximo-cli 0.14.0 → 0.16.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 (83) hide show
  1. package/README.md +24 -1
  2. package/dist/commands/device/device-config.d.ts +2 -2
  3. package/dist/commands/device/device-config.js +5 -8
  4. package/dist/commands/device/index.js +2 -1
  5. package/dist/commands/device/paths.d.ts +4 -0
  6. package/dist/commands/device/paths.js +12 -0
  7. package/dist/commands/device/reset.d.ts +1 -0
  8. package/dist/commands/device/reset.js +18 -3
  9. package/dist/commands/device/reset.test.js +13 -0
  10. package/dist/commands/device/testbox/box.d.ts +90 -0
  11. package/dist/commands/device/testbox/box.js +379 -0
  12. package/dist/commands/device/testbox/box.test.d.ts +1 -0
  13. package/dist/commands/device/testbox/box.test.js +815 -0
  14. package/dist/commands/device/testbox/container.d.ts +58 -0
  15. package/dist/commands/device/testbox/container.js +289 -0
  16. package/dist/commands/device/testbox/docker.d.ts +31 -0
  17. package/dist/commands/device/testbox/docker.js +78 -0
  18. package/dist/commands/device/testbox/docker.test.d.ts +1 -0
  19. package/dist/commands/device/testbox/docker.test.js +82 -0
  20. package/dist/commands/device/testbox/down.d.ts +7 -0
  21. package/dist/commands/device/testbox/down.js +14 -0
  22. package/dist/commands/device/testbox/errors.d.ts +4 -0
  23. package/dist/commands/device/testbox/errors.js +7 -0
  24. package/dist/commands/device/testbox/fake-docker.d.ts +25 -0
  25. package/dist/commands/device/testbox/fake-docker.js +35 -0
  26. package/dist/commands/device/testbox/index.d.ts +2 -0
  27. package/dist/commands/device/testbox/index.js +16 -0
  28. package/dist/commands/device/testbox/logs.d.ts +15 -0
  29. package/dist/commands/device/testbox/logs.js +43 -0
  30. package/dist/commands/device/testbox/prune.d.ts +11 -0
  31. package/dist/commands/device/testbox/prune.js +17 -0
  32. package/dist/commands/device/testbox/registration.d.ts +9 -0
  33. package/dist/commands/device/testbox/registration.js +56 -0
  34. package/dist/commands/device/testbox/reset.d.ts +7 -0
  35. package/dist/commands/device/testbox/reset.js +14 -0
  36. package/dist/commands/device/testbox/result.d.ts +8 -0
  37. package/dist/commands/device/testbox/result.js +23 -0
  38. package/dist/commands/device/testbox/result.test.d.ts +1 -0
  39. package/dist/commands/device/testbox/result.test.js +34 -0
  40. package/dist/commands/device/testbox/self-command.d.ts +12 -0
  41. package/dist/commands/device/testbox/self-command.js +36 -0
  42. package/dist/commands/device/testbox/self-command.test.d.ts +1 -0
  43. package/dist/commands/device/testbox/self-command.test.js +58 -0
  44. package/dist/commands/device/testbox/status.d.ts +7 -0
  45. package/dist/commands/device/testbox/status.js +11 -0
  46. package/dist/commands/device/testbox/up.d.ts +47 -0
  47. package/dist/commands/device/testbox/up.js +76 -0
  48. package/dist/commands/device/testbox/up.test.d.ts +1 -0
  49. package/dist/commands/device/testbox/up.test.js +52 -0
  50. package/dist/commands/device/testbox/url.d.ts +7 -0
  51. package/dist/commands/device/testbox/url.js +20 -0
  52. package/dist/commands/device/user-devices.d.ts +4 -0
  53. package/dist/commands/device/user-devices.js +18 -0
  54. package/dist/commands/mcp/index.js +6 -3
  55. package/dist/commands/mcp/server.d.ts +2 -2
  56. package/dist/commands/mcp/server.test.js +28 -1
  57. package/dist/commands/mcp/testbox-tools.test.d.ts +1 -0
  58. package/dist/commands/mcp/testbox-tools.test.js +278 -0
  59. package/dist/commands/mcp/tools.d.ts +8 -1
  60. package/dist/commands/mcp/tools.js +252 -19
  61. package/dist/commands/mcp/tools.test.js +151 -3
  62. package/dist/commands/mcp/wait.d.ts +20 -0
  63. package/dist/commands/mcp/wait.js +46 -0
  64. package/dist/commands/mcp/wait.test.d.ts +1 -0
  65. package/dist/commands/mcp/wait.test.js +91 -0
  66. package/dist/commands/schema.js +4 -0
  67. package/dist/commands/session/cancel.d.ts +20 -0
  68. package/dist/commands/session/cancel.js +39 -0
  69. package/dist/commands/session/export.d.ts +30 -0
  70. package/dist/commands/session/export.js +58 -0
  71. package/dist/commands/session/index.js +2 -0
  72. package/dist/commands/session/session.test.js +100 -0
  73. package/dist/errors.d.ts +2 -0
  74. package/dist/errors.js +5 -0
  75. package/dist/errors.test.js +6 -1
  76. package/dist/index.d.ts +1 -0
  77. package/dist/index.js +1 -0
  78. package/dist/session-export.d.ts +30 -0
  79. package/dist/session-export.js +72 -0
  80. package/dist/session-export.test.d.ts +1 -0
  81. package/dist/session-export.test.js +107 -0
  82. package/dist/types.d.ts +5 -0
  83. package/package.json +1 -1
@@ -24,15 +24,19 @@ async function connect() {
24
24
  };
25
25
  }
26
26
  describe("createMcpServer wiring", () => {
27
- it("exposes all 29 tools through tools/list with valid JSON Schema", async () => {
27
+ it("exposes all 40 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();
31
31
  expect(tools.map((t) => t.name).sort()).toEqual([
32
32
  "aximo_get_case",
33
33
  "aximo_get_session",
34
+ "aximo_wait_for_session",
35
+ "aximo_cancel_session",
34
36
  "aximo_get_session_capture",
35
37
  "aximo_get_session_snapshot",
38
+ "aximo_export_session_pdf",
39
+ "aximo_export_session_screenshots",
36
40
  "aximo_get_session_messages",
37
41
  "aximo_get_session_screenshots",
38
42
  "aximo_get_session_timeline",
@@ -48,6 +52,7 @@ describe("createMcpServer wiring", () => {
48
52
  "aximo_restore_case",
49
53
  "aximo_delete_case",
50
54
  "aximo_run_case",
55
+ "aximo_create_session",
51
56
  "aximo_create_plan",
52
57
  "aximo_add_case_to_plan",
53
58
  "aximo_remove_case_from_plan",
@@ -61,6 +66,9 @@ describe("createMcpServer wiring", () => {
61
66
  "aximo_list_plan_runs",
62
67
  "aximo_get_plan_run",
63
68
  "aximo_save_session_as_case",
69
+ "aximo_start_testbox",
70
+ "aximo_get_testbox",
71
+ "aximo_stop_testbox",
64
72
  ].sort());
65
73
  const timeline = tools.find((t) => t.name === "aximo_get_session_timeline");
66
74
  expect(timeline?.inputSchema?.required).toContain("sessionId");
@@ -88,6 +96,25 @@ describe("createMcpServer wiring", () => {
88
96
  await cleanup();
89
97
  }
90
98
  });
99
+ it("serves the full tool list without an API key and answers calls with the configure hint", async () => {
100
+ const prev = process.env["AXIMO_API_KEY"];
101
+ delete process.env["AXIMO_API_KEY"];
102
+ const { client, cleanup } = await connect();
103
+ try {
104
+ const { tools } = await client.listTools();
105
+ expect(tools.length).toBe(40);
106
+ const result = await client.callTool({ name: "aximo_list_projects", arguments: {} });
107
+ expect(result.isError).toBe(true);
108
+ const content = result.content;
109
+ const payload = JSON.parse(content[0].text);
110
+ expect(payload.error).toBe("API key not configured");
111
+ expect(payload.hint).toBe("Run `aximo configure`, then call the tool again.");
112
+ }
113
+ finally {
114
+ process.env["AXIMO_API_KEY"] = prev;
115
+ await cleanup();
116
+ }
117
+ });
91
118
  it("returns isError with a validation message when input fails the schema", async () => {
92
119
  const { client, cleanup } = await connect();
93
120
  try {
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,278 @@
1
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
5
+ import { HttpResponse, http, server, TEST_BASE_URL } from "../../test-utils.js";
6
+ import { getTestboxConfigPath, getTestboxStatePath } from "../device/paths.js";
7
+ import { fail, fakeDocker, ok, starts } from "../device/testbox/fake-docker.js";
8
+ import { defineTools } from "./tools.js";
9
+ let tempHome;
10
+ beforeAll(() => {
11
+ tempHome = mkdtempSync(join(tmpdir(), "aximo-mcp-testbox-"));
12
+ process.env["AXIMO_HOME"] = tempHome;
13
+ process.env["AXIMO_API_KEY"] = "test-key";
14
+ process.env["AXIMO_BASE_URL"] = TEST_BASE_URL;
15
+ server.listen();
16
+ });
17
+ afterAll(() => {
18
+ delete process.env["AXIMO_HOME"];
19
+ delete process.env["AXIMO_API_KEY"];
20
+ delete process.env["AXIMO_BASE_URL"];
21
+ server.close();
22
+ rmSync(tempHome, { recursive: true, force: true });
23
+ });
24
+ beforeEach(() => {
25
+ rmSync(join(tempHome, "testbox"), { recursive: true, force: true });
26
+ mkdirSync(join(tempHome, "testbox"), { recursive: true });
27
+ });
28
+ afterEach(() => server.resetHandlers());
29
+ function fakeClock() {
30
+ const start = 1_700_000_000_000;
31
+ let t = start;
32
+ return {
33
+ now: () => t,
34
+ sleep: async (ms) => {
35
+ t += ms;
36
+ },
37
+ elapsed: () => t - start,
38
+ };
39
+ }
40
+ function devices(list) {
41
+ server.use(http.get(`${TEST_BASE_URL}/api/devices/user`, () => HttpResponse.json({ devices: list })));
42
+ }
43
+ function seedRegistration() {
44
+ writeFileSync(getTestboxConfigPath("default"), JSON.stringify({
45
+ deviceId: "dev-1",
46
+ deviceToken: "tok",
47
+ name: "mac-testbox-abcd",
48
+ registeredAt: "2026-01-01T00:00:00.000Z",
49
+ baseUrl: TEST_BASE_URL,
50
+ }));
51
+ }
52
+ function seedState(state) {
53
+ writeFileSync(getTestboxStatePath("default"), JSON.stringify({ startedAt: new Date().toISOString(), pid: process.pid, ...state }));
54
+ }
55
+ const config = {
56
+ image: "ghcr.io/autifyhq/aximo-testbox:0.1.0",
57
+ ports: "3000",
58
+ blockHosts: [],
59
+ network: "bridge",
60
+ novncPort: 6080,
61
+ screen: "1280x720",
62
+ };
63
+ function runningBox(docker) {
64
+ docker.on(starts("inspect"), ok(`true|${config.image}|${JSON.stringify({ "aximo.testbox": "default", "aximo.testbox.config": JSON.stringify(config) })}\n`));
65
+ }
66
+ function toolsWithDetachedUp(docker = fakeDocker(), detachedUp = {}) {
67
+ const spawned = [];
68
+ const clock = fakeClock();
69
+ const deps = {
70
+ testbox: {
71
+ docker: docker.run,
72
+ clock,
73
+ spawn: (args) => {
74
+ spawned.push(args);
75
+ if (detachedUp.writesPullingState ?? true)
76
+ seedState({ phase: "pulling" });
77
+ detachedUp.onSpawn?.();
78
+ return 4242;
79
+ },
80
+ },
81
+ };
82
+ const all = defineTools(deps);
83
+ const tool = (name) => {
84
+ const t = all.find((x) => x.name === name);
85
+ if (!t)
86
+ throw new Error(`tool ${name} not found`);
87
+ return t;
88
+ };
89
+ const payload = (result) => JSON.parse(result.content[0].text);
90
+ return { tool, payload, spawned, clock };
91
+ }
92
+ describe("aximo_start_testbox", () => {
93
+ it("spawns a detached up with the flags and reports the pull in progress", async () => {
94
+ devices([]);
95
+ const { tool, payload, spawned } = toolsWithDetachedUp();
96
+ const result = await tool("aximo_start_testbox").handler({
97
+ ports: "5173",
98
+ blockHosts: ["prod.example.com"],
99
+ startUrl: "http://localhost:5173/",
100
+ screen: "1920x1080",
101
+ timeoutSeconds: 5,
102
+ });
103
+ expect(spawned).toEqual([
104
+ [
105
+ "device",
106
+ "testbox",
107
+ "up",
108
+ "--no-wait",
109
+ "--name",
110
+ "default",
111
+ "--ports",
112
+ "5173",
113
+ "--block-hosts",
114
+ "prod.example.com",
115
+ "--start-url",
116
+ "http://localhost:5173/",
117
+ "--screen",
118
+ "1920x1080",
119
+ ],
120
+ ]);
121
+ expect(result.isError).toBeFalsy();
122
+ expect(payload(result)).toMatchObject({
123
+ ready: false,
124
+ state: "pulling",
125
+ hint: expect.stringContaining("aximo_get_testbox"),
126
+ });
127
+ });
128
+ it("does not spawn a second up while one is still running", async () => {
129
+ devices([]);
130
+ seedState({ phase: "starting" });
131
+ const { tool, payload, spawned } = toolsWithDetachedUp();
132
+ const result = await tool("aximo_start_testbox").handler({ timeoutSeconds: 3 });
133
+ expect(spawned).toEqual([]);
134
+ expect(payload(result)).toMatchObject({ ready: false, state: "starting" });
135
+ });
136
+ it("forgets a previous run's error before spawning, instead of reporting it as this run's", async () => {
137
+ devices([]);
138
+ seedState({ phase: "error", pid: 1, lastError: "old failure" });
139
+ const { tool, payload, spawned } = toolsWithDetachedUp();
140
+ const result = payload(await tool("aximo_start_testbox").handler({ timeoutSeconds: 3 }));
141
+ expect(spawned).toHaveLength(1);
142
+ expect(result["state"]).toBe("pulling");
143
+ expect(result["lastError"]).toBeUndefined();
144
+ });
145
+ it("returns ready with the device id once the box is online", async () => {
146
+ devices([{ id: "dev-1", name: "mac-testbox-abcd", status: "online" }]);
147
+ seedRegistration();
148
+ const docker = fakeDocker();
149
+ const { tool, payload, spawned } = toolsWithDetachedUp(docker, {
150
+ writesPullingState: false,
151
+ onSpawn: () => runningBox(docker),
152
+ });
153
+ const result = payload(await tool("aximo_start_testbox").handler({ timeoutSeconds: 3 }));
154
+ expect(spawned).toHaveLength(1);
155
+ expect(result).toMatchObject({
156
+ ready: true,
157
+ state: "online",
158
+ container: "running",
159
+ agent: "running",
160
+ userDeviceId: "dev-1",
161
+ deviceName: "mac-testbox-abcd",
162
+ deviceStatus: "online",
163
+ liveViewUrl: "http://127.0.0.1:6080/vnc.html?autoconnect=1&resize=scale&password=pw123",
164
+ ports: [{ port: 3000, reachable: true }],
165
+ hint: expect.stringContaining("userDeviceId"),
166
+ });
167
+ });
168
+ it("does not touch a box that is already online unless asked to reconfigure it", async () => {
169
+ devices([{ id: "dev-1", name: "mac-testbox-abcd", status: "online" }]);
170
+ seedRegistration();
171
+ const docker = fakeDocker();
172
+ runningBox(docker);
173
+ const { tool, payload, spawned } = toolsWithDetachedUp(docker, { writesPullingState: false });
174
+ expect(payload(await tool("aximo_start_testbox").handler({}))["ready"]).toBe(true);
175
+ expect(spawned).toEqual([]);
176
+ await tool("aximo_start_testbox").handler({ ports: "5173" });
177
+ expect(spawned).toHaveLength(1);
178
+ });
179
+ it("returns within timeoutSeconds even when the detached up never writes state", async () => {
180
+ devices([]);
181
+ const { tool, payload, clock } = toolsWithDetachedUp(fakeDocker(), {
182
+ writesPullingState: false,
183
+ });
184
+ const result = payload(await tool("aximo_start_testbox").handler({ timeoutSeconds: 7 }));
185
+ expect(result["state"]).toBe("down");
186
+ expect(clock.elapsed()).toBeLessThanOrEqual(7_000 + 3_000);
187
+ });
188
+ it("is a tool error with the install link when Docker is missing", async () => {
189
+ devices([]);
190
+ const docker = fakeDocker();
191
+ docker.on(starts("info"), () => {
192
+ throw Object.assign(new Error("ENOENT"), { code: "ENOENT" });
193
+ });
194
+ const { tool, payload, spawned } = toolsWithDetachedUp(docker);
195
+ const result = await tool("aximo_start_testbox").handler({});
196
+ expect(result.isError).toBe(true);
197
+ expect(payload(result)["error"]).toContain("https://docs.docker.com/get-docker/");
198
+ expect(spawned).toEqual([]);
199
+ });
200
+ it("rejects a name that is not a valid box name", async () => {
201
+ devices([]);
202
+ const { tool } = toolsWithDetachedUp();
203
+ const result = await tool("aximo_start_testbox").handler({ name: "../x" });
204
+ expect(result.isError).toBe(true);
205
+ });
206
+ });
207
+ describe("aximo_get_testbox and aximo_stop_testbox", () => {
208
+ it("get reports down with the start hint when nothing runs", async () => {
209
+ devices([]);
210
+ const { tool, payload } = toolsWithDetachedUp();
211
+ expect(payload(await tool("aximo_get_testbox").handler({}))).toEqual({
212
+ ready: false,
213
+ state: "down",
214
+ name: "default",
215
+ container: "absent",
216
+ hint: "Not running. Call aximo_start_testbox.",
217
+ });
218
+ });
219
+ it("get waits up to timeoutSeconds while the box is still connecting", async () => {
220
+ devices([{ id: "dev-1", name: "mac-testbox-abcd", status: "offline" }]);
221
+ seedRegistration();
222
+ const docker = fakeDocker();
223
+ runningBox(docker);
224
+ const { tool, payload, clock } = toolsWithDetachedUp(docker);
225
+ const result = payload(await tool("aximo_get_testbox").handler({ timeoutSeconds: 7 }));
226
+ expect(result).toMatchObject({
227
+ ready: false,
228
+ state: "connecting",
229
+ hint: expect.stringContaining("aximo_get_testbox"),
230
+ });
231
+ expect(clock.elapsed()).toBe(6_000);
232
+ });
233
+ it("get surfaces a failed detached up as an error with its cause", async () => {
234
+ devices([]);
235
+ seedState({
236
+ phase: "error",
237
+ pid: 1,
238
+ lastError: "Could not pull ghcr.io/autifyhq/aximo-testbox:0.1.0: denied",
239
+ });
240
+ const { tool, payload } = toolsWithDetachedUp();
241
+ expect(payload(await tool("aximo_get_testbox").handler({}))).toMatchObject({
242
+ state: "error",
243
+ lastError: expect.stringContaining("Could not pull"),
244
+ hint: expect.stringContaining("aximo_start_testbox"),
245
+ });
246
+ });
247
+ it("get passes the agent's last message through", async () => {
248
+ devices([{ id: "dev-1", name: "mac-testbox-abcd", status: "offline" }]);
249
+ seedRegistration();
250
+ const docker = fakeDocker();
251
+ runningBox(docker);
252
+ docker.on((args) => args.includes("cat") && args.includes("/run/aximo/agent.status"), ok("Auth failed. Re-register with: aximo device register\n"));
253
+ const { tool, payload } = toolsWithDetachedUp(docker);
254
+ expect(payload(await tool("aximo_get_testbox").handler({}))["agentStatus"]).toContain("Auth failed");
255
+ });
256
+ it("stop removes the container and nothing else", async () => {
257
+ devices([]);
258
+ const docker = fakeDocker();
259
+ runningBox(docker);
260
+ const { tool, payload } = toolsWithDetachedUp(docker);
261
+ expect(payload(await tool("aximo_stop_testbox").handler({}))).toEqual({
262
+ name: "default",
263
+ removed: true,
264
+ });
265
+ expect(docker.find("rm", "-f", "aximo-testbox")).toHaveLength(1);
266
+ expect(docker.find("volume")).toHaveLength(0);
267
+ });
268
+ it("maps a Docker failure to a tool error", async () => {
269
+ devices([]);
270
+ const docker = fakeDocker();
271
+ runningBox(docker);
272
+ docker.on(starts("rm"), fail("permission denied"));
273
+ const { tool, payload } = toolsWithDetachedUp(docker);
274
+ const result = await tool("aximo_stop_testbox").handler({});
275
+ expect(result.isError).toBe(true);
276
+ expect(payload(result)["error"]).toContain("permission denied");
277
+ });
278
+ });
@@ -1,5 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { type ClientDeps } from "../../client.js";
3
+ import { type TestboxDeps } from "../device/testbox/box.js";
4
+ import { type DetachedSpawner } from "../device/testbox/self-command.js";
3
5
  interface ToolResult {
4
6
  content: Array<{
5
7
  type: "text";
@@ -7,6 +9,11 @@ interface ToolResult {
7
9
  }>;
8
10
  isError?: boolean;
9
11
  }
12
+ export interface ToolDeps extends ClientDeps {
13
+ testbox?: TestboxDeps & {
14
+ spawn?: DetachedSpawner;
15
+ };
16
+ }
10
17
  export interface Tool<TInput extends z.ZodRawShape = z.ZodRawShape> {
11
18
  name: string;
12
19
  title: string;
@@ -14,5 +21,5 @@ export interface Tool<TInput extends z.ZodRawShape = z.ZodRawShape> {
14
21
  inputSchema: TInput;
15
22
  handler: (input: z.infer<z.ZodObject<TInput>>) => Promise<ToolResult>;
16
23
  }
17
- export declare function defineTools(deps?: ClientDeps): Tool[];
24
+ export declare function defineTools(deps?: ToolDeps): Tool[];
18
25
  export {};