@supatest/cli 0.0.4 → 0.0.5

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 (69) hide show
  1. package/dist/commands/login.js +392 -0
  2. package/dist/commands/setup.js +234 -0
  3. package/dist/config.js +29 -0
  4. package/dist/core/agent.js +259 -0
  5. package/dist/index.js +154 -6586
  6. package/dist/modes/headless.js +117 -0
  7. package/dist/modes/interactive.js +418 -0
  8. package/dist/presenters/composite.js +32 -0
  9. package/dist/presenters/console.js +163 -0
  10. package/dist/presenters/react.js +217 -0
  11. package/dist/presenters/types.js +1 -0
  12. package/dist/presenters/web.js +78 -0
  13. package/dist/prompts/builder.js +181 -0
  14. package/dist/prompts/fixer.js +148 -0
  15. package/dist/prompts/index.js +3 -0
  16. package/dist/prompts/planner.js +70 -0
  17. package/dist/services/api-client.js +244 -0
  18. package/dist/services/event-streamer.js +130 -0
  19. package/dist/types.js +1 -0
  20. package/dist/ui/App.js +322 -0
  21. package/dist/ui/components/AuthBanner.js +24 -0
  22. package/dist/ui/components/AuthDialog.js +32 -0
  23. package/dist/ui/components/Banner.js +12 -0
  24. package/dist/ui/components/ExpandableSection.js +17 -0
  25. package/dist/ui/components/Header.js +51 -0
  26. package/dist/ui/components/HelpMenu.js +89 -0
  27. package/dist/ui/components/InputPrompt.js +286 -0
  28. package/dist/ui/components/MessageList.js +42 -0
  29. package/dist/ui/components/QueuedMessageDisplay.js +31 -0
  30. package/dist/ui/components/Scrollable.js +103 -0
  31. package/dist/ui/components/SessionSelector.js +196 -0
  32. package/dist/ui/components/StatusBar.js +34 -0
  33. package/dist/ui/components/messages/AssistantMessage.js +20 -0
  34. package/dist/ui/components/messages/ErrorMessage.js +26 -0
  35. package/dist/ui/components/messages/LoadingMessage.js +28 -0
  36. package/dist/ui/components/messages/ThinkingMessage.js +17 -0
  37. package/dist/ui/components/messages/TodoMessage.js +44 -0
  38. package/dist/ui/components/messages/ToolMessage.js +218 -0
  39. package/dist/ui/components/messages/UserMessage.js +14 -0
  40. package/dist/ui/contexts/KeypressContext.js +527 -0
  41. package/dist/ui/contexts/MouseContext.js +98 -0
  42. package/dist/ui/contexts/SessionContext.js +129 -0
  43. package/dist/ui/hooks/useAnimatedScrollbar.js +83 -0
  44. package/dist/ui/hooks/useBatchedScroll.js +22 -0
  45. package/dist/ui/hooks/useBracketedPaste.js +31 -0
  46. package/dist/ui/hooks/useFocus.js +50 -0
  47. package/dist/ui/hooks/useKeypress.js +26 -0
  48. package/dist/ui/hooks/useModeToggle.js +25 -0
  49. package/dist/ui/types/auth.js +13 -0
  50. package/dist/ui/utils/file-completion.js +56 -0
  51. package/dist/ui/utils/input.js +50 -0
  52. package/dist/ui/utils/markdown.js +376 -0
  53. package/dist/ui/utils/mouse.js +189 -0
  54. package/dist/ui/utils/theme.js +59 -0
  55. package/dist/utils/banner.js +9 -0
  56. package/dist/utils/encryption.js +71 -0
  57. package/dist/utils/events.js +36 -0
  58. package/dist/utils/keychain-storage.js +120 -0
  59. package/dist/utils/logger.js +209 -0
  60. package/dist/utils/node-version.js +89 -0
  61. package/dist/utils/plan-file.js +75 -0
  62. package/dist/utils/project-instructions.js +23 -0
  63. package/dist/utils/rich-logger.js +208 -0
  64. package/dist/utils/stdin.js +25 -0
  65. package/dist/utils/stdio.js +80 -0
  66. package/dist/utils/summary.js +94 -0
  67. package/dist/utils/token-storage.js +242 -0
  68. package/dist/version.js +6 -0
  69. package/package.json +3 -4
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Status Bar Component
3
+ * Displays session stats, web URL, and agent status
4
+ */
5
+ import { Box, Text } from "ink";
6
+ import React from "react";
7
+ import { useSession } from "../contexts/SessionContext.js";
8
+ import { theme } from "../utils/theme.js";
9
+ export const StatusBar = () => {
10
+ const { stats, webUrl, isAgentRunning } = useSession();
11
+ const elapsedSeconds = Math.floor((Date.now() - stats.startTime) / 1000);
12
+ const minutes = Math.floor(elapsedSeconds / 60);
13
+ const seconds = elapsedSeconds % 60;
14
+ const timeStr = minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
15
+ return (React.createElement(Box, { borderColor: theme.border.default, borderStyle: "round", flexDirection: "column", paddingX: 1 },
16
+ React.createElement(Box, { flexDirection: "row", justifyContent: "space-between" },
17
+ React.createElement(Box, null,
18
+ React.createElement(Text, { color: theme.text.dim },
19
+ "Files: ",
20
+ React.createElement(Text, { color: theme.text.accent }, stats.filesModified.size)),
21
+ React.createElement(Text, { color: theme.text.dim }, " \u2022 "),
22
+ React.createElement(Text, { color: theme.text.dim },
23
+ "Commands: ",
24
+ React.createElement(Text, { color: theme.text.accent }, stats.commandsRun.length)),
25
+ React.createElement(Text, { color: theme.text.dim }, " \u2022 "),
26
+ React.createElement(Text, { color: theme.text.dim },
27
+ "Time: ",
28
+ React.createElement(Text, { color: theme.text.accent }, timeStr))),
29
+ React.createElement(Box, null, isAgentRunning ? (React.createElement(Text, { color: theme.status.inProgress }, "\u25CF Running")) : (React.createElement(Text, { color: theme.status.completed }, "\u25CF Ready")))),
30
+ webUrl && (React.createElement(Box, { marginTop: 0 },
31
+ React.createElement(Text, { color: theme.text.dim },
32
+ "Web: ",
33
+ React.createElement(Text, { color: theme.text.info, underline: true }, webUrl))))));
34
+ };
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Assistant message component
3
+ * Displays AI responses with markdown rendering
4
+ */
5
+ import { Box, Text } from "ink";
6
+ import Spinner from "ink-spinner";
7
+ import React from "react";
8
+ import { MarkdownDisplay } from "../../utils/markdown.js";
9
+ import { theme } from "../../utils/theme.js";
10
+ export const AssistantMessage = ({ text, isPending = false, terminalWidth = 80, }) => {
11
+ // Use "dots" spinner to match the screenshot look (animating dots)
12
+ // When pending, show the spinner. When done, show the static accent icon.
13
+ const prefix = isPending ? React.createElement(Spinner, { type: "dots" }) : "✦ ";
14
+ const prefixWidth = 2;
15
+ return (React.createElement(Box, { flexDirection: "row", marginTop: 1 },
16
+ React.createElement(Box, { width: prefixWidth },
17
+ React.createElement(Text, { color: isPending ? theme.text.dim : theme.text.accent }, prefix)),
18
+ React.createElement(Box, { flexDirection: "column", flexGrow: 1 },
19
+ React.createElement(MarkdownDisplay, { isPending: isPending, terminalWidth: terminalWidth - prefixWidth, text: text }))));
20
+ };
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Error message component
3
+ * Displays errors and warnings
4
+ */
5
+ import { Box, Text } from "ink";
6
+ import React from "react";
7
+ import { theme } from "../../utils/theme.js";
8
+ export const ErrorMessage = ({ message, type = "error", }) => {
9
+ const { icon, color } = getErrorStyle(type);
10
+ return (React.createElement(Box, { flexDirection: "row", marginTop: 1 },
11
+ React.createElement(Text, { color: color },
12
+ icon,
13
+ " ",
14
+ message)));
15
+ };
16
+ /**
17
+ * Get icon and color based on error type
18
+ */
19
+ function getErrorStyle(type) {
20
+ const styles = {
21
+ error: { icon: "✗", color: theme.text.error },
22
+ warning: { icon: "⚠", color: theme.text.warning },
23
+ info: { icon: "ℹ", color: theme.text.info },
24
+ };
25
+ return styles[type];
26
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Loading message component
3
+ * Displays a spinner with static text while waiting for assistant response
4
+ * Note: We avoid text rotation to prevent layout shifts and flickering
5
+ */
6
+ import { Box, Text } from "ink";
7
+ import Spinner from "ink-spinner";
8
+ import React, { useMemo } from "react";
9
+ import { theme } from "../../utils/theme.js";
10
+ // Fun loading messages - pick one randomly on mount (no rotation to avoid flicker)
11
+ const LOADING_MESSAGES = [
12
+ "Thinking...",
13
+ "Working...",
14
+ "Processing...",
15
+ ];
16
+ export const LoadingMessage = () => {
17
+ // Pick a random message once on mount - no rotation to prevent layout shifts
18
+ const message = useMemo(() => LOADING_MESSAGES[Math.floor(Math.random() * LOADING_MESSAGES.length)], []);
19
+ return (React.createElement(Box, { flexDirection: "row", marginTop: 1 },
20
+ React.createElement(Box, { width: 2 },
21
+ React.createElement(Text, { color: theme.text.accent },
22
+ React.createElement(Spinner, { type: "dots" }))),
23
+ React.createElement(Box, { flexDirection: "column", flexGrow: 1 },
24
+ React.createElement(Text, { color: theme.text.dim },
25
+ message,
26
+ " ",
27
+ React.createElement(Text, { color: theme.text.dim }, "(esc to interrupt)")))));
28
+ };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Thinking message component
3
+ * Displays Claude's thinking process (collapsible by default)
4
+ */
5
+ import { Box, Text } from "ink";
6
+ import React from "react";
7
+ import { theme } from "../../utils/theme.js";
8
+ export const ThinkingMessage = ({ id, content, isExpanded = false, onToggle, }) => {
9
+ return (React.createElement(Box, { flexDirection: "column", marginTop: 1 },
10
+ React.createElement(Box, { flexDirection: "row" },
11
+ React.createElement(Text, { color: theme.text.dim }, "\u25CF Thinking"),
12
+ content && (React.createElement(Text, { color: theme.text.dim },
13
+ " ",
14
+ isExpanded ? "▼" : "▶"))),
15
+ isExpanded && content && (React.createElement(Box, { flexDirection: "column", marginLeft: 2 },
16
+ React.createElement(Text, { color: theme.text.dim }, content)))));
17
+ };
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Todo message component
3
+ * Displays todo list with progress
4
+ */
5
+ import { Box, Text } from "ink";
6
+ import React from "react";
7
+ import { theme } from "../../utils/theme.js";
8
+ export const TodoMessage = ({ todos }) => {
9
+ const completed = todos.filter((t) => t.status === "completed");
10
+ const inProgress = todos.filter((t) => t.status === "in_progress");
11
+ const pending = todos.filter((t) => t.status === "pending");
12
+ const total = todos.length;
13
+ const completedCount = completed.length;
14
+ const progress = total > 0 ? Math.round((completedCount / total) * 100) : 0;
15
+ // Progress bar
16
+ const barLength = 20;
17
+ const filledLength = Math.round((barLength * completedCount) / total);
18
+ const bar = "█".repeat(filledLength) + "░".repeat(barLength - filledLength);
19
+ return (React.createElement(Box, { flexDirection: "column", marginY: 0 },
20
+ React.createElement(Box, { flexDirection: "row" },
21
+ React.createElement(Text, { color: theme.text.info }, "\uD83D\uDCDD "),
22
+ React.createElement(Text, { color: theme.text.dim }, "Todo Progress: "),
23
+ React.createElement(Text, { color: theme.text.accent }, bar),
24
+ React.createElement(Text, { color: theme.text.primary },
25
+ " ",
26
+ progress,
27
+ "%"),
28
+ React.createElement(Text, { color: theme.text.dim },
29
+ " ",
30
+ "(",
31
+ completedCount,
32
+ "/",
33
+ total,
34
+ ")")),
35
+ inProgress.length > 0 && (React.createElement(Box, { flexDirection: "column", marginLeft: 2 }, inProgress.map((todo, idx) => (React.createElement(Box, { flexDirection: "row", key: `in-progress-${idx}` },
36
+ React.createElement(Text, { color: theme.status.inProgress }, "\u2192 "),
37
+ React.createElement(Text, { color: theme.text.primary }, todo.activeForm || todo.content)))))),
38
+ completed.length > 0 && completed.length <= 2 && (React.createElement(Box, { flexDirection: "column", marginLeft: 2 }, completed.map((todo, idx) => (React.createElement(Box, { flexDirection: "row", key: `completed-${idx}` },
39
+ React.createElement(Text, { color: theme.status.completed }, "\u2713 "),
40
+ React.createElement(Text, { color: theme.text.dim }, todo.content)))))),
41
+ pending.length > 0 && pending.length <= 3 && (React.createElement(Box, { flexDirection: "column", marginLeft: 2 }, pending.map((todo, idx) => (React.createElement(Box, { flexDirection: "row", key: `pending-${idx}` },
42
+ React.createElement(Text, { color: theme.status.pending }, "\u23F3 "),
43
+ React.createElement(Text, { color: theme.text.secondary }, todo.content))))))));
44
+ };
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Tool message component
3
+ * Displays tool calls (Read, Write, Edit, Bash, etc.)
4
+ */
5
+ import { structuredPatch } from "diff";
6
+ import { Box, Text } from "ink";
7
+ import React from "react";
8
+ import { getToolDisplayName } from "shared";
9
+ import { theme } from "../../utils/theme.js";
10
+ export const ToolMessage = ({ toolName, description, input, result, isExpanded = true, }) => {
11
+ const displayName = getToolDisplayName(toolName);
12
+ const { icon, color } = getToolStyle(toolName);
13
+ const resultSummary = result ? getResultSummary(toolName, result) : null;
14
+ const hasExpandableContent = !!result;
15
+ const isRunning = !result;
16
+ // Get the actual command/content to display
17
+ const commandDisplay = getCommandDisplay(toolName, input);
18
+ // Check if this is an Edit operation - show diff by default
19
+ const isEditOperation = toolName === "Edit";
20
+ const editDiff = isEditOperation ? getEditDiff(input) : null;
21
+ // Tools that support expand/collapse (Bash has expandable output)
22
+ const isExpandableTool = toolName === "Bash" || toolName === "BashOutput" || toolName === "Command Output";
23
+ return (React.createElement(Box, { flexDirection: "column", marginTop: 1 },
24
+ React.createElement(Box, { flexDirection: "row" },
25
+ hasExpandableContent && isExpandableTool ? (React.createElement(Text, { color: theme.text.dim }, isExpanded ? "▼ " : "▶ ")) : (React.createElement(Text, { color: isRunning ? "yellow" : color }, "\u25CF ")),
26
+ React.createElement(Text, { bold: true, color: color }, displayName),
27
+ React.createElement(Text, { color: theme.text.dim },
28
+ "(",
29
+ description,
30
+ ")"),
31
+ editDiff && (React.createElement(Text, { color: theme.text.dim },
32
+ " with ",
33
+ editDiff.additions,
34
+ " addition",
35
+ editDiff.additions !== 1 ? "s" : "",
36
+ " and ",
37
+ editDiff.removals,
38
+ " removal",
39
+ editDiff.removals !== 1 ? "s" : "")),
40
+ hasExpandableContent && !isExpanded && isExpandableTool && (React.createElement(Text, { color: theme.text.dim }, " (ctrl+o to expand)"))),
41
+ !isEditOperation && resultSummary && (React.createElement(Box, { marginLeft: 2 },
42
+ React.createElement(Text, { color: theme.text.dim },
43
+ "\u2514 ",
44
+ resultSummary))),
45
+ editDiff && (React.createElement(Box, { flexDirection: "column", marginLeft: 2 }, editDiff.lines.map((line, i) => {
46
+ if (line.type === "separator") {
47
+ return (React.createElement(Box, { key: i, flexDirection: "row" },
48
+ React.createElement(Text, { color: theme.text.dim }, "...")));
49
+ }
50
+ const isRemove = line.type === "remove";
51
+ const isAdd = line.type === "add";
52
+ const bgColor = isRemove ? "#5c1b1b" : isAdd ? "#1b3d1b" : undefined;
53
+ const prefix = isRemove ? "- " : isAdd ? "+ " : " ";
54
+ const prefixColor = isRemove ? "red" : isAdd ? "green" : theme.text.dim;
55
+ return (React.createElement(Box, { key: i, flexDirection: "row" },
56
+ React.createElement(Text, { color: theme.text.dim }, line.lineNum.toString().padStart(4, " ")),
57
+ React.createElement(Text, { color: prefixColor },
58
+ " ",
59
+ prefix),
60
+ React.createElement(Text, { backgroundColor: bgColor }, line.content)));
61
+ }))),
62
+ isExpanded && result && isExpandableTool && (React.createElement(Box, { flexDirection: "column", marginLeft: 4, marginTop: 1 },
63
+ React.createElement(Text, { color: theme.text.secondary }, truncate(result, 2000))))));
64
+ };
65
+ /**
66
+ * Generate diff display for Edit operations using jsdiff
67
+ * Shows context lines before/after changes like Claude Code
68
+ */
69
+ function getEditDiff(input) {
70
+ if (!input?.old_string || !input?.new_string)
71
+ return null;
72
+ // Use jsdiff to compute structured patch with 3 lines of context
73
+ const patch = structuredPatch("file", "file", input.old_string, input.new_string, "", "", { context: 3 });
74
+ const lines = [];
75
+ let additions = 0;
76
+ let removals = 0;
77
+ // Process each hunk
78
+ for (let hunkIndex = 0; hunkIndex < patch.hunks.length; hunkIndex++) {
79
+ const hunk = patch.hunks[hunkIndex];
80
+ // Add separator between hunks (except for first)
81
+ if (hunkIndex > 0) {
82
+ lines.push({
83
+ lineNum: "...",
84
+ type: "separator",
85
+ content: "",
86
+ });
87
+ }
88
+ let oldLineNum = hunk.oldStart;
89
+ let newLineNum = hunk.newStart;
90
+ for (const line of hunk.lines) {
91
+ const content = line.substring(1); // Remove the +/- prefix
92
+ if (line.startsWith("+")) {
93
+ lines.push({
94
+ lineNum: newLineNum,
95
+ type: "add",
96
+ content,
97
+ });
98
+ newLineNum++;
99
+ additions++;
100
+ }
101
+ else if (line.startsWith("-")) {
102
+ lines.push({
103
+ lineNum: oldLineNum,
104
+ type: "remove",
105
+ content,
106
+ });
107
+ oldLineNum++;
108
+ removals++;
109
+ }
110
+ else {
111
+ // Context line (starts with space)
112
+ lines.push({
113
+ lineNum: newLineNum,
114
+ type: "context",
115
+ content,
116
+ });
117
+ oldLineNum++;
118
+ newLineNum++;
119
+ }
120
+ }
121
+ }
122
+ return { lines, additions, removals };
123
+ }
124
+ /**
125
+ * Get command/content to display based on tool type
126
+ */
127
+ function getCommandDisplay(toolName, input) {
128
+ if (!input)
129
+ return null;
130
+ switch (toolName) {
131
+ case "Bash":
132
+ case "BashOutput":
133
+ case "Command Output": {
134
+ const command = input.command || input.bash_id;
135
+ if (!command)
136
+ return null;
137
+ // Split long commands into multiple lines
138
+ return [command];
139
+ }
140
+ case "Read":
141
+ case "Write":
142
+ case "Edit":
143
+ return input.file_path ? [input.file_path] : null;
144
+ case "Grep":
145
+ return input.pattern ? [`"${input.pattern}"${input.path ? ` in ${input.path}` : ""}`] : null;
146
+ case "Glob":
147
+ return input.pattern ? [input.pattern] : null;
148
+ default:
149
+ return null;
150
+ }
151
+ }
152
+ /**
153
+ * Get icon and color for tool based on name
154
+ */
155
+ function getToolStyle(toolName) {
156
+ const styles = {
157
+ Read: { icon: "📖", color: theme.tool.read },
158
+ Write: { icon: "✏️", color: theme.tool.write },
159
+ Edit: { icon: "✏️", color: theme.tool.edit },
160
+ Bash: { icon: "🔨", color: theme.tool.bash },
161
+ BashOutput: { icon: "📄", color: theme.tool.bash },
162
+ "Command Output": { icon: "📄", color: theme.tool.bash },
163
+ Glob: { icon: "🔍", color: theme.tool.search },
164
+ Grep: { icon: "🔍", color: theme.tool.search },
165
+ Task: { icon: "🤖", color: theme.tool.agent },
166
+ TodoWrite: { icon: "📝", color: theme.text.info },
167
+ };
168
+ return styles[toolName] || { icon: "🔧", color: theme.text.secondary };
169
+ }
170
+ /**
171
+ * Truncate string to max length
172
+ */
173
+ function truncate(str, maxLength) {
174
+ if (str.length <= maxLength) {
175
+ return str;
176
+ }
177
+ return str.slice(0, maxLength - 3) + "...";
178
+ }
179
+ /**
180
+ * Get result summary for tool output
181
+ */
182
+ function getResultSummary(toolName, result) {
183
+ switch (toolName) {
184
+ case "Glob": {
185
+ // Count lines that look like file paths
186
+ const lines = result.split("\n").filter((line) => line.trim());
187
+ const count = lines.filter((line) => !line.startsWith("Found")).length;
188
+ return count > 0 ? `Found ${count} files` : "No files found";
189
+ }
190
+ case "Grep": {
191
+ // Count lines of output
192
+ const lines = result.split("\n").filter((line) => line.trim());
193
+ return lines.length > 0
194
+ ? `${lines.length} lines of output`
195
+ : "No matches found";
196
+ }
197
+ case "Read": {
198
+ // Count lines
199
+ const lines = result.split("\n").length;
200
+ return `Read ${lines} lines`;
201
+ }
202
+ case "Bash": {
203
+ // Show if there was output
204
+ const hasOutput = result.trim().length > 0;
205
+ return hasOutput ? `${result.split("\n").length} lines of output` : null;
206
+ }
207
+ case "BashOutput":
208
+ case "Command Output": {
209
+ // Show lines of shell output
210
+ const lines = result.split("\n").filter((line) => line.trim());
211
+ return lines.length > 0
212
+ ? `${lines.length} lines of output`
213
+ : "No output";
214
+ }
215
+ default:
216
+ return null;
217
+ }
218
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * User message component
3
+ * Displays user input/prompts
4
+ */
5
+ import { Box, Text } from "ink";
6
+ import React from "react";
7
+ import { theme } from "../../utils/theme.js";
8
+ export const UserMessage = ({ text }) => {
9
+ return (React.createElement(Box, { flexDirection: "row", marginTop: 1 },
10
+ React.createElement(Box, { width: 2 },
11
+ React.createElement(Text, { color: theme.text.info }, "\u276F ")),
12
+ React.createElement(Box, { flexGrow: 1 },
13
+ React.createElement(Text, { color: theme.text.primary }, text))));
14
+ };