@shareai-lab/kode 1.0.70 → 1.0.71
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.
- package/README.md +202 -76
- package/README.zh-CN.md +246 -0
- package/cli.js +62 -0
- package/package.json +45 -25
- package/scripts/postinstall.js +56 -0
- package/src/ProjectOnboarding.tsx +180 -0
- package/src/Tool.ts +53 -0
- package/src/commands/approvedTools.ts +53 -0
- package/src/commands/bug.tsx +20 -0
- package/src/commands/clear.ts +43 -0
- package/src/commands/compact.ts +120 -0
- package/src/commands/config.tsx +19 -0
- package/src/commands/cost.ts +18 -0
- package/src/commands/ctx_viz.ts +209 -0
- package/src/commands/doctor.ts +24 -0
- package/src/commands/help.tsx +19 -0
- package/src/commands/init.ts +37 -0
- package/src/commands/listen.ts +42 -0
- package/src/commands/login.tsx +51 -0
- package/src/commands/logout.tsx +40 -0
- package/src/commands/mcp.ts +41 -0
- package/src/commands/model.tsx +40 -0
- package/src/commands/modelstatus.tsx +20 -0
- package/src/commands/onboarding.tsx +34 -0
- package/src/commands/pr_comments.ts +59 -0
- package/src/commands/refreshCommands.ts +54 -0
- package/src/commands/release-notes.ts +34 -0
- package/src/commands/resume.tsx +30 -0
- package/src/commands/review.ts +49 -0
- package/src/commands/terminalSetup.ts +221 -0
- package/src/commands.ts +136 -0
- package/src/components/ApproveApiKey.tsx +93 -0
- package/src/components/AsciiLogo.tsx +13 -0
- package/src/components/AutoUpdater.tsx +148 -0
- package/src/components/Bug.tsx +367 -0
- package/src/components/Config.tsx +289 -0
- package/src/components/ConsoleOAuthFlow.tsx +326 -0
- package/src/components/Cost.tsx +23 -0
- package/src/components/CostThresholdDialog.tsx +46 -0
- package/src/components/CustomSelect/option-map.ts +42 -0
- package/src/components/CustomSelect/select-option.tsx +52 -0
- package/src/components/CustomSelect/select.tsx +143 -0
- package/src/components/CustomSelect/use-select-state.ts +414 -0
- package/src/components/CustomSelect/use-select.ts +35 -0
- package/src/components/FallbackToolUseRejectedMessage.tsx +15 -0
- package/src/components/FileEditToolUpdatedMessage.tsx +66 -0
- package/src/components/Help.tsx +215 -0
- package/src/components/HighlightedCode.tsx +33 -0
- package/src/components/InvalidConfigDialog.tsx +113 -0
- package/src/components/Link.tsx +32 -0
- package/src/components/LogSelector.tsx +86 -0
- package/src/components/Logo.tsx +145 -0
- package/src/components/MCPServerApprovalDialog.tsx +100 -0
- package/src/components/MCPServerDialogCopy.tsx +25 -0
- package/src/components/MCPServerMultiselectDialog.tsx +109 -0
- package/src/components/Message.tsx +219 -0
- package/src/components/MessageResponse.tsx +15 -0
- package/src/components/MessageSelector.tsx +211 -0
- package/src/components/ModeIndicator.tsx +88 -0
- package/src/components/ModelConfig.tsx +301 -0
- package/src/components/ModelListManager.tsx +223 -0
- package/src/components/ModelSelector.tsx +3208 -0
- package/src/components/ModelStatusDisplay.tsx +228 -0
- package/src/components/Onboarding.tsx +274 -0
- package/src/components/PressEnterToContinue.tsx +11 -0
- package/src/components/PromptInput.tsx +710 -0
- package/src/components/SentryErrorBoundary.ts +33 -0
- package/src/components/Spinner.tsx +129 -0
- package/src/components/StructuredDiff.tsx +184 -0
- package/src/components/TextInput.tsx +246 -0
- package/src/components/TokenWarning.tsx +31 -0
- package/src/components/ToolUseLoader.tsx +40 -0
- package/src/components/TrustDialog.tsx +106 -0
- package/src/components/binary-feedback/BinaryFeedback.tsx +63 -0
- package/src/components/binary-feedback/BinaryFeedbackOption.tsx +111 -0
- package/src/components/binary-feedback/BinaryFeedbackView.tsx +172 -0
- package/src/components/binary-feedback/utils.ts +220 -0
- package/src/components/messages/AssistantBashOutputMessage.tsx +22 -0
- package/src/components/messages/AssistantLocalCommandOutputMessage.tsx +45 -0
- package/src/components/messages/AssistantRedactedThinkingMessage.tsx +19 -0
- package/src/components/messages/AssistantTextMessage.tsx +144 -0
- package/src/components/messages/AssistantThinkingMessage.tsx +40 -0
- package/src/components/messages/AssistantToolUseMessage.tsx +123 -0
- package/src/components/messages/UserBashInputMessage.tsx +28 -0
- package/src/components/messages/UserCommandMessage.tsx +30 -0
- package/src/components/messages/UserKodingInputMessage.tsx +28 -0
- package/src/components/messages/UserPromptMessage.tsx +35 -0
- package/src/components/messages/UserTextMessage.tsx +39 -0
- package/src/components/messages/UserToolResultMessage/UserToolCanceledMessage.tsx +12 -0
- package/src/components/messages/UserToolResultMessage/UserToolErrorMessage.tsx +36 -0
- package/src/components/messages/UserToolResultMessage/UserToolRejectMessage.tsx +31 -0
- package/src/components/messages/UserToolResultMessage/UserToolResultMessage.tsx +57 -0
- package/src/components/messages/UserToolResultMessage/UserToolSuccessMessage.tsx +35 -0
- package/src/components/messages/UserToolResultMessage/utils.tsx +56 -0
- package/src/components/permissions/BashPermissionRequest/BashPermissionRequest.tsx +121 -0
- package/src/components/permissions/FallbackPermissionRequest.tsx +155 -0
- package/src/components/permissions/FileEditPermissionRequest/FileEditPermissionRequest.tsx +182 -0
- package/src/components/permissions/FileEditPermissionRequest/FileEditToolDiff.tsx +75 -0
- package/src/components/permissions/FileWritePermissionRequest/FileWritePermissionRequest.tsx +164 -0
- package/src/components/permissions/FileWritePermissionRequest/FileWriteToolDiff.tsx +81 -0
- package/src/components/permissions/FilesystemPermissionRequest/FilesystemPermissionRequest.tsx +242 -0
- package/src/components/permissions/PermissionRequest.tsx +103 -0
- package/src/components/permissions/PermissionRequestTitle.tsx +69 -0
- package/src/components/permissions/hooks.ts +44 -0
- package/src/components/permissions/toolUseOptions.ts +59 -0
- package/src/components/permissions/utils.ts +23 -0
- package/src/constants/betas.ts +5 -0
- package/src/constants/claude-asterisk-ascii-art.tsx +238 -0
- package/src/constants/figures.ts +4 -0
- package/src/constants/keys.ts +3 -0
- package/src/constants/macros.ts +6 -0
- package/src/constants/models.ts +935 -0
- package/src/constants/oauth.ts +18 -0
- package/src/constants/product.ts +17 -0
- package/src/constants/prompts.ts +177 -0
- package/src/constants/releaseNotes.ts +7 -0
- package/src/context/PermissionContext.tsx +149 -0
- package/src/context.ts +278 -0
- package/src/cost-tracker.ts +84 -0
- package/src/entrypoints/cli.tsx +1498 -0
- package/src/entrypoints/mcp.ts +176 -0
- package/src/history.ts +25 -0
- package/src/hooks/useApiKeyVerification.ts +59 -0
- package/src/hooks/useArrowKeyHistory.ts +55 -0
- package/src/hooks/useCanUseTool.ts +138 -0
- package/src/hooks/useCancelRequest.ts +39 -0
- package/src/hooks/useDoublePress.ts +42 -0
- package/src/hooks/useExitOnCtrlCD.ts +31 -0
- package/src/hooks/useInterval.ts +25 -0
- package/src/hooks/useLogMessages.ts +16 -0
- package/src/hooks/useLogStartupTime.ts +12 -0
- package/src/hooks/useNotifyAfterTimeout.ts +65 -0
- package/src/hooks/usePermissionRequestLogging.ts +44 -0
- package/src/hooks/useSlashCommandTypeahead.ts +137 -0
- package/src/hooks/useTerminalSize.ts +49 -0
- package/src/hooks/useTextInput.ts +315 -0
- package/src/messages.ts +37 -0
- package/src/permissions.ts +268 -0
- package/src/query.ts +704 -0
- package/src/screens/ConfigureNpmPrefix.tsx +197 -0
- package/src/screens/Doctor.tsx +219 -0
- package/src/screens/LogList.tsx +68 -0
- package/src/screens/REPL.tsx +792 -0
- package/src/screens/ResumeConversation.tsx +68 -0
- package/src/services/browserMocks.ts +66 -0
- package/src/services/claude.ts +1947 -0
- package/src/services/customCommands.ts +683 -0
- package/src/services/fileFreshness.ts +377 -0
- package/src/services/mcpClient.ts +564 -0
- package/src/services/mcpServerApproval.tsx +50 -0
- package/src/services/notifier.ts +40 -0
- package/src/services/oauth.ts +357 -0
- package/src/services/openai.ts +796 -0
- package/src/services/sentry.ts +3 -0
- package/src/services/statsig.ts +171 -0
- package/src/services/statsigStorage.ts +86 -0
- package/src/services/systemReminder.ts +406 -0
- package/src/services/vcr.ts +161 -0
- package/src/tools/ArchitectTool/ArchitectTool.tsx +122 -0
- package/src/tools/ArchitectTool/prompt.ts +15 -0
- package/src/tools/AskExpertModelTool/AskExpertModelTool.tsx +505 -0
- package/src/tools/BashTool/BashTool.tsx +270 -0
- package/src/tools/BashTool/BashToolResultMessage.tsx +38 -0
- package/src/tools/BashTool/OutputLine.tsx +48 -0
- package/src/tools/BashTool/prompt.ts +174 -0
- package/src/tools/BashTool/utils.ts +56 -0
- package/src/tools/FileEditTool/FileEditTool.tsx +316 -0
- package/src/tools/FileEditTool/prompt.ts +51 -0
- package/src/tools/FileEditTool/utils.ts +58 -0
- package/src/tools/FileReadTool/FileReadTool.tsx +371 -0
- package/src/tools/FileReadTool/prompt.ts +7 -0
- package/src/tools/FileWriteTool/FileWriteTool.tsx +297 -0
- package/src/tools/FileWriteTool/prompt.ts +10 -0
- package/src/tools/GlobTool/GlobTool.tsx +119 -0
- package/src/tools/GlobTool/prompt.ts +8 -0
- package/src/tools/GrepTool/GrepTool.tsx +147 -0
- package/src/tools/GrepTool/prompt.ts +11 -0
- package/src/tools/MCPTool/MCPTool.tsx +106 -0
- package/src/tools/MCPTool/prompt.ts +3 -0
- package/src/tools/MemoryReadTool/MemoryReadTool.tsx +127 -0
- package/src/tools/MemoryReadTool/prompt.ts +3 -0
- package/src/tools/MemoryWriteTool/MemoryWriteTool.tsx +89 -0
- package/src/tools/MemoryWriteTool/prompt.ts +3 -0
- package/src/tools/MultiEditTool/MultiEditTool.tsx +366 -0
- package/src/tools/MultiEditTool/prompt.ts +45 -0
- package/src/tools/NotebookEditTool/NotebookEditTool.tsx +298 -0
- package/src/tools/NotebookEditTool/prompt.ts +3 -0
- package/src/tools/NotebookReadTool/NotebookReadTool.tsx +266 -0
- package/src/tools/NotebookReadTool/prompt.ts +3 -0
- package/src/tools/StickerRequestTool/StickerRequestTool.tsx +93 -0
- package/src/tools/StickerRequestTool/prompt.ts +19 -0
- package/src/tools/TaskTool/TaskTool.tsx +382 -0
- package/src/tools/TaskTool/constants.ts +1 -0
- package/src/tools/TaskTool/prompt.ts +56 -0
- package/src/tools/ThinkTool/ThinkTool.tsx +56 -0
- package/src/tools/ThinkTool/prompt.ts +12 -0
- package/src/tools/TodoWriteTool/TodoWriteTool.tsx +289 -0
- package/src/tools/TodoWriteTool/prompt.ts +63 -0
- package/src/tools/lsTool/lsTool.tsx +269 -0
- package/src/tools/lsTool/prompt.ts +2 -0
- package/src/tools.ts +63 -0
- package/src/types/PermissionMode.ts +120 -0
- package/src/types/RequestContext.ts +72 -0
- package/src/utils/Cursor.ts +436 -0
- package/src/utils/PersistentShell.ts +373 -0
- package/src/utils/agentStorage.ts +97 -0
- package/src/utils/array.ts +3 -0
- package/src/utils/ask.tsx +98 -0
- package/src/utils/auth.ts +13 -0
- package/src/utils/autoCompactCore.ts +223 -0
- package/src/utils/autoUpdater.ts +318 -0
- package/src/utils/betas.ts +20 -0
- package/src/utils/browser.ts +14 -0
- package/src/utils/cleanup.ts +72 -0
- package/src/utils/commands.ts +261 -0
- package/src/utils/config.ts +771 -0
- package/src/utils/conversationRecovery.ts +54 -0
- package/src/utils/debugLogger.ts +1123 -0
- package/src/utils/diff.ts +42 -0
- package/src/utils/env.ts +57 -0
- package/src/utils/errors.ts +21 -0
- package/src/utils/exampleCommands.ts +108 -0
- package/src/utils/execFileNoThrow.ts +51 -0
- package/src/utils/expertChatStorage.ts +136 -0
- package/src/utils/file.ts +402 -0
- package/src/utils/fileRecoveryCore.ts +71 -0
- package/src/utils/format.tsx +44 -0
- package/src/utils/generators.ts +62 -0
- package/src/utils/git.ts +92 -0
- package/src/utils/globalLogger.ts +77 -0
- package/src/utils/http.ts +10 -0
- package/src/utils/imagePaste.ts +38 -0
- package/src/utils/json.ts +13 -0
- package/src/utils/log.ts +382 -0
- package/src/utils/markdown.ts +213 -0
- package/src/utils/messageContextManager.ts +289 -0
- package/src/utils/messages.tsx +938 -0
- package/src/utils/model.ts +836 -0
- package/src/utils/permissions/filesystem.ts +118 -0
- package/src/utils/ripgrep.ts +167 -0
- package/src/utils/sessionState.ts +49 -0
- package/src/utils/state.ts +25 -0
- package/src/utils/style.ts +29 -0
- package/src/utils/terminal.ts +49 -0
- package/src/utils/theme.ts +122 -0
- package/src/utils/thinking.ts +144 -0
- package/src/utils/todoStorage.ts +431 -0
- package/src/utils/tokens.ts +43 -0
- package/src/utils/toolExecutionController.ts +163 -0
- package/src/utils/unaryLogging.ts +26 -0
- package/src/utils/user.ts +37 -0
- package/src/utils/validate.ts +165 -0
- package/cli.mjs +0 -1803
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { Box, Text, useInput } from 'ink'
|
|
2
|
+
import * as React from 'react'
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import figures from 'figures'
|
|
5
|
+
import { getTheme } from '../utils/theme'
|
|
6
|
+
import {
|
|
7
|
+
GlobalConfig,
|
|
8
|
+
saveGlobalConfig,
|
|
9
|
+
getGlobalConfig,
|
|
10
|
+
} from '../utils/config.js'
|
|
11
|
+
import chalk from 'chalk'
|
|
12
|
+
import { useExitOnCtrlCD } from '../hooks/useExitOnCtrlCD'
|
|
13
|
+
import { getModelManager } from '../utils/model'
|
|
14
|
+
|
|
15
|
+
type Props = {
|
|
16
|
+
onClose: () => void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type Setting =
|
|
20
|
+
| {
|
|
21
|
+
id: string
|
|
22
|
+
label: string
|
|
23
|
+
value: boolean
|
|
24
|
+
onChange(value: boolean): void
|
|
25
|
+
type: 'boolean'
|
|
26
|
+
disabled?: boolean
|
|
27
|
+
}
|
|
28
|
+
| {
|
|
29
|
+
id: string
|
|
30
|
+
label: string
|
|
31
|
+
value: string
|
|
32
|
+
options: string[]
|
|
33
|
+
onChange(value: string): void
|
|
34
|
+
type: 'enum'
|
|
35
|
+
disabled?: boolean
|
|
36
|
+
}
|
|
37
|
+
| {
|
|
38
|
+
id: string
|
|
39
|
+
label: string
|
|
40
|
+
value: string
|
|
41
|
+
onChange(value: string): void
|
|
42
|
+
type: 'string'
|
|
43
|
+
disabled?: boolean
|
|
44
|
+
}
|
|
45
|
+
| {
|
|
46
|
+
id: string
|
|
47
|
+
label: string
|
|
48
|
+
value: number
|
|
49
|
+
onChange(value: number): void
|
|
50
|
+
type: 'number'
|
|
51
|
+
disabled?: boolean
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function Config({ onClose }: Props): React.ReactNode {
|
|
55
|
+
const [globalConfig, setGlobalConfig] = useState(getGlobalConfig())
|
|
56
|
+
const initialConfig = React.useRef(getGlobalConfig())
|
|
57
|
+
const [selectedIndex, setSelectedIndex] = useState(0)
|
|
58
|
+
const exitState = useExitOnCtrlCD(() => process.exit(0))
|
|
59
|
+
const [editingString, setEditingString] = useState(false)
|
|
60
|
+
const [currentInput, setCurrentInput] = useState('')
|
|
61
|
+
const [inputError, setInputError] = useState<string | null>(null)
|
|
62
|
+
|
|
63
|
+
const modelManager = getModelManager()
|
|
64
|
+
const activeProfiles = modelManager.getAvailableModels()
|
|
65
|
+
|
|
66
|
+
const settings: Setting[] = [
|
|
67
|
+
// Global settings
|
|
68
|
+
{
|
|
69
|
+
id: 'theme',
|
|
70
|
+
label: 'Theme',
|
|
71
|
+
value: globalConfig.theme ?? 'dark',
|
|
72
|
+
options: ['dark', 'light'],
|
|
73
|
+
onChange(theme: string) {
|
|
74
|
+
const config = { ...getGlobalConfig(), theme: theme as any }
|
|
75
|
+
saveGlobalConfig(config)
|
|
76
|
+
setGlobalConfig(config)
|
|
77
|
+
},
|
|
78
|
+
type: 'enum',
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: 'verbose',
|
|
82
|
+
label: 'Verbose mode',
|
|
83
|
+
value: globalConfig.verbose ?? false,
|
|
84
|
+
onChange(verbose: boolean) {
|
|
85
|
+
const config = { ...getGlobalConfig(), verbose }
|
|
86
|
+
saveGlobalConfig(config)
|
|
87
|
+
setGlobalConfig(config)
|
|
88
|
+
},
|
|
89
|
+
type: 'boolean',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: 'stream',
|
|
93
|
+
label: 'Stream responses',
|
|
94
|
+
value: globalConfig.stream ?? true,
|
|
95
|
+
onChange(stream: boolean) {
|
|
96
|
+
const config = { ...getGlobalConfig(), stream }
|
|
97
|
+
saveGlobalConfig(config)
|
|
98
|
+
setGlobalConfig(config)
|
|
99
|
+
},
|
|
100
|
+
type: 'boolean',
|
|
101
|
+
},
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
const theme = getTheme()
|
|
105
|
+
|
|
106
|
+
useInput((input, key) => {
|
|
107
|
+
if (editingString) {
|
|
108
|
+
if (key.return) {
|
|
109
|
+
const currentSetting = settings[selectedIndex]
|
|
110
|
+
if (currentSetting?.type === 'string') {
|
|
111
|
+
try {
|
|
112
|
+
currentSetting.onChange(currentInput)
|
|
113
|
+
setEditingString(false)
|
|
114
|
+
setCurrentInput('')
|
|
115
|
+
setInputError(null)
|
|
116
|
+
} catch (error) {
|
|
117
|
+
setInputError(
|
|
118
|
+
error instanceof Error ? error.message : 'Invalid input',
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
} else if (currentSetting?.type === 'number') {
|
|
122
|
+
const numValue = parseFloat(currentInput)
|
|
123
|
+
if (isNaN(numValue)) {
|
|
124
|
+
setInputError('Please enter a valid number')
|
|
125
|
+
} else {
|
|
126
|
+
try {
|
|
127
|
+
;(currentSetting as any).onChange(numValue)
|
|
128
|
+
setEditingString(false)
|
|
129
|
+
setCurrentInput('')
|
|
130
|
+
setInputError(null)
|
|
131
|
+
} catch (error) {
|
|
132
|
+
setInputError(
|
|
133
|
+
error instanceof Error ? error.message : 'Invalid input',
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
} else if (key.escape) {
|
|
139
|
+
setEditingString(false)
|
|
140
|
+
setCurrentInput('')
|
|
141
|
+
setInputError(null)
|
|
142
|
+
} else if (key.delete || key.backspace) {
|
|
143
|
+
setCurrentInput(prev => prev.slice(0, -1))
|
|
144
|
+
} else if (input) {
|
|
145
|
+
setCurrentInput(prev => prev + input)
|
|
146
|
+
}
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (key.upArrow && !exitState.pending) {
|
|
151
|
+
setSelectedIndex(prev => Math.max(0, prev - 1))
|
|
152
|
+
} else if (key.downArrow && !exitState.pending) {
|
|
153
|
+
setSelectedIndex(prev => Math.min(settings.length - 1, prev + 1))
|
|
154
|
+
} else if (key.return && !exitState.pending) {
|
|
155
|
+
const currentSetting = settings[selectedIndex]
|
|
156
|
+
if (currentSetting?.disabled) return
|
|
157
|
+
|
|
158
|
+
if (currentSetting?.type === 'boolean') {
|
|
159
|
+
currentSetting.onChange(!currentSetting.value)
|
|
160
|
+
} else if (currentSetting?.type === 'enum') {
|
|
161
|
+
const currentIndex = currentSetting.options.indexOf(
|
|
162
|
+
currentSetting.value,
|
|
163
|
+
)
|
|
164
|
+
const nextIndex = (currentIndex + 1) % currentSetting.options.length
|
|
165
|
+
currentSetting.onChange(currentSetting.options[nextIndex])
|
|
166
|
+
} else if (
|
|
167
|
+
currentSetting?.type === 'string' ||
|
|
168
|
+
currentSetting?.type === 'number'
|
|
169
|
+
) {
|
|
170
|
+
setCurrentInput(String(currentSetting.value))
|
|
171
|
+
setEditingString(true)
|
|
172
|
+
setInputError(null)
|
|
173
|
+
}
|
|
174
|
+
} else if (key.escape && !exitState.pending) {
|
|
175
|
+
// Check if config has changed
|
|
176
|
+
const currentConfigString = JSON.stringify(getGlobalConfig())
|
|
177
|
+
const initialConfigString = JSON.stringify(initialConfig.current)
|
|
178
|
+
|
|
179
|
+
if (currentConfigString !== initialConfigString) {
|
|
180
|
+
// Config has changed, save it
|
|
181
|
+
saveGlobalConfig(getGlobalConfig())
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
onClose()
|
|
185
|
+
}
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
return (
|
|
189
|
+
<Box flexDirection="column" gap={1}>
|
|
190
|
+
<Box
|
|
191
|
+
flexDirection="column"
|
|
192
|
+
borderStyle="round"
|
|
193
|
+
borderColor={theme.secondaryBorder}
|
|
194
|
+
paddingX={2}
|
|
195
|
+
paddingY={1}
|
|
196
|
+
gap={1}
|
|
197
|
+
>
|
|
198
|
+
<Text bold>
|
|
199
|
+
Configuration{' '}
|
|
200
|
+
{exitState.pending
|
|
201
|
+
? `(press ${exitState.keyName} again to exit)`
|
|
202
|
+
: ''}
|
|
203
|
+
</Text>
|
|
204
|
+
|
|
205
|
+
{/* Model Configuration Summary */}
|
|
206
|
+
<Box flexDirection="column" marginY={1}>
|
|
207
|
+
<Text bold color={theme.success}>
|
|
208
|
+
Model Configuration:
|
|
209
|
+
</Text>
|
|
210
|
+
{activeProfiles.length === 0 ? (
|
|
211
|
+
<Text color={theme.secondaryText}>
|
|
212
|
+
No models configured. Use /model to add models.
|
|
213
|
+
</Text>
|
|
214
|
+
) : (
|
|
215
|
+
<Box flexDirection="column" marginLeft={2}>
|
|
216
|
+
{activeProfiles.map(profile => (
|
|
217
|
+
<Text key={profile.modelName} color={theme.secondaryText}>
|
|
218
|
+
• {profile.name} ({profile.provider})
|
|
219
|
+
</Text>
|
|
220
|
+
))}
|
|
221
|
+
<Text color={theme.suggestion} marginTop={1}>
|
|
222
|
+
Use /model to manage model configurations
|
|
223
|
+
</Text>
|
|
224
|
+
</Box>
|
|
225
|
+
)}
|
|
226
|
+
</Box>
|
|
227
|
+
|
|
228
|
+
{/* Settings List */}
|
|
229
|
+
<Box flexDirection="column">
|
|
230
|
+
{settings.map((setting, index) => (
|
|
231
|
+
<Box key={setting.id} flexDirection="column">
|
|
232
|
+
<Box flexDirection="row" gap={1}>
|
|
233
|
+
<Text
|
|
234
|
+
color={
|
|
235
|
+
index === selectedIndex
|
|
236
|
+
? theme.success
|
|
237
|
+
: setting.disabled
|
|
238
|
+
? theme.secondaryText
|
|
239
|
+
: theme.text
|
|
240
|
+
}
|
|
241
|
+
>
|
|
242
|
+
{index === selectedIndex ? figures.pointer : ' '}{' '}
|
|
243
|
+
{setting.label}
|
|
244
|
+
</Text>
|
|
245
|
+
<Text
|
|
246
|
+
color={
|
|
247
|
+
setting.disabled ? theme.secondaryText : theme.suggestion
|
|
248
|
+
}
|
|
249
|
+
>
|
|
250
|
+
{setting.type === 'boolean'
|
|
251
|
+
? setting.value
|
|
252
|
+
? 'enabled'
|
|
253
|
+
: 'disabled'
|
|
254
|
+
: setting.type === 'enum'
|
|
255
|
+
? setting.value
|
|
256
|
+
: String(setting.value)}
|
|
257
|
+
</Text>
|
|
258
|
+
</Box>
|
|
259
|
+
{index === selectedIndex && editingString && (
|
|
260
|
+
<Box flexDirection="column" marginLeft={2}>
|
|
261
|
+
<Text color={theme.suggestion}>
|
|
262
|
+
Enter new value: {currentInput}
|
|
263
|
+
</Text>
|
|
264
|
+
{inputError && <Text color="red">{inputError}</Text>}
|
|
265
|
+
</Box>
|
|
266
|
+
)}
|
|
267
|
+
</Box>
|
|
268
|
+
))}
|
|
269
|
+
</Box>
|
|
270
|
+
|
|
271
|
+
<Box marginTop={1}>
|
|
272
|
+
<Text dimColor>
|
|
273
|
+
{editingString ? (
|
|
274
|
+
'Enter to save · Esc to cancel'
|
|
275
|
+
) : (
|
|
276
|
+
<>
|
|
277
|
+
↑/↓ to navigate · Enter to change · Esc to close
|
|
278
|
+
<Text color={theme.suggestion}>
|
|
279
|
+
{' '}
|
|
280
|
+
· Use /model for model config
|
|
281
|
+
</Text>
|
|
282
|
+
</>
|
|
283
|
+
)}
|
|
284
|
+
</Text>
|
|
285
|
+
</Box>
|
|
286
|
+
</Box>
|
|
287
|
+
</Box>
|
|
288
|
+
)
|
|
289
|
+
}
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import React, { useEffect, useState, useCallback } from 'react'
|
|
2
|
+
import { Static, Box, Text, useInput } from 'ink'
|
|
3
|
+
import TextInput from './TextInput'
|
|
4
|
+
import { OAuthService, createAndStoreApiKey } from '../services/oauth'
|
|
5
|
+
import { getTheme } from '../utils/theme'
|
|
6
|
+
import { logEvent } from '../services/statsig'
|
|
7
|
+
import { AsciiLogo } from './AsciiLogo'
|
|
8
|
+
import { useTerminalSize } from '../hooks/useTerminalSize'
|
|
9
|
+
import { logError } from '../utils/log'
|
|
10
|
+
import { clearTerminal } from '../utils/terminal'
|
|
11
|
+
import { SimpleSpinner } from './Spinner'
|
|
12
|
+
import { WelcomeBox } from './Onboarding'
|
|
13
|
+
import { PRODUCT_NAME } from '../constants/product'
|
|
14
|
+
import { sendNotification } from '../services/notifier'
|
|
15
|
+
|
|
16
|
+
type Props = {
|
|
17
|
+
onDone(): void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type OAuthStatus =
|
|
21
|
+
| { state: 'idle' }
|
|
22
|
+
| { state: 'ready_to_start' }
|
|
23
|
+
| { state: 'waiting_for_login'; url: string }
|
|
24
|
+
| { state: 'creating_api_key' }
|
|
25
|
+
| { state: 'about_to_retry'; nextState: OAuthStatus }
|
|
26
|
+
| { state: 'success'; apiKey: string }
|
|
27
|
+
| {
|
|
28
|
+
state: 'error'
|
|
29
|
+
message: string
|
|
30
|
+
toRetry?: OAuthStatus
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const PASTE_HERE_MSG = 'Paste code here if prompted > '
|
|
34
|
+
|
|
35
|
+
export function ConsoleOAuthFlow({ onDone }: Props): React.ReactNode {
|
|
36
|
+
const [oauthStatus, setOAuthStatus] = useState<OAuthStatus>({
|
|
37
|
+
state: 'idle',
|
|
38
|
+
})
|
|
39
|
+
const theme = getTheme()
|
|
40
|
+
|
|
41
|
+
const [pastedCode, setPastedCode] = useState('')
|
|
42
|
+
const [cursorOffset, setCursorOffset] = useState(0)
|
|
43
|
+
const [oauthService] = useState(() => new OAuthService())
|
|
44
|
+
// After a few seconds we suggest the user to copy/paste url if the
|
|
45
|
+
// browser did not open automatically. In this flow we expect the user to
|
|
46
|
+
// copy the code from the browser and paste it in the terminal
|
|
47
|
+
const [showPastePrompt, setShowPastePrompt] = useState(false)
|
|
48
|
+
// we need a special clearing state to correctly re-render Static elements
|
|
49
|
+
const [isClearing, setIsClearing] = useState(false)
|
|
50
|
+
|
|
51
|
+
const textInputColumns = useTerminalSize().columns - PASTE_HERE_MSG.length - 1
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (isClearing) {
|
|
55
|
+
clearTerminal()
|
|
56
|
+
setIsClearing(false)
|
|
57
|
+
}
|
|
58
|
+
}, [isClearing])
|
|
59
|
+
|
|
60
|
+
// Retry logic
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (oauthStatus.state === 'about_to_retry') {
|
|
63
|
+
setIsClearing(true)
|
|
64
|
+
setTimeout(() => {
|
|
65
|
+
setOAuthStatus(oauthStatus.nextState)
|
|
66
|
+
}, 1000)
|
|
67
|
+
}
|
|
68
|
+
}, [oauthStatus])
|
|
69
|
+
|
|
70
|
+
useInput(async (_, key) => {
|
|
71
|
+
if (key.return) {
|
|
72
|
+
if (oauthStatus.state === 'idle') {
|
|
73
|
+
logEvent('tengu_oauth_start', {})
|
|
74
|
+
setOAuthStatus({ state: 'ready_to_start' })
|
|
75
|
+
} else if (oauthStatus.state === 'success') {
|
|
76
|
+
logEvent('tengu_oauth_success', {})
|
|
77
|
+
await clearTerminal() // needed to clear out Static components
|
|
78
|
+
onDone()
|
|
79
|
+
} else if (oauthStatus.state === 'error' && oauthStatus.toRetry) {
|
|
80
|
+
setPastedCode('')
|
|
81
|
+
setOAuthStatus({
|
|
82
|
+
state: 'about_to_retry',
|
|
83
|
+
nextState: oauthStatus.toRetry,
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
async function handleSubmitCode(value: string, url: string) {
|
|
90
|
+
try {
|
|
91
|
+
// Expecting format "authorizationCode#state" from the authorization callback URL
|
|
92
|
+
const [authorizationCode, state] = value.split('#')
|
|
93
|
+
|
|
94
|
+
if (!authorizationCode || !state) {
|
|
95
|
+
setOAuthStatus({
|
|
96
|
+
state: 'error',
|
|
97
|
+
message: 'Invalid code. Please make sure the full code was copied',
|
|
98
|
+
toRetry: { state: 'waiting_for_login', url },
|
|
99
|
+
})
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Track which path the user is taking (manual code entry)
|
|
104
|
+
logEvent('tengu_oauth_manual_entry', {})
|
|
105
|
+
oauthService.processCallback({
|
|
106
|
+
authorizationCode,
|
|
107
|
+
state,
|
|
108
|
+
useManualRedirect: true,
|
|
109
|
+
})
|
|
110
|
+
} catch (err) {
|
|
111
|
+
logError(err)
|
|
112
|
+
setOAuthStatus({
|
|
113
|
+
state: 'error',
|
|
114
|
+
message: (err as Error).message,
|
|
115
|
+
toRetry: { state: 'waiting_for_login', url },
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const startOAuth = useCallback(async () => {
|
|
121
|
+
try {
|
|
122
|
+
const result = await oauthService
|
|
123
|
+
.startOAuthFlow(async url => {
|
|
124
|
+
setOAuthStatus({ state: 'waiting_for_login', url })
|
|
125
|
+
setTimeout(() => setShowPastePrompt(true), 3000)
|
|
126
|
+
})
|
|
127
|
+
.catch(err => {
|
|
128
|
+
// Handle token exchange errors specifically
|
|
129
|
+
if (err.message.includes('Token exchange failed')) {
|
|
130
|
+
setOAuthStatus({
|
|
131
|
+
state: 'error',
|
|
132
|
+
message:
|
|
133
|
+
'Failed to exchange authorization code for access token. Please try again.',
|
|
134
|
+
toRetry: { state: 'ready_to_start' },
|
|
135
|
+
})
|
|
136
|
+
logEvent('tengu_oauth_token_exchange_error', { error: err.message })
|
|
137
|
+
} else {
|
|
138
|
+
// Handle other errors
|
|
139
|
+
setOAuthStatus({
|
|
140
|
+
state: 'error',
|
|
141
|
+
message: err.message,
|
|
142
|
+
toRetry: { state: 'ready_to_start' },
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
throw err
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
setOAuthStatus({ state: 'creating_api_key' })
|
|
149
|
+
|
|
150
|
+
const apiKey = await createAndStoreApiKey(result.accessToken).catch(
|
|
151
|
+
err => {
|
|
152
|
+
setOAuthStatus({
|
|
153
|
+
state: 'error',
|
|
154
|
+
message: 'Failed to create API key: ' + err.message,
|
|
155
|
+
toRetry: { state: 'ready_to_start' },
|
|
156
|
+
})
|
|
157
|
+
logEvent('tengu_oauth_api_key_error', { error: err.message })
|
|
158
|
+
throw err
|
|
159
|
+
},
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
if (apiKey) {
|
|
163
|
+
setOAuthStatus({ state: 'success', apiKey })
|
|
164
|
+
sendNotification({ message: 'Kode login successful' })
|
|
165
|
+
} else {
|
|
166
|
+
setOAuthStatus({
|
|
167
|
+
state: 'error',
|
|
168
|
+
message:
|
|
169
|
+
"Unable to create API key. The server accepted the request but didn't return a key.",
|
|
170
|
+
toRetry: { state: 'ready_to_start' },
|
|
171
|
+
})
|
|
172
|
+
logEvent('tengu_oauth_api_key_error', {
|
|
173
|
+
error: 'server_returned_no_key',
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
} catch (err) {
|
|
177
|
+
const errorMessage = (err as Error).message
|
|
178
|
+
logEvent('tengu_oauth_error', { error: errorMessage })
|
|
179
|
+
}
|
|
180
|
+
}, [oauthService, setShowPastePrompt])
|
|
181
|
+
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
if (oauthStatus.state === 'ready_to_start') {
|
|
184
|
+
startOAuth()
|
|
185
|
+
}
|
|
186
|
+
}, [oauthStatus.state, startOAuth])
|
|
187
|
+
|
|
188
|
+
// Helper function to render the appropriate status message
|
|
189
|
+
function renderStatusMessage(): React.ReactNode {
|
|
190
|
+
switch (oauthStatus.state) {
|
|
191
|
+
case 'idle':
|
|
192
|
+
return (
|
|
193
|
+
<Box flexDirection="column" gap={1}>
|
|
194
|
+
<Text bold>
|
|
195
|
+
{PRODUCT_NAME} is billed based on API usage through your Anthropic
|
|
196
|
+
Console account.
|
|
197
|
+
</Text>
|
|
198
|
+
|
|
199
|
+
<Box>
|
|
200
|
+
<Text>
|
|
201
|
+
Pricing may evolve as we move towards general availability.
|
|
202
|
+
</Text>
|
|
203
|
+
</Box>
|
|
204
|
+
|
|
205
|
+
<Box marginTop={1}>
|
|
206
|
+
<Text color={theme.permission}>
|
|
207
|
+
Press <Text bold>Enter</Text> to login to your Anthropic Console
|
|
208
|
+
account…
|
|
209
|
+
</Text>
|
|
210
|
+
</Box>
|
|
211
|
+
</Box>
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
case 'waiting_for_login':
|
|
215
|
+
return (
|
|
216
|
+
<Box flexDirection="column" gap={1}>
|
|
217
|
+
{!showPastePrompt && (
|
|
218
|
+
<Box>
|
|
219
|
+
<SimpleSpinner />
|
|
220
|
+
<Text>Opening browser to sign in…</Text>
|
|
221
|
+
</Box>
|
|
222
|
+
)}
|
|
223
|
+
|
|
224
|
+
{showPastePrompt && (
|
|
225
|
+
<Box>
|
|
226
|
+
<Text>{PASTE_HERE_MSG}</Text>
|
|
227
|
+
<TextInput
|
|
228
|
+
value={pastedCode}
|
|
229
|
+
onChange={setPastedCode}
|
|
230
|
+
onSubmit={(value: string) =>
|
|
231
|
+
handleSubmitCode(value, oauthStatus.url)
|
|
232
|
+
}
|
|
233
|
+
cursorOffset={cursorOffset}
|
|
234
|
+
onChangeCursorOffset={setCursorOffset}
|
|
235
|
+
columns={textInputColumns}
|
|
236
|
+
/>
|
|
237
|
+
</Box>
|
|
238
|
+
)}
|
|
239
|
+
</Box>
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
case 'creating_api_key':
|
|
243
|
+
return (
|
|
244
|
+
<Box flexDirection="column" gap={1}>
|
|
245
|
+
<Box>
|
|
246
|
+
<SimpleSpinner />
|
|
247
|
+
<Text>Creating API key for Kode…</Text>
|
|
248
|
+
</Box>
|
|
249
|
+
</Box>
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
case 'about_to_retry':
|
|
253
|
+
return (
|
|
254
|
+
<Box flexDirection="column" gap={1}>
|
|
255
|
+
<Text color={theme.permission}>Retrying…</Text>
|
|
256
|
+
</Box>
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
case 'success':
|
|
260
|
+
return (
|
|
261
|
+
<Box flexDirection="column" gap={1}>
|
|
262
|
+
<Text color={theme.success}>
|
|
263
|
+
Login successful. Press <Text bold>Enter</Text> to continue…
|
|
264
|
+
</Text>
|
|
265
|
+
</Box>
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
case 'error':
|
|
269
|
+
return (
|
|
270
|
+
<Box flexDirection="column" gap={1}>
|
|
271
|
+
<Text color={theme.error}>OAuth error: {oauthStatus.message}</Text>
|
|
272
|
+
|
|
273
|
+
{oauthStatus.toRetry && (
|
|
274
|
+
<Box marginTop={1}>
|
|
275
|
+
<Text color={theme.permission}>
|
|
276
|
+
Press <Text bold>Enter</Text> to retry.
|
|
277
|
+
</Text>
|
|
278
|
+
</Box>
|
|
279
|
+
)}
|
|
280
|
+
</Box>
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
default:
|
|
284
|
+
return null
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// We need to render the copy-able URL statically to prevent Ink <Text> from inserting
|
|
289
|
+
// newlines in the middle of the URL (this breaks Safari). Because <Static> components are
|
|
290
|
+
// only rendered once top-to-bottom, we also need to make everything above the URL static.
|
|
291
|
+
const staticItems: Record<string, JSX.Element> = {}
|
|
292
|
+
if (!isClearing) {
|
|
293
|
+
staticItems.header = (
|
|
294
|
+
<Box key="header" flexDirection="column" gap={1}>
|
|
295
|
+
<WelcomeBox />
|
|
296
|
+
<Box paddingBottom={1} paddingLeft={1}>
|
|
297
|
+
<AsciiLogo />
|
|
298
|
+
</Box>
|
|
299
|
+
</Box>
|
|
300
|
+
)
|
|
301
|
+
}
|
|
302
|
+
if (oauthStatus.state === 'waiting_for_login' && showPastePrompt) {
|
|
303
|
+
staticItems.urlToCopy = (
|
|
304
|
+
<Box flexDirection="column" key="urlToCopy" gap={1} paddingBottom={1}>
|
|
305
|
+
<Box paddingX={1}>
|
|
306
|
+
<Text dimColor>
|
|
307
|
+
Browser didn't open? Use the url below to sign in:
|
|
308
|
+
</Text>
|
|
309
|
+
</Box>
|
|
310
|
+
<Box width={1000}>
|
|
311
|
+
<Text dimColor>{oauthStatus.url}</Text>
|
|
312
|
+
</Box>
|
|
313
|
+
</Box>
|
|
314
|
+
)
|
|
315
|
+
}
|
|
316
|
+
return (
|
|
317
|
+
<Box flexDirection="column" gap={1}>
|
|
318
|
+
<Static items={Object.keys(staticItems)}>
|
|
319
|
+
{item => staticItems[item]}
|
|
320
|
+
</Static>
|
|
321
|
+
<Box paddingLeft={1} flexDirection="column" gap={1}>
|
|
322
|
+
{renderStatusMessage()}
|
|
323
|
+
</Box>
|
|
324
|
+
</Box>
|
|
325
|
+
)
|
|
326
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { Box, Text } from 'ink'
|
|
3
|
+
|
|
4
|
+
type Props = {
|
|
5
|
+
costUSD: number
|
|
6
|
+
durationMs: number
|
|
7
|
+
debug: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function Cost({ costUSD, durationMs, debug }: Props): React.ReactNode {
|
|
11
|
+
if (!debug) {
|
|
12
|
+
return null
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const durationInSeconds = (durationMs / 1000).toFixed(1)
|
|
16
|
+
return (
|
|
17
|
+
<Box flexDirection="column" minWidth={23} width={23}>
|
|
18
|
+
<Text dimColor>
|
|
19
|
+
Cost: ${costUSD.toFixed(4)} ({durationInSeconds}s)
|
|
20
|
+
</Text>
|
|
21
|
+
</Box>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Box, Text, useInput } from 'ink'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import { Select } from './CustomSelect/select'
|
|
4
|
+
import { getTheme } from '../utils/theme'
|
|
5
|
+
import Link from './Link'
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
onDone: () => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function CostThresholdDialog({ onDone }: Props): React.ReactNode {
|
|
12
|
+
// Handle Ctrl+C, Ctrl+D and Esc
|
|
13
|
+
useInput((input, key) => {
|
|
14
|
+
if ((key.ctrl && (input === 'c' || input === 'd')) || key.escape) {
|
|
15
|
+
onDone()
|
|
16
|
+
}
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<Box
|
|
21
|
+
flexDirection="column"
|
|
22
|
+
borderStyle="round"
|
|
23
|
+
padding={1}
|
|
24
|
+
borderColor={getTheme().secondaryBorder}
|
|
25
|
+
>
|
|
26
|
+
<Box marginBottom={1} flexDirection="column">
|
|
27
|
+
<Text bold>
|
|
28
|
+
You've spent $5 on AI model API calls this session.
|
|
29
|
+
</Text>
|
|
30
|
+
<Text>Learn more about monitoring your AI usage costs:</Text>
|
|
31
|
+
<Link url="https://github.com/anthropics/claude-code/docs/cost-monitoring" />
|
|
32
|
+
</Box>
|
|
33
|
+
<Box>
|
|
34
|
+
<Select
|
|
35
|
+
options={[
|
|
36
|
+
{
|
|
37
|
+
value: 'ok',
|
|
38
|
+
label: 'Got it, thanks!',
|
|
39
|
+
},
|
|
40
|
+
]}
|
|
41
|
+
onChange={onDone}
|
|
42
|
+
/>
|
|
43
|
+
</Box>
|
|
44
|
+
</Box>
|
|
45
|
+
)
|
|
46
|
+
}
|