@shareai-lab/kode 1.0.81 → 1.0.83
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/LICENSE +201 -661
- package/README.md +23 -2
- package/README.zh-CN.md +18 -2
- package/cli.js +52 -23
- package/package.json +7 -4
- package/src/components/ModelSelector.tsx +2 -1
- package/src/query.ts +11 -3
- package/src/services/claude.ts +166 -52
- package/src/tools/URLFetcherTool/URLFetcherTool.tsx +178 -0
- package/src/tools/URLFetcherTool/cache.ts +55 -0
- package/src/tools/URLFetcherTool/htmlToMarkdown.ts +55 -0
- package/src/tools/URLFetcherTool/prompt.ts +17 -0
- package/src/tools/WebSearchTool/WebSearchTool.tsx +103 -0
- package/src/tools/WebSearchTool/prompt.ts +13 -0
- package/src/tools/WebSearchTool/searchProviders.ts +66 -0
- package/src/tools.ts +4 -0
- package/src/utils/PersistentShell.ts +196 -17
- package/src/utils/debugLogger.ts +1 -0
- package/src/utils/log.ts +5 -6
|
@@ -37,6 +37,165 @@ const SHELL_CONFIGS: Record<string, string> = {
|
|
|
37
37
|
'/bin/zsh': '.zshrc',
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
type DetectedShell = {
|
|
41
|
+
bin: string
|
|
42
|
+
args: string[]
|
|
43
|
+
type: 'posix' | 'msys' | 'wsl'
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function quoteForBash(str: string): string {
|
|
47
|
+
return `'${str.replace(/'/g, "'\\''")}'`
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function toBashPath(pathStr: string, type: 'posix' | 'msys' | 'wsl'): string {
|
|
51
|
+
// Already POSIX absolute path
|
|
52
|
+
if (pathStr.startsWith('/')) return pathStr
|
|
53
|
+
if (type === 'posix') return pathStr
|
|
54
|
+
|
|
55
|
+
// Normalize backslashes
|
|
56
|
+
const normalized = pathStr.replace(/\\/g, '/').replace(/\\\\/g, '/')
|
|
57
|
+
const driveMatch = /^[A-Za-z]:/.exec(normalized)
|
|
58
|
+
if (driveMatch) {
|
|
59
|
+
const drive = normalized[0].toLowerCase()
|
|
60
|
+
const rest = normalized.slice(2)
|
|
61
|
+
if (type === 'msys') {
|
|
62
|
+
return `/` + drive + (rest.startsWith('/') ? rest : `/${rest}`)
|
|
63
|
+
}
|
|
64
|
+
// wsl
|
|
65
|
+
return `/mnt/` + drive + (rest.startsWith('/') ? rest : `/${rest}`)
|
|
66
|
+
}
|
|
67
|
+
// Relative path: just convert slashes
|
|
68
|
+
return normalized
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function fileExists(p: string | undefined): p is string {
|
|
72
|
+
return !!p && existsSync(p)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Robust PATH splitter for Windows and POSIX
|
|
76
|
+
function splitPathEntries(pathEnv: string, platform: NodeJS.Platform): string[] {
|
|
77
|
+
if (!pathEnv) return []
|
|
78
|
+
|
|
79
|
+
// POSIX: ':' is the separator
|
|
80
|
+
if (platform !== 'win32') {
|
|
81
|
+
return pathEnv
|
|
82
|
+
.split(':')
|
|
83
|
+
.map(s => s.trim().replace(/^"|"$/g, ''))
|
|
84
|
+
.filter(Boolean)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Windows: primarily ';', but some environments may use ':'
|
|
88
|
+
// We must not split drive letters like 'C:\\' or 'D:foo\\bar'
|
|
89
|
+
const entries: string[] = []
|
|
90
|
+
let current = ''
|
|
91
|
+
const pushCurrent = () => {
|
|
92
|
+
const cleaned = current.trim().replace(/^"|"$/g, '')
|
|
93
|
+
if (cleaned) entries.push(cleaned)
|
|
94
|
+
current = ''
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
for (let i = 0; i < pathEnv.length; i++) {
|
|
98
|
+
const ch = pathEnv[i]
|
|
99
|
+
|
|
100
|
+
if (ch === ';') {
|
|
101
|
+
pushCurrent()
|
|
102
|
+
continue
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (ch === ':') {
|
|
106
|
+
const segmentLength = current.length
|
|
107
|
+
const firstChar = current[0]
|
|
108
|
+
const isDriveLetterPrefix = segmentLength === 1 && /[A-Za-z]/.test(firstChar || '')
|
|
109
|
+
// Treat ':' as separator only if it's NOT the drive letter colon
|
|
110
|
+
if (!isDriveLetterPrefix) {
|
|
111
|
+
pushCurrent()
|
|
112
|
+
continue
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
current += ch
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Flush the final segment
|
|
120
|
+
pushCurrent()
|
|
121
|
+
|
|
122
|
+
return entries
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function detectShell(): DetectedShell {
|
|
126
|
+
const isWin = process.platform === 'win32'
|
|
127
|
+
if (!isWin) {
|
|
128
|
+
const bin = process.env.SHELL || '/bin/bash'
|
|
129
|
+
return { bin, args: ['-l'], type: 'posix' }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 1) Respect SHELL if it points to a bash.exe that exists
|
|
133
|
+
if (process.env.SHELL && /bash\.exe$/i.test(process.env.SHELL) && existsSync(process.env.SHELL)) {
|
|
134
|
+
return { bin: process.env.SHELL, args: ['-l'], type: 'msys' }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 1.1) Explicit override
|
|
138
|
+
if (process.env.KODE_BASH && existsSync(process.env.KODE_BASH)) {
|
|
139
|
+
return { bin: process.env.KODE_BASH, args: ['-l'], type: 'msys' }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 2) Common Git Bash/MSYS2 locations
|
|
143
|
+
const programFiles = [
|
|
144
|
+
process.env['ProgramFiles'],
|
|
145
|
+
process.env['ProgramFiles(x86)'],
|
|
146
|
+
process.env['ProgramW6432'],
|
|
147
|
+
].filter(Boolean) as string[]
|
|
148
|
+
|
|
149
|
+
const localAppData = process.env['LocalAppData']
|
|
150
|
+
|
|
151
|
+
const candidates: string[] = []
|
|
152
|
+
for (const base of programFiles) {
|
|
153
|
+
candidates.push(
|
|
154
|
+
join(base, 'Git', 'bin', 'bash.exe'),
|
|
155
|
+
join(base, 'Git', 'usr', 'bin', 'bash.exe'),
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
if (localAppData) {
|
|
159
|
+
candidates.push(
|
|
160
|
+
join(localAppData, 'Programs', 'Git', 'bin', 'bash.exe'),
|
|
161
|
+
join(localAppData, 'Programs', 'Git', 'usr', 'bin', 'bash.exe'),
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
// MSYS2 default
|
|
165
|
+
candidates.push('C:/msys64/usr/bin/bash.exe')
|
|
166
|
+
|
|
167
|
+
for (const c of candidates) {
|
|
168
|
+
if (existsSync(c)) {
|
|
169
|
+
return { bin: c, args: ['-l'], type: 'msys' }
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 2.1) Search in PATH for bash.exe
|
|
174
|
+
const pathEnv = process.env.PATH || process.env.Path || process.env.path || ''
|
|
175
|
+
const pathEntries = splitPathEntries(pathEnv, process.platform)
|
|
176
|
+
for (const p of pathEntries) {
|
|
177
|
+
const candidate = join(p, 'bash.exe')
|
|
178
|
+
if (existsSync(candidate)) {
|
|
179
|
+
return { bin: candidate, args: ['-l'], type: 'msys' }
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 3) WSL
|
|
184
|
+
try {
|
|
185
|
+
// Quick probe to ensure WSL+bash exists
|
|
186
|
+
execSync('wsl.exe -e bash -lc "echo KODE_OK"', { stdio: 'ignore', timeout: 1500 })
|
|
187
|
+
return { bin: 'wsl.exe', args: ['-e', 'bash', '-l'], type: 'wsl' }
|
|
188
|
+
} catch {}
|
|
189
|
+
|
|
190
|
+
// 4) Last resort: meaningful error
|
|
191
|
+
const hint = [
|
|
192
|
+
'无法找到可用的 bash。请安装 Git for Windows 或启用 WSL。',
|
|
193
|
+
'推荐安装 Git: https://git-scm.com/download/win',
|
|
194
|
+
'或启用 WSL 并安装 Ubuntu: https://learn.microsoft.com/windows/wsl/install',
|
|
195
|
+
].join('\n')
|
|
196
|
+
throw new Error(hint)
|
|
197
|
+
}
|
|
198
|
+
|
|
40
199
|
export class PersistentShell {
|
|
41
200
|
private commandQueue: QueuedCommand[] = []
|
|
42
201
|
private isExecuting: boolean = false
|
|
@@ -49,10 +208,20 @@ export class PersistentShell {
|
|
|
49
208
|
private cwdFile: string
|
|
50
209
|
private cwd: string
|
|
51
210
|
private binShell: string
|
|
211
|
+
private shellArgs: string[]
|
|
212
|
+
private shellType: 'posix' | 'msys' | 'wsl'
|
|
213
|
+
private statusFileBashPath: string
|
|
214
|
+
private stdoutFileBashPath: string
|
|
215
|
+
private stderrFileBashPath: string
|
|
216
|
+
private cwdFileBashPath: string
|
|
52
217
|
|
|
53
218
|
constructor(cwd: string) {
|
|
54
|
-
|
|
55
|
-
this.
|
|
219
|
+
const { bin, args, type } = detectShell()
|
|
220
|
+
this.binShell = bin
|
|
221
|
+
this.shellArgs = args
|
|
222
|
+
this.shellType = type
|
|
223
|
+
|
|
224
|
+
this.shell = spawn(this.binShell, this.shellArgs, {
|
|
56
225
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
57
226
|
cwd,
|
|
58
227
|
env: {
|
|
@@ -98,13 +267,15 @@ export class PersistentShell {
|
|
|
98
267
|
}
|
|
99
268
|
// Initialize CWD file with initial directory
|
|
100
269
|
fs.writeFileSync(this.cwdFile, cwd)
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
270
|
+
|
|
271
|
+
// Compute bash-visible paths for redirections
|
|
272
|
+
this.statusFileBashPath = toBashPath(this.statusFile, this.shellType)
|
|
273
|
+
this.stdoutFileBashPath = toBashPath(this.stdoutFile, this.shellType)
|
|
274
|
+
this.stderrFileBashPath = toBashPath(this.stderrFile, this.shellType)
|
|
275
|
+
this.cwdFileBashPath = toBashPath(this.cwdFile, this.shellType)
|
|
276
|
+
|
|
277
|
+
// Source ~/.bashrc when available (works for bash on POSIX/MSYS/WSL)
|
|
278
|
+
this.sendToShell('[ -f ~/.bashrc ] && source ~/.bashrc || true')
|
|
108
279
|
}
|
|
109
280
|
|
|
110
281
|
private static instance: PersistentShell | null = null
|
|
@@ -232,10 +403,17 @@ export class PersistentShell {
|
|
|
232
403
|
|
|
233
404
|
// Check the syntax of the command
|
|
234
405
|
try {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
406
|
+
if (this.shellType === 'wsl') {
|
|
407
|
+
execSync(`wsl.exe -e bash -n -c ${quotedCommand}`, {
|
|
408
|
+
stdio: 'ignore',
|
|
409
|
+
timeout: 1000,
|
|
410
|
+
})
|
|
411
|
+
} else {
|
|
412
|
+
execSync(`${this.binShell} -n -c ${quotedCommand}`, {
|
|
413
|
+
stdio: 'ignore',
|
|
414
|
+
timeout: 1000,
|
|
415
|
+
})
|
|
416
|
+
}
|
|
239
417
|
} catch (stderr) {
|
|
240
418
|
// If there's a syntax error, return an error and log it
|
|
241
419
|
const errorStr =
|
|
@@ -264,17 +442,17 @@ export class PersistentShell {
|
|
|
264
442
|
|
|
265
443
|
// 1. Execute the main command with redirections
|
|
266
444
|
commandParts.push(
|
|
267
|
-
`eval ${quotedCommand} < /dev/null > ${this.
|
|
445
|
+
`eval ${quotedCommand} < /dev/null > ${quoteForBash(this.stdoutFileBashPath)} 2> ${quoteForBash(this.stderrFileBashPath)}`,
|
|
268
446
|
)
|
|
269
447
|
|
|
270
448
|
// 2. Capture exit code immediately after command execution to avoid losing it
|
|
271
449
|
commandParts.push(`EXEC_EXIT_CODE=$?`)
|
|
272
450
|
|
|
273
451
|
// 3. Update CWD file
|
|
274
|
-
commandParts.push(`pwd > ${this.
|
|
452
|
+
commandParts.push(`pwd > ${quoteForBash(this.cwdFileBashPath)}`)
|
|
275
453
|
|
|
276
454
|
// 4. Write the preserved exit code to status file to avoid race with pwd
|
|
277
|
-
commandParts.push(`echo $EXEC_EXIT_CODE > ${this.
|
|
455
|
+
commandParts.push(`echo $EXEC_EXIT_CODE > ${quoteForBash(this.statusFileBashPath)}`)
|
|
278
456
|
|
|
279
457
|
// Send the combined commands as a single operation to maintain atomicity
|
|
280
458
|
this.sendToShell(commandParts.join('\n'))
|
|
@@ -363,7 +541,8 @@ export class PersistentShell {
|
|
|
363
541
|
if (!existsSync(resolved)) {
|
|
364
542
|
throw new Error(`Path "${resolved}" does not exist`)
|
|
365
543
|
}
|
|
366
|
-
|
|
544
|
+
const bashPath = toBashPath(resolved, this.shellType)
|
|
545
|
+
await this.exec(`cd ${quoteForBash(bashPath)}`)
|
|
367
546
|
}
|
|
368
547
|
|
|
369
548
|
close(): void {
|
package/src/utils/debugLogger.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { join } from 'path'
|
|
|
3
3
|
import { homedir } from 'os'
|
|
4
4
|
import { randomUUID } from 'crypto'
|
|
5
5
|
import chalk from 'chalk'
|
|
6
|
+
import envPaths from 'env-paths'
|
|
6
7
|
import { PRODUCT_COMMAND } from '../constants/product'
|
|
7
8
|
import { SESSION_ID } from './log'
|
|
8
9
|
import type { Message } from '../types/conversation'
|
package/src/utils/log.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { existsSync, mkdirSync } from 'fs'
|
|
2
2
|
import { dirname, join } from 'path'
|
|
3
|
-
import { homedir } from 'os'
|
|
4
3
|
import { writeFileSync, readFileSync } from 'fs'
|
|
5
4
|
import { captureException } from '../services/sentry'
|
|
6
5
|
import { randomUUID } from 'crypto'
|
|
6
|
+
import envPaths from 'env-paths'
|
|
7
7
|
import { promises as fsPromises } from 'fs'
|
|
8
8
|
import type { LogOption, SerializedMessage } from '../types/logs'
|
|
9
9
|
import { MACRO } from '../constants/macros'
|
|
@@ -16,18 +16,17 @@ const MAX_IN_MEMORY_ERRORS = 100 // Limit to prevent memory issues
|
|
|
16
16
|
|
|
17
17
|
export const SESSION_ID = randomUUID()
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
const KODE_DIR = join(homedir(), '.kode')
|
|
19
|
+
const paths = envPaths(PRODUCT_COMMAND)
|
|
21
20
|
|
|
22
21
|
function getProjectDir(cwd: string): string {
|
|
23
22
|
return cwd.replace(/[^a-zA-Z0-9]/g, '-')
|
|
24
23
|
}
|
|
25
24
|
|
|
26
25
|
export const CACHE_PATHS = {
|
|
27
|
-
errors: () => join(
|
|
28
|
-
messages: () => join(
|
|
26
|
+
errors: () => join(paths.cache, getProjectDir(process.cwd()), 'errors'),
|
|
27
|
+
messages: () => join(paths.cache, getProjectDir(process.cwd()), 'messages'),
|
|
29
28
|
mcpLogs: (serverName: string) =>
|
|
30
|
-
join(
|
|
29
|
+
join(paths.cache, getProjectDir(process.cwd()), `mcp-logs-${serverName}`),
|
|
31
30
|
}
|
|
32
31
|
|
|
33
32
|
export function dateToFilename(date: Date): string {
|