toolpack-cli 0.1.0-SNAPSHOT

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 (99) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +131 -0
  3. package/dist/app.d.ts +1 -0
  4. package/dist/app.js +15 -0
  5. package/dist/cli.d.ts +2 -0
  6. package/dist/cli.js +29 -0
  7. package/dist/commands/clear.d.ts +3 -0
  8. package/dist/commands/clear.js +15 -0
  9. package/dist/commands/help.d.ts +3 -0
  10. package/dist/commands/help.js +29 -0
  11. package/dist/commands/index.d.ts +15 -0
  12. package/dist/commands/index.js +16 -0
  13. package/dist/commands/info.d.ts +3 -0
  14. package/dist/commands/info.js +24 -0
  15. package/dist/commands/mode.d.ts +3 -0
  16. package/dist/commands/mode.js +51 -0
  17. package/dist/commands/model.d.ts +3 -0
  18. package/dist/commands/model.js +14 -0
  19. package/dist/commands/registry.d.ts +32 -0
  20. package/dist/commands/registry.js +86 -0
  21. package/dist/commands/tool-log.d.ts +3 -0
  22. package/dist/commands/tool-log.js +17 -0
  23. package/dist/commands/tool-search.d.ts +3 -0
  24. package/dist/commands/tool-search.js +57 -0
  25. package/dist/commands/tools.d.ts +3 -0
  26. package/dist/commands/tools.js +45 -0
  27. package/dist/commands/types.d.ts +25 -0
  28. package/dist/commands/types.js +4 -0
  29. package/dist/commands/version.d.ts +3 -0
  30. package/dist/commands/version.js +25 -0
  31. package/dist/components/AppInfo.d.ts +1 -0
  32. package/dist/components/AppInfo.js +10 -0
  33. package/dist/components/HomeInput.d.ts +11 -0
  34. package/dist/components/HomeInput.js +328 -0
  35. package/dist/components/Logo.d.ts +1 -0
  36. package/dist/components/Logo.js +15 -0
  37. package/dist/components/Markdown.d.ts +5 -0
  38. package/dist/components/Markdown.js +121 -0
  39. package/dist/components/ProviderBar.d.ts +12 -0
  40. package/dist/components/ProviderBar.js +32 -0
  41. package/dist/components/ShimmerText.d.ts +8 -0
  42. package/dist/components/ShimmerText.js +20 -0
  43. package/dist/components/ToolLogPopup.d.ts +7 -0
  44. package/dist/components/ToolLogPopup.js +87 -0
  45. package/dist/components/common/HistorySelect.d.ts +6 -0
  46. package/dist/components/common/HistorySelect.js +57 -0
  47. package/dist/components/common/Modal.d.ts +10 -0
  48. package/dist/components/common/Modal.js +13 -0
  49. package/dist/components/common/ModeSelect.d.ts +6 -0
  50. package/dist/components/common/ModeSelect.js +13 -0
  51. package/dist/components/common/ModelSelect.d.ts +9 -0
  52. package/dist/components/common/ModelSelect.js +45 -0
  53. package/dist/context/ConversationContext.d.ts +44 -0
  54. package/dist/context/ConversationContext.js +113 -0
  55. package/dist/context/ToolpackContext.d.ts +55 -0
  56. package/dist/context/ToolpackContext.js +221 -0
  57. package/dist/custom-providers/AnthropicCustomAdapter.d.ts +49 -0
  58. package/dist/custom-providers/AnthropicCustomAdapter.js +297 -0
  59. package/dist/custom-providers/XAIAdapter.d.ts +40 -0
  60. package/dist/custom-providers/XAIAdapter.js +295 -0
  61. package/dist/custom-tools/skill-tools/index.d.ts +33 -0
  62. package/dist/custom-tools/skill-tools/index.js +63 -0
  63. package/dist/custom-tools/skill-tools/tools/create/index.d.ts +2 -0
  64. package/dist/custom-tools/skill-tools/tools/create/index.js +93 -0
  65. package/dist/custom-tools/skill-tools/tools/create/schema.d.ts +6 -0
  66. package/dist/custom-tools/skill-tools/tools/create/schema.js +41 -0
  67. package/dist/custom-tools/skill-tools/tools/list/index.d.ts +2 -0
  68. package/dist/custom-tools/skill-tools/tools/list/index.js +113 -0
  69. package/dist/custom-tools/skill-tools/tools/list/schema.d.ts +6 -0
  70. package/dist/custom-tools/skill-tools/tools/list/schema.js +19 -0
  71. package/dist/custom-tools/skill-tools/tools/read/index.d.ts +2 -0
  72. package/dist/custom-tools/skill-tools/tools/read/index.js +124 -0
  73. package/dist/custom-tools/skill-tools/tools/read/schema.d.ts +6 -0
  74. package/dist/custom-tools/skill-tools/tools/read/schema.js +27 -0
  75. package/dist/custom-tools/skill-tools/tools/search/bm25.d.ts +71 -0
  76. package/dist/custom-tools/skill-tools/tools/search/bm25.js +305 -0
  77. package/dist/custom-tools/skill-tools/tools/search/index.d.ts +8 -0
  78. package/dist/custom-tools/skill-tools/tools/search/index.js +63 -0
  79. package/dist/custom-tools/skill-tools/tools/search/schema.d.ts +6 -0
  80. package/dist/custom-tools/skill-tools/tools/search/schema.js +19 -0
  81. package/dist/custom-tools/skill-tools/tools/search/skill-index.d.ts +54 -0
  82. package/dist/custom-tools/skill-tools/tools/search/skill-index.js +251 -0
  83. package/dist/custom-tools/skill-tools/tools/update/index.d.ts +2 -0
  84. package/dist/custom-tools/skill-tools/tools/update/index.js +115 -0
  85. package/dist/custom-tools/skill-tools/tools/update/schema.d.ts +6 -0
  86. package/dist/custom-tools/skill-tools/tools/update/schema.js +41 -0
  87. package/dist/screens/ChatScreen.d.ts +1 -0
  88. package/dist/screens/ChatScreen.js +327 -0
  89. package/dist/screens/HomeScreen.d.ts +1 -0
  90. package/dist/screens/HomeScreen.js +68 -0
  91. package/dist/screens/SettingsScreen.d.ts +1 -0
  92. package/dist/screens/SettingsScreen.js +35 -0
  93. package/dist/services/db.d.ts +31 -0
  94. package/dist/services/db.js +108 -0
  95. package/dist/theme/ThemeContext.d.ts +11 -0
  96. package/dist/theme/ThemeContext.js +31 -0
  97. package/dist/theme/theme.d.ts +17 -0
  98. package/dist/theme/theme.js +82 -0
  99. package/package.json +101 -0
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Command Registry
3
+ * Manages command registration and lookup
4
+ */
5
+ const commands = new Map();
6
+ /**
7
+ * Register a command
8
+ */
9
+ export function registerCommand(command) {
10
+ commands.set(command.name, command);
11
+ if (command.aliases) {
12
+ for (const alias of command.aliases) {
13
+ commands.set(alias, command);
14
+ }
15
+ }
16
+ }
17
+ /**
18
+ * Get a command by name
19
+ */
20
+ export function getCommand(name) {
21
+ return commands.get(name.toLowerCase());
22
+ }
23
+ /**
24
+ * Get all unique commands (excluding aliases)
25
+ */
26
+ export function getAllCommands() {
27
+ const seen = new Set();
28
+ const result = [];
29
+ for (const cmd of commands.values()) {
30
+ if (!seen.has(cmd.name)) {
31
+ seen.add(cmd.name);
32
+ result.push(cmd);
33
+ }
34
+ }
35
+ return result.sort((a, b) => a.name.localeCompare(b.name));
36
+ }
37
+ /**
38
+ * Check if input is a command
39
+ */
40
+ export function isCommand(input) {
41
+ return input.trim().startsWith('/');
42
+ }
43
+ /**
44
+ * Parse command input into name and arguments
45
+ */
46
+ export function parseCommand(input) {
47
+ const trimmed = input.trim();
48
+ if (!trimmed.startsWith('/')) {
49
+ return { name: '', args: [] };
50
+ }
51
+ const parts = trimmed.slice(1).split(/\s+/);
52
+ const name = parts[0]?.toLowerCase() || '';
53
+ const args = parts.slice(1);
54
+ return { name, args };
55
+ }
56
+ /**
57
+ * Execute a command
58
+ */
59
+ export async function executeCommand(input, context) {
60
+ const { name, args } = parseCommand(input);
61
+ if (!name) {
62
+ return {
63
+ success: false,
64
+ message: 'Invalid command. Type /help for available commands.',
65
+ action: 'display',
66
+ };
67
+ }
68
+ const command = getCommand(name);
69
+ if (!command) {
70
+ return {
71
+ success: false,
72
+ message: `Unknown command: /${name}. Type /help for available commands.`,
73
+ action: 'display',
74
+ };
75
+ }
76
+ try {
77
+ return await command.execute(args, context);
78
+ }
79
+ catch (error) {
80
+ return {
81
+ success: false,
82
+ message: `Command error: ${error.message || 'Unknown error'}`,
83
+ action: 'display',
84
+ };
85
+ }
86
+ }
@@ -0,0 +1,3 @@
1
+ import { Command } from './types.js';
2
+ declare const toolLogCommand: Command;
3
+ export default toolLogCommand;
@@ -0,0 +1,17 @@
1
+ import { registerCommand } from './registry.js';
2
+ const toolLogCommand = {
3
+ name: 'tool_log',
4
+ aliases: ['tl', 'log'],
5
+ description: 'Show tool execution log for current session',
6
+ usage: '/tool_log',
7
+ execute: async () => {
8
+ // Tool history is now managed via SDK events in ToolpackContext.
9
+ // ChatScreen will display the popup using toolHistory from context.
10
+ return {
11
+ success: true,
12
+ action: 'popup',
13
+ };
14
+ },
15
+ };
16
+ registerCommand(toolLogCommand);
17
+ export default toolLogCommand;
@@ -0,0 +1,3 @@
1
+ import { Command } from './types.js';
2
+ declare const toolSearchCommand: Command;
3
+ export default toolSearchCommand;
@@ -0,0 +1,57 @@
1
+ import { registerCommand } from './registry.js';
2
+ const toolSearchCommand = {
3
+ name: 'tool_search',
4
+ aliases: ['ts', 'search'],
5
+ description: 'Search for tools by query',
6
+ usage: '/tool_search <query>',
7
+ execute: (args, context) => {
8
+ if (args.length === 0) {
9
+ return {
10
+ success: false,
11
+ message: 'Usage: /tool_search <query>',
12
+ action: 'display',
13
+ };
14
+ }
15
+ if (!context.toolpack) {
16
+ return {
17
+ success: false,
18
+ message: 'Toolpack not initialized',
19
+ action: 'display',
20
+ };
21
+ }
22
+ const query = args.join(' ');
23
+ const registry = context.toolpack.getToolRegistry?.();
24
+ if (!registry) {
25
+ return {
26
+ success: false,
27
+ message: 'Tool registry not available',
28
+ action: 'display',
29
+ };
30
+ }
31
+ // Simple search through tool names and descriptions
32
+ const tools = registry.getAll();
33
+ const queryLower = query.toLowerCase();
34
+ const matches = tools.filter((t) => t.name.toLowerCase().includes(queryLower) ||
35
+ t.description?.toLowerCase().includes(queryLower) ||
36
+ t.displayName?.toLowerCase().includes(queryLower));
37
+ if (matches.length === 0) {
38
+ return {
39
+ success: true,
40
+ message: `No tools found matching: "${query}"`,
41
+ action: 'display',
42
+ };
43
+ }
44
+ const lines = [
45
+ `**Tools matching "${query}":** ${matches.length} found`,
46
+ '',
47
+ ...matches.map((t) => `- **${t.name}**: ${t.description || 'No description'}`),
48
+ ];
49
+ return {
50
+ success: true,
51
+ message: lines.join('\n'),
52
+ action: 'display',
53
+ };
54
+ },
55
+ };
56
+ registerCommand(toolSearchCommand);
57
+ export default toolSearchCommand;
@@ -0,0 +1,3 @@
1
+ import { Command } from './types.js';
2
+ declare const toolsCommand: Command;
3
+ export default toolsCommand;
@@ -0,0 +1,45 @@
1
+ import { registerCommand } from './registry.js';
2
+ const toolsCommand = {
3
+ name: 'tools',
4
+ aliases: ['t'],
5
+ description: 'List available tools',
6
+ execute: (_, context) => {
7
+ if (!context.toolpack) {
8
+ return {
9
+ success: false,
10
+ message: 'Toolpack not initialized',
11
+ action: 'display',
12
+ };
13
+ }
14
+ const registry = context.toolpack.getToolRegistry?.();
15
+ if (!registry) {
16
+ return {
17
+ success: false,
18
+ message: 'Tool registry not available',
19
+ action: 'display',
20
+ };
21
+ }
22
+ const tools = registry.getAll();
23
+ const byCategory = new Map();
24
+ for (const tool of tools) {
25
+ const cat = tool.category || 'uncategorized';
26
+ if (!byCategory.has(cat)) {
27
+ byCategory.set(cat, []);
28
+ }
29
+ byCategory.get(cat).push(tool.name);
30
+ }
31
+ const lines = [`**Available Tools:** ${tools.length} total`, ''];
32
+ for (const [category, toolNames] of byCategory) {
33
+ lines.push(`**${category}** (${toolNames.length}):`);
34
+ lines.push(toolNames.map(n => ` - ${n}`).join('\n'));
35
+ lines.push('');
36
+ }
37
+ return {
38
+ success: true,
39
+ message: lines.join('\n'),
40
+ action: 'display',
41
+ };
42
+ },
43
+ };
44
+ registerCommand(toolsCommand);
45
+ export default toolsCommand;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Command types and interfaces
3
+ */
4
+ export interface CommandContext {
5
+ toolpack: any;
6
+ activeMode: any;
7
+ activeModel: string;
8
+ activeConversation: any;
9
+ addMessage: (role: 'user' | 'assistant' | 'system', content: string) => void;
10
+ clearHistory: () => void;
11
+ setScreen: (screen: 'home' | 'chat') => void;
12
+ }
13
+ export interface CommandResult {
14
+ success: boolean;
15
+ message?: string;
16
+ action?: 'display' | 'navigate' | 'silent' | 'popup';
17
+ popupData?: any;
18
+ }
19
+ export interface Command {
20
+ name: string;
21
+ aliases?: string[];
22
+ description: string;
23
+ usage?: string;
24
+ execute: (args: string[], context: CommandContext) => Promise<CommandResult> | CommandResult;
25
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Command types and interfaces
3
+ */
4
+ export {};
@@ -0,0 +1,3 @@
1
+ import { Command } from './types.js';
2
+ declare const versionCommand: Command;
3
+ export default versionCommand;
@@ -0,0 +1,25 @@
1
+ import { registerCommand } from './registry.js';
2
+ const versionCommand = {
3
+ name: 'version',
4
+ aliases: ['v'],
5
+ description: 'Show version information',
6
+ execute: async () => {
7
+ const { readFileSync } = await import('fs');
8
+ const { join } = await import('path');
9
+ let version = 'unknown';
10
+ try {
11
+ const pkg = JSON.parse(readFileSync(join(process.cwd(), 'package.json'), 'utf-8'));
12
+ version = pkg.version || 'unknown';
13
+ }
14
+ catch {
15
+ // Ignore
16
+ }
17
+ return {
18
+ success: true,
19
+ message: `**Toolpack SDK CLI** v${version}`,
20
+ action: 'display',
21
+ };
22
+ },
23
+ };
24
+ registerCommand(versionCommand);
25
+ export default versionCommand;
@@ -0,0 +1 @@
1
+ export declare function AppInfo(): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { useTheme } from '../theme/ThemeContext.js';
4
+ export function AppInfo() {
5
+ const { theme } = useTheme();
6
+ // Hardcoded for now, mimicking original behavior
7
+ const currentPath = process.cwd();
8
+ const version = 'v1.0.0';
9
+ return (_jsxs(Box, { flexDirection: "column", alignItems: "center", marginBottom: 2, children: [_jsx(Text, { color: theme.colors.textSecondary, children: currentPath }), _jsx(Text, { color: theme.colors.textSecondary, children: version })] }));
10
+ }
@@ -0,0 +1,11 @@
1
+ interface HomeInputProps {
2
+ focusedIndex: number;
3
+ showModeSelect: boolean;
4
+ setShowModeSelect: (val: boolean) => void;
5
+ showModelSelect: boolean;
6
+ setShowModelSelect: (val: boolean) => void;
7
+ showHistorySelect: boolean;
8
+ setShowHistorySelect: (val: boolean) => void;
9
+ }
10
+ export declare function HomeInput({ focusedIndex, showModeSelect, setShowModeSelect, showModelSelect, setShowModelSelect, showHistorySelect, setShowHistorySelect, }: HomeInputProps): import("react/jsx-runtime").JSX.Element;
11
+ export {};
@@ -0,0 +1,328 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React, { useState, useEffect, useRef } from 'react';
3
+ import { Box, Text, useInput, useStdout } from 'ink';
4
+ import { useTheme } from '../theme/ThemeContext.js';
5
+ import { useConversation } from '../context/ConversationContext.js';
6
+ import { useToolpack } from '../context/ToolpackContext.js';
7
+ import { ProviderBar } from './ProviderBar.js';
8
+ import clipboardy from 'clipboardy';
9
+ // Bracketed paste mode sequences
10
+ const PASTE_START = '\x1b[200~';
11
+ const PASTE_END = '\x1b[201~';
12
+ /**
13
+ * Wraps text to fit within a given width, preserving existing newlines.
14
+ */
15
+ function wrapText(text, maxWidth) {
16
+ if (maxWidth <= 0)
17
+ return text.split('\n');
18
+ const result = [];
19
+ const paragraphs = text.split('\n');
20
+ for (const paragraph of paragraphs) {
21
+ if (paragraph.length <= maxWidth) {
22
+ result.push(paragraph);
23
+ }
24
+ else {
25
+ // Word-wrap long lines
26
+ let remaining = paragraph;
27
+ while (remaining.length > maxWidth) {
28
+ // Try to break at a space
29
+ let breakPoint = remaining.lastIndexOf(' ', maxWidth);
30
+ if (breakPoint <= 0) {
31
+ // No space found, hard break
32
+ breakPoint = maxWidth;
33
+ }
34
+ result.push(remaining.slice(0, breakPoint));
35
+ remaining = remaining.slice(breakPoint).trimStart();
36
+ }
37
+ if (remaining) {
38
+ result.push(remaining);
39
+ }
40
+ }
41
+ }
42
+ return result;
43
+ }
44
+ function sanitizeText(input) {
45
+ // Remove ANSI escape sequences
46
+ let sanitized = input.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
47
+ // Remove any remaining escape characters
48
+ sanitized = sanitized.replace(/\x1b/g, '');
49
+ // Normalize line endings
50
+ sanitized = sanitized.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
51
+ // Convert tabs to spaces
52
+ sanitized = sanitized.replace(/\t/g, ' ');
53
+ return sanitized;
54
+ }
55
+ export function HomeInput({ focusedIndex, showModeSelect, setShowModeSelect, showModelSelect, setShowModelSelect, showHistorySelect, setShowHistorySelect, }) {
56
+ const { theme } = useTheme();
57
+ const { stdout } = useStdout();
58
+ const [query, setQuery] = useState('');
59
+ const [cursorPos, setCursorPos] = useState(0);
60
+ const [scrollOffset, setScrollOffset] = useState(0);
61
+ // Ref to track current cursor position for use in closures
62
+ const cursorPosRef = useRef(0);
63
+ cursorPosRef.current = cursorPos;
64
+ // Ref to track current query for use in closures
65
+ const queryRef = useRef('');
66
+ queryRef.current = query;
67
+ // Refs for bracketed paste handling
68
+ const isPastingRef = useRef(false);
69
+ const pasteBufferRef = useRef('');
70
+ // Hardcode the max visible lines the input block is allowed to consume
71
+ const MAX_VISIBLE_LINES = 4;
72
+ // Calculate available width for text wrapping
73
+ // The input box is 80% of terminal width, minus border (2) and padding (2)
74
+ const boxWidth = Math.floor((stdout.columns || 80) * 0.8);
75
+ const textWidth = Math.max(10, boxWidth - 6); // 2 for border + 2 for paddingX on each side
76
+ // Wrap text to fit within the available width
77
+ const wrappedLines = wrapText(query, textWidth);
78
+ // focusedIndex = 0 means the text input has focus
79
+ const isInputFocused = focusedIndex === 0 && !showModeSelect && !showModelSelect;
80
+ const { createConversation } = useConversation();
81
+ const { activeMode, activeModel } = useToolpack();
82
+ // Auto-scroll to the bottom when the user adds new lines
83
+ useEffect(() => {
84
+ if (wrappedLines.length > MAX_VISIBLE_LINES) {
85
+ setScrollOffset(wrappedLines.length - MAX_VISIBLE_LINES);
86
+ }
87
+ else {
88
+ setScrollOffset(0);
89
+ }
90
+ }, [wrappedLines.length]);
91
+ // Ref to track if we handled an escape sequence (to prevent useInput from processing it)
92
+ const handledEscapeRef = useRef(false);
93
+ // Handle raw stdin for bracketed paste mode, mouse wheel, and Option+arrow keys
94
+ useEffect(() => {
95
+ if (!isInputFocused)
96
+ return;
97
+ const handleData = (data) => {
98
+ const str = data.toString();
99
+ // Option+Left (Esc+b) - move word backward
100
+ if (str === '\x1bb' || str === '\x1bB') {
101
+ handledEscapeRef.current = true;
102
+ const q = queryRef.current;
103
+ let pos = cursorPosRef.current - 1;
104
+ // Skip whitespace
105
+ while (pos > 0 && /\s/.test(q.charAt(pos)))
106
+ pos--;
107
+ // Find start of word
108
+ while (pos > 0 && !/\s/.test(q.charAt(pos - 1)))
109
+ pos--;
110
+ setCursorPos(Math.max(0, pos));
111
+ setTimeout(() => { handledEscapeRef.current = false; }, 10);
112
+ return;
113
+ }
114
+ // Option+Right (Esc+f) - move word forward
115
+ if (str === '\x1bf' || str === '\x1bF') {
116
+ handledEscapeRef.current = true;
117
+ const q = queryRef.current;
118
+ let pos = cursorPosRef.current;
119
+ // Skip current word
120
+ while (pos < q.length && !/\s/.test(q.charAt(pos)))
121
+ pos++;
122
+ // Skip whitespace
123
+ while (pos < q.length && /\s/.test(q.charAt(pos)))
124
+ pos++;
125
+ setCursorPos(pos);
126
+ setTimeout(() => { handledEscapeRef.current = false; }, 10);
127
+ return;
128
+ }
129
+ // Check for bracketed paste start
130
+ if (str.includes(PASTE_START)) {
131
+ isPastingRef.current = true;
132
+ // Extract content after paste start marker
133
+ const startIdx = str.indexOf(PASTE_START) + PASTE_START.length;
134
+ let content = str.slice(startIdx);
135
+ // Check if paste end is in the same chunk
136
+ if (content.includes(PASTE_END)) {
137
+ const endIdx = content.indexOf(PASTE_END);
138
+ content = content.slice(0, endIdx);
139
+ isPastingRef.current = false;
140
+ const sanitized = sanitizeText(content);
141
+ const pos = cursorPosRef.current;
142
+ setQuery(prev => prev.slice(0, pos) + sanitized + prev.slice(pos));
143
+ setCursorPos(pos + sanitized.length);
144
+ }
145
+ else {
146
+ pasteBufferRef.current = content;
147
+ }
148
+ return;
149
+ }
150
+ // If we're in paste mode, accumulate content
151
+ if (isPastingRef.current) {
152
+ if (str.includes(PASTE_END)) {
153
+ const endIdx = str.indexOf(PASTE_END);
154
+ pasteBufferRef.current += str.slice(0, endIdx);
155
+ isPastingRef.current = false;
156
+ const sanitized = sanitizeText(pasteBufferRef.current);
157
+ const pos = cursorPosRef.current;
158
+ setQuery(prev => prev.slice(0, pos) + sanitized + prev.slice(pos));
159
+ setCursorPos(pos + sanitized.length);
160
+ pasteBufferRef.current = '';
161
+ }
162
+ else {
163
+ pasteBufferRef.current += str;
164
+ }
165
+ return;
166
+ }
167
+ // Handle mouse wheel events
168
+ const match = str.match(/\x1b\[<(64|65);\d+;\d+M/);
169
+ if (match && match[1]) {
170
+ const button = parseInt(match[1], 10);
171
+ if (button === 64) {
172
+ setScrollOffset(prev => Math.max(0, prev - 1));
173
+ }
174
+ else if (button === 65) {
175
+ const maxOffset = Math.max(0, wrappedLines.length - MAX_VISIBLE_LINES);
176
+ setScrollOffset(prev => Math.min(maxOffset, prev + 1));
177
+ }
178
+ }
179
+ };
180
+ process.stdin.on('data', handleData);
181
+ return () => {
182
+ process.stdin.off('data', handleData);
183
+ };
184
+ }, [isInputFocused, wrappedLines.length, MAX_VISIBLE_LINES]);
185
+ // Handle keyboard input (non-paste)
186
+ useInput((input, key) => {
187
+ if (!isInputFocused)
188
+ return;
189
+ // Skip if we're in paste mode (handled by raw stdin)
190
+ if (isPastingRef.current)
191
+ return;
192
+ if (key.ctrl && input === 'l') {
193
+ setQuery('');
194
+ setCursorPos(0);
195
+ setScrollOffset(0);
196
+ return;
197
+ }
198
+ if (key.ctrl && input === 'k') {
199
+ try {
200
+ clipboardy.writeSync(query);
201
+ }
202
+ catch (err) { }
203
+ return;
204
+ }
205
+ // Ctrl+A to go to beginning of line
206
+ if (key.ctrl && input === 'a') {
207
+ setCursorPos(0);
208
+ return;
209
+ }
210
+ // Ctrl+E to go to end of line
211
+ if (key.ctrl && input === 'e') {
212
+ setCursorPos(queryRef.current.length);
213
+ return;
214
+ }
215
+ // Alt/Option + Left (sends Esc+b in terminal) - move word backward
216
+ if (input === 'b' && key.escape) {
217
+ const q = queryRef.current;
218
+ let pos = cursorPosRef.current - 1;
219
+ // Skip whitespace
220
+ while (pos > 0 && /\s/.test(q.charAt(pos)))
221
+ pos--;
222
+ // Find start of word
223
+ while (pos > 0 && !/\s/.test(q.charAt(pos - 1)))
224
+ pos--;
225
+ setCursorPos(Math.max(0, pos));
226
+ return;
227
+ }
228
+ // Alt/Option + Right (sends Esc+f in terminal) - move word forward
229
+ if (input === 'f' && key.escape) {
230
+ const q = queryRef.current;
231
+ let pos = cursorPosRef.current;
232
+ // Skip current word
233
+ while (pos < q.length && !/\s/.test(q.charAt(pos)))
234
+ pos++;
235
+ // Skip whitespace
236
+ while (pos < q.length && /\s/.test(q.charAt(pos)))
237
+ pos++;
238
+ setCursorPos(pos);
239
+ return;
240
+ }
241
+ // Left/Right arrow keys for cursor movement
242
+ if (key.leftArrow) {
243
+ setCursorPos(prev => Math.max(0, prev - 1));
244
+ return;
245
+ }
246
+ if (key.rightArrow) {
247
+ setCursorPos(prev => Math.min(queryRef.current.length, prev + 1));
248
+ return;
249
+ }
250
+ // Up/Down arrow keys for scrolling
251
+ if (key.upArrow) {
252
+ setScrollOffset(prev => Math.max(0, prev - 1));
253
+ return;
254
+ }
255
+ if (key.downArrow) {
256
+ const maxOffset = Math.max(0, wrappedLines.length - MAX_VISIBLE_LINES);
257
+ setScrollOffset(prev => Math.min(maxOffset, prev + 1));
258
+ return;
259
+ }
260
+ if (key.return) {
261
+ if (key.shift) {
262
+ setQuery(prev => prev.slice(0, cursorPos) + '\n' + prev.slice(cursorPos));
263
+ setCursorPos(prev => prev + 1);
264
+ return;
265
+ }
266
+ if (query.endsWith('\\')) {
267
+ setQuery(prev => prev.slice(0, -1) + '\n');
268
+ setCursorPos(query.length);
269
+ return;
270
+ }
271
+ if (query.trim()) {
272
+ createConversation(query.trim(), activeMode?.name, activeModel);
273
+ setQuery('');
274
+ setCursorPos(0);
275
+ setScrollOffset(0);
276
+ }
277
+ return;
278
+ }
279
+ const handleBackspace = () => {
280
+ setCursorPos(prev => {
281
+ if (prev > 0) {
282
+ setQuery(q => q.slice(0, prev - 1) + q.slice(prev));
283
+ return prev - 1;
284
+ }
285
+ return prev;
286
+ });
287
+ };
288
+ const handleForwardDelete = () => {
289
+ const pos = cursorPosRef.current;
290
+ const len = queryRef.current.length;
291
+ if (pos < len) {
292
+ setQuery(prev => prev.slice(0, pos) + prev.slice(pos + 1));
293
+ }
294
+ };
295
+ const hasModifier = key.ctrl || key.meta || key.shift;
296
+ if (key.backspace || (key.delete && !hasModifier)) {
297
+ handleBackspace();
298
+ return;
299
+ }
300
+ if (key.delete && hasModifier) {
301
+ handleForwardDelete();
302
+ return;
303
+ }
304
+ if (input) {
305
+ // Skip if this was part of an escape sequence we already handled
306
+ if (handledEscapeRef.current) {
307
+ return;
308
+ }
309
+ // Skip 'b' and 'f' if escape key is also pressed (Option+arrow on macOS)
310
+ if (key.escape && (input === 'b' || input === 'f')) {
311
+ return;
312
+ }
313
+ const sanitized = sanitizeText(input);
314
+ if (sanitized) {
315
+ const pos = cursorPosRef.current;
316
+ setQuery(prev => prev.slice(0, pos) + sanitized + prev.slice(pos));
317
+ setCursorPos(pos + sanitized.length);
318
+ }
319
+ }
320
+ }, { isActive: true });
321
+ // The border should be active (primary) if ANYTHING in this box is focused
322
+ const isActive = focusedIndex !== -1;
323
+ const borderColor = isActive ? theme.colors.border : theme.colors.secondary;
324
+ return (_jsxs(Box, { width: "100%", flexDirection: "column", children: [_jsxs(Box, { width: "100%", borderStyle: "single", borderColor: borderColor, flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsx(Box, { height: MAX_VISIBLE_LINES, overflowY: "hidden", width: "100%", flexDirection: "column", children: _jsx(Box, { marginTop: -scrollOffset, flexDirection: "column", width: "100%", children: query ? (_jsxs(Text, { color: theme.colors.text, children: [query.slice(0, cursorPos), isInputFocused && _jsx(Text, { color: theme.colors.primary, children: "\u258E" }), query.slice(cursorPos)] })) : (_jsxs(Text, { color: theme.colors.textMuted, children: [isInputFocused && _jsx(Text, { color: theme.colors.primary, children: "\u258E" }), "Type a message or ask a question..."] })) }) }), _jsx(Box, { width: "100%", flexDirection: "column", marginTop: 1, children: _jsx(Box, { width: "100%", children: _jsx(ProviderBar, { isFocused: isActive, focusedIndex: focusedIndex, showModeSelect: showModeSelect, setShowModeSelect: setShowModeSelect, showModelSelect: showModelSelect, setShowModelSelect: setShowModelSelect, showHistorySelect: showHistorySelect, setShowHistorySelect: setShowHistorySelect }) }) })] }), _jsx(Hints, { theme: theme })] }));
325
+ }
326
+ const Hints = React.memo(({ theme }) => {
327
+ return (_jsxs(Box, { width: "100%", justifyContent: "flex-end", paddingRight: 1, marginTop: 1, gap: 2, children: [_jsxs(Text, { children: [_jsx(Text, { color: theme.colors.textSecondary, bold: true, children: "^L" }), _jsx(Text, { color: theme.colors.textMuted, children: ":Clear" })] }), _jsxs(Text, { children: [_jsx(Text, { color: theme.colors.textSecondary, bold: true, children: "^K" }), _jsx(Text, { color: theme.colors.textMuted, children: ":Copy" })] }), _jsxs(Text, { children: [_jsx(Text, { color: theme.colors.textSecondary, bold: true, children: "\u2191/\u2193" }), _jsx(Text, { color: theme.colors.textMuted, children: ":Scroll" })] }), _jsxs(Text, { children: [_jsx(Text, { color: theme.colors.textSecondary, bold: true, children: "Tab" }), _jsx(Text, { color: theme.colors.textMuted, children: ":Select" })] }), _jsxs(Text, { children: [_jsx(Text, { color: theme.colors.textSecondary, bold: true, children: "\\+Ent" }), _jsx(Text, { color: theme.colors.textMuted, children: ":Line" })] }), _jsxs(Text, { children: [_jsx(Text, { color: theme.colors.textSecondary, bold: true, children: "Ent" }), _jsx(Text, { color: theme.colors.textMuted, children: ":Send" })] })] }));
328
+ });
@@ -0,0 +1 @@
1
+ export declare function Logo(): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { useTheme } from '../theme/ThemeContext.js';
4
+ export function Logo() {
5
+ const { theme } = useTheme();
6
+ const asciiArt = `
7
+ ████████╗ ██████╗ ██████╗ ██╗ ██████╗ █████╗ ██████╗██╗ ██╗ ███████╗██████╗ ██╗ ██╗
8
+ ╚══██╔══╝██╔═══██╗██╔═══██╗██║ ██╔══██╗██╔══██╗██╔════╝██║ ██╔╝ ██╔════╝██╔══██╗██║ ██╔╝
9
+ ██║ ██║ ██║██║ ██║██║ ██████╔╝███████║██║ █████╔╝ ███████╗██║ ██║█████╔╝
10
+ ██║ ██║ ██║██║ ██║██║ ██╔═══╝ ██╔══██║██║ ██╔═██╗ ╚════██║██║ ██║██╔═██╗
11
+ ██║ ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║╚██████╗██║ ██╗ ███████║██████╔╝██║ ██╗
12
+ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ ╚══════╝╚═════╝ ╚═╝ ╚═╝
13
+ `;
14
+ return (_jsx(Box, { justifyContent: "center", width: "100%", children: _jsx(Text, { color: theme.colors.text, bold: true, children: asciiArt }) }));
15
+ }
@@ -0,0 +1,5 @@
1
+ interface MarkdownProps {
2
+ children: string;
3
+ }
4
+ export declare function Markdown({ children }: MarkdownProps): import("react/jsx-runtime").JSX.Element;
5
+ export {};