@shareai-lab/kode 1.0.80 → 1.0.82

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.
@@ -0,0 +1,55 @@
1
+ interface CacheEntry {
2
+ content: string
3
+ timestamp: number
4
+ }
5
+
6
+ class URLCache {
7
+ private cache = new Map<string, CacheEntry>()
8
+ private readonly CACHE_DURATION = 15 * 60 * 1000 // 15 minutes in milliseconds
9
+
10
+ set(url: string, content: string): void {
11
+ this.cache.set(url, {
12
+ content,
13
+ timestamp: Date.now()
14
+ })
15
+ }
16
+
17
+ get(url: string): string | null {
18
+ const entry = this.cache.get(url)
19
+ if (!entry) {
20
+ return null
21
+ }
22
+
23
+ // Check if entry has expired
24
+ if (Date.now() - entry.timestamp > this.CACHE_DURATION) {
25
+ this.cache.delete(url)
26
+ return null
27
+ }
28
+
29
+ return entry.content
30
+ }
31
+
32
+ clear(): void {
33
+ this.cache.clear()
34
+ }
35
+
36
+ // Clean expired entries
37
+ private cleanExpired(): void {
38
+ const now = Date.now()
39
+ for (const [url, entry] of this.cache.entries()) {
40
+ if (now - entry.timestamp > this.CACHE_DURATION) {
41
+ this.cache.delete(url)
42
+ }
43
+ }
44
+ }
45
+
46
+ // Auto-clean expired entries every 5 minutes
47
+ constructor() {
48
+ setInterval(() => {
49
+ this.cleanExpired()
50
+ }, 5 * 60 * 1000) // 5 minutes
51
+ }
52
+ }
53
+
54
+ // Export singleton instance
55
+ export const urlCache = new URLCache()
@@ -0,0 +1,55 @@
1
+ import TurndownService from 'turndown'
2
+
3
+ const turndownService = new TurndownService({
4
+ headingStyle: 'atx',
5
+ hr: '---',
6
+ bulletListMarker: '-',
7
+ codeBlockStyle: 'fenced',
8
+ fence: '```',
9
+ emDelimiter: '_',
10
+ strongDelimiter: '**'
11
+ })
12
+
13
+ // Configure rules to handle common HTML elements
14
+ turndownService.addRule('removeScripts', {
15
+ filter: ['script', 'style', 'noscript'],
16
+ replacement: () => ''
17
+ })
18
+
19
+ turndownService.addRule('removeComments', {
20
+ filter: (node) => node.nodeType === 8, // Comment nodes
21
+ replacement: () => ''
22
+ })
23
+
24
+ turndownService.addRule('cleanLinks', {
25
+ filter: 'a',
26
+ replacement: (content, node) => {
27
+ const href = node.getAttribute('href')
28
+ if (!href || href.startsWith('javascript:') || href.startsWith('#')) {
29
+ return content
30
+ }
31
+ return `[${content}](${href})`
32
+ }
33
+ })
34
+
35
+ export function convertHtmlToMarkdown(html: string): string {
36
+ try {
37
+ // Clean up the HTML before conversion
38
+ const cleanHtml = html
39
+ .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '') // Remove script tags
40
+ .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '') // Remove style tags
41
+ .replace(/<!--[\s\S]*?-->/g, '') // Remove HTML comments
42
+ .replace(/\s+/g, ' ') // Normalize whitespace
43
+ .trim()
44
+
45
+ const markdown = turndownService.turndown(cleanHtml)
46
+
47
+ // Clean up the resulting markdown
48
+ return markdown
49
+ .replace(/\n{3,}/g, '\n\n') // Remove excessive line breaks
50
+ .replace(/^\s+|\s+$/gm, '') // Remove leading/trailing spaces on each line
51
+ .trim()
52
+ } catch (error) {
53
+ throw new Error(`Failed to convert HTML to markdown: ${error instanceof Error ? error.message : String(error)}`)
54
+ }
55
+ }
@@ -0,0 +1,17 @@
1
+ export const TOOL_NAME_FOR_PROMPT = 'URLFetcher'
2
+ export const DESCRIPTION = `- Fetches content from a specified URL and processes it using an AI model
3
+ - Takes a URL and a prompt as input
4
+ - Fetches the URL content, converts HTML to markdown
5
+ - Processes the content with the prompt using a small, fast model
6
+ - Returns the model's response about the content
7
+ - Use this tool when you need to retrieve and analyze web content
8
+
9
+ Usage notes:
10
+ - IMPORTANT: If an MCP-provided web fetch tool is available, prefer using that tool instead of this one, as it may have fewer restrictions. All MCP-provided tools start with "mcp__".
11
+ - The URL must be a fully-formed valid URL (e.g., https://example.com)
12
+ - HTTP URLs will be automatically upgraded to HTTPS
13
+ - The prompt should describe what information you want to extract from the page
14
+ - This tool is read-only and does not modify any files
15
+ - Results may be summarized if the content is very large
16
+ - Includes a self-cleaning 15-minute cache for faster responses when repeatedly accessing the same URL
17
+ - When a URL redirects, the tool will inform you and provide the redirect URL in a special format. You should then make a new URLFetcher request with the redirect URL to fetch the content.`
@@ -0,0 +1,103 @@
1
+ import { Box, Text } from 'ink'
2
+ import React from 'react'
3
+ import { z } from 'zod'
4
+ import { Cost } from '../../components/Cost'
5
+ import { FallbackToolUseRejectedMessage } from '../../components/FallbackToolUseRejectedMessage'
6
+ import { Tool, ToolUseContext } from '../../Tool'
7
+ import { DESCRIPTION, TOOL_NAME_FOR_PROMPT } from './prompt'
8
+ import { SearchResult, searchProviders } from './searchProviders'
9
+
10
+ const inputSchema = z.strictObject({
11
+ query: z.string().describe('The search query'),
12
+ })
13
+
14
+ type Input = z.infer<typeof inputSchema>
15
+ type Output = {
16
+ durationMs: number
17
+ results: SearchResult[]
18
+ }
19
+
20
+
21
+ export const WebSearchTool = {
22
+ name: TOOL_NAME_FOR_PROMPT,
23
+ async description() {
24
+ return DESCRIPTION
25
+ },
26
+ userFacingName: () => 'Web Search',
27
+ inputSchema,
28
+ isReadOnly: () => true,
29
+ isConcurrencySafe: () => true,
30
+ async isEnabled() {
31
+ return true
32
+ },
33
+ needsPermissions() {
34
+ return false
35
+ },
36
+ async prompt() {
37
+ return DESCRIPTION
38
+ },
39
+ renderToolUseMessage({ query }: Input) {
40
+ return `Searching for: "${query}" using DuckDuckGo`
41
+ },
42
+ renderToolUseRejectedMessage() {
43
+ return <FallbackToolUseRejectedMessage />
44
+ },
45
+ renderToolResultMessage(output: Output) {
46
+ return (
47
+ <Box justifyContent="space-between" width="100%">
48
+ <Box flexDirection="row">
49
+ <Text>&nbsp;&nbsp;⎿ &nbsp;Found </Text>
50
+ <Text bold>{output.results.length} </Text>
51
+ <Text>
52
+ {output.results.length === 1 ? 'result' : 'results'} using DuckDuckGo
53
+ </Text>
54
+ </Box>
55
+ <Cost costUSD={0} durationMs={output.durationMs} debug={false} />
56
+ </Box>
57
+ )
58
+ },
59
+ renderResultForAssistant(output: Output) {
60
+ if (output.results.length === 0) {
61
+ return `No results found using DuckDuckGo.`
62
+ }
63
+
64
+ let result = `Found ${output.results.length} search results using DuckDuckGo:\n\n`
65
+
66
+ output.results.forEach((item, index) => {
67
+ result += `${index + 1}. **${item.title}**\n`
68
+ result += ` ${item.snippet}\n`
69
+ result += ` Link: ${item.link}\n\n`
70
+ })
71
+
72
+ result += `You can reference these results to provide current, accurate information to the user.`
73
+ return result
74
+ },
75
+ async *call({ query }: Input, {}: ToolUseContext) {
76
+ const start = Date.now()
77
+
78
+ try {
79
+ const searchResults = await searchProviders.duckduckgo.search(query)
80
+
81
+ const output: Output = {
82
+ results: searchResults,
83
+ durationMs: Date.now() - start,
84
+ }
85
+
86
+ yield {
87
+ type: 'result' as const,
88
+ resultForAssistant: this.renderResultForAssistant(output),
89
+ data: output,
90
+ }
91
+ } catch (error: any) {
92
+ const output: Output = {
93
+ results: [],
94
+ durationMs: Date.now() - start,
95
+ }
96
+ yield {
97
+ type: 'result' as const,
98
+ resultForAssistant: `An error occurred during web search with DuckDuckGo: ${error.message}`,
99
+ data: output,
100
+ }
101
+ }
102
+ },
103
+ } satisfies Tool<typeof inputSchema, Output>
@@ -0,0 +1,13 @@
1
+
2
+ export const TOOL_NAME_FOR_PROMPT = 'WebSearch'
3
+ export const DESCRIPTION = `- Allows Kode to search the web and use the results to inform responses
4
+ - Provides up-to-date information for current events and recent data
5
+ - Returns search result information formatted as search result blocks
6
+ - Use this tool for accessing information beyond the Kode's knowledge cutoff
7
+ - Searches are performed automatically within a single API call using DuckDuckGo
8
+
9
+ Usage notes:
10
+ - Use when you need current information not in training data
11
+ - Effective for recent news, current events, product updates, or real-time data
12
+ - Search queries should be specific and well-targeted for best results
13
+ - Results include both title and snippet content for context`
@@ -0,0 +1,66 @@
1
+ import fetch from 'node-fetch'
2
+ import { parse } from 'node-html-parser'
3
+
4
+ export interface SearchResult {
5
+ title: string
6
+ snippet: string
7
+ link: string
8
+ }
9
+
10
+ export interface SearchProvider {
11
+ search: (query: string, apiKey?: string) => Promise<SearchResult[]>
12
+ isEnabled: (apiKey?: string) => boolean
13
+ }
14
+
15
+
16
+ const duckDuckGoSearchProvider: SearchProvider = {
17
+ isEnabled: () => true,
18
+ search: async (query: string): Promise<SearchResult[]> => {
19
+ const response = await fetch(`https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`, {
20
+ headers: {
21
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
22
+ }
23
+ });
24
+
25
+ if (!response.ok) {
26
+ throw new Error(`DuckDuckGo search failed with status: ${response.status}`);
27
+ }
28
+
29
+ const html = await response.text();
30
+ const root = parse(html);
31
+ const results: SearchResult[] = [];
32
+
33
+ const resultNodes = root.querySelectorAll('.result.web-result');
34
+
35
+ for (const node of resultNodes) {
36
+ const titleNode = node.querySelector('.result__a');
37
+ const snippetNode = node.querySelector('.result__snippet');
38
+
39
+ if (titleNode && snippetNode) {
40
+ const title = titleNode.text;
41
+ const link = titleNode.getAttribute('href');
42
+ const snippet = snippetNode.text;
43
+
44
+ if (title && link && snippet) {
45
+ // Clean the link - DuckDuckGo doesn't use uddg parameter anymore
46
+ let cleanLink = link;
47
+ if (link.startsWith('https://duckduckgo.com/l/?uddg=')) {
48
+ try {
49
+ const url = new URL(link);
50
+ cleanLink = url.searchParams.get('uddg') || link;
51
+ } catch {
52
+ cleanLink = link;
53
+ }
54
+ }
55
+ results.push({ title: title.trim(), snippet: snippet.trim(), link: cleanLink });
56
+ }
57
+ }
58
+ }
59
+
60
+ return results;
61
+ },
62
+ }
63
+
64
+ export const searchProviders = {
65
+ duckduckgo: duckDuckGoSearchProvider,
66
+ }
package/src/tools.ts CHANGED
@@ -16,6 +16,8 @@ import { NotebookEditTool } from './tools/NotebookEditTool/NotebookEditTool'
16
16
  import { NotebookReadTool } from './tools/NotebookReadTool/NotebookReadTool'
17
17
  import { ThinkTool } from './tools/ThinkTool/ThinkTool'
18
18
  import { TodoWriteTool } from './tools/TodoWriteTool/TodoWriteTool'
19
+ import { WebSearchTool } from './tools/WebSearchTool/WebSearchTool'
20
+ import { URLFetcherTool } from './tools/URLFetcherTool/URLFetcherTool'
19
21
  import { getMCPTools } from './services/mcpClient'
20
22
  import { memoize } from 'lodash-es'
21
23
 
@@ -38,6 +40,8 @@ export const getAllTools = (): Tool[] => {
38
40
  NotebookEditTool as unknown as Tool,
39
41
  ThinkTool as unknown as Tool,
40
42
  TodoWriteTool as unknown as Tool,
43
+ WebSearchTool as unknown as Tool,
44
+ URLFetcherTool as unknown as Tool,
41
45
  ...ANT_ONLY_TOOLS,
42
46
  ]
43
47
  }
@@ -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
- this.binShell = process.env.SHELL || '/bin/bash'
55
- this.shell = spawn(this.binShell, ['-l'], {
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
- 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
- }
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
- execSync(`${this.binShell} -n -c ${quotedCommand}`, {
236
- stdio: 'ignore',
237
- timeout: 1000,
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.stdoutFile} 2> ${this.stderrFile}`,
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.cwdFile}`)
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.statusFile}`)
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
- await this.exec(`cd ${resolved}`)
544
+ const bashPath = toBashPath(resolved, this.shellType)
545
+ await this.exec(`cd ${quoteForBash(bashPath)}`)
367
546
  }
368
547
 
369
548
  close(): void {
@@ -1,5 +1,6 @@
1
1
  import { existsSync, mkdirSync, appendFileSync } from 'fs'
2
2
  import { join } from 'path'
3
+ import { homedir } from 'os'
3
4
  import { randomUUID } from 'crypto'
4
5
  import chalk from 'chalk'
5
6
  import envPaths from 'env-paths'
@@ -60,14 +61,14 @@ const USER_FRIENDLY_LEVELS = new Set([
60
61
  const STARTUP_TIMESTAMP = new Date().toISOString().replace(/[:.]/g, '-')
61
62
  const REQUEST_START_TIME = Date.now()
62
63
 
63
- // 路径配置
64
- const paths = envPaths(PRODUCT_COMMAND)
64
+ // 路径配置 - 统一使用 ~/.kode 目录
65
+ const KODE_DIR = join(homedir(), '.kode')
65
66
  function getProjectDir(cwd: string): string {
66
67
  return cwd.replace(/[^a-zA-Z0-9]/g, '-')
67
68
  }
68
69
 
69
70
  const DEBUG_PATHS = {
70
- base: () => join(paths.cache, getProjectDir(process.cwd()), 'debug'),
71
+ base: () => join(KODE_DIR, getProjectDir(process.cwd()), 'debug'),
71
72
  detailed: () => join(DEBUG_PATHS.base(), `${STARTUP_TIMESTAMP}-detailed.log`),
72
73
  flow: () => join(DEBUG_PATHS.base(), `${STARTUP_TIMESTAMP}-flow.log`),
73
74
  api: () => join(DEBUG_PATHS.base(), `${STARTUP_TIMESTAMP}-api.log`),
@@ -467,11 +468,16 @@ export function logAPIError(context: {
467
468
  response?: any
468
469
  provider?: string
469
470
  }) {
470
- const errorDir = join(paths.cache, getProjectDir(process.cwd()), 'logs', 'error', 'api')
471
+ const errorDir = join(KODE_DIR, 'logs', 'error', 'api')
471
472
 
472
473
  // 确保目录存在
473
474
  if (!existsSync(errorDir)) {
474
- mkdirSync(errorDir, { recursive: true })
475
+ try {
476
+ mkdirSync(errorDir, { recursive: true })
477
+ } catch (err) {
478
+ console.error('Failed to create error log directory:', err)
479
+ return // Exit early if we can't create the directory
480
+ }
475
481
  }
476
482
 
477
483
  // 生成文件名
@@ -557,7 +563,7 @@ export function logAPIError(context: {
557
563
  }
558
564
 
559
565
  console.log()
560
- console.log(chalk.dim(` 📁 Full log: ~/.kode/logs/error/api/${filename}`))
566
+ console.log(chalk.dim(` 📁 Full log: ${filepath}`))
561
567
  console.log(chalk.red('━'.repeat(60)))
562
568
  console.log()
563
569
  }