@shareai-lab/kode 1.0.70 → 1.0.71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +202 -76
- package/README.zh-CN.md +246 -0
- package/cli.js +62 -0
- package/package.json +45 -25
- package/scripts/postinstall.js +56 -0
- package/src/ProjectOnboarding.tsx +180 -0
- package/src/Tool.ts +53 -0
- package/src/commands/approvedTools.ts +53 -0
- package/src/commands/bug.tsx +20 -0
- package/src/commands/clear.ts +43 -0
- package/src/commands/compact.ts +120 -0
- package/src/commands/config.tsx +19 -0
- package/src/commands/cost.ts +18 -0
- package/src/commands/ctx_viz.ts +209 -0
- package/src/commands/doctor.ts +24 -0
- package/src/commands/help.tsx +19 -0
- package/src/commands/init.ts +37 -0
- package/src/commands/listen.ts +42 -0
- package/src/commands/login.tsx +51 -0
- package/src/commands/logout.tsx +40 -0
- package/src/commands/mcp.ts +41 -0
- package/src/commands/model.tsx +40 -0
- package/src/commands/modelstatus.tsx +20 -0
- package/src/commands/onboarding.tsx +34 -0
- package/src/commands/pr_comments.ts +59 -0
- package/src/commands/refreshCommands.ts +54 -0
- package/src/commands/release-notes.ts +34 -0
- package/src/commands/resume.tsx +30 -0
- package/src/commands/review.ts +49 -0
- package/src/commands/terminalSetup.ts +221 -0
- package/src/commands.ts +136 -0
- package/src/components/ApproveApiKey.tsx +93 -0
- package/src/components/AsciiLogo.tsx +13 -0
- package/src/components/AutoUpdater.tsx +148 -0
- package/src/components/Bug.tsx +367 -0
- package/src/components/Config.tsx +289 -0
- package/src/components/ConsoleOAuthFlow.tsx +326 -0
- package/src/components/Cost.tsx +23 -0
- package/src/components/CostThresholdDialog.tsx +46 -0
- package/src/components/CustomSelect/option-map.ts +42 -0
- package/src/components/CustomSelect/select-option.tsx +52 -0
- package/src/components/CustomSelect/select.tsx +143 -0
- package/src/components/CustomSelect/use-select-state.ts +414 -0
- package/src/components/CustomSelect/use-select.ts +35 -0
- package/src/components/FallbackToolUseRejectedMessage.tsx +15 -0
- package/src/components/FileEditToolUpdatedMessage.tsx +66 -0
- package/src/components/Help.tsx +215 -0
- package/src/components/HighlightedCode.tsx +33 -0
- package/src/components/InvalidConfigDialog.tsx +113 -0
- package/src/components/Link.tsx +32 -0
- package/src/components/LogSelector.tsx +86 -0
- package/src/components/Logo.tsx +145 -0
- package/src/components/MCPServerApprovalDialog.tsx +100 -0
- package/src/components/MCPServerDialogCopy.tsx +25 -0
- package/src/components/MCPServerMultiselectDialog.tsx +109 -0
- package/src/components/Message.tsx +219 -0
- package/src/components/MessageResponse.tsx +15 -0
- package/src/components/MessageSelector.tsx +211 -0
- package/src/components/ModeIndicator.tsx +88 -0
- package/src/components/ModelConfig.tsx +301 -0
- package/src/components/ModelListManager.tsx +223 -0
- package/src/components/ModelSelector.tsx +3208 -0
- package/src/components/ModelStatusDisplay.tsx +228 -0
- package/src/components/Onboarding.tsx +274 -0
- package/src/components/PressEnterToContinue.tsx +11 -0
- package/src/components/PromptInput.tsx +710 -0
- package/src/components/SentryErrorBoundary.ts +33 -0
- package/src/components/Spinner.tsx +129 -0
- package/src/components/StructuredDiff.tsx +184 -0
- package/src/components/TextInput.tsx +246 -0
- package/src/components/TokenWarning.tsx +31 -0
- package/src/components/ToolUseLoader.tsx +40 -0
- package/src/components/TrustDialog.tsx +106 -0
- package/src/components/binary-feedback/BinaryFeedback.tsx +63 -0
- package/src/components/binary-feedback/BinaryFeedbackOption.tsx +111 -0
- package/src/components/binary-feedback/BinaryFeedbackView.tsx +172 -0
- package/src/components/binary-feedback/utils.ts +220 -0
- package/src/components/messages/AssistantBashOutputMessage.tsx +22 -0
- package/src/components/messages/AssistantLocalCommandOutputMessage.tsx +45 -0
- package/src/components/messages/AssistantRedactedThinkingMessage.tsx +19 -0
- package/src/components/messages/AssistantTextMessage.tsx +144 -0
- package/src/components/messages/AssistantThinkingMessage.tsx +40 -0
- package/src/components/messages/AssistantToolUseMessage.tsx +123 -0
- package/src/components/messages/UserBashInputMessage.tsx +28 -0
- package/src/components/messages/UserCommandMessage.tsx +30 -0
- package/src/components/messages/UserKodingInputMessage.tsx +28 -0
- package/src/components/messages/UserPromptMessage.tsx +35 -0
- package/src/components/messages/UserTextMessage.tsx +39 -0
- package/src/components/messages/UserToolResultMessage/UserToolCanceledMessage.tsx +12 -0
- package/src/components/messages/UserToolResultMessage/UserToolErrorMessage.tsx +36 -0
- package/src/components/messages/UserToolResultMessage/UserToolRejectMessage.tsx +31 -0
- package/src/components/messages/UserToolResultMessage/UserToolResultMessage.tsx +57 -0
- package/src/components/messages/UserToolResultMessage/UserToolSuccessMessage.tsx +35 -0
- package/src/components/messages/UserToolResultMessage/utils.tsx +56 -0
- package/src/components/permissions/BashPermissionRequest/BashPermissionRequest.tsx +121 -0
- package/src/components/permissions/FallbackPermissionRequest.tsx +155 -0
- package/src/components/permissions/FileEditPermissionRequest/FileEditPermissionRequest.tsx +182 -0
- package/src/components/permissions/FileEditPermissionRequest/FileEditToolDiff.tsx +75 -0
- package/src/components/permissions/FileWritePermissionRequest/FileWritePermissionRequest.tsx +164 -0
- package/src/components/permissions/FileWritePermissionRequest/FileWriteToolDiff.tsx +81 -0
- package/src/components/permissions/FilesystemPermissionRequest/FilesystemPermissionRequest.tsx +242 -0
- package/src/components/permissions/PermissionRequest.tsx +103 -0
- package/src/components/permissions/PermissionRequestTitle.tsx +69 -0
- package/src/components/permissions/hooks.ts +44 -0
- package/src/components/permissions/toolUseOptions.ts +59 -0
- package/src/components/permissions/utils.ts +23 -0
- package/src/constants/betas.ts +5 -0
- package/src/constants/claude-asterisk-ascii-art.tsx +238 -0
- package/src/constants/figures.ts +4 -0
- package/src/constants/keys.ts +3 -0
- package/src/constants/macros.ts +6 -0
- package/src/constants/models.ts +935 -0
- package/src/constants/oauth.ts +18 -0
- package/src/constants/product.ts +17 -0
- package/src/constants/prompts.ts +177 -0
- package/src/constants/releaseNotes.ts +7 -0
- package/src/context/PermissionContext.tsx +149 -0
- package/src/context.ts +278 -0
- package/src/cost-tracker.ts +84 -0
- package/src/entrypoints/cli.tsx +1498 -0
- package/src/entrypoints/mcp.ts +176 -0
- package/src/history.ts +25 -0
- package/src/hooks/useApiKeyVerification.ts +59 -0
- package/src/hooks/useArrowKeyHistory.ts +55 -0
- package/src/hooks/useCanUseTool.ts +138 -0
- package/src/hooks/useCancelRequest.ts +39 -0
- package/src/hooks/useDoublePress.ts +42 -0
- package/src/hooks/useExitOnCtrlCD.ts +31 -0
- package/src/hooks/useInterval.ts +25 -0
- package/src/hooks/useLogMessages.ts +16 -0
- package/src/hooks/useLogStartupTime.ts +12 -0
- package/src/hooks/useNotifyAfterTimeout.ts +65 -0
- package/src/hooks/usePermissionRequestLogging.ts +44 -0
- package/src/hooks/useSlashCommandTypeahead.ts +137 -0
- package/src/hooks/useTerminalSize.ts +49 -0
- package/src/hooks/useTextInput.ts +315 -0
- package/src/messages.ts +37 -0
- package/src/permissions.ts +268 -0
- package/src/query.ts +704 -0
- package/src/screens/ConfigureNpmPrefix.tsx +197 -0
- package/src/screens/Doctor.tsx +219 -0
- package/src/screens/LogList.tsx +68 -0
- package/src/screens/REPL.tsx +792 -0
- package/src/screens/ResumeConversation.tsx +68 -0
- package/src/services/browserMocks.ts +66 -0
- package/src/services/claude.ts +1947 -0
- package/src/services/customCommands.ts +683 -0
- package/src/services/fileFreshness.ts +377 -0
- package/src/services/mcpClient.ts +564 -0
- package/src/services/mcpServerApproval.tsx +50 -0
- package/src/services/notifier.ts +40 -0
- package/src/services/oauth.ts +357 -0
- package/src/services/openai.ts +796 -0
- package/src/services/sentry.ts +3 -0
- package/src/services/statsig.ts +171 -0
- package/src/services/statsigStorage.ts +86 -0
- package/src/services/systemReminder.ts +406 -0
- package/src/services/vcr.ts +161 -0
- package/src/tools/ArchitectTool/ArchitectTool.tsx +122 -0
- package/src/tools/ArchitectTool/prompt.ts +15 -0
- package/src/tools/AskExpertModelTool/AskExpertModelTool.tsx +505 -0
- package/src/tools/BashTool/BashTool.tsx +270 -0
- package/src/tools/BashTool/BashToolResultMessage.tsx +38 -0
- package/src/tools/BashTool/OutputLine.tsx +48 -0
- package/src/tools/BashTool/prompt.ts +174 -0
- package/src/tools/BashTool/utils.ts +56 -0
- package/src/tools/FileEditTool/FileEditTool.tsx +316 -0
- package/src/tools/FileEditTool/prompt.ts +51 -0
- package/src/tools/FileEditTool/utils.ts +58 -0
- package/src/tools/FileReadTool/FileReadTool.tsx +371 -0
- package/src/tools/FileReadTool/prompt.ts +7 -0
- package/src/tools/FileWriteTool/FileWriteTool.tsx +297 -0
- package/src/tools/FileWriteTool/prompt.ts +10 -0
- package/src/tools/GlobTool/GlobTool.tsx +119 -0
- package/src/tools/GlobTool/prompt.ts +8 -0
- package/src/tools/GrepTool/GrepTool.tsx +147 -0
- package/src/tools/GrepTool/prompt.ts +11 -0
- package/src/tools/MCPTool/MCPTool.tsx +106 -0
- package/src/tools/MCPTool/prompt.ts +3 -0
- package/src/tools/MemoryReadTool/MemoryReadTool.tsx +127 -0
- package/src/tools/MemoryReadTool/prompt.ts +3 -0
- package/src/tools/MemoryWriteTool/MemoryWriteTool.tsx +89 -0
- package/src/tools/MemoryWriteTool/prompt.ts +3 -0
- package/src/tools/MultiEditTool/MultiEditTool.tsx +366 -0
- package/src/tools/MultiEditTool/prompt.ts +45 -0
- package/src/tools/NotebookEditTool/NotebookEditTool.tsx +298 -0
- package/src/tools/NotebookEditTool/prompt.ts +3 -0
- package/src/tools/NotebookReadTool/NotebookReadTool.tsx +266 -0
- package/src/tools/NotebookReadTool/prompt.ts +3 -0
- package/src/tools/StickerRequestTool/StickerRequestTool.tsx +93 -0
- package/src/tools/StickerRequestTool/prompt.ts +19 -0
- package/src/tools/TaskTool/TaskTool.tsx +382 -0
- package/src/tools/TaskTool/constants.ts +1 -0
- package/src/tools/TaskTool/prompt.ts +56 -0
- package/src/tools/ThinkTool/ThinkTool.tsx +56 -0
- package/src/tools/ThinkTool/prompt.ts +12 -0
- package/src/tools/TodoWriteTool/TodoWriteTool.tsx +289 -0
- package/src/tools/TodoWriteTool/prompt.ts +63 -0
- package/src/tools/lsTool/lsTool.tsx +269 -0
- package/src/tools/lsTool/prompt.ts +2 -0
- package/src/tools.ts +63 -0
- package/src/types/PermissionMode.ts +120 -0
- package/src/types/RequestContext.ts +72 -0
- package/src/utils/Cursor.ts +436 -0
- package/src/utils/PersistentShell.ts +373 -0
- package/src/utils/agentStorage.ts +97 -0
- package/src/utils/array.ts +3 -0
- package/src/utils/ask.tsx +98 -0
- package/src/utils/auth.ts +13 -0
- package/src/utils/autoCompactCore.ts +223 -0
- package/src/utils/autoUpdater.ts +318 -0
- package/src/utils/betas.ts +20 -0
- package/src/utils/browser.ts +14 -0
- package/src/utils/cleanup.ts +72 -0
- package/src/utils/commands.ts +261 -0
- package/src/utils/config.ts +771 -0
- package/src/utils/conversationRecovery.ts +54 -0
- package/src/utils/debugLogger.ts +1123 -0
- package/src/utils/diff.ts +42 -0
- package/src/utils/env.ts +57 -0
- package/src/utils/errors.ts +21 -0
- package/src/utils/exampleCommands.ts +108 -0
- package/src/utils/execFileNoThrow.ts +51 -0
- package/src/utils/expertChatStorage.ts +136 -0
- package/src/utils/file.ts +402 -0
- package/src/utils/fileRecoveryCore.ts +71 -0
- package/src/utils/format.tsx +44 -0
- package/src/utils/generators.ts +62 -0
- package/src/utils/git.ts +92 -0
- package/src/utils/globalLogger.ts +77 -0
- package/src/utils/http.ts +10 -0
- package/src/utils/imagePaste.ts +38 -0
- package/src/utils/json.ts +13 -0
- package/src/utils/log.ts +382 -0
- package/src/utils/markdown.ts +213 -0
- package/src/utils/messageContextManager.ts +289 -0
- package/src/utils/messages.tsx +938 -0
- package/src/utils/model.ts +836 -0
- package/src/utils/permissions/filesystem.ts +118 -0
- package/src/utils/ripgrep.ts +167 -0
- package/src/utils/sessionState.ts +49 -0
- package/src/utils/state.ts +25 -0
- package/src/utils/style.ts +29 -0
- package/src/utils/terminal.ts +49 -0
- package/src/utils/theme.ts +122 -0
- package/src/utils/thinking.ts +144 -0
- package/src/utils/todoStorage.ts +431 -0
- package/src/utils/tokens.ts +43 -0
- package/src/utils/toolExecutionController.ts +163 -0
- package/src/utils/unaryLogging.ts +26 -0
- package/src/utils/user.ts +37 -0
- package/src/utils/validate.ts +165 -0
- package/cli.mjs +0 -1803
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { memoize } from 'lodash-es'
|
|
3
|
+
import chalk from 'chalk'
|
|
4
|
+
import {
|
|
5
|
+
StatsigClient,
|
|
6
|
+
StatsigOptions,
|
|
7
|
+
StatsigEvent,
|
|
8
|
+
LogLevel,
|
|
9
|
+
} from '@statsig/js-client'
|
|
10
|
+
import './browserMocks.js' // Initialize browser mocks
|
|
11
|
+
import { FileSystemStorageProvider } from './statsigStorage'
|
|
12
|
+
import { STATSIG_CLIENT_KEY } from '../constants/keys'
|
|
13
|
+
import { env } from '../utils/env'
|
|
14
|
+
import { getUser } from '../utils/user'
|
|
15
|
+
import { logError } from '../utils/log'
|
|
16
|
+
import { SESSION_ID } from '../utils/log'
|
|
17
|
+
import { getBetas } from '../utils/betas'
|
|
18
|
+
import { getIsGit } from '../utils/git'
|
|
19
|
+
import { getModelManager } from '../utils/model'
|
|
20
|
+
import { MACRO } from '../constants/macros'
|
|
21
|
+
const gateValues: Record<string, boolean> = {}
|
|
22
|
+
let client: StatsigClient | null = null
|
|
23
|
+
|
|
24
|
+
export const initializeStatsig = memoize(
|
|
25
|
+
async (): Promise<StatsigClient | null> => {
|
|
26
|
+
if (env.isCI || process.env.NODE_ENV === 'test') {
|
|
27
|
+
return null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const user = await getUser()
|
|
31
|
+
const options: StatsigOptions = {
|
|
32
|
+
networkConfig: {
|
|
33
|
+
api: 'https://statsig.anthropic.com/v1/',
|
|
34
|
+
},
|
|
35
|
+
environment: {
|
|
36
|
+
tier:
|
|
37
|
+
env.isCI ||
|
|
38
|
+
['test', 'development'].includes(process.env.NODE_ENV ?? '')
|
|
39
|
+
? 'dev'
|
|
40
|
+
: 'production',
|
|
41
|
+
},
|
|
42
|
+
logLevel: LogLevel.None,
|
|
43
|
+
storageProvider: new FileSystemStorageProvider(),
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
client = new StatsigClient(STATSIG_CLIENT_KEY, user, options)
|
|
47
|
+
client.on('error', errorEvent => {
|
|
48
|
+
logError(`Statsig error: ${errorEvent}`)
|
|
49
|
+
})
|
|
50
|
+
await client.initializeAsync()
|
|
51
|
+
process.on('exit', () => {
|
|
52
|
+
client?.flush()
|
|
53
|
+
})
|
|
54
|
+
return client
|
|
55
|
+
},
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
export function logEvent(
|
|
59
|
+
eventName: string,
|
|
60
|
+
metadata: { [key: string]: string | undefined },
|
|
61
|
+
): void {
|
|
62
|
+
// console.log('logEvent', eventName, metadata)
|
|
63
|
+
if (env.isCI || process.env.NODE_ENV === 'test') {
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
Promise.all([
|
|
67
|
+
initializeStatsig(),
|
|
68
|
+
getIsGit(),
|
|
69
|
+
getBetas(),
|
|
70
|
+
metadata.model ? Promise.resolve(metadata.model) : Promise.resolve(getModelManager().getModelName('main') || 'unknown'),
|
|
71
|
+
]).then(([statsigClient, isGit, betas, model]) => {
|
|
72
|
+
if (!statsigClient) return
|
|
73
|
+
|
|
74
|
+
const eventMetadata: Record<string, string> = {
|
|
75
|
+
...metadata,
|
|
76
|
+
model,
|
|
77
|
+
sessionId: SESSION_ID,
|
|
78
|
+
userType: process.env.USER_TYPE || '',
|
|
79
|
+
...(process.env.SWE_BENCH_RUN_ID
|
|
80
|
+
? { sweBenchId: process.env.SWE_BENCH_RUN_ID }
|
|
81
|
+
: {}),
|
|
82
|
+
...(betas.length > 0 ? { betas: betas.join(',') } : {}),
|
|
83
|
+
env: JSON.stringify({
|
|
84
|
+
isGit,
|
|
85
|
+
platform: env.platform,
|
|
86
|
+
nodeVersion: env.nodeVersion,
|
|
87
|
+
terminal: env.terminal,
|
|
88
|
+
version: MACRO.VERSION,
|
|
89
|
+
}),
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Debug logging when debug mode is enabled
|
|
93
|
+
if (process.argv.includes('--debug') || process.argv.includes('-d')) {
|
|
94
|
+
console.log(
|
|
95
|
+
chalk.dim(
|
|
96
|
+
`[DEBUG-ONLY] Statsig event: ${eventName} ${JSON.stringify(metadata, null, 0)}`,
|
|
97
|
+
),
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const event: StatsigEvent = {
|
|
102
|
+
eventName,
|
|
103
|
+
metadata: eventMetadata,
|
|
104
|
+
}
|
|
105
|
+
// statsigClient.logEvent(event)
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export const checkGate = memoize(async (gateName: string): Promise<boolean> => {
|
|
110
|
+
return true
|
|
111
|
+
// if (env.isCI || process.env.NODE_ENV === 'test') {
|
|
112
|
+
// return false
|
|
113
|
+
// }
|
|
114
|
+
// const statsigClient = await initializeStatsig()
|
|
115
|
+
// if (!statsigClient) return false
|
|
116
|
+
|
|
117
|
+
// const value = statsigClient.checkGate(gateName)
|
|
118
|
+
// gateValues[gateName] = value
|
|
119
|
+
// return value
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
export const useStatsigGate = (gateName: string, defaultValue = false) => {
|
|
123
|
+
return true
|
|
124
|
+
// const [gateValue, setGateValue] = React.useState(defaultValue)
|
|
125
|
+
// React.useEffect(() => {
|
|
126
|
+
// checkGate(gateName).then(setGateValue)
|
|
127
|
+
// }, [gateName])
|
|
128
|
+
// return gateValue
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function getGateValues(): Record<string, boolean> {
|
|
132
|
+
return { ...gateValues }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export const getExperimentValue = memoize(
|
|
136
|
+
async <T>(experimentName: string, defaultValue: T): Promise<T> => {
|
|
137
|
+
return defaultValue
|
|
138
|
+
// if (env.isCI || process.env.NODE_ENV === 'test') {
|
|
139
|
+
// return defaultValue
|
|
140
|
+
// }
|
|
141
|
+
// const statsigClient = await initializeStatsig()
|
|
142
|
+
// if (!statsigClient) return defaultValue
|
|
143
|
+
|
|
144
|
+
// const experiment = statsigClient.getExperiment(experimentName)
|
|
145
|
+
// if (Object.keys(experiment.value).length === 0) {
|
|
146
|
+
// logError(`getExperimentValue got empty value for ${experimentName}`)
|
|
147
|
+
// return defaultValue
|
|
148
|
+
// }
|
|
149
|
+
// return experiment.value as T
|
|
150
|
+
},
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
// NB Not memoized like other methods, to allow for dynamic config changes
|
|
154
|
+
export const getDynamicConfig = async <T>(
|
|
155
|
+
configName: string,
|
|
156
|
+
defaultValue: T,
|
|
157
|
+
): Promise<T> => {
|
|
158
|
+
return defaultValue
|
|
159
|
+
// if (env.isCI || process.env.NODE_ENV === 'test') {
|
|
160
|
+
// return defaultValue
|
|
161
|
+
// }
|
|
162
|
+
// const statsigClient = await initializeStatsig()
|
|
163
|
+
// if (!statsigClient) return defaultValue
|
|
164
|
+
|
|
165
|
+
// const config = statsigClient.getDynamicConfig(configName)
|
|
166
|
+
// if (Object.keys(config.value).length === 0) {
|
|
167
|
+
// logError(`getDynamicConfig got empty value for ${configName}`)
|
|
168
|
+
// return defaultValue
|
|
169
|
+
// }
|
|
170
|
+
// return config.value as T
|
|
171
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { StorageProvider } from '@statsig/client-core'
|
|
2
|
+
import * as fs from 'fs'
|
|
3
|
+
import * as path from 'path'
|
|
4
|
+
import { homedir } from 'os'
|
|
5
|
+
import { logError } from '../utils/log'
|
|
6
|
+
import { existsSync, unlinkSync } from 'fs'
|
|
7
|
+
import { CONFIG_BASE_DIR } from '../constants/product'
|
|
8
|
+
|
|
9
|
+
// Support both KODE_CONFIG_DIR and CLAUDE_CONFIG_DIR environment variables
|
|
10
|
+
const CONFIG_DIR = process.env.KODE_CONFIG_DIR ?? process.env.CLAUDE_CONFIG_DIR ?? path.join(homedir(), CONFIG_BASE_DIR)
|
|
11
|
+
const STATSIG_DIR = path.join(CONFIG_DIR, 'statsig')
|
|
12
|
+
|
|
13
|
+
// Ensure the directory exists
|
|
14
|
+
try {
|
|
15
|
+
fs.mkdirSync(STATSIG_DIR, { recursive: true })
|
|
16
|
+
} catch (error) {
|
|
17
|
+
logError(`Failed to create statsig storage directory: ${error}`)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class FileSystemStorageProvider implements StorageProvider {
|
|
21
|
+
private cache: Map<string, string> = new Map()
|
|
22
|
+
private ready = false
|
|
23
|
+
|
|
24
|
+
constructor() {
|
|
25
|
+
// Load all existing files into cache on startup
|
|
26
|
+
try {
|
|
27
|
+
if (!fs.existsSync(STATSIG_DIR)) {
|
|
28
|
+
fs.mkdirSync(STATSIG_DIR, { recursive: true })
|
|
29
|
+
}
|
|
30
|
+
const files = fs.readdirSync(STATSIG_DIR)
|
|
31
|
+
for (const file of files) {
|
|
32
|
+
const key = decodeURIComponent(file)
|
|
33
|
+
const value = fs.readFileSync(path.join(STATSIG_DIR, file), 'utf8')
|
|
34
|
+
this.cache.set(key, value)
|
|
35
|
+
}
|
|
36
|
+
this.ready = true
|
|
37
|
+
} catch (error) {
|
|
38
|
+
logError(`Failed to initialize statsig storage: ${error}`)
|
|
39
|
+
this.ready = true // Still mark as ready to avoid blocking
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
isReady(): boolean {
|
|
44
|
+
return this.ready
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
isReadyResolver(): Promise<void> | null {
|
|
48
|
+
return this.ready ? Promise.resolve() : null
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
getProviderName(): string {
|
|
52
|
+
return 'FileSystemStorageProvider'
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
getItem(key: string): string | null {
|
|
56
|
+
return this.cache.get(key) ?? null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
setItem(key: string, value: string): void {
|
|
60
|
+
this.cache.set(key, value)
|
|
61
|
+
try {
|
|
62
|
+
const encodedKey = encodeURIComponent(key)
|
|
63
|
+
fs.writeFileSync(path.join(STATSIG_DIR, encodedKey), value, 'utf8')
|
|
64
|
+
} catch (error) {
|
|
65
|
+
logError(`Failed to write statsig storage item: ${error}`)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
removeItem(key: string): void {
|
|
70
|
+
this.cache.delete(key)
|
|
71
|
+
const encodedKey = encodeURIComponent(key)
|
|
72
|
+
const file = path.join(STATSIG_DIR, encodedKey)
|
|
73
|
+
if (!existsSync(file)) {
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
unlinkSync(file)
|
|
78
|
+
} catch (error) {
|
|
79
|
+
logError(`Failed to remove statsig storage item: ${error}`)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
getAllKeys(): readonly string[] {
|
|
84
|
+
return Array.from(this.cache.keys())
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import { getTodos, TodoItem } from '../utils/todoStorage'
|
|
2
|
+
import { logEvent } from './statsig'
|
|
3
|
+
|
|
4
|
+
export interface ReminderMessage {
|
|
5
|
+
role: 'system'
|
|
6
|
+
content: string
|
|
7
|
+
isMeta: boolean
|
|
8
|
+
timestamp: number
|
|
9
|
+
type: string
|
|
10
|
+
priority: 'low' | 'medium' | 'high'
|
|
11
|
+
category: 'task' | 'security' | 'performance' | 'general'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface ReminderConfig {
|
|
15
|
+
todoEmptyReminder: boolean
|
|
16
|
+
securityReminder: boolean
|
|
17
|
+
performanceReminder: boolean
|
|
18
|
+
maxRemindersPerSession: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface SessionReminderState {
|
|
22
|
+
lastTodoUpdate: number
|
|
23
|
+
lastFileAccess: number
|
|
24
|
+
sessionStartTime: number
|
|
25
|
+
remindersSent: Set<string>
|
|
26
|
+
contextPresent: boolean
|
|
27
|
+
reminderCount: number
|
|
28
|
+
config: ReminderConfig
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
class SystemReminderService {
|
|
32
|
+
private sessionState: SessionReminderState = {
|
|
33
|
+
lastTodoUpdate: 0,
|
|
34
|
+
lastFileAccess: 0,
|
|
35
|
+
sessionStartTime: Date.now(),
|
|
36
|
+
remindersSent: new Set(),
|
|
37
|
+
contextPresent: false,
|
|
38
|
+
reminderCount: 0,
|
|
39
|
+
config: {
|
|
40
|
+
todoEmptyReminder: true,
|
|
41
|
+
securityReminder: true,
|
|
42
|
+
performanceReminder: true,
|
|
43
|
+
maxRemindersPerSession: 10,
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private eventDispatcher = new Map<string, Array<(context: any) => void>>()
|
|
48
|
+
private reminderCache = new Map<string, ReminderMessage>()
|
|
49
|
+
|
|
50
|
+
constructor() {
|
|
51
|
+
this.setupEventDispatcher()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Conditional reminder injection - only when context is present
|
|
56
|
+
* Enhanced with performance optimizations and priority management
|
|
57
|
+
*/
|
|
58
|
+
public generateReminders(
|
|
59
|
+
hasContext: boolean = false,
|
|
60
|
+
agentId?: string,
|
|
61
|
+
): ReminderMessage[] {
|
|
62
|
+
this.sessionState.contextPresent = hasContext
|
|
63
|
+
|
|
64
|
+
// Only inject when context is present (matching original behavior)
|
|
65
|
+
if (!hasContext) {
|
|
66
|
+
return []
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Check session reminder limit to prevent overload
|
|
70
|
+
if (
|
|
71
|
+
this.sessionState.reminderCount >=
|
|
72
|
+
this.sessionState.config.maxRemindersPerSession
|
|
73
|
+
) {
|
|
74
|
+
return []
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const reminders: ReminderMessage[] = []
|
|
78
|
+
const currentTime = Date.now()
|
|
79
|
+
|
|
80
|
+
// Use lazy evaluation for performance with agent context
|
|
81
|
+
const reminderGenerators = [
|
|
82
|
+
() => this.dispatchTodoEvent(agentId),
|
|
83
|
+
() => this.dispatchSecurityEvent(),
|
|
84
|
+
() => this.dispatchPerformanceEvent(),
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
for (const generator of reminderGenerators) {
|
|
88
|
+
if (reminders.length >= 3) break // Limit concurrent reminders
|
|
89
|
+
|
|
90
|
+
const reminder = generator()
|
|
91
|
+
if (reminder) {
|
|
92
|
+
reminders.push(reminder)
|
|
93
|
+
this.sessionState.reminderCount++
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Log aggregated metrics instead of individual events for performance
|
|
98
|
+
if (reminders.length > 0) {
|
|
99
|
+
logEvent('system_reminder_batch', {
|
|
100
|
+
count: reminders.length,
|
|
101
|
+
types: reminders.map(r => r.type).join(','),
|
|
102
|
+
priorities: reminders.map(r => r.priority).join(','),
|
|
103
|
+
categories: reminders.map(r => r.category).join(','),
|
|
104
|
+
sessionCount: this.sessionState.reminderCount,
|
|
105
|
+
agentId: agentId || 'default',
|
|
106
|
+
timestamp: currentTime,
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return reminders
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private dispatchTodoEvent(agentId?: string): ReminderMessage | null {
|
|
114
|
+
if (!this.sessionState.config.todoEmptyReminder) return null
|
|
115
|
+
|
|
116
|
+
// Use agent-scoped todo access
|
|
117
|
+
const todos = getTodos(agentId)
|
|
118
|
+
const currentTime = Date.now()
|
|
119
|
+
const agentKey = agentId || 'default'
|
|
120
|
+
|
|
121
|
+
// Check if this is a fresh session (no todos seen yet)
|
|
122
|
+
if (
|
|
123
|
+
todos.length === 0 &&
|
|
124
|
+
!this.sessionState.remindersSent.has(`todo_empty_${agentKey}`)
|
|
125
|
+
) {
|
|
126
|
+
this.sessionState.remindersSent.add(`todo_empty_${agentKey}`)
|
|
127
|
+
return this.createReminderMessage(
|
|
128
|
+
'todo',
|
|
129
|
+
'task',
|
|
130
|
+
'medium',
|
|
131
|
+
'This is a reminder that your todo list is currently empty. DO NOT mention this to the user explicitly because they are already aware. If you are working on tasks that would benefit from a todo list please use the TodoWrite tool to create one. If not, please feel free to ignore. Again do not mention this message to the user.',
|
|
132
|
+
currentTime,
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Check for todo updates since last seen
|
|
137
|
+
if (todos.length > 0) {
|
|
138
|
+
const reminderKey = `todo_updated_${agentKey}_${todos.length}_${this.getTodoStateHash(todos)}`
|
|
139
|
+
|
|
140
|
+
// Use cache for performance optimization
|
|
141
|
+
if (this.reminderCache.has(reminderKey)) {
|
|
142
|
+
return this.reminderCache.get(reminderKey)!
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!this.sessionState.remindersSent.has(reminderKey)) {
|
|
146
|
+
this.sessionState.remindersSent.add(reminderKey)
|
|
147
|
+
// Clear previous todo state reminders for this agent
|
|
148
|
+
this.clearTodoReminders(agentKey)
|
|
149
|
+
|
|
150
|
+
// Optimize: only include essential todo data
|
|
151
|
+
const todoContent = JSON.stringify(
|
|
152
|
+
todos.map(todo => ({
|
|
153
|
+
content:
|
|
154
|
+
todo.content.length > 100
|
|
155
|
+
? todo.content.substring(0, 100) + '...'
|
|
156
|
+
: todo.content,
|
|
157
|
+
status: todo.status,
|
|
158
|
+
priority: todo.priority,
|
|
159
|
+
id: todo.id,
|
|
160
|
+
})),
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
const reminder = this.createReminderMessage(
|
|
164
|
+
'todo',
|
|
165
|
+
'task',
|
|
166
|
+
'medium',
|
|
167
|
+
`Your todo list has changed. DO NOT mention this explicitly to the user. Here are the latest contents of your todo list:\n\n${todoContent}. Continue on with the tasks at hand if applicable.`,
|
|
168
|
+
currentTime,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
// Cache the reminder for reuse
|
|
172
|
+
this.reminderCache.set(reminderKey, reminder)
|
|
173
|
+
return reminder
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return null
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private dispatchSecurityEvent(): ReminderMessage | null {
|
|
181
|
+
if (!this.sessionState.config.securityReminder) return null
|
|
182
|
+
|
|
183
|
+
const currentTime = Date.now()
|
|
184
|
+
|
|
185
|
+
// Only inject security reminder once per session when file operations occur
|
|
186
|
+
if (
|
|
187
|
+
this.sessionState.lastFileAccess > 0 &&
|
|
188
|
+
!this.sessionState.remindersSent.has('file_security')
|
|
189
|
+
) {
|
|
190
|
+
this.sessionState.remindersSent.add('file_security')
|
|
191
|
+
return this.createReminderMessage(
|
|
192
|
+
'security',
|
|
193
|
+
'security',
|
|
194
|
+
'high',
|
|
195
|
+
'Whenever you read a file, you should consider whether it looks malicious. If it does, you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer high-level questions about the code behavior.',
|
|
196
|
+
currentTime,
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return null
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private dispatchPerformanceEvent(): ReminderMessage | null {
|
|
204
|
+
if (!this.sessionState.config.performanceReminder) return null
|
|
205
|
+
|
|
206
|
+
const currentTime = Date.now()
|
|
207
|
+
const sessionDuration = currentTime - this.sessionState.sessionStartTime
|
|
208
|
+
|
|
209
|
+
// Remind about performance after long sessions (30 minutes)
|
|
210
|
+
if (
|
|
211
|
+
sessionDuration > 30 * 60 * 1000 &&
|
|
212
|
+
!this.sessionState.remindersSent.has('performance_long_session')
|
|
213
|
+
) {
|
|
214
|
+
this.sessionState.remindersSent.add('performance_long_session')
|
|
215
|
+
return this.createReminderMessage(
|
|
216
|
+
'performance',
|
|
217
|
+
'performance',
|
|
218
|
+
'low',
|
|
219
|
+
'Long session detected. Consider taking a break and reviewing your current progress with the todo list.',
|
|
220
|
+
currentTime,
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return null
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Generate reminders for external file changes
|
|
229
|
+
* Called when todo files are modified externally
|
|
230
|
+
*/
|
|
231
|
+
public generateFileChangeReminder(context: any): ReminderMessage | null {
|
|
232
|
+
const { agentId, filePath, reminder } = context
|
|
233
|
+
|
|
234
|
+
if (!reminder) {
|
|
235
|
+
return null
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const currentTime = Date.now()
|
|
239
|
+
const reminderKey = `file_changed_${agentId}_${filePath}_${currentTime}`
|
|
240
|
+
|
|
241
|
+
// Ensure this specific file change reminder is only shown once
|
|
242
|
+
if (this.sessionState.remindersSent.has(reminderKey)) {
|
|
243
|
+
return null
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
this.sessionState.remindersSent.add(reminderKey)
|
|
247
|
+
|
|
248
|
+
return this.createReminderMessage(
|
|
249
|
+
'file_changed',
|
|
250
|
+
'general',
|
|
251
|
+
'medium',
|
|
252
|
+
reminder,
|
|
253
|
+
currentTime,
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private createReminderMessage(
|
|
258
|
+
type: string,
|
|
259
|
+
category: ReminderMessage['category'],
|
|
260
|
+
priority: ReminderMessage['priority'],
|
|
261
|
+
content: string,
|
|
262
|
+
timestamp: number,
|
|
263
|
+
): ReminderMessage {
|
|
264
|
+
return {
|
|
265
|
+
role: 'system',
|
|
266
|
+
content: `<system-reminder>\n${content}\n</system-reminder>`,
|
|
267
|
+
isMeta: true,
|
|
268
|
+
timestamp,
|
|
269
|
+
type,
|
|
270
|
+
priority,
|
|
271
|
+
category,
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private getTodoStateHash(todos: TodoItem[]): string {
|
|
276
|
+
return todos
|
|
277
|
+
.map(t => `${t.id}:${t.status}`)
|
|
278
|
+
.sort()
|
|
279
|
+
.join('|')
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private clearTodoReminders(agentId?: string): void {
|
|
283
|
+
const agentKey = agentId || 'default'
|
|
284
|
+
for (const key of this.sessionState.remindersSent) {
|
|
285
|
+
if (key.startsWith(`todo_updated_${agentKey}_`)) {
|
|
286
|
+
this.sessionState.remindersSent.delete(key)
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
private setupEventDispatcher(): void {
|
|
292
|
+
// Session startup events
|
|
293
|
+
this.addEventListener('session:startup', context => {
|
|
294
|
+
// Reset session state on startup
|
|
295
|
+
this.resetSession()
|
|
296
|
+
|
|
297
|
+
// Initialize session tracking
|
|
298
|
+
this.sessionState.sessionStartTime = Date.now()
|
|
299
|
+
this.sessionState.contextPresent =
|
|
300
|
+
Object.keys(context.context || {}).length > 0
|
|
301
|
+
|
|
302
|
+
// Log session startup
|
|
303
|
+
logEvent('system_reminder_session_startup', {
|
|
304
|
+
agentId: context.agentId || 'default',
|
|
305
|
+
contextKeys: Object.keys(context.context || {}),
|
|
306
|
+
messageCount: context.messages || 0,
|
|
307
|
+
timestamp: context.timestamp,
|
|
308
|
+
})
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
// Todo change events
|
|
312
|
+
this.addEventListener('todo:changed', context => {
|
|
313
|
+
this.sessionState.lastTodoUpdate = Date.now()
|
|
314
|
+
this.clearTodoReminders(context.agentId)
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
// Todo file changed externally
|
|
318
|
+
this.addEventListener('todo:file_changed', context => {
|
|
319
|
+
// External file change detected, trigger reminder injection
|
|
320
|
+
const agentId = context.agentId || 'default'
|
|
321
|
+
this.clearTodoReminders(agentId)
|
|
322
|
+
this.sessionState.lastTodoUpdate = Date.now()
|
|
323
|
+
|
|
324
|
+
// Generate and inject file change reminder immediately
|
|
325
|
+
const reminder = this.generateFileChangeReminder(context)
|
|
326
|
+
if (reminder) {
|
|
327
|
+
// Inject reminder into the latest user message through event system
|
|
328
|
+
this.emitEvent('reminder:inject', {
|
|
329
|
+
reminder: reminder.content,
|
|
330
|
+
agentId,
|
|
331
|
+
type: 'file_changed',
|
|
332
|
+
timestamp: Date.now(),
|
|
333
|
+
})
|
|
334
|
+
}
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
// File access events
|
|
338
|
+
this.addEventListener('file:read', context => {
|
|
339
|
+
this.sessionState.lastFileAccess = Date.now()
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
// File edit events for freshness detection
|
|
343
|
+
this.addEventListener('file:edited', context => {
|
|
344
|
+
// File edit handling
|
|
345
|
+
})
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
public addEventListener(
|
|
349
|
+
event: string,
|
|
350
|
+
callback: (context: any) => void,
|
|
351
|
+
): void {
|
|
352
|
+
if (!this.eventDispatcher.has(event)) {
|
|
353
|
+
this.eventDispatcher.set(event, [])
|
|
354
|
+
}
|
|
355
|
+
this.eventDispatcher.get(event)!.push(callback)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
public emitEvent(event: string, context: any): void {
|
|
359
|
+
const listeners = this.eventDispatcher.get(event) || []
|
|
360
|
+
listeners.forEach(callback => {
|
|
361
|
+
try {
|
|
362
|
+
callback(context)
|
|
363
|
+
} catch (error) {
|
|
364
|
+
console.error(`Error in event listener for ${event}:`, error)
|
|
365
|
+
}
|
|
366
|
+
})
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
public resetSession(): void {
|
|
370
|
+
this.sessionState = {
|
|
371
|
+
lastTodoUpdate: 0,
|
|
372
|
+
lastFileAccess: 0,
|
|
373
|
+
sessionStartTime: Date.now(),
|
|
374
|
+
remindersSent: new Set(),
|
|
375
|
+
contextPresent: false,
|
|
376
|
+
reminderCount: 0,
|
|
377
|
+
config: { ...this.sessionState.config }, // Preserve config across resets
|
|
378
|
+
}
|
|
379
|
+
this.reminderCache.clear() // Clear cache on session reset
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
public updateConfig(config: Partial<ReminderConfig>): void {
|
|
383
|
+
this.sessionState.config = { ...this.sessionState.config, ...config }
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
public getSessionState(): SessionReminderState {
|
|
387
|
+
return { ...this.sessionState }
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export const systemReminderService = new SystemReminderService()
|
|
392
|
+
|
|
393
|
+
export const generateSystemReminders = (
|
|
394
|
+
hasContext: boolean = false,
|
|
395
|
+
agentId?: string,
|
|
396
|
+
) => systemReminderService.generateReminders(hasContext, agentId)
|
|
397
|
+
|
|
398
|
+
export const generateFileChangeReminder = (context: any) =>
|
|
399
|
+
systemReminderService.generateFileChangeReminder(context)
|
|
400
|
+
|
|
401
|
+
export const emitReminderEvent = (event: string, context: any) =>
|
|
402
|
+
systemReminderService.emitEvent(event, context)
|
|
403
|
+
|
|
404
|
+
export const resetReminderSession = () => systemReminderService.resetSession()
|
|
405
|
+
export const getReminderSessionState = () =>
|
|
406
|
+
systemReminderService.getSessionState()
|