@shareai-lab/kode 1.0.69 → 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 +205 -72
- 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,373 @@
|
|
|
1
|
+
import * as fs from 'fs'
|
|
2
|
+
import { homedir } from 'os'
|
|
3
|
+
import { existsSync } from 'fs'
|
|
4
|
+
import shellquote from 'shell-quote'
|
|
5
|
+
import { spawn, execSync, type ChildProcess } from 'child_process'
|
|
6
|
+
import { isAbsolute, resolve, join } from 'path'
|
|
7
|
+
import { logError } from './log'
|
|
8
|
+
import * as os from 'os'
|
|
9
|
+
import { logEvent } from '../services/statsig'
|
|
10
|
+
import { PRODUCT_COMMAND } from '../constants/product'
|
|
11
|
+
|
|
12
|
+
type ExecResult = {
|
|
13
|
+
stdout: string
|
|
14
|
+
stderr: string
|
|
15
|
+
code: number
|
|
16
|
+
interrupted: boolean
|
|
17
|
+
}
|
|
18
|
+
type QueuedCommand = {
|
|
19
|
+
command: string
|
|
20
|
+
abortSignal?: AbortSignal
|
|
21
|
+
timeout?: number
|
|
22
|
+
resolve: (result: ExecResult) => void
|
|
23
|
+
reject: (error: Error) => void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const TEMPFILE_PREFIX = os.tmpdir() + `/${PRODUCT_COMMAND}-`
|
|
27
|
+
const DEFAULT_TIMEOUT = 30 * 60 * 1000
|
|
28
|
+
const SIGTERM_CODE = 143 // Standard exit code for SIGTERM
|
|
29
|
+
const FILE_SUFFIXES = {
|
|
30
|
+
STATUS: '-status',
|
|
31
|
+
STDOUT: '-stdout',
|
|
32
|
+
STDERR: '-stderr',
|
|
33
|
+
CWD: '-cwd',
|
|
34
|
+
}
|
|
35
|
+
const SHELL_CONFIGS: Record<string, string> = {
|
|
36
|
+
'/bin/bash': '.bashrc',
|
|
37
|
+
'/bin/zsh': '.zshrc',
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class PersistentShell {
|
|
41
|
+
private commandQueue: QueuedCommand[] = []
|
|
42
|
+
private isExecuting: boolean = false
|
|
43
|
+
private shell: ChildProcess
|
|
44
|
+
private isAlive: boolean = true
|
|
45
|
+
private commandInterrupted: boolean = false
|
|
46
|
+
private statusFile: string
|
|
47
|
+
private stdoutFile: string
|
|
48
|
+
private stderrFile: string
|
|
49
|
+
private cwdFile: string
|
|
50
|
+
private cwd: string
|
|
51
|
+
private binShell: string
|
|
52
|
+
|
|
53
|
+
constructor(cwd: string) {
|
|
54
|
+
this.binShell = process.env.SHELL || '/bin/bash'
|
|
55
|
+
this.shell = spawn(this.binShell, ['-l'], {
|
|
56
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
57
|
+
cwd,
|
|
58
|
+
env: {
|
|
59
|
+
...process.env,
|
|
60
|
+
GIT_EDITOR: 'true',
|
|
61
|
+
},
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
this.cwd = cwd
|
|
65
|
+
|
|
66
|
+
this.shell.on('exit', (code, signal) => {
|
|
67
|
+
if (code) {
|
|
68
|
+
// TODO: It would be nice to alert the user that shell crashed
|
|
69
|
+
logError(`Shell exited with code ${code} and signal ${signal}`)
|
|
70
|
+
logEvent('persistent_shell_exit', {
|
|
71
|
+
code: code?.toString() || 'null',
|
|
72
|
+
signal: signal || 'null',
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
for (const file of [
|
|
76
|
+
this.statusFile,
|
|
77
|
+
this.stdoutFile,
|
|
78
|
+
this.stderrFile,
|
|
79
|
+
this.cwdFile,
|
|
80
|
+
]) {
|
|
81
|
+
if (fs.existsSync(file)) {
|
|
82
|
+
fs.unlinkSync(file)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
this.isAlive = false
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
const id = Math.floor(Math.random() * 0x10000)
|
|
89
|
+
.toString(16)
|
|
90
|
+
.padStart(4, '0')
|
|
91
|
+
|
|
92
|
+
this.statusFile = TEMPFILE_PREFIX + id + FILE_SUFFIXES.STATUS
|
|
93
|
+
this.stdoutFile = TEMPFILE_PREFIX + id + FILE_SUFFIXES.STDOUT
|
|
94
|
+
this.stderrFile = TEMPFILE_PREFIX + id + FILE_SUFFIXES.STDERR
|
|
95
|
+
this.cwdFile = TEMPFILE_PREFIX + id + FILE_SUFFIXES.CWD
|
|
96
|
+
for (const file of [this.statusFile, this.stdoutFile, this.stderrFile]) {
|
|
97
|
+
fs.writeFileSync(file, '')
|
|
98
|
+
}
|
|
99
|
+
// Initialize CWD file with initial directory
|
|
100
|
+
fs.writeFileSync(this.cwdFile, cwd)
|
|
101
|
+
const configFile = SHELL_CONFIGS[this.binShell]
|
|
102
|
+
if (configFile) {
|
|
103
|
+
const configFilePath = join(homedir(), configFile)
|
|
104
|
+
if (existsSync(configFilePath)) {
|
|
105
|
+
this.sendToShell(`source ${configFilePath}`)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private static instance: PersistentShell | null = null
|
|
111
|
+
|
|
112
|
+
static restart() {
|
|
113
|
+
if (PersistentShell.instance) {
|
|
114
|
+
PersistentShell.instance.close()
|
|
115
|
+
PersistentShell.instance = null
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
static getInstance(): PersistentShell {
|
|
120
|
+
if (!PersistentShell.instance || !PersistentShell.instance.isAlive) {
|
|
121
|
+
PersistentShell.instance = new PersistentShell(process.cwd())
|
|
122
|
+
}
|
|
123
|
+
return PersistentShell.instance
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
killChildren() {
|
|
127
|
+
const parentPid = this.shell.pid
|
|
128
|
+
try {
|
|
129
|
+
const childPids = execSync(`pgrep -P ${parentPid}`)
|
|
130
|
+
.toString()
|
|
131
|
+
.trim()
|
|
132
|
+
.split('\n')
|
|
133
|
+
.filter(Boolean) // Filter out empty strings
|
|
134
|
+
|
|
135
|
+
if (childPids.length > 0) {
|
|
136
|
+
logEvent('persistent_shell_command_interrupted', {
|
|
137
|
+
numChildProcesses: childPids.length.toString(),
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
childPids.forEach(pid => {
|
|
142
|
+
try {
|
|
143
|
+
process.kill(Number(pid), 'SIGTERM')
|
|
144
|
+
} catch (error) {
|
|
145
|
+
logError(`Failed to kill process ${pid}: ${error}`)
|
|
146
|
+
logEvent('persistent_shell_kill_process_error', {
|
|
147
|
+
error: (error as Error).message.substring(0, 10),
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
} catch {
|
|
152
|
+
// pgrep returns non-zero when no processes are found - this is expected
|
|
153
|
+
} finally {
|
|
154
|
+
this.commandInterrupted = true
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private async processQueue() {
|
|
159
|
+
/**
|
|
160
|
+
* Processes commands from the queue one at a time.
|
|
161
|
+
* Concurrency invariants:
|
|
162
|
+
* - Only one instance runs at a time (controlled by isExecuting)
|
|
163
|
+
* - Is the only caller of updateCwd() in the system
|
|
164
|
+
* - Calls updateCwd() after each command completes
|
|
165
|
+
* - Ensures commands execute serially via the queue
|
|
166
|
+
* - Handles interruption via abortSignal by calling killChildren()
|
|
167
|
+
* - Cleans up abortSignal listeners after command completion or interruption
|
|
168
|
+
*/
|
|
169
|
+
if (this.isExecuting || this.commandQueue.length === 0) return
|
|
170
|
+
|
|
171
|
+
this.isExecuting = true
|
|
172
|
+
const { command, abortSignal, timeout, resolve, reject } =
|
|
173
|
+
this.commandQueue.shift()!
|
|
174
|
+
|
|
175
|
+
const killChildren = () => this.killChildren()
|
|
176
|
+
if (abortSignal) {
|
|
177
|
+
abortSignal.addEventListener('abort', killChildren)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
const result = await this.exec_(command, timeout)
|
|
182
|
+
|
|
183
|
+
// No need to update cwd - it's handled in exec_ via the CWD file
|
|
184
|
+
|
|
185
|
+
resolve(result)
|
|
186
|
+
} catch (error) {
|
|
187
|
+
logEvent('persistent_shell_command_error', {
|
|
188
|
+
error: (error as Error).message.substring(0, 10),
|
|
189
|
+
})
|
|
190
|
+
reject(error as Error)
|
|
191
|
+
} finally {
|
|
192
|
+
this.isExecuting = false
|
|
193
|
+
if (abortSignal) {
|
|
194
|
+
abortSignal.removeEventListener('abort', killChildren)
|
|
195
|
+
}
|
|
196
|
+
// Process next command in queue
|
|
197
|
+
this.processQueue()
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async exec(
|
|
202
|
+
command: string,
|
|
203
|
+
abortSignal?: AbortSignal,
|
|
204
|
+
timeout?: number,
|
|
205
|
+
): Promise<ExecResult> {
|
|
206
|
+
return new Promise((resolve, reject) => {
|
|
207
|
+
this.commandQueue.push({ command, abortSignal, timeout, resolve, reject })
|
|
208
|
+
this.processQueue()
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private async exec_(command: string, timeout?: number): Promise<ExecResult> {
|
|
213
|
+
/**
|
|
214
|
+
* Direct command execution without going through the queue.
|
|
215
|
+
* Concurrency invariants:
|
|
216
|
+
* - Not safe for concurrent calls (uses shared files)
|
|
217
|
+
* - Called only when queue is idle
|
|
218
|
+
* - Relies on file-based IPC to handle shell interaction
|
|
219
|
+
* - Does not modify the command queue state
|
|
220
|
+
* - Tracks interruption state via commandInterrupted flag
|
|
221
|
+
* - Resets interruption state at start of new command
|
|
222
|
+
* - Reports interruption status in result object
|
|
223
|
+
*
|
|
224
|
+
* Exit Code & CWD Handling:
|
|
225
|
+
* - Executes command and immediately captures its exit code into a shell variable
|
|
226
|
+
* - Updates the CWD file with the working directory after capturing exit code
|
|
227
|
+
* - Writes the preserved exit code to the status file as the final step
|
|
228
|
+
* - This sequence eliminates race conditions between exit code capture and CWD updates
|
|
229
|
+
* - The pwd() method reads the CWD file directly for current directory info
|
|
230
|
+
*/
|
|
231
|
+
const quotedCommand = shellquote.quote([command])
|
|
232
|
+
|
|
233
|
+
// Check the syntax of the command
|
|
234
|
+
try {
|
|
235
|
+
execSync(`${this.binShell} -n -c ${quotedCommand}`, {
|
|
236
|
+
stdio: 'ignore',
|
|
237
|
+
timeout: 1000,
|
|
238
|
+
})
|
|
239
|
+
} catch (stderr) {
|
|
240
|
+
// If there's a syntax error, return an error and log it
|
|
241
|
+
const errorStr =
|
|
242
|
+
typeof stderr === 'string' ? stderr : String(stderr || '')
|
|
243
|
+
logEvent('persistent_shell_syntax_error', {
|
|
244
|
+
error: errorStr.substring(0, 10),
|
|
245
|
+
})
|
|
246
|
+
return Promise.resolve({
|
|
247
|
+
stdout: '',
|
|
248
|
+
stderr: errorStr,
|
|
249
|
+
code: 128,
|
|
250
|
+
interrupted: false,
|
|
251
|
+
})
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const commandTimeout = timeout || DEFAULT_TIMEOUT
|
|
255
|
+
// Reset interrupted state for new command
|
|
256
|
+
this.commandInterrupted = false
|
|
257
|
+
return new Promise<ExecResult>(resolve => {
|
|
258
|
+
// Truncate output files
|
|
259
|
+
fs.writeFileSync(this.stdoutFile, '')
|
|
260
|
+
fs.writeFileSync(this.stderrFile, '')
|
|
261
|
+
fs.writeFileSync(this.statusFile, '')
|
|
262
|
+
// Break up the command sequence for clarity using an array of commands
|
|
263
|
+
const commandParts = []
|
|
264
|
+
|
|
265
|
+
// 1. Execute the main command with redirections
|
|
266
|
+
commandParts.push(
|
|
267
|
+
`eval ${quotedCommand} < /dev/null > ${this.stdoutFile} 2> ${this.stderrFile}`,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
// 2. Capture exit code immediately after command execution to avoid losing it
|
|
271
|
+
commandParts.push(`EXEC_EXIT_CODE=$?`)
|
|
272
|
+
|
|
273
|
+
// 3. Update CWD file
|
|
274
|
+
commandParts.push(`pwd > ${this.cwdFile}`)
|
|
275
|
+
|
|
276
|
+
// 4. Write the preserved exit code to status file to avoid race with pwd
|
|
277
|
+
commandParts.push(`echo $EXEC_EXIT_CODE > ${this.statusFile}`)
|
|
278
|
+
|
|
279
|
+
// Send the combined commands as a single operation to maintain atomicity
|
|
280
|
+
this.sendToShell(commandParts.join('\n'))
|
|
281
|
+
|
|
282
|
+
// Check for command completion or timeout
|
|
283
|
+
const start = Date.now()
|
|
284
|
+
const checkCompletion = setInterval(() => {
|
|
285
|
+
try {
|
|
286
|
+
let statusFileSize = 0
|
|
287
|
+
if (fs.existsSync(this.statusFile)) {
|
|
288
|
+
statusFileSize = fs.statSync(this.statusFile).size
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (
|
|
292
|
+
statusFileSize > 0 ||
|
|
293
|
+
Date.now() - start > commandTimeout ||
|
|
294
|
+
this.commandInterrupted
|
|
295
|
+
) {
|
|
296
|
+
clearInterval(checkCompletion)
|
|
297
|
+
const stdout = fs.existsSync(this.stdoutFile)
|
|
298
|
+
? fs.readFileSync(this.stdoutFile, 'utf8')
|
|
299
|
+
: ''
|
|
300
|
+
let stderr = fs.existsSync(this.stderrFile)
|
|
301
|
+
? fs.readFileSync(this.stderrFile, 'utf8')
|
|
302
|
+
: ''
|
|
303
|
+
let code: number
|
|
304
|
+
if (statusFileSize) {
|
|
305
|
+
code = Number(fs.readFileSync(this.statusFile, 'utf8'))
|
|
306
|
+
} else {
|
|
307
|
+
// Timeout occurred - kill any running processes
|
|
308
|
+
this.killChildren()
|
|
309
|
+
code = SIGTERM_CODE
|
|
310
|
+
stderr += (stderr ? '\n' : '') + 'Command execution timed out'
|
|
311
|
+
logEvent('persistent_shell_command_timeout', {
|
|
312
|
+
command: command.substring(0, 10),
|
|
313
|
+
timeout: commandTimeout.toString(),
|
|
314
|
+
})
|
|
315
|
+
}
|
|
316
|
+
resolve({
|
|
317
|
+
stdout,
|
|
318
|
+
stderr,
|
|
319
|
+
code,
|
|
320
|
+
interrupted: this.commandInterrupted,
|
|
321
|
+
})
|
|
322
|
+
}
|
|
323
|
+
} catch {
|
|
324
|
+
// Ignore file system errors during polling - they are expected
|
|
325
|
+
// as we check for completion before files exist
|
|
326
|
+
}
|
|
327
|
+
}, 10) // increasing this will introduce latency
|
|
328
|
+
})
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private sendToShell(command: string) {
|
|
332
|
+
try {
|
|
333
|
+
this.shell!.stdin!.write(command + '\n')
|
|
334
|
+
} catch (error) {
|
|
335
|
+
const errorString =
|
|
336
|
+
error instanceof Error
|
|
337
|
+
? error.message
|
|
338
|
+
: String(error || 'Unknown error')
|
|
339
|
+
logError(`Error in sendToShell: ${errorString}`)
|
|
340
|
+
logEvent('persistent_shell_write_error', {
|
|
341
|
+
error: errorString.substring(0, 100),
|
|
342
|
+
command: command.substring(0, 30),
|
|
343
|
+
})
|
|
344
|
+
throw error
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
pwd(): string {
|
|
349
|
+
try {
|
|
350
|
+
const newCwd = fs.readFileSync(this.cwdFile, 'utf8').trim()
|
|
351
|
+
if (newCwd) {
|
|
352
|
+
this.cwd = newCwd
|
|
353
|
+
}
|
|
354
|
+
} catch (error) {
|
|
355
|
+
logError(`Shell pwd error ${error}`)
|
|
356
|
+
}
|
|
357
|
+
// Always return the cached value
|
|
358
|
+
return this.cwd
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async setCwd(cwd: string) {
|
|
362
|
+
const resolved = isAbsolute(cwd) ? cwd : resolve(process.cwd(), cwd)
|
|
363
|
+
if (!existsSync(resolved)) {
|
|
364
|
+
throw new Error(`Path "${resolved}" does not exist`)
|
|
365
|
+
}
|
|
366
|
+
await this.exec(`cd ${resolved}`)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
close(): void {
|
|
370
|
+
this.shell!.stdin!.end()
|
|
371
|
+
this.shell.kill()
|
|
372
|
+
}
|
|
373
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs'
|
|
2
|
+
import { join } from 'path'
|
|
3
|
+
import { homedir } from 'os'
|
|
4
|
+
import { randomUUID } from 'crypto'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Agent Storage Utilities
|
|
8
|
+
* Provides file-based state isolation for different agents
|
|
9
|
+
* Based on Kode's Agent ID architecture
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Get the kode config directory
|
|
14
|
+
*/
|
|
15
|
+
function getConfigDirectory(): string {
|
|
16
|
+
return process.env.KODE_CONFIG_DIR ?? process.env.ANYKODE_CONFIG_DIR ?? join(homedir(), '.kode')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get the current session ID
|
|
21
|
+
*/
|
|
22
|
+
function getSessionId(): string {
|
|
23
|
+
// This should be set when the session starts
|
|
24
|
+
return process.env.ANYKODE_SESSION_ID ?? 'default-session'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Generate agent-specific file path
|
|
29
|
+
* Pattern: ${sessionId}-agent-${agentId}.json
|
|
30
|
+
* Stored in ~/.kode/ directory
|
|
31
|
+
*/
|
|
32
|
+
export function getAgentFilePath(agentId: string): string {
|
|
33
|
+
const sessionId = getSessionId()
|
|
34
|
+
const filename = `${sessionId}-agent-${agentId}.json`
|
|
35
|
+
const configDir = getConfigDirectory()
|
|
36
|
+
|
|
37
|
+
// Ensure kode config directory exists
|
|
38
|
+
if (!existsSync(configDir)) {
|
|
39
|
+
mkdirSync(configDir, { recursive: true })
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return join(configDir, filename)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Read agent-specific data from storage
|
|
47
|
+
*/
|
|
48
|
+
export function readAgentData<T = any>(agentId: string): T | null {
|
|
49
|
+
const filePath = getAgentFilePath(agentId)
|
|
50
|
+
|
|
51
|
+
if (!existsSync(filePath)) {
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const content = readFileSync(filePath, 'utf-8')
|
|
57
|
+
return JSON.parse(content) as T
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error(`Failed to read agent data for ${agentId}:`, error)
|
|
60
|
+
return null
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Write agent-specific data to storage
|
|
66
|
+
*/
|
|
67
|
+
export function writeAgentData<T = any>(agentId: string, data: T): void {
|
|
68
|
+
const filePath = getAgentFilePath(agentId)
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8')
|
|
72
|
+
} catch (error) {
|
|
73
|
+
console.error(`Failed to write agent data for ${agentId}:`, error)
|
|
74
|
+
throw error
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get default agent ID if none is provided
|
|
80
|
+
*/
|
|
81
|
+
export function getDefaultAgentId(): string {
|
|
82
|
+
return 'default'
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Resolve agent ID from context
|
|
87
|
+
*/
|
|
88
|
+
export function resolveAgentId(agentId?: string): string {
|
|
89
|
+
return agentId || getDefaultAgentId()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Generate a new unique Agent ID
|
|
94
|
+
*/
|
|
95
|
+
export function generateAgentId(): string {
|
|
96
|
+
return randomUUID()
|
|
97
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { last } from 'lodash-es'
|
|
2
|
+
import { Command } from '../commands'
|
|
3
|
+
import { getSystemPrompt } from '../constants/prompts'
|
|
4
|
+
import { getContext } from '../context'
|
|
5
|
+
import { getTotalCost } from '../cost-tracker'
|
|
6
|
+
import { Message, query } from '../query'
|
|
7
|
+
import { CanUseToolFn } from '../hooks/useCanUseTool'
|
|
8
|
+
import { Tool } from '../Tool'
|
|
9
|
+
import { getModelManager } from '../utils/model'
|
|
10
|
+
import { setCwd } from './state'
|
|
11
|
+
import { getMessagesPath, overwriteLog } from './log'
|
|
12
|
+
import { createUserMessage } from './messages'
|
|
13
|
+
|
|
14
|
+
type Props = {
|
|
15
|
+
commands: Command[]
|
|
16
|
+
safeMode?: boolean
|
|
17
|
+
hasPermissionsToUseTool: CanUseToolFn
|
|
18
|
+
messageLogName: string
|
|
19
|
+
prompt: string
|
|
20
|
+
cwd: string
|
|
21
|
+
tools: Tool[]
|
|
22
|
+
verbose?: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Sends a single prompt to the Claude API and returns the response.
|
|
26
|
+
// Assumes that claude is being used non-interactively -- will not
|
|
27
|
+
// ask the user for permissions or further input.
|
|
28
|
+
export async function ask({
|
|
29
|
+
commands,
|
|
30
|
+
safeMode,
|
|
31
|
+
hasPermissionsToUseTool,
|
|
32
|
+
messageLogName,
|
|
33
|
+
prompt,
|
|
34
|
+
cwd,
|
|
35
|
+
tools,
|
|
36
|
+
verbose = false,
|
|
37
|
+
}: Props): Promise<{
|
|
38
|
+
resultText: string
|
|
39
|
+
totalCost: number
|
|
40
|
+
messageHistoryFile: string
|
|
41
|
+
}> {
|
|
42
|
+
await setCwd(cwd)
|
|
43
|
+
const message = createUserMessage(prompt)
|
|
44
|
+
const messages: Message[] = [message]
|
|
45
|
+
|
|
46
|
+
const [systemPrompt, context, model] = await Promise.all([
|
|
47
|
+
getSystemPrompt(),
|
|
48
|
+
getContext(),
|
|
49
|
+
getModelManager().getModelName('main'),
|
|
50
|
+
])
|
|
51
|
+
|
|
52
|
+
for await (const m of query(
|
|
53
|
+
messages,
|
|
54
|
+
systemPrompt,
|
|
55
|
+
context,
|
|
56
|
+
hasPermissionsToUseTool,
|
|
57
|
+
{
|
|
58
|
+
options: {
|
|
59
|
+
commands,
|
|
60
|
+
tools,
|
|
61
|
+
verbose,
|
|
62
|
+
safeMode,
|
|
63
|
+
forkNumber: 0,
|
|
64
|
+
messageLogName: 'unused',
|
|
65
|
+
maxThinkingTokens: 0,
|
|
66
|
+
},
|
|
67
|
+
abortController: new AbortController(),
|
|
68
|
+
messageId: undefined,
|
|
69
|
+
readFileTimestamps: {},
|
|
70
|
+
},
|
|
71
|
+
)) {
|
|
72
|
+
messages.push(m)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const result = last(messages)
|
|
76
|
+
if (!result || result.type !== 'assistant') {
|
|
77
|
+
throw new Error('Expected content to be an assistant message')
|
|
78
|
+
}
|
|
79
|
+
if (result.message.content[0]?.type !== 'text') {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`Expected first content item to be text, but got ${JSON.stringify(
|
|
82
|
+
result.message.content[0],
|
|
83
|
+
null,
|
|
84
|
+
2,
|
|
85
|
+
)}`,
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Write log that can be retrieved with `claude log`
|
|
90
|
+
const messageHistoryFile = getMessagesPath(messageLogName, 0, 0)
|
|
91
|
+
overwriteLog(messageHistoryFile, messages)
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
resultText: result.message.content[0].text,
|
|
95
|
+
totalCost: getTotalCost(),
|
|
96
|
+
messageHistoryFile,
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { USE_BEDROCK, USE_VERTEX } from './model'
|
|
2
|
+
import { getGlobalConfig } from './config'
|
|
3
|
+
|
|
4
|
+
export function isAnthropicAuthEnabled(): boolean {
|
|
5
|
+
return false
|
|
6
|
+
// return !(USE_BEDROCK || USE_VERTEX)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function isLoggedInToAnthropic(): boolean {
|
|
10
|
+
return false
|
|
11
|
+
// const config = getGlobalConfig()
|
|
12
|
+
// return !!config.primaryApiKey
|
|
13
|
+
}
|