@timetotest/cli 0.2.3 → 0.2.4

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 (118) hide show
  1. package/dist/package.json +8 -5
  2. package/dist/src/commands/chat/ChatApp.js +30 -42
  3. package/dist/src/commands/chat/ChatApp.js.map +1 -1
  4. package/dist/src/commands/chat/components/Banner.js +1 -1
  5. package/dist/src/commands/chat/components/ChatInput.js +39 -17
  6. package/dist/src/commands/chat/components/ChatInput.js.map +1 -1
  7. package/dist/src/commands/chat/components/MessageBubble.js +2 -1
  8. package/dist/src/commands/chat/components/MessageBubble.js.map +1 -1
  9. package/dist/src/commands/chat-ink.js +118 -9
  10. package/dist/src/commands/chat-ink.js.map +1 -1
  11. package/dist/src/commands/start-test.js +61 -0
  12. package/dist/src/commands/start-test.js.map +1 -1
  13. package/dist/src/commands/stream/StreamApp.js +127 -0
  14. package/dist/src/commands/stream/StreamApp.js.map +1 -0
  15. package/dist/src/commands/stream.js +43 -8
  16. package/dist/src/commands/stream.js.map +1 -1
  17. package/dist/src/commands/test/TestRunApp.js +183 -0
  18. package/dist/src/commands/test/TestRunApp.js.map +1 -0
  19. package/dist/src/commands/test.js +97 -0
  20. package/dist/src/commands/test.js.map +1 -1
  21. package/dist/src/lib/agent-orchestrator.js +5 -0
  22. package/dist/src/lib/agent-orchestrator.js.map +1 -1
  23. package/dist/src/lib/local-tools/ui/playwright-mcp.js +1 -1
  24. package/dist/src/lib/tui/ink/theme.js +21 -0
  25. package/dist/src/lib/tui/ink/theme.js.map +1 -0
  26. package/package.json +8 -5
  27. package/dist/src/commands/ask/AskApp.js +0 -121
  28. package/dist/src/commands/ask/AskApp.js.map +0 -1
  29. package/dist/src/commands/ask/components/AssistantResponse.js +0 -31
  30. package/dist/src/commands/ask/components/AssistantResponse.js.map +0 -1
  31. package/dist/src/commands/ask/components/Banner.js +0 -15
  32. package/dist/src/commands/ask/components/Banner.js.map +0 -1
  33. package/dist/src/commands/ask/components/ChatInput.js +0 -93
  34. package/dist/src/commands/ask/components/ChatInput.js.map +0 -1
  35. package/dist/src/commands/ask/components/Divider.js +0 -17
  36. package/dist/src/commands/ask/components/Divider.js.map +0 -1
  37. package/dist/src/commands/ask/components/IntroTips.js +0 -19
  38. package/dist/src/commands/ask/components/IntroTips.js.map +0 -1
  39. package/dist/src/commands/ask/components/MessageBubble.js +0 -47
  40. package/dist/src/commands/ask/components/MessageBubble.js.map +0 -1
  41. package/dist/src/commands/ask/components/SessionInfo.js +0 -20
  42. package/dist/src/commands/ask/components/SessionInfo.js.map +0 -1
  43. package/dist/src/commands/ask/components/StatusIndicator.js +0 -67
  44. package/dist/src/commands/ask/components/StatusIndicator.js.map +0 -1
  45. package/dist/src/commands/ask-ink.js +0 -380
  46. package/dist/src/commands/ask-ink.js.map +0 -1
  47. package/dist/src/commands/ask.js +0 -991
  48. package/dist/src/commands/ask.js.map +0 -1
  49. package/dist/src/commands/chat/components/Divider.js +0 -7
  50. package/dist/src/commands/chat/components/Divider.js.map +0 -1
  51. package/dist/src/commands/chat/components/SessionInfo.js +0 -11
  52. package/dist/src/commands/chat/components/SessionInfo.js.map +0 -1
  53. package/dist/src/commands/chat.js +0 -82
  54. package/dist/src/commands/chat.js.map +0 -1
  55. package/dist/src/lib/legacy-chat-runner.js +0 -37
  56. package/dist/src/lib/legacy-chat-runner.js.map +0 -1
  57. package/dist/src/lib/local-tools/ui/click-element.js +0 -105
  58. package/dist/src/lib/local-tools/ui/click-element.js.map +0 -1
  59. package/dist/src/lib/local-tools/ui/dom-rag.js +0 -201
  60. package/dist/src/lib/local-tools/ui/dom-rag.js.map +0 -1
  61. package/dist/src/lib/local-tools/ui/find-element.js +0 -31
  62. package/dist/src/lib/local-tools/ui/find-element.js.map +0 -1
  63. package/dist/src/lib/local-tools/ui/hover-element.js +0 -94
  64. package/dist/src/lib/local-tools/ui/hover-element.js.map +0 -1
  65. package/dist/src/lib/local-tools/ui/manage-tab.js +0 -65
  66. package/dist/src/lib/local-tools/ui/manage-tab.js.map +0 -1
  67. package/dist/src/lib/local-tools/ui/navigate.js +0 -35
  68. package/dist/src/lib/local-tools/ui/navigate.js.map +0 -1
  69. package/dist/src/lib/local-tools/ui/page-discovery.js +0 -32
  70. package/dist/src/lib/local-tools/ui/page-discovery.js.map +0 -1
  71. package/dist/src/lib/local-tools/ui/screenshot.js +0 -19
  72. package/dist/src/lib/local-tools/ui/screenshot.js.map +0 -1
  73. package/dist/src/lib/local-tools/ui/search-interactive-elements.js +0 -18
  74. package/dist/src/lib/local-tools/ui/search-interactive-elements.js.map +0 -1
  75. package/dist/src/lib/local-tools/ui/selector-resolver.js +0 -153
  76. package/dist/src/lib/local-tools/ui/selector-resolver.js.map +0 -1
  77. package/dist/src/lib/local-tools/ui/type-text.js +0 -40
  78. package/dist/src/lib/local-tools/ui/type-text.js.map +0 -1
  79. package/dist/src/lib/tui/components/AskIntro.js +0 -6
  80. package/dist/src/lib/tui/components/AskIntro.js.map +0 -1
  81. package/dist/src/lib/tui/components/Banner.js +0 -15
  82. package/dist/src/lib/tui/components/Banner.js.map +0 -1
  83. package/dist/src/lib/tui/components/Divider.js +0 -17
  84. package/dist/src/lib/tui/components/Divider.js.map +0 -1
  85. package/dist/src/lib/tui/components/EventLine.js +0 -110
  86. package/dist/src/lib/tui/components/EventLine.js.map +0 -1
  87. package/dist/src/lib/tui/components/Header.js +0 -15
  88. package/dist/src/lib/tui/components/Header.js.map +0 -1
  89. package/dist/src/lib/tui/components/InputBox.js +0 -9
  90. package/dist/src/lib/tui/components/InputBox.js.map +0 -1
  91. package/dist/src/lib/tui/components/Mapping.js +0 -8
  92. package/dist/src/lib/tui/components/Mapping.js.map +0 -1
  93. package/dist/src/lib/tui/components/ProjectList.js +0 -6
  94. package/dist/src/lib/tui/components/ProjectList.js.map +0 -1
  95. package/dist/src/lib/tui/components/Spinner.js +0 -20
  96. package/dist/src/lib/tui/components/Spinner.js.map +0 -1
  97. package/dist/src/lib/tui/components/StatusBanner.js +0 -12
  98. package/dist/src/lib/tui/components/StatusBanner.js.map +0 -1
  99. package/dist/src/lib/tui/components/StatusBar.js +0 -11
  100. package/dist/src/lib/tui/components/StatusBar.js.map +0 -1
  101. package/dist/src/lib/tui/components/UserBubble.js +0 -6
  102. package/dist/src/lib/tui/components/UserBubble.js.map +0 -1
  103. package/dist/src/lib/tui/components/index.js +0 -16
  104. package/dist/src/lib/tui/components/index.js.map +0 -1
  105. package/dist/src/lib/tui/ink-print.js +0 -41
  106. package/dist/src/lib/tui/ink-print.js.map +0 -1
  107. package/dist/src/test-agent-flow.js +0 -148
  108. package/dist/src/test-agent-flow.js.map +0 -1
  109. package/dist/src/test-browser-session.js +0 -152
  110. package/dist/src/test-browser-session.js.map +0 -1
  111. package/dist/src/test-browser-snapshot.js +0 -187
  112. package/dist/src/test-browser-snapshot.js.map +0 -1
  113. package/dist/src/test-snapshot-detailed.js +0 -219
  114. package/dist/src/test-snapshot-detailed.js.map +0 -1
  115. package/dist/src/test-snapshot-simple.js +0 -85
  116. package/dist/src/test-snapshot-simple.js.map +0 -1
  117. package/dist/src/test-snapshot-tabs.js +0 -110
  118. package/dist/src/test-snapshot-tabs.js.map +0 -1
@@ -1,991 +0,0 @@
1
- import { Command } from "commander";
2
- import chalk from "chalk";
3
- import ora from "ora";
4
- import os from "node:os";
5
- import { createHttpClient } from "../lib/http.js";
6
- import { connectForAsk, subscribeAsk, stopSession } from "../lib/socket.js";
7
- import { registerUnifiedEventLogging, canonicalizeEventType, } from "../lib/events.js";
8
- import { promptInBox, printDivider, formatStatusSegments, accentText, highlightText, printBanner, printUserBubble, printStatusBanner, } from "../lib/tui.js";
9
- import { getAuthToken, getUserLastProject, setUserLastProject, } from "../lib/config.js";
10
- import { clearAuthToken } from "../lib/config.js";
11
- import { performInteractiveLogin } from "./login.js";
12
- function formatResponseText(text) {
13
- if (!text)
14
- return text;
15
- // Convert HTML-like tags to CLI-friendly formatting
16
- let formatted = text
17
- // Convert <list> to a simple list
18
- .replace(/<list>/g, "")
19
- .replace(/<\/list>/g, "")
20
- // Convert <item> to bullet points
21
- .replace(/<item>/g, "• ")
22
- .replace(/<\/item>/g, "")
23
- // Convert <strong> to bold
24
- .replace(/<strong>/g, chalk.bold(""))
25
- .replace(/<\/strong>/g, chalk.reset(""))
26
- // Convert <em> to italic
27
- .replace(/<em>/g, chalk.italic(""))
28
- .replace(/<\/em>/g, chalk.reset(""))
29
- // Convert <code> to monospace
30
- .replace(/<code>/g, chalk.gray(""))
31
- .replace(/<\/code>/g, chalk.reset(""))
32
- // Convert line breaks to proper newlines
33
- .replace(/\n/g, "\n");
34
- return formatted;
35
- }
36
- export const ask = new Command("ask")
37
- .description("Start interactive chat with TimetoTest AI agent")
38
- .option("--project-id <id>", "Project ID to use for the conversation")
39
- .option("--conversation-id <id>", "Continue existing conversation")
40
- .action(async (opts) => {
41
- try {
42
- // Check authentication
43
- const token = getAuthToken();
44
- if (!token) {
45
- console.log(chalk.yellow("You are not logged in. Opening browser to authenticate…"));
46
- try {
47
- await performInteractiveLogin();
48
- }
49
- catch (e) {
50
- console.log(chalk.red(e?.message || "Login failed"));
51
- process.exit(1);
52
- }
53
- }
54
- let http = createHttpClient();
55
- const drawIntro = () => {
56
- console.clear();
57
- printBanner();
58
- console.log(chalk.white("Tips for getting started:"));
59
- console.log(`${chalk.dim("1.")} ${chalk.white("Ask questions, inspect files, or run commands.")}`);
60
- console.log(`${chalk.dim("2.")} ${chalk.white("Be specific for the clearest answers.")}`);
61
- console.log(`${chalk.dim("3.")} ${chalk.white(`Share extra context with ${accentText("@path/to/file")}.`)}`);
62
- console.log(`${chalk.dim("4.")} ${chalk.white(`${accentText("/help")} shows slash commands and shortcuts.`)}`);
63
- console.log();
64
- };
65
- // Gemini-like intro: banner + tips, then single active input box (no duplicate)
66
- drawIntro();
67
- let conversationId = opts.conversationId;
68
- let projectId;
69
- if (opts.projectId !== undefined) {
70
- const parsedProjectId = Number(opts.projectId);
71
- if (!Number.isInteger(parsedProjectId)) {
72
- console.log(chalk.red(`❌ Invalid project id '${opts.projectId}'. Please provide a numeric value.`));
73
- process.exit(1);
74
- }
75
- projectId = parsedProjectId;
76
- }
77
- let sessionInfoWarningShown = false;
78
- let lastKnownUser = "Unknown";
79
- let lastKnownUserKey;
80
- let lastKnownProject = projectId ? `Project ${projectId}` : "Not set";
81
- const renderSessionSummary = async (refresh = true, retried = false) => {
82
- if (refresh) {
83
- try {
84
- const userResp = await http.get("/api/v1/auth/me");
85
- lastKnownUserKey =
86
- userResp.data.email || String(userResp.data.user_id || "");
87
- lastKnownUser =
88
- userResp.data.email || userResp.data.display_name || "Unknown";
89
- if (projectId) {
90
- const projectResp = await http.get(`/api/v1/projects/${projectId}`);
91
- lastKnownProject =
92
- projectResp.data.name || `Project ${projectId}`;
93
- }
94
- else {
95
- lastKnownProject = "Not set";
96
- }
97
- sessionInfoWarningShown = false;
98
- }
99
- catch (error) {
100
- if (!retried) {
101
- // Attempt to re-auth then retry once
102
- const msg = chalk.gray("Re-authenticating to refresh session information…");
103
- console.log(msg);
104
- try {
105
- await performInteractiveLogin();
106
- http = createHttpClient();
107
- await renderSessionSummary(true, true);
108
- return;
109
- }
110
- catch (reauthErr) {
111
- // fall through to warning message below
112
- }
113
- }
114
- if (!sessionInfoWarningShown) {
115
- console.log(chalk.gray("ℹ️ Unable to refresh session info from the API."));
116
- sessionInfoWarningShown = true;
117
- }
118
- }
119
- }
120
- console.log(`${chalk.dim("•")} ${chalk.gray("Signed in as")} ${chalk.white(lastKnownUser)}`);
121
- console.log(`${chalk.dim("•")} ${chalk.gray("Project")} ${accentText(lastKnownProject)} ${chalk.gray("· use /switch to change")}`);
122
- console.log();
123
- };
124
- let spinner = null;
125
- let messageCount = 1;
126
- let socket;
127
- let userCancelled = false;
128
- const sendAskStop = () => {
129
- if (userCancelled)
130
- return;
131
- userCancelled = true;
132
- try {
133
- if (spinner) {
134
- spinner.stop();
135
- spinner = null;
136
- }
137
- console.log(chalk.yellow("🛑 Cancelled — the assistant has stopped. Type whenever you're ready."));
138
- if (socket && conversationId) {
139
- stopSession(socket, {
140
- session_kind: "ask",
141
- ids: { conversation_id: conversationId },
142
- });
143
- }
144
- }
145
- catch { }
146
- };
147
- // Function to switch projects
148
- const switchProject = async (searchTerm) => {
149
- try {
150
- const fetchSpinner = ora("Fetching your projects...").start();
151
- const projectsResp = await http.get("/api/v1/projects");
152
- const projectsRaw = projectsResp.data;
153
- const projects = Array.isArray(projectsRaw?.items)
154
- ? projectsRaw.items
155
- : Array.isArray(projectsRaw)
156
- ? projectsRaw
157
- : [];
158
- fetchSpinner.succeed("Projects loaded!");
159
- if (!projects || projects.length === 0) {
160
- console.log(chalk.yellow("❌ No projects available."));
161
- return;
162
- }
163
- const findProject = (query) => {
164
- const normalized = query.trim().toLowerCase();
165
- return projects.find((project) => {
166
- if (!project)
167
- return false;
168
- const name = String(project.name || "").toLowerCase();
169
- const idMatch = String(project.id || "") === query.trim();
170
- return idMatch || name.includes(normalized);
171
- });
172
- };
173
- let selectedProject;
174
- if (searchTerm && searchTerm.trim().length > 0) {
175
- selectedProject = findProject(searchTerm.trim());
176
- if (!selectedProject) {
177
- console.log(chalk.yellow(`No project matched "${searchTerm}". Showing the list instead.`));
178
- }
179
- }
180
- if (!selectedProject) {
181
- console.log();
182
- printDivider("Available Projects");
183
- projects.forEach((project, index) => {
184
- const isCurrent = project.id === projectId;
185
- const marker = isCurrent ? chalk.green("●") : chalk.gray("○");
186
- console.log(`${marker} ${chalk.cyan(project.name)}`);
187
- console.log(` ${chalk.gray(project.description || "No description")}`);
188
- if (index < projects.length - 1)
189
- console.log();
190
- });
191
- printDivider();
192
- const { value: selection, aborted } = await promptInBox({
193
- label: "Switch Project",
194
- placeholder: "Start typing a project name or ID...",
195
- hint: "Enter to switch · leave empty to cancel",
196
- infoLines: [
197
- chalk.gray("Matching is case-insensitive and accepts partial names."),
198
- chalk.gray("Press Ctrl+C to cancel the switch."),
199
- ],
200
- });
201
- if (aborted || selection.length === 0) {
202
- console.log(chalk.gray("Project switch cancelled."));
203
- return;
204
- }
205
- selectedProject = findProject(selection);
206
- if (!selectedProject) {
207
- console.log(chalk.red(`❌ Project "${selection}" not found.`));
208
- return;
209
- }
210
- }
211
- if (!selectedProject) {
212
- console.log(chalk.red("❌ Unable to determine project."));
213
- return;
214
- }
215
- if (selectedProject.id === projectId) {
216
- console.log(chalk.yellow(`ℹ️ Already on project "${selectedProject.name}".`));
217
- return;
218
- }
219
- projectId = selectedProject.id;
220
- lastKnownProject = selectedProject.name || `Project ${projectId}`;
221
- conversationId = undefined;
222
- messageCount = 1;
223
- // Do not create a conversation yet; wait for the next user message
224
- if (socket) {
225
- try {
226
- socket.removeAllListeners?.();
227
- await socket.disconnect();
228
- }
229
- catch (disconnectError) {
230
- console.log(chalk.gray(`ℹ️ Reconnecting socket: ${disconnectError?.message || disconnectError}`));
231
- }
232
- }
233
- if (lastKnownUserKey && typeof projectId === "number") {
234
- setUserLastProject(lastKnownUserKey, projectId);
235
- }
236
- await renderSessionSummary();
237
- console.log(chalk.green(`✅ Switched to project: ${selectedProject.name}`));
238
- console.log();
239
- }
240
- catch (error) {
241
- console.log(chalk.red(`❌ Failed to switch project: ${error?.message || error}`));
242
- }
243
- };
244
- const slashCommands = [];
245
- const helpCommand = {
246
- name: "help",
247
- description: "Show available slash commands",
248
- aliases: ["?", "commands", "h"],
249
- run: async () => {
250
- console.log(chalk.white("Slash commands:"));
251
- slashCommands.forEach((cmd) => {
252
- const aliasText = cmd.aliases?.length
253
- ? chalk.gray(` (aliases: ${cmd.aliases
254
- .map((alias) => `/${alias}`)
255
- .join(", ")})`)
256
- : "";
257
- console.log(` ${accentText(`/${cmd.name}`)} ${chalk.gray(cmd.description)}${aliasText}`);
258
- });
259
- console.log(chalk.gray("Prefix commands with '/' (for example, /switch or /summary)."));
260
- console.log();
261
- return "continue";
262
- },
263
- };
264
- const summaryCommand = {
265
- name: "summary",
266
- description: "Display the latest session information",
267
- aliases: ["session", "status"],
268
- run: async () => {
269
- await renderSessionSummary();
270
- return "continue";
271
- },
272
- };
273
- const switchCommand = {
274
- name: "switch",
275
- description: "Switch to a different project",
276
- aliases: ["project", "s"],
277
- run: async (args) => {
278
- const query = args.join(" ");
279
- await switchProject(query);
280
- return "continue";
281
- },
282
- };
283
- const clearCommand = {
284
- name: "clear",
285
- description: "Clear the screen and redraw the header",
286
- aliases: ["cls"],
287
- run: async () => {
288
- drawIntro();
289
- await renderSessionSummary(false);
290
- return "continue";
291
- },
292
- };
293
- const projectsCommand = {
294
- name: "projects",
295
- description: "List your projects",
296
- aliases: ["list-projects", "proj"],
297
- run: async () => {
298
- try {
299
- const resp = await http.get("/api/v1/projects");
300
- const items = Array.isArray(resp.data?.items)
301
- ? resp.data.items
302
- : Array.isArray(resp.data)
303
- ? resp.data
304
- : [];
305
- if (!items.length) {
306
- console.log(chalk.yellow("No projects found."));
307
- return "continue";
308
- }
309
- console.log();
310
- printDivider("Projects");
311
- items.forEach((p, i) => {
312
- const isCurrent = p.id === projectId;
313
- const marker = isCurrent ? chalk.green("●") : chalk.gray("○");
314
- const name = p.name || `Project ${p.id}`;
315
- const def = p.is_default ? chalk.cyan(" (default)") : "";
316
- console.log(`${marker} ${chalk.cyan(name)}${def}`);
317
- console.log(` ${chalk.gray(p.description || "No description")}`);
318
- if (i < items.length - 1)
319
- console.log();
320
- });
321
- printDivider();
322
- }
323
- catch (e) {
324
- console.log(chalk.red(`❌ Failed to list projects: ${e?.message || e}`));
325
- }
326
- return "continue";
327
- },
328
- };
329
- const testsCommand = {
330
- name: "tests",
331
- description: "List recent tests",
332
- aliases: ["list-tests"],
333
- run: async () => {
334
- try {
335
- const params = projectId ? `?project_id=${projectId}` : "";
336
- const resp = await http.get(`/api/v1/my-tests${params}`);
337
- const items = Array.isArray(resp.data?.items)
338
- ? resp.data.items
339
- : Array.isArray(resp.data)
340
- ? resp.data
341
- : [];
342
- if (!items.length) {
343
- console.log(chalk.yellow("No tests yet."));
344
- return "continue";
345
- }
346
- console.log();
347
- printDivider("Tests");
348
- items.forEach((t, i) => {
349
- const id = t.id;
350
- const status = t.status || "";
351
- const url = t.url || "";
352
- const type = t.test_type || "";
353
- console.log(`${chalk.gray("•")} ${chalk.white(`#${id}`)} ${chalk.gray(`(${type} · ${status})`)} ${chalk.cyan(url)}`);
354
- if (i < items.length - 1)
355
- console.log();
356
- });
357
- printDivider();
358
- }
359
- catch (e) {
360
- console.log(chalk.red(`❌ Failed to list tests: ${e?.message || e}`));
361
- }
362
- return "continue";
363
- },
364
- };
365
- const statusCommand = {
366
- name: "status",
367
- description: "Get test status: /status <testId>",
368
- aliases: ["test-status"],
369
- run: async (args) => {
370
- const id = Number(args[0]);
371
- if (!id || Number.isNaN(id)) {
372
- console.log(chalk.yellow("Usage: /status <testId>"));
373
- return "continue";
374
- }
375
- try {
376
- const resp = await http.get(`/api/v1/tests/${id}/status`);
377
- const s = resp.data;
378
- console.log();
379
- printDivider(`Test #${id} Status`);
380
- console.log(formatStatusSegments([
381
- { label: "status", value: s.status, color: chalk.cyan },
382
- {
383
- label: "progress",
384
- value: `${s.progress_percentage}%`,
385
- color: chalk.green,
386
- },
387
- {
388
- label: "stage",
389
- value: s.current_stage || s.currentStage || "",
390
- color: chalk.white,
391
- },
392
- ]));
393
- printDivider();
394
- }
395
- catch (e) {
396
- console.log(chalk.red(`❌ Failed to get status: ${e?.message || e}`));
397
- }
398
- return "continue";
399
- },
400
- };
401
- const reportCommand = {
402
- name: "report",
403
- description: "Generate a report: /report <testId> [html|json]",
404
- aliases: ["make-report"],
405
- run: async (args) => {
406
- const id = Number(args[0]);
407
- const fmt = (args[1] || "html").toLowerCase();
408
- if (!id || Number.isNaN(id)) {
409
- console.log(chalk.yellow("Usage: /report <testId> [html|json]"));
410
- return "continue";
411
- }
412
- try {
413
- const resp = await http.get(`/api/v1/reports/`, {
414
- // axios get with params? route is POST /reports/, but it expects query params via function signature
415
- });
416
- }
417
- catch (_) {
418
- // Fallback to POST with query params as defined in backend
419
- }
420
- try {
421
- const resp = await http.post(`/api/v1/reports/`, null, {
422
- params: { test_id: id, report_format: fmt },
423
- });
424
- const r = resp.data;
425
- console.log();
426
- printDivider(`Report for Test #${id}`);
427
- console.log(`${chalk.gray("report id")} ${chalk.white(r.id)}`);
428
- if (r.file_path)
429
- console.log(`${chalk.gray("file")} ${chalk.cyan(r.file_path)}`);
430
- printDivider();
431
- }
432
- catch (e) {
433
- console.log(chalk.red(`❌ Failed to generate report: ${e?.message || e}`));
434
- }
435
- return "continue";
436
- },
437
- };
438
- const startTestCommand = {
439
- name: "test",
440
- description: "Start a test: /test <prompt> [ui|api] [docs:<openapi-url>]",
441
- aliases: ["start-test", "start"],
442
- run: async (args) => {
443
- // Parse type and optional docs: url tokens; the rest is the prompt
444
- let type = "ui";
445
- let docsUrl;
446
- const tokens = Array.isArray(args) ? [...args] : [];
447
- const remaining = [];
448
- for (const t of tokens) {
449
- const low = (t || "").toLowerCase();
450
- if (low === "ui" || low === "api") {
451
- type = low;
452
- continue;
453
- }
454
- if (low.startsWith("docs:")) {
455
- const v = t.slice(5);
456
- if (v && v.startsWith("http"))
457
- docsUrl = v;
458
- continue;
459
- }
460
- remaining.push(t);
461
- }
462
- let prompt = remaining.join(" ").trim();
463
- if (!prompt) {
464
- const { value, aborted } = await promptInBox({
465
- label: "Start Test",
466
- placeholder: "Describe what to test (prompt)",
467
- hint: "Enter to confirm · Ctrl+C to cancel",
468
- fullWidth: true,
469
- });
470
- if (aborted || !value.trim())
471
- return "continue";
472
- prompt = value.trim();
473
- }
474
- try {
475
- const resp = await http.post(`/api/v1/start-test`, {
476
- // No URL: let backend derive from project settings
477
- url: undefined,
478
- test_type: type,
479
- project_id: projectId,
480
- user_prompt: prompt,
481
- test_metadata: {
482
- docs_url: docsUrl,
483
- surface: "cli",
484
- },
485
- });
486
- const data = resp.data;
487
- console.log();
488
- printDivider("Test Started");
489
- console.log(formatStatusSegments([
490
- { label: "id", value: String(data.test_id), color: chalk.white },
491
- { label: "status", value: data.status, color: chalk.cyan },
492
- {
493
- label: "project",
494
- value: lastKnownProject,
495
- color: highlightText,
496
- },
497
- ]));
498
- printDivider();
499
- }
500
- catch (e) {
501
- console.log(chalk.red(`❌ Failed to start test: ${e?.message || e}`));
502
- }
503
- return "continue";
504
- },
505
- };
506
- const conversationsCommand = {
507
- name: "conversations",
508
- description: "List your recent conversations",
509
- aliases: ["convos", "sessions"],
510
- run: async () => {
511
- try {
512
- const params = projectId ? `?project_id=${projectId}` : "";
513
- const resp = await http.get(`/api/v1/ask/sessions${params}`);
514
- const items = Array.isArray(resp.data?.items)
515
- ? resp.data.items
516
- : Array.isArray(resp.data)
517
- ? resp.data
518
- : [];
519
- if (!items.length) {
520
- console.log(chalk.yellow("No conversations yet."));
521
- return "continue";
522
- }
523
- console.log();
524
- printDivider("Conversations");
525
- items.forEach((c, i) => {
526
- const id = c.conversation_id || c.id;
527
- const created = c.created_at || "";
528
- const status = c.status || "";
529
- const title = c.goal || c.title || "";
530
- console.log(`${chalk.gray("•")} ${chalk.white(title || id)} ${chalk.gray(`(${status})`)}`);
531
- if (created)
532
- console.log(` ${chalk.gray(created)}`);
533
- if (i < items.length - 1)
534
- console.log();
535
- });
536
- printDivider();
537
- }
538
- catch (e) {
539
- console.log(chalk.red(`❌ Failed to list conversations: ${e?.message || e}`));
540
- }
541
- return "continue";
542
- },
543
- };
544
- const exitCommand = {
545
- name: "exit",
546
- description: "End the conversation",
547
- aliases: ["quit", "bye"],
548
- run: async () => "exit",
549
- };
550
- const logoutCommand = {
551
- name: "logout",
552
- description: "Log out and clear local credentials",
553
- aliases: ["signout"],
554
- run: async () => {
555
- try {
556
- clearAuthToken();
557
- console.log(chalk.green("✅ Logged out. Credentials cleared."));
558
- }
559
- catch (e) {
560
- console.log(chalk.red(`❌ Failed to logout: ${e?.message || String(e)}`));
561
- }
562
- return "exit";
563
- },
564
- };
565
- const stopCommand = {
566
- name: "stop",
567
- description: "Stop the assistant for this conversation (same as ESC)",
568
- aliases: ["cancel", "halt"],
569
- run: async () => {
570
- sendAskStop();
571
- return "continue";
572
- },
573
- };
574
- slashCommands.push(helpCommand, summaryCommand, switchCommand, clearCommand, testsCommand, statusCommand, reportCommand, startTestCommand, projectsCommand, stopCommand, conversationsCommand, logoutCommand, exitCommand);
575
- const commandLookup = new Map();
576
- slashCommands.forEach((command) => {
577
- commandLookup.set(command.name, command);
578
- command.aliases?.forEach((alias) => {
579
- commandLookup.set(alias, command);
580
- });
581
- });
582
- const executeSlashCommand = async (rawInput) => {
583
- const trimmed = rawInput.trim();
584
- if (!trimmed) {
585
- console.log(chalk.gray("Type / to see the command list."));
586
- return "continue";
587
- }
588
- const [keyword, ...rawArgs] = trimmed.split(/\s+/);
589
- const command = commandLookup.get(keyword.toLowerCase());
590
- if (!command) {
591
- console.log(chalk.yellow(`Unknown command: /${keyword}`));
592
- console.log(chalk.gray(`Type / to see available commands.`));
593
- return "continue";
594
- }
595
- return await command.run(rawArgs);
596
- };
597
- // Load session summary without creating conversation yet
598
- const summarySpinner = ora("Loading session info...").start();
599
- // Auto-select last used project for this user, else default
600
- try {
601
- const me = await http.get("/api/v1/auth/me");
602
- lastKnownUserKey = me.data?.email || String(me.data?.user_id || "");
603
- if (!projectId && lastKnownUserKey) {
604
- const remembered = getUserLastProject(lastKnownUserKey);
605
- if (remembered) {
606
- projectId = remembered;
607
- }
608
- }
609
- // Fetch user's projects and pick the default if present; otherwise the most recent
610
- const projectsResp = await http.get("/api/v1/projects");
611
- const projectsRaw = projectsResp.data;
612
- const projects = Array.isArray(projectsRaw?.items)
613
- ? projectsRaw.items
614
- : Array.isArray(projectsRaw)
615
- ? projectsRaw
616
- : [];
617
- if (!projectId && projects && projects.length > 0) {
618
- const defaultProj = projects.find((p) => p?.is_default === true);
619
- const selected = defaultProj || projects[0];
620
- if (selected?.id) {
621
- projectId = selected.id;
622
- lastKnownProject = selected.name || `Project ${projectId}`;
623
- }
624
- }
625
- // If we loaded a remembered project, populate the name proactively
626
- if (projectId &&
627
- (!lastKnownProject || lastKnownProject === "Not set")) {
628
- try {
629
- const p = await http.get(`/api/v1/projects/${projectId}`);
630
- lastKnownProject = p.data?.name || `Project ${projectId}`;
631
- }
632
- catch { }
633
- }
634
- }
635
- catch (_) {
636
- // non-fatal; continue without preselecting
637
- }
638
- await renderSessionSummary();
639
- summarySpinner.stop();
640
- printStatusBanner("Assistant ready. Type /help to see available commands.", "success");
641
- const attachSocketHandlers = (socketInstance) => {
642
- registerUnifiedEventLogging(socketInstance, (line) => console.log(line));
643
- socketInstance.onAny?.((eventType, data) => {
644
- const t = canonicalizeEventType(eventType) || eventType;
645
- if (userCancelled)
646
- return;
647
- if (t === "thinking_started") {
648
- if (spinner)
649
- spinner.text = chalk.cyan("🤔 Thinking...");
650
- }
651
- else if (t === "thinking_stopped") {
652
- if (spinner && spinner.text?.includes("Thinking"))
653
- spinner.text = "Waiting for assistant...";
654
- }
655
- else if (t === "agent_thought") {
656
- const msg = data?.data?.message || data?.data?.content || "Thinking...";
657
- if (spinner)
658
- spinner.text = chalk.cyan(`💭 ${msg}`);
659
- }
660
- else if (t === "tool_screenshot") {
661
- const url = data?.data?.image_url || data?.data?.screenshot?.image_url;
662
- if (url)
663
- console.log(chalk.gray(`🖼️ Live screenshot: ${url}`));
664
- }
665
- else if (t === "tool_start") {
666
- const message = data.data?.message || `Running ${data.data?.tool || "tool"}...`;
667
- if (spinner)
668
- spinner.text = chalk.blue(`⚙️ ${message}`);
669
- }
670
- else if (t === "tool_result") {
671
- // Tool completion intentionally silent to reduce noise.
672
- }
673
- else if (t === "session_completed") {
674
- if (spinner) {
675
- spinner.stop();
676
- spinner = null;
677
- }
678
- console.log();
679
- printDivider("Assistant");
680
- const summary = data.data?.summary || "No response";
681
- console.log(formatResponseText(summary));
682
- printDivider();
683
- console.log();
684
- }
685
- else if (t === "session_error") {
686
- if (spinner) {
687
- spinner.fail("Something went wrong");
688
- spinner = null;
689
- }
690
- printDivider("Assistant Error");
691
- console.log(chalk.red(`❌ ${data.data?.error || "An unexpected issue occurred"}`));
692
- printDivider();
693
- console.log();
694
- }
695
- });
696
- };
697
- const establishSocket = (conversation) => {
698
- const newSocket = connectForAsk();
699
- attachSocketHandlers(newSocket);
700
- subscribeAsk(newSocket, conversation);
701
- return newSocket;
702
- };
703
- if (conversationId) {
704
- socket = establishSocket(conversationId);
705
- }
706
- // Main conversation loop
707
- const workspacePath = process.cwd();
708
- const homeDir = os.homedir();
709
- const displayPath = workspacePath.startsWith(homeDir)
710
- ? `~${workspacePath.slice(homeDir.length)}`
711
- : workspacePath;
712
- while (true) {
713
- // Show input box for user message
714
- const statusLine = formatStatusSegments([
715
- { label: "path", value: displayPath, color: accentText },
716
- { label: "project", value: lastKnownProject, color: highlightText },
717
- ]);
718
- // Removed usage line per request
719
- // Removed extra hint to keep UI clean
720
- const getSlashSuggestions = (input) => {
721
- const q = (input || "").replace(/^\//, "").toLowerCase();
722
- const priority = {
723
- test: 0,
724
- switch: 1,
725
- status: 2,
726
- };
727
- const items = slashCommands
728
- .map((c) => ({
729
- label: `/${c.name}`,
730
- value: c.name,
731
- description: c.description,
732
- p: priority[c.name] ?? 100,
733
- }))
734
- .sort((a, b) => a.p - b.p || a.value.localeCompare(b.value));
735
- if (!q)
736
- return items;
737
- return items
738
- .filter((i) => i.value.toLowerCase().startsWith(q) ||
739
- i.label.toLowerCase().includes(q))
740
- .sort((a, b) => a.p - b.p || a.value.localeCompare(b.value));
741
- };
742
- const { value, aborted } = await promptInBox({
743
- placeholder: "Type your message or @path/to/file",
744
- footerLines: [
745
- statusLine,
746
- chalk.gray("Press ESC to stop the assistant"),
747
- ],
748
- fullWidth: true,
749
- getSuggestions: getSlashSuggestions,
750
- renderSubmitted: (submitted) => {
751
- try {
752
- printUserBubble(submitted);
753
- }
754
- catch { }
755
- },
756
- });
757
- if (aborted) {
758
- console.log(chalk.gray("Session cancelled."));
759
- break;
760
- }
761
- const userInput = value.trim();
762
- const normalizedInput = userInput.toLowerCase();
763
- if (!userInput) {
764
- console.log(chalk.yellow("Please enter a message or type /exit to leave."));
765
- continue;
766
- }
767
- // printUserBubble(userInput);
768
- if (normalizedInput === "exit" || normalizedInput === "quit") {
769
- console.log(chalk.gray("Goodbye! 👋"));
770
- break;
771
- }
772
- const legacyCommand = {
773
- help: "help",
774
- commands: "help",
775
- "?": "help",
776
- summary: "summary",
777
- status: "summary",
778
- switch: "switch",
779
- clear: "clear",
780
- cls: "clear",
781
- }[normalizedInput];
782
- if (legacyCommand) {
783
- console.log(chalk.gray("Tip: commands work best with '/'. Running it for you now..."));
784
- const outcome = await executeSlashCommand(legacyCommand);
785
- if (outcome === "exit") {
786
- console.log(chalk.gray("Goodbye! 👋"));
787
- break;
788
- }
789
- continue;
790
- }
791
- if (userInput.startsWith("/")) {
792
- const outcome = await executeSlashCommand(userInput.slice(1));
793
- if (outcome === "exit") {
794
- console.log(chalk.gray("Goodbye! 👋"));
795
- break;
796
- }
797
- continue;
798
- }
799
- userCancelled = false;
800
- // Create conversation and establish socket on first message
801
- if (!conversationId) {
802
- spinner = ora("Creating conversation...").start();
803
- try {
804
- const sessionResp = await http.post("/api/v1/ask/sessions", {
805
- question: userInput,
806
- project_id: projectId,
807
- surface: "cli",
808
- });
809
- conversationId = sessionResp.data.conversation_id;
810
- projectId = sessionResp.data.project_id;
811
- if (lastKnownUserKey && typeof projectId === "number") {
812
- setUserLastProject(lastKnownUserKey, projectId);
813
- }
814
- // Establish socket connection
815
- socket = establishSocket(conversationId);
816
- spinner.text = "Waiting for assistant...";
817
- }
818
- catch (e) {
819
- const status = e?.response?.status;
820
- if (status === 401) {
821
- spinner.text = "Session expired. Re-authenticating...";
822
- await performInteractiveLogin();
823
- http = createHttpClient();
824
- spinner.text = "Creating conversation...";
825
- const sessionResp = await http.post("/api/v1/ask/sessions", {
826
- question: userInput,
827
- project_id: projectId,
828
- surface: "cli",
829
- });
830
- conversationId = sessionResp.data.conversation_id;
831
- projectId = sessionResp.data.project_id;
832
- if (lastKnownUserKey && typeof projectId === "number") {
833
- setUserLastProject(lastKnownUserKey, projectId);
834
- }
835
- // Establish socket connection
836
- socket = establishSocket(conversationId);
837
- spinner.text = "Waiting for assistant...";
838
- }
839
- else {
840
- spinner.fail("Unable to create conversation");
841
- throw e;
842
- }
843
- }
844
- }
845
- else {
846
- // Send question to existing conversation
847
- spinner = ora("Sending...").start();
848
- try {
849
- await http.post(`/api/v1/ask/sessions/${conversationId}/message`, {
850
- message: userInput,
851
- project_id: projectId,
852
- surface: "cli",
853
- });
854
- spinner.text = "Waiting for assistant...";
855
- }
856
- catch (e) {
857
- const status = e?.response?.status;
858
- if (status === 401) {
859
- spinner.text = "Session expired. Re-authenticating...";
860
- await performInteractiveLogin();
861
- http = createHttpClient();
862
- spinner.text = "Sending...";
863
- await http.post(`/api/v1/ask/sessions/${conversationId}/message`, {
864
- message: userInput,
865
- project_id: projectId,
866
- surface: "cli",
867
- });
868
- spinner.text = "Waiting for assistant...";
869
- }
870
- else {
871
- throw e;
872
- }
873
- }
874
- }
875
- // Create a promise that resolves when agent responds
876
- const waitForResponse = new Promise((resolve) => {
877
- const responseHandler = (data) => {
878
- // Remove the listener once we get a response
879
- socket.off("session_completed", responseHandler);
880
- socket.off("session_error", errorHandler);
881
- cleanupEsc();
882
- resolve();
883
- };
884
- const errorHandler = (data) => {
885
- // Remove the listener once we get an error
886
- socket.off("session_completed", responseHandler);
887
- socket.off("session_error", errorHandler);
888
- cleanupEsc();
889
- resolve();
890
- };
891
- // Listen for agent response
892
- socket.on("session_completed", responseHandler);
893
- socket.on("session_error", errorHandler);
894
- // Also allow ESC to stop the ask session
895
- let escArmed = true;
896
- const onKeypress = (_str, key) => {
897
- try {
898
- if (!escArmed)
899
- return;
900
- if (key?.name === "escape") {
901
- escArmed = false;
902
- sendAskStop();
903
- }
904
- }
905
- catch { }
906
- };
907
- const cleanupEsc = () => {
908
- try {
909
- process.stdin.removeListener("keypress", onKeypress);
910
- }
911
- catch { }
912
- };
913
- try {
914
- import("node:readline")
915
- .then((readline) => {
916
- try {
917
- readline.emitKeypressEvents(process.stdin);
918
- if (process.stdin.isTTY)
919
- process.stdin.setRawMode(true);
920
- process.stdin.on("keypress", onKeypress);
921
- }
922
- catch { }
923
- })
924
- .catch(() => { });
925
- }
926
- catch { }
927
- });
928
- let messageDelivered = false;
929
- try {
930
- // Wait for the agent response before continuing
931
- await waitForResponse;
932
- messageDelivered = true;
933
- }
934
- catch (error) {
935
- if (spinner) {
936
- spinner.fail();
937
- spinner = null;
938
- }
939
- const status = error?.response?.status;
940
- if (status === 401) {
941
- console.log(chalk.yellow("Session expired. Re-authenticating…"));
942
- try {
943
- await performInteractiveLogin();
944
- }
945
- catch (loginError) {
946
- console.log(chalk.red(loginError?.message || "Re-authentication failed."));
947
- break;
948
- }
949
- http = createHttpClient();
950
- spinner = ora("Sending...").start();
951
- try {
952
- await http.post(`/api/v1/ask/sessions/${conversationId}/message`, {
953
- message: userInput,
954
- project_id: projectId,
955
- surface: "cli",
956
- });
957
- spinner.text = "Waiting for assistant...";
958
- // Wait for response even after re-auth
959
- await waitForResponse;
960
- messageDelivered = true;
961
- }
962
- catch (err2) {
963
- if (spinner) {
964
- spinner.fail();
965
- spinner = null;
966
- }
967
- console.log(chalk.red(`❌ Error sending message: ${err2?.message || err2}`));
968
- }
969
- }
970
- else {
971
- console.log(chalk.red(`❌ Error sending message: ${error?.message || error}`));
972
- }
973
- }
974
- if (messageDelivered) {
975
- messageCount += 1;
976
- }
977
- }
978
- // Clean up
979
- if (spinner) {
980
- spinner.stop();
981
- }
982
- if (socket) {
983
- await socket.disconnect();
984
- }
985
- }
986
- catch (error) {
987
- console.error(chalk.red(`❌ Error: ${error.message}`));
988
- process.exit(1);
989
- }
990
- });
991
- //# sourceMappingURL=ask.js.map