ethagent 3.3.4 → 4.0.0

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 (322) hide show
  1. package/.claude-plugin/marketplace.json +11 -0
  2. package/.claude-plugin/plugin.json +35 -0
  3. package/LICENSE +1 -1
  4. package/README.md +64 -104
  5. package/commands/ethagent.md +40 -0
  6. package/package.json +16 -16
  7. package/src/app/keybindings/KeybindingProvider.tsx +1 -6
  8. package/src/app/keybindings/types.ts +1 -6
  9. package/src/cli/ResetConfirmView.tsx +54 -53
  10. package/src/cli/demo.ts +86 -0
  11. package/src/cli/hookIo.ts +45 -0
  12. package/src/cli/main.tsx +94 -123
  13. package/src/cli/memoryGuard.ts +49 -0
  14. package/src/cli/reset.ts +28 -70
  15. package/src/cli/sessionStart.ts +33 -0
  16. package/src/cli/status.ts +46 -0
  17. package/src/cli/sync.ts +167 -0
  18. package/src/cli/syncAdapters/claude-code.ts +86 -0
  19. package/src/cli/syncAdapters/codex.ts +66 -0
  20. package/src/cli/syncAdapters/index.ts +45 -0
  21. package/src/cli/syncAdapters/managedBlock.ts +175 -0
  22. package/src/cli/syncAdapters/shared.ts +63 -0
  23. package/src/identity/continuity/envelopeParse.ts +20 -1
  24. package/src/identity/continuity/publicSkills.ts +3 -1
  25. package/src/identity/continuity/skills/publicSkillsSync.ts +2 -1
  26. package/src/identity/continuity/skills/scaffold.ts +5 -2
  27. package/src/identity/continuity/snapshots.ts +12 -5
  28. package/src/identity/continuity/storage/defaults.ts +20 -19
  29. package/src/identity/continuity/storage/status.ts +1 -1
  30. package/src/identity/ens/ensLookup/constants.ts +1 -1
  31. package/src/identity/manager/IdentityManager.tsx +33 -0
  32. package/src/identity/{hub → manager}/OperationalRoutes.tsx +37 -18
  33. package/src/identity/{hub → manager}/Routes.tsx +48 -34
  34. package/src/identity/{hub → manager}/continuity/ContinuityDashboardScreen.tsx +9 -19
  35. package/src/identity/{hub → manager}/continuity/RebackupStorageScreen.tsx +3 -3
  36. package/src/identity/manager/continuity/RecoveryConfirmScreen.tsx +102 -0
  37. package/src/identity/{hub → manager}/continuity/SavePromptScreen.tsx +2 -3
  38. package/src/identity/{hub → manager}/continuity/completion.ts +1 -1
  39. package/src/identity/{hub → manager}/continuity/effects.ts +1 -1
  40. package/src/identity/{hub → manager}/continuity/skills/DeleteSkillConfirmScreen.tsx +2 -2
  41. package/src/identity/{hub → manager}/continuity/skills/NewSkillScreen.tsx +0 -5
  42. package/src/identity/{hub → manager}/continuity/skills/NewSkillVisibilityScreen.tsx +4 -4
  43. package/src/identity/{hub → manager}/continuity/skills/SkillActionsScreen.tsx +6 -22
  44. package/src/identity/{hub → manager}/continuity/skills/SkillsTreeScreen.tsx +5 -17
  45. package/src/identity/{hub → manager}/continuity/snapshot.ts +1 -1
  46. package/src/identity/{hub → manager}/continuity/vault.ts +1 -1
  47. package/src/identity/{hub → manager}/create/CreateFlow.tsx +59 -32
  48. package/src/identity/{hub → manager}/create/effects.ts +19 -10
  49. package/src/identity/manager/create/importScan.ts +122 -0
  50. package/src/identity/{hub → manager}/custody/CustodyEditFlow.tsx +17 -61
  51. package/src/identity/{hub → manager}/custody/actions.ts +1 -15
  52. package/src/identity/{hub → manager}/custody/routes.tsx +20 -40
  53. package/src/identity/{hub → manager}/custody/transactions.ts +1 -0
  54. package/src/identity/{hub → manager}/custody/types.ts +1 -2
  55. package/src/identity/{hub → manager}/custody/useCustodyEffects.ts +1 -1
  56. package/src/identity/{hub → manager}/ens/EnsEditAdvancedScreens.tsx +2 -2
  57. package/src/identity/{hub → manager}/ens/EnsEditMaintenanceScreens.tsx +12 -23
  58. package/src/identity/{hub → manager}/ens/EnsEditReviewScreens.tsx +18 -42
  59. package/src/identity/{hub → manager}/ens/EnsEditRunners.tsx +1 -1
  60. package/src/identity/{hub → manager}/ens/EnsEditShared.tsx +0 -2
  61. package/src/identity/{hub → manager}/ens/EnsEditSimpleScreens.tsx +10 -19
  62. package/src/identity/{hub → manager}/ens/EnsFlow.tsx +133 -41
  63. package/src/identity/{hub → manager}/ens/EnsOperatorWalletsScreen.tsx +14 -19
  64. package/src/identity/{hub → manager}/ens/editCopy.ts +1 -14
  65. package/src/identity/{hub → manager}/profile/EditProfileFlow.tsx +99 -66
  66. package/src/identity/{hub → manager}/profile/effects.ts +1 -3
  67. package/src/identity/{hub → manager}/profile/operatorSave.ts +1 -1
  68. package/src/identity/{hub → manager}/profile/state.ts +1 -1
  69. package/src/identity/{hub/identityHubReducer.ts → manager/reducer.ts} +25 -26
  70. package/src/identity/{hub → manager}/restore/RestoreFlow.tsx +16 -24
  71. package/src/identity/{hub → manager}/restore/apply.ts +1 -1
  72. package/src/identity/{hub → manager}/restore/auth.ts +1 -1
  73. package/src/identity/{hub → manager}/restore/discover.ts +1 -1
  74. package/src/identity/{hub → manager}/restore/fetch.ts +1 -1
  75. package/src/identity/{hub → manager}/restore/restoreAdmin.ts +1 -1
  76. package/src/identity/{hub → manager}/restore/useRestoreEffects.ts +2 -9
  77. package/src/identity/{hub → manager}/settings/StorageCredentialScreen.tsx +10 -25
  78. package/src/identity/{hub → manager}/shared/components/DetailsScreen.tsx +5 -7
  79. package/src/identity/{hub → manager}/shared/components/ErrorScreen.tsx +6 -10
  80. package/src/identity/{hub → manager}/shared/components/FlowTimeline.tsx +4 -3
  81. package/src/identity/{hub → manager}/shared/components/IdentitySummary.tsx +19 -59
  82. package/src/identity/manager/shared/components/LazyMenu.tsx +147 -0
  83. package/src/identity/manager/shared/components/MenuScreen.tsx +220 -0
  84. package/src/identity/manager/shared/components/OperationCompleteScreen.tsx +28 -0
  85. package/src/identity/{hub → manager}/shared/components/UnlinkedIdentityScreen.tsx +9 -10
  86. package/src/identity/{hub → manager}/shared/components/WalletApprovalScreen.tsx +1 -2
  87. package/src/identity/manager/shared/components/Wordmark.tsx +54 -0
  88. package/src/identity/{hub → manager}/shared/components/menuFlagsFromReconciliation.ts +39 -15
  89. package/src/identity/{hub → manager}/shared/effects/profilePrep.ts +1 -1
  90. package/src/identity/manager/shared/effects/types.ts +30 -0
  91. package/src/identity/{hub → manager}/shared/model/copy.ts +0 -4
  92. package/src/identity/{hub → manager}/shared/model/errors.ts +32 -3
  93. package/src/identity/{hub → manager}/shared/model/network.ts +2 -2
  94. package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/hook.ts +5 -0
  95. package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/run.ts +1 -1
  96. package/src/identity/{hub/shared/reconciliation/useAgentReconciliation.ts → manager/shared/reconciliation/index.ts} +6 -0
  97. package/src/identity/{hub → manager}/shared/utils.ts +6 -10
  98. package/src/identity/{hub → manager}/transfer/TokenTransferFlow.tsx +3 -3
  99. package/src/identity/{hub → manager}/transfer/TokenTransferScreens.tsx +4 -10
  100. package/src/identity/{hub → manager}/transfer/effects.ts +1 -1
  101. package/src/identity/{hub → manager}/types.ts +5 -6
  102. package/src/identity/{hub/useIdentityHubContinuity.ts → manager/useContinuity.ts} +59 -27
  103. package/src/identity/{hub/useIdentityHubController.ts → manager/useController.ts} +38 -35
  104. package/src/identity/{hub/useIdentityHubSideEffects.ts → manager/useSideEffects.ts} +40 -4
  105. package/src/identity/registry/erc8004/discovery.ts +3 -17
  106. package/src/identity/registry/erc8004/utils.ts +1 -1
  107. package/src/identity/storage/ipfs.ts +21 -1
  108. package/src/identity/wallet/browserWallet/html.ts +10 -2
  109. package/src/identity/wallet/browserWallet/http.ts +18 -0
  110. package/src/identity/wallet/browserWallet/requestServer.ts +5 -1
  111. package/src/identity/wallet/browserWallet/requests.ts +10 -28
  112. package/src/identity/wallet/browserWallet/session.ts +26 -33
  113. package/src/identity/wallet/browserWallet/validation.ts +14 -0
  114. package/src/identity/wallet/browserWallet/walletPageSource.ts +22 -40
  115. package/src/identity/wallet/page/boot.ts +43 -0
  116. package/src/identity/wallet/page/config.ts +59 -0
  117. package/src/identity/wallet/page/constants.ts +12 -0
  118. package/src/identity/wallet/page/copy.ts +47 -68
  119. package/src/identity/wallet/page/css.ts +638 -0
  120. package/src/identity/wallet/page/{errorView.ts → errors.ts} +5 -14
  121. package/src/identity/wallet/page/{controller.ts → flow.ts} +4 -71
  122. package/src/identity/wallet/page/markup.ts +44 -34
  123. package/src/identity/wallet/page/{walletProvider.ts → provider.ts} +0 -3
  124. package/src/identity/wallet/page/resize.ts +95 -0
  125. package/src/identity/wallet/page/state.ts +135 -8
  126. package/src/identity/wallet/page/timeline.ts +161 -0
  127. package/src/identity/wallet/page/view.ts +22 -302
  128. package/src/storage/config.ts +30 -80
  129. package/src/storage/reset.ts +31 -0
  130. package/src/storage/secrets.ts +1 -16
  131. package/src/ui/Select.tsx +27 -5
  132. package/src/ui/Spinner.tsx +16 -15
  133. package/src/ui/Surface.tsx +21 -17
  134. package/src/ui/TextArea.tsx +173 -0
  135. package/src/ui/TextInput.tsx +31 -133
  136. package/src/ui/theme.ts +22 -13
  137. package/src/utils/clipboard.ts +0 -140
  138. package/src/app/FirstRun.tsx +0 -577
  139. package/src/app/FirstRunTimeline.tsx +0 -51
  140. package/src/app/firstRunConfig.ts +0 -26
  141. package/src/app/hooks/useCancelRequest.ts +0 -22
  142. package/src/app/hooks/useDoublePress.ts +0 -46
  143. package/src/app/hooks/useExitOnCtrlC.ts +0 -36
  144. package/src/auth/openaiOAuth/credentials.ts +0 -47
  145. package/src/auth/openaiOAuth/crypto.ts +0 -23
  146. package/src/auth/openaiOAuth/index.ts +0 -238
  147. package/src/auth/openaiOAuth/landingPage.ts +0 -116
  148. package/src/auth/openaiOAuth/listener.ts +0 -151
  149. package/src/auth/openaiOAuth/refresh.ts +0 -70
  150. package/src/auth/openaiOAuth/shared.ts +0 -115
  151. package/src/chat/ChatBottomPane.tsx +0 -296
  152. package/src/chat/ChatScreen.tsx +0 -1685
  153. package/src/chat/ConversationStack.tsx +0 -56
  154. package/src/chat/MessageList.tsx +0 -638
  155. package/src/chat/SessionStatus.tsx +0 -53
  156. package/src/chat/chatEnvironment.ts +0 -16
  157. package/src/chat/chatScreenUtils.ts +0 -194
  158. package/src/chat/chatSessionState.ts +0 -146
  159. package/src/chat/chatTurnContext.ts +0 -50
  160. package/src/chat/chatTurnOrchestrator.ts +0 -603
  161. package/src/chat/chatTurnRows.ts +0 -64
  162. package/src/chat/commands.ts +0 -494
  163. package/src/chat/continuityEditReview.ts +0 -42
  164. package/src/chat/display/DiffView.tsx +0 -193
  165. package/src/chat/display/SyntaxText.tsx +0 -192
  166. package/src/chat/display/toolCallDisplay.ts +0 -103
  167. package/src/chat/display/toolResultDisplay.ts +0 -19
  168. package/src/chat/input/ChatInput.tsx +0 -625
  169. package/src/chat/input/chatInputHelpers.ts +0 -62
  170. package/src/chat/input/chatInputState.ts +0 -247
  171. package/src/chat/input/chatPaste.ts +0 -49
  172. package/src/chat/input/imageRefs.ts +0 -30
  173. package/src/chat/input/inputRendering.tsx +0 -93
  174. package/src/chat/input/textCursor.ts +0 -212
  175. package/src/chat/messageMarkdown.ts +0 -220
  176. package/src/chat/messageRows.ts +0 -43
  177. package/src/chat/planImplementation.ts +0 -62
  178. package/src/chat/slashCommandHandlers.ts +0 -122
  179. package/src/chat/slashCommandViews.ts +0 -120
  180. package/src/chat/transcript/TranscriptView.tsx +0 -184
  181. package/src/chat/transcript/transcriptViewport.ts +0 -295
  182. package/src/chat/views/ContextLimitView.tsx +0 -95
  183. package/src/chat/views/ContinuityEditReviewView.tsx +0 -50
  184. package/src/chat/views/CopyPicker.tsx +0 -50
  185. package/src/chat/views/PermissionPrompt.tsx +0 -156
  186. package/src/chat/views/PermissionsView.tsx +0 -165
  187. package/src/chat/views/PlanApprovalView.tsx +0 -91
  188. package/src/chat/views/ResumeView.tsx +0 -273
  189. package/src/chat/views/RewindView.tsx +0 -412
  190. package/src/cli/preview.tsx +0 -14
  191. package/src/cli/updateNotice.ts +0 -54
  192. package/src/identity/continuity/privateEdit/apply.ts +0 -170
  193. package/src/identity/continuity/privateEdit/diff.ts +0 -6
  194. package/src/identity/continuity/privateEdit/files.ts +0 -23
  195. package/src/identity/continuity/privateEdit/types.ts +0 -28
  196. package/src/identity/continuity/privateEdit.ts +0 -46
  197. package/src/identity/hub/IdentityHub.tsx +0 -14
  198. package/src/identity/hub/continuity/RecoveryConfirmScreen.tsx +0 -104
  199. package/src/identity/hub/ens/effects.ts +0 -218
  200. package/src/identity/hub/shared/components/MenuScreen.tsx +0 -241
  201. package/src/identity/hub/shared/effects/types.ts +0 -53
  202. package/src/identity/hub/shared/reconciliation/index.ts +0 -14
  203. package/src/identity/wallet/page/grainient.ts +0 -278
  204. package/src/identity/wallet/page/html.ts +0 -28
  205. package/src/identity/wallet/page/styles/base.ts +0 -260
  206. package/src/identity/wallet/page/styles/components.ts +0 -262
  207. package/src/identity/wallet/page/styles/index.ts +0 -5
  208. package/src/identity/wallet/page/styles/responsive.ts +0 -247
  209. package/src/identity/wallet/page.tsx +0 -38
  210. package/src/mcp/approvals.ts +0 -113
  211. package/src/mcp/config.ts +0 -235
  212. package/src/mcp/manager.ts +0 -482
  213. package/src/mcp/managerHelpers.ts +0 -70
  214. package/src/mcp/names.ts +0 -19
  215. package/src/mcp/output.ts +0 -96
  216. package/src/models/ModelPicker.tsx +0 -1009
  217. package/src/models/catalog.ts +0 -327
  218. package/src/models/huggingface.ts +0 -712
  219. package/src/models/huggingfaceStorage.ts +0 -136
  220. package/src/models/llamacpp.ts +0 -848
  221. package/src/models/llamacppCommands.ts +0 -44
  222. package/src/models/llamacppConfig.ts +0 -34
  223. package/src/models/llamacppDiscovery.ts +0 -176
  224. package/src/models/llamacppOutput.ts +0 -65
  225. package/src/models/llamacppPreflight.ts +0 -158
  226. package/src/models/modelDisplay.ts +0 -180
  227. package/src/models/modelPickerCatalogFlow.ts +0 -56
  228. package/src/models/modelPickerCredentials.ts +0 -166
  229. package/src/models/modelPickerData.ts +0 -41
  230. package/src/models/modelPickerDisplay.tsx +0 -132
  231. package/src/models/modelPickerHfFlow.ts +0 -192
  232. package/src/models/modelPickerLocalRunnerFlow.ts +0 -115
  233. package/src/models/modelPickerOptions.ts +0 -457
  234. package/src/models/modelPickerTypes.ts +0 -69
  235. package/src/models/modelPickerUninstallFlow.ts +0 -48
  236. package/src/models/modelPickerViewHelpers.ts +0 -174
  237. package/src/models/modelRecommendation.ts +0 -139
  238. package/src/models/providerDisplay.ts +0 -16
  239. package/src/models/runtimeDetection.ts +0 -81
  240. package/src/models/uncensoredCatalog.ts +0 -86
  241. package/src/providers/anthropic.ts +0 -290
  242. package/src/providers/contracts.ts +0 -71
  243. package/src/providers/errors.ts +0 -80
  244. package/src/providers/gemini.ts +0 -391
  245. package/src/providers/openai-chat.ts +0 -474
  246. package/src/providers/openai-responses-format.ts +0 -177
  247. package/src/providers/openai-responses.ts +0 -306
  248. package/src/providers/openaiChatWire.ts +0 -124
  249. package/src/providers/registry.ts +0 -120
  250. package/src/providers/retry.ts +0 -58
  251. package/src/providers/sse.ts +0 -93
  252. package/src/runtime/compaction.ts +0 -395
  253. package/src/runtime/cwd.ts +0 -43
  254. package/src/runtime/providerTurn.ts +0 -38
  255. package/src/runtime/sessionMode.ts +0 -55
  256. package/src/runtime/systemPrompt.ts +0 -213
  257. package/src/runtime/textToolParser.ts +0 -161
  258. package/src/runtime/toolClaimGuards.ts +0 -143
  259. package/src/runtime/toolExecution.ts +0 -304
  260. package/src/runtime/toolIntent.ts +0 -143
  261. package/src/runtime/turn.ts +0 -369
  262. package/src/runtime/turnNudges.ts +0 -223
  263. package/src/runtime/turnTypes.ts +0 -86
  264. package/src/storage/factoryReset.ts +0 -127
  265. package/src/storage/history.ts +0 -58
  266. package/src/storage/permissions.ts +0 -76
  267. package/src/storage/rewind.ts +0 -266
  268. package/src/storage/sessionExport.ts +0 -49
  269. package/src/storage/sessions.ts +0 -495
  270. package/src/tools/bashSafety.ts +0 -186
  271. package/src/tools/bashTool.ts +0 -140
  272. package/src/tools/changeDirectoryTool.ts +0 -213
  273. package/src/tools/contracts.ts +0 -192
  274. package/src/tools/deleteFileTool.ts +0 -116
  275. package/src/tools/editTool.ts +0 -165
  276. package/src/tools/editUtils.ts +0 -170
  277. package/src/tools/fileDiff.ts +0 -261
  278. package/src/tools/listDirectoryTool.ts +0 -55
  279. package/src/tools/listSkillFilesTool.ts +0 -77
  280. package/src/tools/listSkillsTool.ts +0 -68
  281. package/src/tools/mcpResourceTools.ts +0 -95
  282. package/src/tools/permissionRules.ts +0 -85
  283. package/src/tools/privateContinuityEditTool.ts +0 -187
  284. package/src/tools/privateContinuityReadTool.ts +0 -106
  285. package/src/tools/readSkillTool.ts +0 -107
  286. package/src/tools/readTool.ts +0 -85
  287. package/src/tools/registry.ts +0 -103
  288. package/src/tools/writeFileTool.ts +0 -167
  289. package/src/ui/BrandSplash.tsx +0 -133
  290. package/src/ui/terminalTitle.ts +0 -30
  291. package/src/utils/images.ts +0 -140
  292. package/src/utils/markdownSegments.ts +0 -51
  293. package/src/utils/messages.ts +0 -37
  294. package/src/utils/withRetry.ts +0 -324
  295. /package/src/identity/{hub → manager}/continuity/state.ts +0 -0
  296. /package/src/identity/{hub → manager}/custody/helpers.ts +0 -0
  297. /package/src/identity/{hub → manager}/custody/preflight.ts +0 -0
  298. /package/src/identity/{hub → manager}/custody/state.ts +0 -0
  299. /package/src/identity/{hub → manager}/custody/useCustodyFlow.tsx +0 -0
  300. /package/src/identity/{hub → manager}/ens/EnsEditFlow.tsx +0 -0
  301. /package/src/identity/{hub → manager}/ens/advancedEnsValidation.ts +0 -0
  302. /package/src/identity/{hub → manager}/ens/state.ts +0 -0
  303. /package/src/identity/{hub → manager}/ens/transactions.ts +0 -0
  304. /package/src/identity/{hub → manager}/ens/types.ts +0 -0
  305. /package/src/identity/{hub → manager}/profile/identity.ts +0 -0
  306. /package/src/identity/{hub → manager}/restore/envelopes.ts +0 -0
  307. /package/src/identity/{hub → manager}/restore/helpers.ts +0 -0
  308. /package/src/identity/{hub → manager}/restore/recovery.ts +0 -0
  309. /package/src/identity/{hub → manager}/restore/resolve.ts +0 -0
  310. /package/src/identity/{hub → manager}/shared/components/BusyScreen.tsx +0 -0
  311. /package/src/identity/{hub → manager}/shared/components/NetworkScreen.tsx +0 -0
  312. /package/src/identity/{hub → manager}/shared/components/PinataJwtInput.tsx +0 -0
  313. /package/src/identity/{hub → manager}/shared/effects/receipts.ts +0 -0
  314. /package/src/identity/{hub → manager}/shared/effects/sync.ts +0 -0
  315. /package/src/identity/{hub → manager}/shared/model/format.ts +0 -0
  316. /package/src/identity/{hub → manager}/shared/operatorWallets.ts +0 -0
  317. /package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/ownership.ts +0 -0
  318. /package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/types.ts +0 -0
  319. /package/src/identity/{hub → manager}/shared/reconciliation/walletSetup.ts +0 -0
  320. /package/src/identity/{hub → manager}/shared/txGuard.ts +0 -0
  321. /package/src/identity/{hub → manager}/transfer/progress.ts +0 -0
  322. /package/src/identity/{hub → manager}/transfer/state.ts +0 -0
@@ -1,495 +0,0 @@
1
- import fs from 'node:fs/promises'
2
- import path from 'node:path'
3
- import { randomUUID } from 'node:crypto'
4
- import { getConfigDir } from './config.js'
5
- import type { Message } from '../providers/contracts.js'
6
- import { getCwd } from '../runtime/cwd.js'
7
- import type { SessionMode } from '../runtime/sessionMode.js'
8
- import { atomicWriteText } from './atomicWrite.js'
9
- import { stripFileChangeResultDiff } from '../tools/fileDiff.js'
10
- import {
11
- isUserCorrectionOfToolState,
12
- looksLikeToolStateClaim,
13
- } from '../runtime/toolClaimGuards.js'
14
- import { userTextToContentBlocks } from '../utils/images.js'
15
-
16
- export type SessionMessage =
17
- | { version?: 2; role: 'user'; content: string; providerContent?: Message['content']; createdAt: string; turnId?: string; synthetic?: boolean }
18
- | { version?: 2; role: 'assistant'; content: string; createdAt: string; model?: string; usage?: { in?: number; out?: number }; turnId?: string; synthetic?: boolean }
19
- | { version?: 2; role: 'system'; content: string; createdAt: string; turnId?: string; synthetic?: boolean }
20
- | { version: 2; role: 'tool_use'; toolUseId: string; name: string; input: Record<string, unknown>; createdAt: string; turnId?: string }
21
- | { version: 2; role: 'tool_result'; toolUseId: string; name: string; content: string; isError?: boolean; createdAt: string; turnId?: string }
22
-
23
- export type SessionMetadata = {
24
- id: string
25
- startedAt: string
26
- updatedAt: string
27
- projectRoot: string
28
- workspaceRoot: string
29
- lastCwd: string
30
- provider?: string
31
- model?: string
32
- mode?: SessionMode
33
- firstUserMessage: string
34
- turnCount: number
35
- archivedAt?: string
36
- compactedToSessionId?: string
37
- compactedFromSessionId?: string
38
- }
39
-
40
- export type SessionSummary = SessionMetadata & {
41
- path: string
42
- mtimeMs: number
43
- projectLabel: string
44
- directoryLabel: string
45
- }
46
-
47
- export type SessionWriteContext = {
48
- cwd: string
49
- provider?: string
50
- model?: string
51
- mode?: SessionMode
52
- }
53
-
54
- export type ClearAllSessionsResult = {
55
- sessionFiles: number
56
- metadataFiles: number
57
- }
58
-
59
- const SessionMetadataSchemaVersion = 1
60
-
61
- export function getSessionsDir(): string {
62
- return path.join(getConfigDir(), 'sessions')
63
- }
64
-
65
- export function newSessionId(): string {
66
- return randomUUID()
67
- }
68
-
69
- function sessionPath(id: string): string {
70
- return path.join(getSessionsDir(), `${id}.jsonl`)
71
- }
72
-
73
- function sessionMetaPath(id: string): string {
74
- return path.join(getSessionsDir(), `${id}.meta.json`)
75
- }
76
-
77
- async function ensureSessionsDir(): Promise<void> {
78
- await fs.mkdir(getSessionsDir(), { recursive: true })
79
- }
80
-
81
- export async function appendSessionMessage(
82
- id: string,
83
- message: SessionMessage,
84
- context?: SessionWriteContext,
85
- ): Promise<void> {
86
- await ensureSessionsDir()
87
- await fs.appendFile(sessionPath(id), JSON.stringify(message) + '\n', { mode: 0o600 })
88
- if (context) {
89
- await updateSessionMetadata(id, message, context)
90
- }
91
- }
92
-
93
- export async function loadSession(id: string): Promise<SessionMessage[]> {
94
- let raw: string
95
- try {
96
- raw = await fs.readFile(sessionPath(id), 'utf8')
97
- } catch (err: unknown) {
98
- if ((err as NodeJS.ErrnoException).code === 'ENOENT') return []
99
- throw err
100
- }
101
- const out: SessionMessage[] = []
102
- for (const line of raw.split('\n')) {
103
- const trimmed = line.trim()
104
- if (!trimmed) continue
105
- try {
106
- out.push(normalizeSessionMessage(JSON.parse(trimmed) as SessionMessage))
107
- } catch {
108
- continue
109
- }
110
- }
111
- return out
112
- }
113
-
114
- export async function loadSessionMetadata(id: string): Promise<SessionMetadata | null> {
115
- try {
116
- const raw = await fs.readFile(sessionMetaPath(id), 'utf8')
117
- return normalizeMetadata(JSON.parse(raw) as Partial<SessionMetadata> & { version?: number }, id)
118
- } catch (err: unknown) {
119
- if ((err as NodeJS.ErrnoException).code === 'ENOENT') return null
120
- throw err
121
- }
122
- }
123
-
124
- export async function ensureSessionMetadata(id: string, context: SessionWriteContext): Promise<SessionMetadata> {
125
- const existing = await loadSessionMetadata(id)
126
- if (existing) return existing
127
- const now = new Date().toISOString()
128
- const metadata: SessionMetadata = {
129
- id,
130
- startedAt: now,
131
- updatedAt: now,
132
- projectRoot: await detectProjectRoot(context.cwd),
133
- workspaceRoot: context.cwd,
134
- lastCwd: context.cwd,
135
- provider: context.provider,
136
- model: context.model,
137
- mode: context.mode,
138
- firstUserMessage: '',
139
- turnCount: 0,
140
- }
141
- await writeSessionMetadata(metadata)
142
- return metadata
143
- }
144
-
145
- export async function updateSessionActivity(
146
- id: string,
147
- context: SessionWriteContext,
148
- changes: Partial<Pick<SessionMetadata, 'workspaceRoot' | 'lastCwd' | 'provider' | 'model' | 'mode' | 'compactedFromSessionId'>>,
149
- ): Promise<SessionMetadata> {
150
- const base = await ensureSessionMetadata(id, context)
151
- const next: SessionMetadata = {
152
- ...base,
153
- updatedAt: new Date().toISOString(),
154
- projectRoot: changes.workspaceRoot ? await detectProjectRoot(changes.workspaceRoot) : base.projectRoot,
155
- workspaceRoot: changes.workspaceRoot ?? base.workspaceRoot,
156
- lastCwd: changes.lastCwd ?? context.cwd,
157
- provider: changes.provider ?? context.provider ?? base.provider,
158
- model: changes.model ?? context.model ?? base.model,
159
- mode: changes.mode ?? context.mode ?? base.mode,
160
- compactedFromSessionId: changes.compactedFromSessionId ?? base.compactedFromSessionId,
161
- }
162
- await writeSessionMetadata(next)
163
- return next
164
- }
165
-
166
- export async function archiveSession(
167
- id: string,
168
- context: SessionWriteContext,
169
- details: { compactedToSessionId?: string } = {},
170
- ): Promise<SessionMetadata> {
171
- const base = await ensureSessionMetadata(id, context)
172
- const next: SessionMetadata = {
173
- ...base,
174
- updatedAt: new Date().toISOString(),
175
- archivedAt: base.archivedAt ?? new Date().toISOString(),
176
- compactedToSessionId: details.compactedToSessionId ?? base.compactedToSessionId,
177
- }
178
- await writeSessionMetadata(next)
179
- return next
180
- }
181
-
182
- export async function listSessions(limit = 50): Promise<SessionSummary[]> {
183
- try {
184
- await ensureSessionsDir()
185
- } catch {
186
- return []
187
- }
188
-
189
- let files: string[]
190
- try {
191
- files = await fs.readdir(getSessionsDir())
192
- } catch {
193
- return []
194
- }
195
-
196
- const sessionIds = files
197
- .filter(file => file.endsWith('.jsonl'))
198
- .map(file => file.slice(0, -'.jsonl'.length))
199
-
200
- const summaries = await Promise.all(sessionIds.map(async id => summarizeSession(id)))
201
- return summaries
202
- .filter((value): value is SessionSummary => value !== null)
203
- .sort((a, b) => b.mtimeMs - a.mtimeMs)
204
- .slice(0, limit)
205
- }
206
-
207
- export async function clearAllSessions(): Promise<ClearAllSessionsResult> {
208
- const dir = getSessionsDir()
209
- let files: string[]
210
- try {
211
- files = await fs.readdir(dir)
212
- } catch (err: unknown) {
213
- if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
214
- return { sessionFiles: 0, metadataFiles: 0 }
215
- }
216
- throw err
217
- }
218
-
219
- let sessionFiles = 0
220
- let metadataFiles = 0
221
- for (const file of files) {
222
- const isSession = file.endsWith('.jsonl')
223
- const isMetadata = file.endsWith('.meta.json')
224
- if (!isSession && !isMetadata) continue
225
-
226
- const target = path.join(dir, file)
227
- const relative = path.relative(dir, target)
228
- if (relative.startsWith('..') || path.isAbsolute(relative)) {
229
- throw new Error(`refusing to delete session path outside storage: ${file}`)
230
- }
231
-
232
- await fs.rm(target, { force: true })
233
- if (isSession) sessionFiles += 1
234
- else metadataFiles += 1
235
- }
236
-
237
- return { sessionFiles, metadataFiles }
238
- }
239
-
240
- export type ProviderMessageProjectionOptions = {
241
- compactToolHistory?: boolean
242
- preserveTurnId?: string
243
- }
244
-
245
- export const TOOL_CORRECTION_CONTEXT_MESSAGE =
246
- 'The latest user message corrects a prior assistant claim about tool or filesystem state. Treat user correction and tool_result messages as authoritative. Ignore any recent assistant claim about files, directories, cwd, or tool execution unless it is backed by a tool_result, and retry with the appropriate tool.'
247
-
248
- function resolveUserContent(
249
- message: Extract<SessionMessage, { role: 'system' | 'user' | 'assistant' }>,
250
- ): Message['content'] {
251
- if (message.role !== 'user') return message.content
252
- if (message.providerContent) return message.providerContent
253
- if (message.content.includes('[image:')) {
254
- return userTextToContentBlocks(message.content)
255
- }
256
- return message.content
257
- }
258
-
259
- export function sessionMessagesToProviderMessages(
260
- messages: SessionMessage[],
261
- options: ProviderMessageProjectionOptions = {},
262
- ): Message[] {
263
- const out: Message[] = []
264
- const pendingToolUses = new Map<string, { name: string; input: Record<string, unknown> }>()
265
- const invalidatedAssistantMessages = invalidatedAssistantClaimIndexes(messages)
266
-
267
- for (const [index, message] of messages.entries()) {
268
- if (message.role === 'system' || message.role === 'user' || message.role === 'assistant') {
269
- if (message.role === 'assistant' && invalidatedAssistantMessages.has(index)) continue
270
- out.push({ role: message.role, content: resolveUserContent(message) })
271
- continue
272
- }
273
- if (message.role === 'tool_use') {
274
- if (shouldCompactToolMessage(message, options)) continue
275
- pendingToolUses.set(message.toolUseId, { name: message.name, input: message.input })
276
- continue
277
- }
278
- if (shouldCompactToolMessage(message, options)) {
279
- pendingToolUses.delete(message.toolUseId)
280
- continue
281
- }
282
- const pendingToolUse = pendingToolUses.get(message.toolUseId)
283
- if (!pendingToolUse) continue
284
- out.push({
285
- role: 'assistant',
286
- content: [{
287
- type: 'tool_use',
288
- id: message.toolUseId,
289
- name: pendingToolUse.name,
290
- input: pendingToolUse.input,
291
- }],
292
- })
293
- out.push({
294
- role: 'user',
295
- content: [{
296
- type: 'tool_result',
297
- toolUseId: message.toolUseId,
298
- content: stripFileChangeResultDiff(message.content),
299
- isError: message.isError,
300
- }],
301
- })
302
- pendingToolUses.delete(message.toolUseId)
303
- }
304
-
305
- return out
306
- }
307
-
308
- export function latestUserMessageCorrectsToolState(messages: SessionMessage[]): boolean {
309
- for (let index = messages.length - 1; index >= 0; index -= 1) {
310
- const message = messages[index]
311
- if (!message) continue
312
- if (message.role === 'system') continue
313
- return message.role === 'user' && isUserCorrectionOfToolState(message.content)
314
- }
315
- return false
316
- }
317
-
318
- function invalidatedAssistantClaimIndexes(messages: SessionMessage[]): Set<number> {
319
- const invalidated = new Set<number>()
320
-
321
- for (let index = 0; index < messages.length; index += 1) {
322
- const message = messages[index]
323
- if (message?.role !== 'user' || !isUserCorrectionOfToolState(message.content)) continue
324
-
325
- for (let cursor = index - 1; cursor >= 0; cursor -= 1) {
326
- const prior = messages[cursor]
327
- if (!prior) continue
328
- if (prior.role === 'user') break
329
- if (prior.role !== 'assistant') continue
330
- if (!looksLikeToolStateClaim(prior.content)) continue
331
- if (hasToolEvidenceBetween(messages, cursor, index)) continue
332
- invalidated.add(cursor)
333
- }
334
- }
335
-
336
- return invalidated
337
- }
338
-
339
- function hasToolEvidenceBetween(messages: SessionMessage[], start: number, end: number): boolean {
340
- for (let index = start + 1; index < end; index += 1) {
341
- const message = messages[index]
342
- if (message?.role === 'tool_result') return true
343
- }
344
- return false
345
- }
346
-
347
- function shouldCompactToolMessage(
348
- message: Extract<SessionMessage, { role: 'tool_use' | 'tool_result' }>,
349
- options: ProviderMessageProjectionOptions,
350
- ): boolean {
351
- if (!options.compactToolHistory) return false
352
- return !message.turnId || message.turnId !== options.preserveTurnId
353
- }
354
-
355
- function normalizeSessionMessage(message: SessionMessage): SessionMessage {
356
- if ('version' in message && message.version === 2) return message
357
- return message
358
- }
359
-
360
- async function summarizeSession(id: string): Promise<SessionSummary | null> {
361
- const full = sessionPath(id)
362
- let stat
363
- try {
364
- stat = await fs.stat(full)
365
- } catch {
366
- return null
367
- }
368
-
369
- const metadata = await loadSessionMetadata(id)
370
- if (metadata) {
371
- return toSummary(metadata, full, stat.mtimeMs)
372
- }
373
-
374
- const messages = await loadSession(id)
375
- if (messages.length === 0) return null
376
- const firstUser = messages.find(isNonSyntheticUserMessage)
377
- if (!firstUser) return null
378
-
379
- const inferredCwd = getCwd()
380
- const projectRoot = await detectProjectRoot(inferredCwd)
381
- const fallback: SessionMetadata = {
382
- id,
383
- startedAt: firstUser.createdAt,
384
- updatedAt: new Date(stat.mtimeMs).toISOString(),
385
- projectRoot,
386
- workspaceRoot: inferredCwd,
387
- lastCwd: inferredCwd,
388
- firstUserMessage: firstUser.content.slice(0, 120),
389
- turnCount: messages.filter(isNonSyntheticUserMessage).length,
390
- }
391
- return toSummary(fallback, full, stat.mtimeMs)
392
- }
393
-
394
- function isNonSyntheticUserMessage(
395
- message: SessionMessage,
396
- ): message is Extract<SessionMessage, { role: 'user' }> {
397
- return message.role === 'user' && !message.synthetic
398
- }
399
-
400
- async function updateSessionMetadata(
401
- id: string,
402
- message: SessionMessage,
403
- context: SessionWriteContext,
404
- ): Promise<void> {
405
- const current = await ensureSessionMetadata(id, context)
406
- const next: SessionMetadata = {
407
- ...current,
408
- updatedAt: message.createdAt,
409
- projectRoot: await detectProjectRoot(context.cwd),
410
- workspaceRoot: current.workspaceRoot || context.cwd,
411
- lastCwd: context.cwd,
412
- provider: context.provider ?? current.provider,
413
- model: context.model ?? current.model,
414
- mode: context.mode ?? current.mode,
415
- firstUserMessage: current.firstUserMessage || (message.role === 'user' && !message.synthetic ? message.content.slice(0, 120) : ''),
416
- turnCount: current.turnCount + (message.role === 'user' && !message.synthetic ? 1 : 0),
417
- }
418
- await writeSessionMetadata(next)
419
- }
420
-
421
- async function writeSessionMetadata(metadata: SessionMetadata): Promise<void> {
422
- await ensureSessionsDir()
423
- const file = sessionMetaPath(metadata.id)
424
- const payload = {
425
- version: SessionMetadataSchemaVersion,
426
- ...metadata,
427
- }
428
- await atomicWriteText(file, JSON.stringify(payload, null, 2) + '\n')
429
- }
430
-
431
- function normalizeMetadata(
432
- raw: Partial<SessionMetadata> & { version?: number },
433
- id: string,
434
- ): SessionMetadata {
435
- const cwd = raw.lastCwd || raw.workspaceRoot || getCwd()
436
- const now = new Date().toISOString()
437
- return {
438
- id,
439
- startedAt: raw.startedAt || now,
440
- updatedAt: raw.updatedAt || raw.startedAt || now,
441
- projectRoot: raw.projectRoot || cwd,
442
- workspaceRoot: raw.workspaceRoot || cwd,
443
- lastCwd: raw.lastCwd || raw.workspaceRoot || cwd,
444
- provider: raw.provider,
445
- model: raw.model,
446
- mode: raw.mode,
447
- firstUserMessage: raw.firstUserMessage || '',
448
- turnCount: raw.turnCount ?? 0,
449
- archivedAt: raw.archivedAt,
450
- compactedToSessionId: raw.compactedToSessionId,
451
- compactedFromSessionId: raw.compactedFromSessionId,
452
- }
453
- }
454
-
455
- function toSummary(metadata: SessionMetadata, fullPath: string, mtimeMs: number): SessionSummary {
456
- const projectLabel = path.basename(metadata.projectRoot) || metadata.projectRoot
457
- const directoryLabel = formatDirectoryLabel(metadata.projectRoot, metadata.workspaceRoot, metadata.lastCwd)
458
- return {
459
- ...metadata,
460
- path: fullPath,
461
- mtimeMs,
462
- projectLabel,
463
- directoryLabel,
464
- }
465
- }
466
-
467
- function formatDirectoryLabel(projectRoot: string, workspaceRoot: string, lastCwd: string): string {
468
- const workspaceRel = path.relative(projectRoot, workspaceRoot)
469
- const cwdRel = path.relative(workspaceRoot, lastCwd)
470
- const workspaceLabel = workspaceRel && !workspaceRel.startsWith('..') ? workspaceRel : path.basename(workspaceRoot)
471
- if (!cwdRel || cwdRel === '') return workspaceLabel || '.'
472
- if (cwdRel.startsWith('..')) return workspaceLabel || path.basename(lastCwd)
473
- return workspaceLabel === '.'
474
- ? `./${cwdRel}`
475
- : `${workspaceLabel}/${cwdRel}`.replaceAll('\\', '/')
476
- }
477
-
478
- async function detectProjectRoot(start: string): Promise<string> {
479
- let current = path.resolve(start)
480
- while (true) {
481
- if (await exists(path.join(current, '.git'))) return current
482
- const parent = path.dirname(current)
483
- if (parent === current) return path.resolve(start)
484
- current = parent
485
- }
486
- }
487
-
488
- async function exists(target: string): Promise<boolean> {
489
- try {
490
- await fs.access(target)
491
- return true
492
- } catch {
493
- return false
494
- }
495
- }
@@ -1,186 +0,0 @@
1
- const RISKY_PATTERN_CHECKS: Array<{ pattern: RegExp; message: string }> = [
2
- { pattern: /[`]/, message: 'contains backtick command substitution' },
3
- { pattern: /\$\(/, message: 'contains $() command substitution' },
4
- { pattern: /\$\{/, message: 'contains parameter expansion' },
5
- { pattern: /(^|[^\\])[|]/, message: 'contains a pipe' },
6
- { pattern: /(^|[^\\])&&/, message: 'contains && chaining' },
7
- { pattern: /(^|[^\\])\|\|/, message: 'contains || chaining' },
8
- { pattern: /(^|[^\\]);/, message: 'contains ; chaining' },
9
- { pattern: /(^|[^\\])[<>]/, message: 'contains shell redirection' },
10
- { pattern: /\r|\n/, message: 'contains a newline' },
11
- { pattern: /<<|<\(|>\(/, message: 'contains heredoc or process substitution syntax' },
12
- ]
13
-
14
- const HIGH_RISK_COMMANDS = new Set([
15
- 'chmod',
16
- 'chown',
17
- 'curl',
18
- 'dd',
19
- 'del',
20
- 'diskpart',
21
- 'erase',
22
- 'format',
23
- 'git',
24
- 'icacls',
25
- 'mkfs',
26
- 'powershell',
27
- 'pwsh',
28
- 'reg',
29
- 'rm',
30
- 'rmdir',
31
- 'scp',
32
- 'ssh',
33
- 'takeown',
34
- 'wget',
35
- ])
36
-
37
- const NON_PERSISTABLE_COMMANDS = new Set([
38
- 'rm',
39
- 'rmdir',
40
- 'del',
41
- 'erase',
42
- 'format',
43
- 'mkfs',
44
- 'dd',
45
- 'diskpart',
46
- 'reg',
47
- 'powershell',
48
- 'pwsh',
49
- ])
50
-
51
- const NATIVE_TOOL_COMMANDS = new Map([
52
- ['change_directory', 'Use the change_directory tool directly instead of passing change_directory to run_bash.'],
53
- ['edit_file', 'Use the edit_file tool directly instead of passing edit_file to run_bash.'],
54
- ['propose_private_continuity_edit', 'Use the propose_private_continuity_edit tool directly instead of passing it to run_bash.'],
55
- ['read_private_continuity_file', 'Use the read_private_continuity_file tool directly instead of passing it to run_bash.'],
56
- ['list_directory', 'Use the list_directory tool directly instead of passing list_directory to run_bash.'],
57
- ['list_mcp_resources', 'Use the list_mcp_resources tool directly instead of passing it to run_bash.'],
58
- ['read_mcp_resource', 'Use the read_mcp_resource tool directly instead of passing it to run_bash.'],
59
- ['read_file', 'Use the read_file tool directly instead of passing read_file to run_bash.'],
60
- ['run_bash', 'run_bash cannot run itself. Put an actual shell command in the command field.'],
61
- ])
62
-
63
- export type BashSafetyAssessment = {
64
- warning?: string
65
- canPersistExact: boolean
66
- canPersistPrefix: boolean
67
- commandPrefix: string
68
- }
69
-
70
- const PROSE_STARTERS = new Set([
71
- 'a',
72
- 'an',
73
- 'and',
74
- 'for',
75
- 'here',
76
- 'i',
77
- 'it',
78
- 'lets',
79
- 'now',
80
- 'okay',
81
- 'please',
82
- 'snake',
83
- 'sure',
84
- 'that',
85
- 'the',
86
- 'then',
87
- 'this',
88
- 'we',
89
- 'you',
90
- 'youll',
91
- ])
92
-
93
- export function assessBashCommand(command: string): BashSafetyAssessment {
94
- const trimmed = command.trim()
95
- const firstToken = extractFirstToken(trimmed)
96
- const highRisk = firstToken ? HIGH_RISK_COMMANDS.has(firstToken.toLowerCase()) : false
97
- const nonPersistable = firstToken ? NON_PERSISTABLE_COMMANDS.has(firstToken.toLowerCase()) : false
98
- const triggeredChecks = RISKY_PATTERN_CHECKS.filter(check => check.pattern.test(command)).map(check => check.message)
99
-
100
- const warning = triggeredChecks.length > 0
101
- ? `Warning: ${sentenceCase(triggeredChecks[0] ?? 'command is risky')}. Reusable approval is limited for this command.`
102
- : highRisk
103
- ? `Warning: ${sentenceCase(firstToken ?? '')} is a high-impact command. Reusable approval is limited for this command.`
104
- : undefined
105
-
106
- return {
107
- warning,
108
- canPersistExact: triggeredChecks.length === 0 && !nonPersistable,
109
- canPersistPrefix: triggeredChecks.length === 0 && !highRisk && Boolean(firstToken),
110
- commandPrefix: firstToken ?? '',
111
- }
112
- }
113
-
114
- export function validateBashCommandInput(command: string): string | undefined {
115
- const trimmed = command.trim()
116
- if (!trimmed) return 'command must not be empty'
117
-
118
- const firstToken = extractFirstToken(trimmed)
119
- if (!firstToken) return 'command must start with an executable or shell builtin'
120
-
121
- const normalizedFirstToken = normalizeCommandToken(firstToken)
122
- if (!normalizedFirstToken) {
123
- return 'command must start with an executable or shell builtin'
124
- }
125
-
126
- if (PROSE_STARTERS.has(normalizedFirstToken)) {
127
- return 'command must be an actual shell command, not explanatory prose'
128
- }
129
-
130
- const nativeToolMessage = NATIVE_TOOL_COMMANDS.get(normalizedFirstToken)
131
- if (nativeToolMessage) {
132
- return `command must be an actual shell command, not an ethagent tool name. ${nativeToolMessage}`
133
- }
134
-
135
- if (normalizedFirstToken === 'echo' || normalizedFirstToken === 'printf') {
136
- const rest = trimmed.slice(firstToken.length)
137
- const hasShellMeta = /[|&;<>`$]/.test(rest)
138
- if (!hasShellMeta) {
139
- return `do not use ${normalizedFirstToken} to emit conversational text; reply directly in your assistant message instead.`
140
- }
141
- }
142
-
143
- if (
144
- /\b(you can|you should|you need|run the following command|written in|under the|to run(?: the game)?|copy and paste|save (?:it|this))/i.test(trimmed)
145
- ) {
146
- return 'command must be an actual shell command, not explanatory prose'
147
- }
148
-
149
- const words = trimmed.split(/\s+/).filter(Boolean)
150
- const hasShellSyntax = /[|&;<>]/.test(trimmed)
151
- if (!hasShellSyntax && /[.!?]$/.test(trimmed) && words.length >= 4) {
152
- return 'command must be an actual shell command, not explanatory prose'
153
- }
154
-
155
- return undefined
156
- }
157
-
158
- function extractFirstToken(command: string): string {
159
- const trimmed = command.trim()
160
- if (trimmed.startsWith('"')) {
161
- const end = trimmed.indexOf('"', 1)
162
- if (end > 1) return trimmed.slice(0, end + 1)
163
- }
164
- if (trimmed.startsWith("'")) {
165
- const end = trimmed.indexOf("'", 1)
166
- if (end > 1) return trimmed.slice(0, end + 1)
167
- }
168
- const match = trimmed.match(/^([^\s"'`]+)/)
169
- return match?.[1] ?? ''
170
- }
171
-
172
- function normalizeCommandToken(token: string): string {
173
- return token
174
- .trim()
175
- .replace(/^["']|["']$/g, '')
176
- .replace(/\\/g, '/')
177
- .split('/')
178
- .at(-1)
179
- ?.replace(/\.(exe|cmd|bat|ps1)$/i, '')
180
- .toLowerCase()
181
- .replace(/[^a-z0-9_.:-]/g, '') ?? ''
182
- }
183
-
184
- function sentenceCase(value: string): string {
185
- return value ? value[0]!.toUpperCase() + value.slice(1) : value
186
- }