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