@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.
Files changed (253) hide show
  1. package/README.md +202 -76
  2. package/README.zh-CN.md +246 -0
  3. package/cli.js +62 -0
  4. package/package.json +45 -25
  5. package/scripts/postinstall.js +56 -0
  6. package/src/ProjectOnboarding.tsx +180 -0
  7. package/src/Tool.ts +53 -0
  8. package/src/commands/approvedTools.ts +53 -0
  9. package/src/commands/bug.tsx +20 -0
  10. package/src/commands/clear.ts +43 -0
  11. package/src/commands/compact.ts +120 -0
  12. package/src/commands/config.tsx +19 -0
  13. package/src/commands/cost.ts +18 -0
  14. package/src/commands/ctx_viz.ts +209 -0
  15. package/src/commands/doctor.ts +24 -0
  16. package/src/commands/help.tsx +19 -0
  17. package/src/commands/init.ts +37 -0
  18. package/src/commands/listen.ts +42 -0
  19. package/src/commands/login.tsx +51 -0
  20. package/src/commands/logout.tsx +40 -0
  21. package/src/commands/mcp.ts +41 -0
  22. package/src/commands/model.tsx +40 -0
  23. package/src/commands/modelstatus.tsx +20 -0
  24. package/src/commands/onboarding.tsx +34 -0
  25. package/src/commands/pr_comments.ts +59 -0
  26. package/src/commands/refreshCommands.ts +54 -0
  27. package/src/commands/release-notes.ts +34 -0
  28. package/src/commands/resume.tsx +30 -0
  29. package/src/commands/review.ts +49 -0
  30. package/src/commands/terminalSetup.ts +221 -0
  31. package/src/commands.ts +136 -0
  32. package/src/components/ApproveApiKey.tsx +93 -0
  33. package/src/components/AsciiLogo.tsx +13 -0
  34. package/src/components/AutoUpdater.tsx +148 -0
  35. package/src/components/Bug.tsx +367 -0
  36. package/src/components/Config.tsx +289 -0
  37. package/src/components/ConsoleOAuthFlow.tsx +326 -0
  38. package/src/components/Cost.tsx +23 -0
  39. package/src/components/CostThresholdDialog.tsx +46 -0
  40. package/src/components/CustomSelect/option-map.ts +42 -0
  41. package/src/components/CustomSelect/select-option.tsx +52 -0
  42. package/src/components/CustomSelect/select.tsx +143 -0
  43. package/src/components/CustomSelect/use-select-state.ts +414 -0
  44. package/src/components/CustomSelect/use-select.ts +35 -0
  45. package/src/components/FallbackToolUseRejectedMessage.tsx +15 -0
  46. package/src/components/FileEditToolUpdatedMessage.tsx +66 -0
  47. package/src/components/Help.tsx +215 -0
  48. package/src/components/HighlightedCode.tsx +33 -0
  49. package/src/components/InvalidConfigDialog.tsx +113 -0
  50. package/src/components/Link.tsx +32 -0
  51. package/src/components/LogSelector.tsx +86 -0
  52. package/src/components/Logo.tsx +145 -0
  53. package/src/components/MCPServerApprovalDialog.tsx +100 -0
  54. package/src/components/MCPServerDialogCopy.tsx +25 -0
  55. package/src/components/MCPServerMultiselectDialog.tsx +109 -0
  56. package/src/components/Message.tsx +219 -0
  57. package/src/components/MessageResponse.tsx +15 -0
  58. package/src/components/MessageSelector.tsx +211 -0
  59. package/src/components/ModeIndicator.tsx +88 -0
  60. package/src/components/ModelConfig.tsx +301 -0
  61. package/src/components/ModelListManager.tsx +223 -0
  62. package/src/components/ModelSelector.tsx +3208 -0
  63. package/src/components/ModelStatusDisplay.tsx +228 -0
  64. package/src/components/Onboarding.tsx +274 -0
  65. package/src/components/PressEnterToContinue.tsx +11 -0
  66. package/src/components/PromptInput.tsx +710 -0
  67. package/src/components/SentryErrorBoundary.ts +33 -0
  68. package/src/components/Spinner.tsx +129 -0
  69. package/src/components/StructuredDiff.tsx +184 -0
  70. package/src/components/TextInput.tsx +246 -0
  71. package/src/components/TokenWarning.tsx +31 -0
  72. package/src/components/ToolUseLoader.tsx +40 -0
  73. package/src/components/TrustDialog.tsx +106 -0
  74. package/src/components/binary-feedback/BinaryFeedback.tsx +63 -0
  75. package/src/components/binary-feedback/BinaryFeedbackOption.tsx +111 -0
  76. package/src/components/binary-feedback/BinaryFeedbackView.tsx +172 -0
  77. package/src/components/binary-feedback/utils.ts +220 -0
  78. package/src/components/messages/AssistantBashOutputMessage.tsx +22 -0
  79. package/src/components/messages/AssistantLocalCommandOutputMessage.tsx +45 -0
  80. package/src/components/messages/AssistantRedactedThinkingMessage.tsx +19 -0
  81. package/src/components/messages/AssistantTextMessage.tsx +144 -0
  82. package/src/components/messages/AssistantThinkingMessage.tsx +40 -0
  83. package/src/components/messages/AssistantToolUseMessage.tsx +123 -0
  84. package/src/components/messages/UserBashInputMessage.tsx +28 -0
  85. package/src/components/messages/UserCommandMessage.tsx +30 -0
  86. package/src/components/messages/UserKodingInputMessage.tsx +28 -0
  87. package/src/components/messages/UserPromptMessage.tsx +35 -0
  88. package/src/components/messages/UserTextMessage.tsx +39 -0
  89. package/src/components/messages/UserToolResultMessage/UserToolCanceledMessage.tsx +12 -0
  90. package/src/components/messages/UserToolResultMessage/UserToolErrorMessage.tsx +36 -0
  91. package/src/components/messages/UserToolResultMessage/UserToolRejectMessage.tsx +31 -0
  92. package/src/components/messages/UserToolResultMessage/UserToolResultMessage.tsx +57 -0
  93. package/src/components/messages/UserToolResultMessage/UserToolSuccessMessage.tsx +35 -0
  94. package/src/components/messages/UserToolResultMessage/utils.tsx +56 -0
  95. package/src/components/permissions/BashPermissionRequest/BashPermissionRequest.tsx +121 -0
  96. package/src/components/permissions/FallbackPermissionRequest.tsx +155 -0
  97. package/src/components/permissions/FileEditPermissionRequest/FileEditPermissionRequest.tsx +182 -0
  98. package/src/components/permissions/FileEditPermissionRequest/FileEditToolDiff.tsx +75 -0
  99. package/src/components/permissions/FileWritePermissionRequest/FileWritePermissionRequest.tsx +164 -0
  100. package/src/components/permissions/FileWritePermissionRequest/FileWriteToolDiff.tsx +81 -0
  101. package/src/components/permissions/FilesystemPermissionRequest/FilesystemPermissionRequest.tsx +242 -0
  102. package/src/components/permissions/PermissionRequest.tsx +103 -0
  103. package/src/components/permissions/PermissionRequestTitle.tsx +69 -0
  104. package/src/components/permissions/hooks.ts +44 -0
  105. package/src/components/permissions/toolUseOptions.ts +59 -0
  106. package/src/components/permissions/utils.ts +23 -0
  107. package/src/constants/betas.ts +5 -0
  108. package/src/constants/claude-asterisk-ascii-art.tsx +238 -0
  109. package/src/constants/figures.ts +4 -0
  110. package/src/constants/keys.ts +3 -0
  111. package/src/constants/macros.ts +6 -0
  112. package/src/constants/models.ts +935 -0
  113. package/src/constants/oauth.ts +18 -0
  114. package/src/constants/product.ts +17 -0
  115. package/src/constants/prompts.ts +177 -0
  116. package/src/constants/releaseNotes.ts +7 -0
  117. package/src/context/PermissionContext.tsx +149 -0
  118. package/src/context.ts +278 -0
  119. package/src/cost-tracker.ts +84 -0
  120. package/src/entrypoints/cli.tsx +1498 -0
  121. package/src/entrypoints/mcp.ts +176 -0
  122. package/src/history.ts +25 -0
  123. package/src/hooks/useApiKeyVerification.ts +59 -0
  124. package/src/hooks/useArrowKeyHistory.ts +55 -0
  125. package/src/hooks/useCanUseTool.ts +138 -0
  126. package/src/hooks/useCancelRequest.ts +39 -0
  127. package/src/hooks/useDoublePress.ts +42 -0
  128. package/src/hooks/useExitOnCtrlCD.ts +31 -0
  129. package/src/hooks/useInterval.ts +25 -0
  130. package/src/hooks/useLogMessages.ts +16 -0
  131. package/src/hooks/useLogStartupTime.ts +12 -0
  132. package/src/hooks/useNotifyAfterTimeout.ts +65 -0
  133. package/src/hooks/usePermissionRequestLogging.ts +44 -0
  134. package/src/hooks/useSlashCommandTypeahead.ts +137 -0
  135. package/src/hooks/useTerminalSize.ts +49 -0
  136. package/src/hooks/useTextInput.ts +315 -0
  137. package/src/messages.ts +37 -0
  138. package/src/permissions.ts +268 -0
  139. package/src/query.ts +704 -0
  140. package/src/screens/ConfigureNpmPrefix.tsx +197 -0
  141. package/src/screens/Doctor.tsx +219 -0
  142. package/src/screens/LogList.tsx +68 -0
  143. package/src/screens/REPL.tsx +792 -0
  144. package/src/screens/ResumeConversation.tsx +68 -0
  145. package/src/services/browserMocks.ts +66 -0
  146. package/src/services/claude.ts +1947 -0
  147. package/src/services/customCommands.ts +683 -0
  148. package/src/services/fileFreshness.ts +377 -0
  149. package/src/services/mcpClient.ts +564 -0
  150. package/src/services/mcpServerApproval.tsx +50 -0
  151. package/src/services/notifier.ts +40 -0
  152. package/src/services/oauth.ts +357 -0
  153. package/src/services/openai.ts +796 -0
  154. package/src/services/sentry.ts +3 -0
  155. package/src/services/statsig.ts +171 -0
  156. package/src/services/statsigStorage.ts +86 -0
  157. package/src/services/systemReminder.ts +406 -0
  158. package/src/services/vcr.ts +161 -0
  159. package/src/tools/ArchitectTool/ArchitectTool.tsx +122 -0
  160. package/src/tools/ArchitectTool/prompt.ts +15 -0
  161. package/src/tools/AskExpertModelTool/AskExpertModelTool.tsx +505 -0
  162. package/src/tools/BashTool/BashTool.tsx +270 -0
  163. package/src/tools/BashTool/BashToolResultMessage.tsx +38 -0
  164. package/src/tools/BashTool/OutputLine.tsx +48 -0
  165. package/src/tools/BashTool/prompt.ts +174 -0
  166. package/src/tools/BashTool/utils.ts +56 -0
  167. package/src/tools/FileEditTool/FileEditTool.tsx +316 -0
  168. package/src/tools/FileEditTool/prompt.ts +51 -0
  169. package/src/tools/FileEditTool/utils.ts +58 -0
  170. package/src/tools/FileReadTool/FileReadTool.tsx +371 -0
  171. package/src/tools/FileReadTool/prompt.ts +7 -0
  172. package/src/tools/FileWriteTool/FileWriteTool.tsx +297 -0
  173. package/src/tools/FileWriteTool/prompt.ts +10 -0
  174. package/src/tools/GlobTool/GlobTool.tsx +119 -0
  175. package/src/tools/GlobTool/prompt.ts +8 -0
  176. package/src/tools/GrepTool/GrepTool.tsx +147 -0
  177. package/src/tools/GrepTool/prompt.ts +11 -0
  178. package/src/tools/MCPTool/MCPTool.tsx +106 -0
  179. package/src/tools/MCPTool/prompt.ts +3 -0
  180. package/src/tools/MemoryReadTool/MemoryReadTool.tsx +127 -0
  181. package/src/tools/MemoryReadTool/prompt.ts +3 -0
  182. package/src/tools/MemoryWriteTool/MemoryWriteTool.tsx +89 -0
  183. package/src/tools/MemoryWriteTool/prompt.ts +3 -0
  184. package/src/tools/MultiEditTool/MultiEditTool.tsx +366 -0
  185. package/src/tools/MultiEditTool/prompt.ts +45 -0
  186. package/src/tools/NotebookEditTool/NotebookEditTool.tsx +298 -0
  187. package/src/tools/NotebookEditTool/prompt.ts +3 -0
  188. package/src/tools/NotebookReadTool/NotebookReadTool.tsx +266 -0
  189. package/src/tools/NotebookReadTool/prompt.ts +3 -0
  190. package/src/tools/StickerRequestTool/StickerRequestTool.tsx +93 -0
  191. package/src/tools/StickerRequestTool/prompt.ts +19 -0
  192. package/src/tools/TaskTool/TaskTool.tsx +382 -0
  193. package/src/tools/TaskTool/constants.ts +1 -0
  194. package/src/tools/TaskTool/prompt.ts +56 -0
  195. package/src/tools/ThinkTool/ThinkTool.tsx +56 -0
  196. package/src/tools/ThinkTool/prompt.ts +12 -0
  197. package/src/tools/TodoWriteTool/TodoWriteTool.tsx +289 -0
  198. package/src/tools/TodoWriteTool/prompt.ts +63 -0
  199. package/src/tools/lsTool/lsTool.tsx +269 -0
  200. package/src/tools/lsTool/prompt.ts +2 -0
  201. package/src/tools.ts +63 -0
  202. package/src/types/PermissionMode.ts +120 -0
  203. package/src/types/RequestContext.ts +72 -0
  204. package/src/utils/Cursor.ts +436 -0
  205. package/src/utils/PersistentShell.ts +373 -0
  206. package/src/utils/agentStorage.ts +97 -0
  207. package/src/utils/array.ts +3 -0
  208. package/src/utils/ask.tsx +98 -0
  209. package/src/utils/auth.ts +13 -0
  210. package/src/utils/autoCompactCore.ts +223 -0
  211. package/src/utils/autoUpdater.ts +318 -0
  212. package/src/utils/betas.ts +20 -0
  213. package/src/utils/browser.ts +14 -0
  214. package/src/utils/cleanup.ts +72 -0
  215. package/src/utils/commands.ts +261 -0
  216. package/src/utils/config.ts +771 -0
  217. package/src/utils/conversationRecovery.ts +54 -0
  218. package/src/utils/debugLogger.ts +1123 -0
  219. package/src/utils/diff.ts +42 -0
  220. package/src/utils/env.ts +57 -0
  221. package/src/utils/errors.ts +21 -0
  222. package/src/utils/exampleCommands.ts +108 -0
  223. package/src/utils/execFileNoThrow.ts +51 -0
  224. package/src/utils/expertChatStorage.ts +136 -0
  225. package/src/utils/file.ts +402 -0
  226. package/src/utils/fileRecoveryCore.ts +71 -0
  227. package/src/utils/format.tsx +44 -0
  228. package/src/utils/generators.ts +62 -0
  229. package/src/utils/git.ts +92 -0
  230. package/src/utils/globalLogger.ts +77 -0
  231. package/src/utils/http.ts +10 -0
  232. package/src/utils/imagePaste.ts +38 -0
  233. package/src/utils/json.ts +13 -0
  234. package/src/utils/log.ts +382 -0
  235. package/src/utils/markdown.ts +213 -0
  236. package/src/utils/messageContextManager.ts +289 -0
  237. package/src/utils/messages.tsx +938 -0
  238. package/src/utils/model.ts +836 -0
  239. package/src/utils/permissions/filesystem.ts +118 -0
  240. package/src/utils/ripgrep.ts +167 -0
  241. package/src/utils/sessionState.ts +49 -0
  242. package/src/utils/state.ts +25 -0
  243. package/src/utils/style.ts +29 -0
  244. package/src/utils/terminal.ts +49 -0
  245. package/src/utils/theme.ts +122 -0
  246. package/src/utils/thinking.ts +144 -0
  247. package/src/utils/todoStorage.ts +431 -0
  248. package/src/utils/tokens.ts +43 -0
  249. package/src/utils/toolExecutionController.ts +163 -0
  250. package/src/utils/unaryLogging.ts +26 -0
  251. package/src/utils/user.ts +37 -0
  252. package/src/utils/validate.ts +165 -0
  253. package/cli.mjs +0 -1803
@@ -0,0 +1,3 @@
1
+ export function initSentry(): void {}
2
+
3
+ export async function captureException(error: unknown): Promise<void> {}
@@ -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()