@shareai-lab/kode 1.0.70 → 1.0.73
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 +342 -75
- package/README.zh-CN.md +292 -0
- package/cli.js +62 -0
- package/package.json +49 -25
- package/scripts/postinstall.js +56 -0
- package/src/ProjectOnboarding.tsx +198 -0
- package/src/Tool.ts +82 -0
- package/src/commands/agents.tsx +3401 -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 +31 -0
- package/src/commands/review.ts +49 -0
- package/src/commands/terminalSetup.ts +221 -0
- package/src/commands.ts +139 -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 +293 -0
- package/src/components/ConsoleOAuthFlow.tsx +327 -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 +78 -0
- package/src/components/CustomSelect/select.tsx +152 -0
- package/src/components/CustomSelect/theme.ts +45 -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 +221 -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 +227 -0
- package/src/components/ModelSelector.tsx +3386 -0
- package/src/components/ModelStatusDisplay.tsx +230 -0
- package/src/components/Onboarding.tsx +274 -0
- package/src/components/PressEnterToContinue.tsx +11 -0
- package/src/components/PromptInput.tsx +740 -0
- package/src/components/SentryErrorBoundary.ts +33 -0
- package/src/components/Spinner.tsx +129 -0
- package/src/components/StickerRequestForm.tsx +16 -0
- package/src/components/StructuredDiff.tsx +191 -0
- package/src/components/TextInput.tsx +259 -0
- package/src/components/TodoItem.tsx +11 -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 +49 -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 +133 -0
- package/src/components/messages/TaskProgressMessage.tsx +32 -0
- package/src/components/messages/TaskToolMessage.tsx +58 -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 +153 -0
- package/src/components/permissions/FileEditPermissionRequest/FileEditPermissionRequest.tsx +182 -0
- package/src/components/permissions/FileEditPermissionRequest/FileEditToolDiff.tsx +77 -0
- package/src/components/permissions/FileWritePermissionRequest/FileWritePermissionRequest.tsx +164 -0
- package/src/components/permissions/FileWritePermissionRequest/FileWriteToolDiff.tsx +83 -0
- package/src/components/permissions/FilesystemPermissionRequest/FilesystemPermissionRequest.tsx +240 -0
- package/src/components/permissions/PermissionRequest.tsx +101 -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 +8 -0
- package/src/constants/modelCapabilities.ts +179 -0
- package/src/constants/models.ts +1025 -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 +1518 -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/useTerminalSize.ts +49 -0
- package/src/hooks/useTextInput.ts +318 -0
- package/src/hooks/useUnifiedCompletion.ts +1404 -0
- package/src/messages.ts +38 -0
- package/src/permissions.ts +268 -0
- package/src/query.ts +707 -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 +798 -0
- package/src/screens/ResumeConversation.tsx +68 -0
- package/src/services/adapters/base.ts +38 -0
- package/src/services/adapters/chatCompletions.ts +90 -0
- package/src/services/adapters/responsesAPI.ts +170 -0
- package/src/services/browserMocks.ts +66 -0
- package/src/services/claude.ts +2083 -0
- package/src/services/customCommands.ts +704 -0
- package/src/services/fileFreshness.ts +377 -0
- package/src/services/gpt5ConnectionTest.ts +340 -0
- package/src/services/mcpClient.ts +564 -0
- package/src/services/mcpServerApproval.tsx +50 -0
- package/src/services/mentionProcessor.ts +273 -0
- package/src/services/modelAdapterFactory.ts +69 -0
- package/src/services/notifier.ts +40 -0
- package/src/services/oauth.ts +357 -0
- package/src/services/openai.ts +1305 -0
- package/src/services/responseStateManager.ts +90 -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 +507 -0
- package/src/services/vcr.ts +161 -0
- package/src/test/testAdapters.ts +96 -0
- package/src/tools/ArchitectTool/ArchitectTool.tsx +122 -0
- package/src/tools/ArchitectTool/prompt.ts +15 -0
- package/src/tools/AskExpertModelTool/AskExpertModelTool.tsx +569 -0
- package/src/tools/BashTool/BashTool.tsx +243 -0
- package/src/tools/BashTool/BashToolResultMessage.tsx +38 -0
- package/src/tools/BashTool/OutputLine.tsx +49 -0
- package/src/tools/BashTool/prompt.ts +174 -0
- package/src/tools/BashTool/utils.ts +56 -0
- package/src/tools/FileEditTool/FileEditTool.tsx +315 -0
- package/src/tools/FileEditTool/prompt.ts +51 -0
- package/src/tools/FileEditTool/utils.ts +58 -0
- package/src/tools/FileReadTool/FileReadTool.tsx +404 -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 +107 -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 +258 -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 +466 -0
- package/src/tools/TaskTool/constants.ts +1 -0
- package/src/tools/TaskTool/prompt.ts +92 -0
- package/src/tools/ThinkTool/ThinkTool.tsx +54 -0
- package/src/tools/ThinkTool/prompt.ts +12 -0
- package/src/tools/TodoWriteTool/TodoWriteTool.tsx +290 -0
- package/src/tools/TodoWriteTool/prompt.ts +63 -0
- package/src/tools/lsTool/lsTool.tsx +272 -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/types/conversation.ts +51 -0
- package/src/types/logs.ts +58 -0
- package/src/types/modelCapabilities.ts +64 -0
- package/src/types/notebook.ts +87 -0
- package/src/utils/Cursor.ts +436 -0
- package/src/utils/PersistentShell.ts +373 -0
- package/src/utils/advancedFuzzyMatcher.ts +290 -0
- package/src/utils/agentLoader.ts +284 -0
- package/src/utils/agentStorage.ts +97 -0
- package/src/utils/array.ts +3 -0
- package/src/utils/ask.tsx +99 -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/commonUnixCommands.ts +161 -0
- package/src/utils/config.ts +942 -0
- package/src/utils/conversationRecovery.ts +55 -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 +109 -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/fuzzyMatcher.ts +328 -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 +939 -0
- package/src/utils/model.ts +836 -0
- package/src/utils/permissions/filesystem.ts +118 -0
- package/src/utils/responseState.ts +23 -0
- package/src/utils/ripgrep.ts +167 -0
- package/src/utils/secureFile.ts +559 -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 +50 -0
- package/src/utils/theme.ts +133 -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,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Advanced Fuzzy Matching Algorithm
|
|
3
|
+
*
|
|
4
|
+
* Inspired by:
|
|
5
|
+
* - Chinese Pinyin input methods (Sogou, Baidu)
|
|
6
|
+
* - IDE intelligent completion (VSCode, IntelliJ)
|
|
7
|
+
* - Terminal fuzzy finders (fzf, peco)
|
|
8
|
+
*
|
|
9
|
+
* Key features:
|
|
10
|
+
* - Hyphen-aware matching (dao → dao-qi-harmony)
|
|
11
|
+
* - Numeric suffix matching (py3 → python3)
|
|
12
|
+
* - Abbreviation matching (dq → dao-qi)
|
|
13
|
+
* - Subsequence matching
|
|
14
|
+
* - Word boundary bonus
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export interface MatchResult {
|
|
18
|
+
score: number
|
|
19
|
+
matched: boolean
|
|
20
|
+
algorithm: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class AdvancedFuzzyMatcher {
|
|
24
|
+
/**
|
|
25
|
+
* Main matching function - combines multiple algorithms
|
|
26
|
+
*/
|
|
27
|
+
match(candidate: string, query: string): MatchResult {
|
|
28
|
+
// Normalize inputs
|
|
29
|
+
const text = candidate.toLowerCase()
|
|
30
|
+
const pattern = query.toLowerCase()
|
|
31
|
+
|
|
32
|
+
// Quick exact match - give HUGE score for exact matches
|
|
33
|
+
if (text === pattern) {
|
|
34
|
+
return { score: 10000, matched: true, algorithm: 'exact' }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Try all algorithms and combine scores
|
|
38
|
+
const algorithms = [
|
|
39
|
+
this.exactPrefixMatch(text, pattern),
|
|
40
|
+
this.hyphenAwareMatch(text, pattern),
|
|
41
|
+
this.wordBoundaryMatch(text, pattern),
|
|
42
|
+
this.abbreviationMatch(text, pattern),
|
|
43
|
+
this.numericSuffixMatch(text, pattern),
|
|
44
|
+
this.subsequenceMatch(text, pattern),
|
|
45
|
+
this.fuzzySegmentMatch(text, pattern),
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
// Get best score
|
|
49
|
+
let bestScore = 0
|
|
50
|
+
let bestAlgorithm = 'none'
|
|
51
|
+
|
|
52
|
+
for (const result of algorithms) {
|
|
53
|
+
if (result.score > bestScore) {
|
|
54
|
+
bestScore = result.score
|
|
55
|
+
bestAlgorithm = result.algorithm
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
score: bestScore,
|
|
61
|
+
matched: bestScore > 10,
|
|
62
|
+
algorithm: bestAlgorithm
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Exact prefix matching
|
|
68
|
+
*/
|
|
69
|
+
private exactPrefixMatch(text: string, pattern: string): { score: number; algorithm: string } {
|
|
70
|
+
if (text.startsWith(pattern)) {
|
|
71
|
+
const coverage = pattern.length / text.length
|
|
72
|
+
// Higher base score for prefix matches to prioritize them
|
|
73
|
+
return { score: 1000 + coverage * 500, algorithm: 'prefix' }
|
|
74
|
+
}
|
|
75
|
+
return { score: 0, algorithm: 'prefix' }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Hyphen-aware matching (dao → dao-qi-harmony-designer)
|
|
80
|
+
* Treats hyphens as optional word boundaries
|
|
81
|
+
*/
|
|
82
|
+
private hyphenAwareMatch(text: string, pattern: string): { score: number; algorithm: string } {
|
|
83
|
+
// Split by hyphens and try to match
|
|
84
|
+
const words = text.split('-')
|
|
85
|
+
|
|
86
|
+
// Check if pattern matches the beginning of hyphenated words
|
|
87
|
+
if (words[0].startsWith(pattern)) {
|
|
88
|
+
const coverage = pattern.length / words[0].length
|
|
89
|
+
return { score: 300 + coverage * 100, algorithm: 'hyphen-prefix' }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Check if pattern matches concatenated words (ignoring hyphens)
|
|
93
|
+
const concatenated = words.join('')
|
|
94
|
+
if (concatenated.startsWith(pattern)) {
|
|
95
|
+
const coverage = pattern.length / concatenated.length
|
|
96
|
+
return { score: 250 + coverage * 100, algorithm: 'hyphen-concat' }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Check if pattern matches any word start
|
|
100
|
+
for (let i = 0; i < words.length; i++) {
|
|
101
|
+
if (words[i].startsWith(pattern)) {
|
|
102
|
+
return { score: 200 - i * 10, algorithm: 'hyphen-word' }
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return { score: 0, algorithm: 'hyphen' }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Word boundary matching (dq → dao-qi)
|
|
111
|
+
* Matches characters at word boundaries
|
|
112
|
+
*/
|
|
113
|
+
private wordBoundaryMatch(text: string, pattern: string): { score: number; algorithm: string } {
|
|
114
|
+
const words = text.split(/[-_\s]+/)
|
|
115
|
+
let patternIdx = 0
|
|
116
|
+
let score = 0
|
|
117
|
+
let matched = false
|
|
118
|
+
|
|
119
|
+
for (const word of words) {
|
|
120
|
+
if (patternIdx >= pattern.length) break
|
|
121
|
+
|
|
122
|
+
if (word[0] === pattern[patternIdx]) {
|
|
123
|
+
score += 50 // Bonus for word boundary match
|
|
124
|
+
patternIdx++
|
|
125
|
+
matched = true
|
|
126
|
+
|
|
127
|
+
// Try to match more characters in this word
|
|
128
|
+
for (let i = 1; i < word.length && patternIdx < pattern.length; i++) {
|
|
129
|
+
if (word[i] === pattern[patternIdx]) {
|
|
130
|
+
score += 20
|
|
131
|
+
patternIdx++
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (matched && patternIdx === pattern.length) {
|
|
138
|
+
return { score, algorithm: 'word-boundary' }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return { score: 0, algorithm: 'word-boundary' }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Abbreviation matching (nde → node, daoqi → dao-qi)
|
|
146
|
+
*/
|
|
147
|
+
private abbreviationMatch(text: string, pattern: string): { score: number; algorithm: string } {
|
|
148
|
+
let textIdx = 0
|
|
149
|
+
let patternIdx = 0
|
|
150
|
+
let score = 0
|
|
151
|
+
let lastMatchIdx = -1
|
|
152
|
+
|
|
153
|
+
while (patternIdx < pattern.length && textIdx < text.length) {
|
|
154
|
+
if (text[textIdx] === pattern[patternIdx]) {
|
|
155
|
+
// Calculate position score
|
|
156
|
+
const gap = lastMatchIdx === -1 ? 0 : textIdx - lastMatchIdx - 1
|
|
157
|
+
|
|
158
|
+
if (textIdx === 0) {
|
|
159
|
+
score += 50 // First character match
|
|
160
|
+
} else if (lastMatchIdx >= 0 && gap === 0) {
|
|
161
|
+
score += 30 // Consecutive match
|
|
162
|
+
} else if (text[textIdx - 1] === '-' || text[textIdx - 1] === '_') {
|
|
163
|
+
score += 40 // Word boundary match
|
|
164
|
+
} else {
|
|
165
|
+
score += Math.max(5, 20 - gap * 2) // Distance penalty
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
lastMatchIdx = textIdx
|
|
169
|
+
patternIdx++
|
|
170
|
+
}
|
|
171
|
+
textIdx++
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (patternIdx === pattern.length) {
|
|
175
|
+
// Bonus for compact matches
|
|
176
|
+
const spread = lastMatchIdx / pattern.length
|
|
177
|
+
if (spread <= 3) score += 50
|
|
178
|
+
else if (spread <= 5) score += 30
|
|
179
|
+
|
|
180
|
+
return { score, algorithm: 'abbreviation' }
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return { score: 0, algorithm: 'abbreviation' }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Numeric suffix matching (py3 → python3, np18 → node18)
|
|
188
|
+
*/
|
|
189
|
+
private numericSuffixMatch(text: string, pattern: string): { score: number; algorithm: string } {
|
|
190
|
+
// Check if pattern has numeric suffix
|
|
191
|
+
const patternMatch = pattern.match(/^(.+?)(\d+)$/)
|
|
192
|
+
if (!patternMatch) return { score: 0, algorithm: 'numeric' }
|
|
193
|
+
|
|
194
|
+
const [, prefix, suffix] = patternMatch
|
|
195
|
+
|
|
196
|
+
// Check if text ends with same number
|
|
197
|
+
if (!text.endsWith(suffix)) return { score: 0, algorithm: 'numeric' }
|
|
198
|
+
|
|
199
|
+
// Check if prefix matches start of text
|
|
200
|
+
const textWithoutSuffix = text.slice(0, -suffix.length)
|
|
201
|
+
if (textWithoutSuffix.startsWith(prefix)) {
|
|
202
|
+
const coverage = prefix.length / textWithoutSuffix.length
|
|
203
|
+
return { score: 200 + coverage * 100, algorithm: 'numeric-suffix' }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Check abbreviation match for prefix
|
|
207
|
+
const abbrevResult = this.abbreviationMatch(textWithoutSuffix, prefix)
|
|
208
|
+
if (abbrevResult.score > 0) {
|
|
209
|
+
return { score: abbrevResult.score + 50, algorithm: 'numeric-abbrev' }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return { score: 0, algorithm: 'numeric' }
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Subsequence matching - characters appear in order
|
|
217
|
+
*/
|
|
218
|
+
private subsequenceMatch(text: string, pattern: string): { score: number; algorithm: string } {
|
|
219
|
+
let textIdx = 0
|
|
220
|
+
let patternIdx = 0
|
|
221
|
+
let score = 0
|
|
222
|
+
|
|
223
|
+
while (patternIdx < pattern.length && textIdx < text.length) {
|
|
224
|
+
if (text[textIdx] === pattern[patternIdx]) {
|
|
225
|
+
score += 10
|
|
226
|
+
patternIdx++
|
|
227
|
+
}
|
|
228
|
+
textIdx++
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (patternIdx === pattern.length) {
|
|
232
|
+
// Penalty for spread
|
|
233
|
+
const spread = textIdx / pattern.length
|
|
234
|
+
score = Math.max(10, score - spread * 5)
|
|
235
|
+
return { score, algorithm: 'subsequence' }
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return { score: 0, algorithm: 'subsequence' }
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Fuzzy segment matching (dao → dao-qi-harmony)
|
|
243
|
+
* Matches segments flexibly
|
|
244
|
+
*/
|
|
245
|
+
private fuzzySegmentMatch(text: string, pattern: string): { score: number; algorithm: string } {
|
|
246
|
+
// Remove hyphens and underscores for matching
|
|
247
|
+
const cleanText = text.replace(/[-_]/g, '')
|
|
248
|
+
const cleanPattern = pattern.replace(/[-_]/g, '')
|
|
249
|
+
|
|
250
|
+
// Check if clean pattern is a prefix of clean text
|
|
251
|
+
if (cleanText.startsWith(cleanPattern)) {
|
|
252
|
+
const coverage = cleanPattern.length / cleanText.length
|
|
253
|
+
return { score: 150 + coverage * 100, algorithm: 'fuzzy-segment' }
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Check if pattern appears anywhere in clean text
|
|
257
|
+
const index = cleanText.indexOf(cleanPattern)
|
|
258
|
+
if (index !== -1) {
|
|
259
|
+
const positionPenalty = index * 5
|
|
260
|
+
return { score: Math.max(50, 100 - positionPenalty), algorithm: 'fuzzy-contains' }
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return { score: 0, algorithm: 'fuzzy-segment' }
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Export singleton instance and helper functions
|
|
268
|
+
export const advancedMatcher = new AdvancedFuzzyMatcher()
|
|
269
|
+
|
|
270
|
+
export function matchAdvanced(candidate: string, query: string): MatchResult {
|
|
271
|
+
return advancedMatcher.match(candidate, query)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function matchManyAdvanced(
|
|
275
|
+
candidates: string[],
|
|
276
|
+
query: string,
|
|
277
|
+
minScore: number = 10
|
|
278
|
+
): Array<{ candidate: string; score: number; algorithm: string }> {
|
|
279
|
+
return candidates
|
|
280
|
+
.map(candidate => {
|
|
281
|
+
const result = advancedMatcher.match(candidate, query)
|
|
282
|
+
return {
|
|
283
|
+
candidate,
|
|
284
|
+
score: result.score,
|
|
285
|
+
algorithm: result.algorithm
|
|
286
|
+
}
|
|
287
|
+
})
|
|
288
|
+
.filter(item => item.score >= minScore)
|
|
289
|
+
.sort((a, b) => b.score - a.score)
|
|
290
|
+
}
|