ethagent 3.3.3 → 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.
- package/.claude-plugin/marketplace.json +11 -0
- package/.claude-plugin/plugin.json +35 -0
- package/LICENSE +1 -1
- package/README.md +64 -104
- package/commands/ethagent.md +40 -0
- package/package.json +16 -16
- package/src/app/keybindings/KeybindingProvider.tsx +1 -6
- package/src/app/keybindings/types.ts +1 -6
- package/src/cli/ResetConfirmView.tsx +54 -53
- package/src/cli/demo.ts +86 -0
- package/src/cli/hookIo.ts +45 -0
- package/src/cli/main.tsx +94 -123
- package/src/cli/memoryGuard.ts +49 -0
- package/src/cli/reset.ts +28 -70
- package/src/cli/sessionStart.ts +33 -0
- package/src/cli/status.ts +46 -0
- package/src/cli/sync.ts +167 -0
- package/src/cli/syncAdapters/claude-code.ts +86 -0
- package/src/cli/syncAdapters/codex.ts +66 -0
- package/src/cli/syncAdapters/index.ts +45 -0
- package/src/cli/syncAdapters/managedBlock.ts +175 -0
- package/src/cli/syncAdapters/shared.ts +63 -0
- package/src/identity/continuity/envelopeParse.ts +20 -1
- package/src/identity/continuity/publicSkills.ts +3 -1
- package/src/identity/continuity/skills/publicSkillsSync.ts +2 -1
- package/src/identity/continuity/skills/scaffold.ts +5 -2
- package/src/identity/continuity/snapshots.ts +12 -5
- package/src/identity/continuity/storage/defaults.ts +20 -19
- package/src/identity/continuity/storage/status.ts +1 -1
- package/src/identity/ens/ensLookup/constants.ts +1 -1
- package/src/identity/manager/IdentityManager.tsx +33 -0
- package/src/identity/{hub → manager}/OperationalRoutes.tsx +37 -18
- package/src/identity/{hub → manager}/Routes.tsx +48 -34
- package/src/identity/{hub → manager}/continuity/ContinuityDashboardScreen.tsx +9 -19
- package/src/identity/{hub → manager}/continuity/RebackupStorageScreen.tsx +3 -3
- package/src/identity/manager/continuity/RecoveryConfirmScreen.tsx +102 -0
- package/src/identity/{hub → manager}/continuity/SavePromptScreen.tsx +2 -3
- package/src/identity/{hub → manager}/continuity/completion.ts +1 -1
- package/src/identity/{hub → manager}/continuity/effects.ts +1 -1
- package/src/identity/{hub → manager}/continuity/skills/DeleteSkillConfirmScreen.tsx +2 -2
- package/src/identity/{hub → manager}/continuity/skills/NewSkillScreen.tsx +0 -5
- package/src/identity/{hub → manager}/continuity/skills/NewSkillVisibilityScreen.tsx +4 -4
- package/src/identity/{hub → manager}/continuity/skills/SkillActionsScreen.tsx +6 -22
- package/src/identity/{hub → manager}/continuity/skills/SkillsTreeScreen.tsx +5 -17
- package/src/identity/{hub → manager}/continuity/snapshot.ts +1 -1
- package/src/identity/{hub → manager}/continuity/vault.ts +1 -1
- package/src/identity/{hub → manager}/create/CreateFlow.tsx +59 -32
- package/src/identity/{hub → manager}/create/effects.ts +19 -10
- package/src/identity/manager/create/importScan.ts +122 -0
- package/src/identity/{hub → manager}/custody/CustodyEditFlow.tsx +17 -61
- package/src/identity/{hub → manager}/custody/actions.ts +1 -15
- package/src/identity/{hub → manager}/custody/routes.tsx +20 -40
- package/src/identity/{hub → manager}/custody/transactions.ts +1 -0
- package/src/identity/{hub → manager}/custody/types.ts +1 -2
- package/src/identity/{hub → manager}/custody/useCustodyEffects.ts +1 -1
- package/src/identity/{hub → manager}/ens/EnsEditAdvancedScreens.tsx +2 -2
- package/src/identity/{hub → manager}/ens/EnsEditMaintenanceScreens.tsx +12 -23
- package/src/identity/{hub → manager}/ens/EnsEditReviewScreens.tsx +18 -42
- package/src/identity/{hub → manager}/ens/EnsEditRunners.tsx +1 -1
- package/src/identity/{hub → manager}/ens/EnsEditShared.tsx +0 -2
- package/src/identity/{hub → manager}/ens/EnsEditSimpleScreens.tsx +10 -19
- package/src/identity/{hub → manager}/ens/EnsFlow.tsx +133 -41
- package/src/identity/{hub → manager}/ens/EnsOperatorWalletsScreen.tsx +14 -19
- package/src/identity/{hub → manager}/ens/editCopy.ts +1 -14
- package/src/identity/{hub → manager}/profile/EditProfileFlow.tsx +99 -66
- package/src/identity/{hub → manager}/profile/effects.ts +1 -3
- package/src/identity/{hub → manager}/profile/operatorSave.ts +1 -1
- package/src/identity/{hub → manager}/profile/state.ts +1 -1
- package/src/identity/{hub/identityHubReducer.ts → manager/reducer.ts} +25 -26
- package/src/identity/{hub → manager}/restore/RestoreFlow.tsx +16 -24
- package/src/identity/{hub → manager}/restore/apply.ts +1 -1
- package/src/identity/{hub → manager}/restore/auth.ts +1 -1
- package/src/identity/{hub → manager}/restore/discover.ts +1 -1
- package/src/identity/{hub → manager}/restore/fetch.ts +1 -1
- package/src/identity/{hub → manager}/restore/restoreAdmin.ts +1 -1
- package/src/identity/{hub → manager}/restore/useRestoreEffects.ts +2 -9
- package/src/identity/{hub → manager}/settings/StorageCredentialScreen.tsx +10 -25
- package/src/identity/{hub → manager}/shared/components/DetailsScreen.tsx +5 -7
- package/src/identity/{hub → manager}/shared/components/ErrorScreen.tsx +6 -10
- package/src/identity/{hub → manager}/shared/components/FlowTimeline.tsx +4 -3
- package/src/identity/{hub → manager}/shared/components/IdentitySummary.tsx +19 -59
- package/src/identity/manager/shared/components/LazyMenu.tsx +147 -0
- package/src/identity/manager/shared/components/MenuScreen.tsx +220 -0
- package/src/identity/manager/shared/components/OperationCompleteScreen.tsx +28 -0
- package/src/identity/{hub → manager}/shared/components/UnlinkedIdentityScreen.tsx +9 -10
- package/src/identity/{hub → manager}/shared/components/WalletApprovalScreen.tsx +1 -2
- package/src/identity/manager/shared/components/Wordmark.tsx +54 -0
- package/src/identity/{hub → manager}/shared/components/menuFlagsFromReconciliation.ts +39 -15
- package/src/identity/{hub → manager}/shared/effects/profilePrep.ts +1 -1
- package/src/identity/manager/shared/effects/types.ts +30 -0
- package/src/identity/{hub → manager}/shared/model/copy.ts +0 -4
- package/src/identity/{hub → manager}/shared/model/errors.ts +32 -3
- package/src/identity/{hub → manager}/shared/model/network.ts +2 -2
- package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/hook.ts +5 -0
- package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/run.ts +1 -1
- package/src/identity/{hub/shared/reconciliation/useAgentReconciliation.ts → manager/shared/reconciliation/index.ts} +6 -0
- package/src/identity/{hub → manager}/shared/utils.ts +6 -10
- package/src/identity/{hub → manager}/transfer/TokenTransferFlow.tsx +3 -3
- package/src/identity/{hub → manager}/transfer/TokenTransferScreens.tsx +4 -10
- package/src/identity/{hub → manager}/transfer/effects.ts +1 -1
- package/src/identity/{hub → manager}/types.ts +5 -6
- package/src/identity/{hub/useIdentityHubContinuity.ts → manager/useContinuity.ts} +59 -27
- package/src/identity/{hub/useIdentityHubController.ts → manager/useController.ts} +38 -35
- package/src/identity/{hub/useIdentityHubSideEffects.ts → manager/useSideEffects.ts} +40 -4
- package/src/identity/registry/erc8004/discovery.ts +3 -17
- package/src/identity/registry/erc8004/utils.ts +1 -1
- package/src/identity/storage/ipfs.ts +21 -1
- package/src/identity/wallet/browserWallet/html.ts +10 -2
- package/src/identity/wallet/browserWallet/http.ts +18 -0
- package/src/identity/wallet/browserWallet/requestServer.ts +5 -1
- package/src/identity/wallet/browserWallet/requests.ts +10 -28
- package/src/identity/wallet/browserWallet/session.ts +26 -33
- package/src/identity/wallet/browserWallet/validation.ts +14 -0
- package/src/identity/wallet/browserWallet/walletPageSource.ts +22 -40
- package/src/identity/wallet/page/boot.ts +43 -0
- package/src/identity/wallet/page/config.ts +59 -0
- package/src/identity/wallet/page/constants.ts +12 -0
- package/src/identity/wallet/page/copy.ts +47 -68
- package/src/identity/wallet/page/css.ts +638 -0
- package/src/identity/wallet/page/{errorView.ts → errors.ts} +5 -14
- package/src/identity/wallet/page/{controller.ts → flow.ts} +4 -71
- package/src/identity/wallet/page/markup.ts +44 -34
- package/src/identity/wallet/page/{walletProvider.ts → provider.ts} +0 -3
- package/src/identity/wallet/page/resize.ts +95 -0
- package/src/identity/wallet/page/state.ts +135 -8
- package/src/identity/wallet/page/timeline.ts +161 -0
- package/src/identity/wallet/page/view.ts +22 -302
- package/src/storage/config.ts +30 -80
- package/src/storage/reset.ts +31 -0
- package/src/storage/secrets.ts +1 -16
- package/src/ui/Select.tsx +27 -5
- package/src/ui/Spinner.tsx +16 -15
- package/src/ui/Surface.tsx +21 -17
- package/src/ui/TextArea.tsx +173 -0
- package/src/ui/TextInput.tsx +31 -133
- package/src/ui/theme.ts +22 -13
- package/src/utils/clipboard.ts +0 -140
- package/src/app/FirstRun.tsx +0 -577
- package/src/app/FirstRunTimeline.tsx +0 -51
- package/src/app/firstRunConfig.ts +0 -26
- package/src/app/hooks/useCancelRequest.ts +0 -22
- package/src/app/hooks/useDoublePress.ts +0 -46
- package/src/app/hooks/useExitOnCtrlC.ts +0 -36
- package/src/auth/openaiOAuth/credentials.ts +0 -47
- package/src/auth/openaiOAuth/crypto.ts +0 -23
- package/src/auth/openaiOAuth/index.ts +0 -238
- package/src/auth/openaiOAuth/landingPage.ts +0 -116
- package/src/auth/openaiOAuth/listener.ts +0 -151
- package/src/auth/openaiOAuth/refresh.ts +0 -70
- package/src/auth/openaiOAuth/shared.ts +0 -115
- package/src/chat/ChatBottomPane.tsx +0 -296
- package/src/chat/ChatScreen.tsx +0 -1685
- package/src/chat/ConversationStack.tsx +0 -56
- package/src/chat/MessageList.tsx +0 -638
- package/src/chat/SessionStatus.tsx +0 -53
- package/src/chat/chatEnvironment.ts +0 -16
- package/src/chat/chatScreenUtils.ts +0 -194
- package/src/chat/chatSessionState.ts +0 -146
- package/src/chat/chatTurnContext.ts +0 -50
- package/src/chat/chatTurnOrchestrator.ts +0 -603
- package/src/chat/chatTurnRows.ts +0 -64
- package/src/chat/commands.ts +0 -494
- package/src/chat/continuityEditReview.ts +0 -42
- package/src/chat/display/DiffView.tsx +0 -193
- package/src/chat/display/SyntaxText.tsx +0 -192
- package/src/chat/display/toolCallDisplay.ts +0 -103
- package/src/chat/display/toolResultDisplay.ts +0 -19
- package/src/chat/input/ChatInput.tsx +0 -625
- package/src/chat/input/chatInputHelpers.ts +0 -62
- package/src/chat/input/chatInputState.ts +0 -247
- package/src/chat/input/chatPaste.ts +0 -49
- package/src/chat/input/imageRefs.ts +0 -30
- package/src/chat/input/inputRendering.tsx +0 -93
- package/src/chat/input/textCursor.ts +0 -212
- package/src/chat/messageMarkdown.ts +0 -220
- package/src/chat/messageRows.ts +0 -43
- package/src/chat/planImplementation.ts +0 -62
- package/src/chat/slashCommandHandlers.ts +0 -122
- package/src/chat/slashCommandViews.ts +0 -120
- package/src/chat/transcript/TranscriptView.tsx +0 -184
- package/src/chat/transcript/transcriptViewport.ts +0 -295
- package/src/chat/views/ContextLimitView.tsx +0 -95
- package/src/chat/views/ContinuityEditReviewView.tsx +0 -50
- package/src/chat/views/CopyPicker.tsx +0 -50
- package/src/chat/views/PermissionPrompt.tsx +0 -156
- package/src/chat/views/PermissionsView.tsx +0 -165
- package/src/chat/views/PlanApprovalView.tsx +0 -91
- package/src/chat/views/ResumeView.tsx +0 -273
- package/src/chat/views/RewindView.tsx +0 -412
- package/src/cli/preview.tsx +0 -14
- package/src/cli/updateNotice.ts +0 -54
- package/src/identity/continuity/privateEdit/apply.ts +0 -170
- package/src/identity/continuity/privateEdit/diff.ts +0 -6
- package/src/identity/continuity/privateEdit/files.ts +0 -23
- package/src/identity/continuity/privateEdit/types.ts +0 -28
- package/src/identity/continuity/privateEdit.ts +0 -46
- package/src/identity/hub/IdentityHub.tsx +0 -14
- package/src/identity/hub/continuity/RecoveryConfirmScreen.tsx +0 -104
- package/src/identity/hub/ens/effects.ts +0 -218
- package/src/identity/hub/shared/components/MenuScreen.tsx +0 -241
- package/src/identity/hub/shared/effects/types.ts +0 -53
- package/src/identity/hub/shared/reconciliation/index.ts +0 -14
- package/src/identity/wallet/page/grainient.ts +0 -278
- package/src/identity/wallet/page/html.ts +0 -28
- package/src/identity/wallet/page/styles/base.ts +0 -259
- package/src/identity/wallet/page/styles/components.ts +0 -262
- package/src/identity/wallet/page/styles/index.ts +0 -5
- package/src/identity/wallet/page/styles/responsive.ts +0 -247
- package/src/identity/wallet/page.tsx +0 -38
- package/src/mcp/approvals.ts +0 -113
- package/src/mcp/config.ts +0 -235
- package/src/mcp/manager.ts +0 -482
- package/src/mcp/managerHelpers.ts +0 -70
- package/src/mcp/names.ts +0 -19
- package/src/mcp/output.ts +0 -96
- package/src/models/ModelPicker.tsx +0 -1009
- package/src/models/catalog.ts +0 -327
- package/src/models/huggingface.ts +0 -712
- package/src/models/huggingfaceStorage.ts +0 -136
- package/src/models/llamacpp.ts +0 -848
- package/src/models/llamacppCommands.ts +0 -44
- package/src/models/llamacppConfig.ts +0 -34
- package/src/models/llamacppDiscovery.ts +0 -176
- package/src/models/llamacppOutput.ts +0 -65
- package/src/models/llamacppPreflight.ts +0 -158
- package/src/models/modelDisplay.ts +0 -180
- package/src/models/modelPickerCatalogFlow.ts +0 -56
- package/src/models/modelPickerCredentials.ts +0 -166
- package/src/models/modelPickerData.ts +0 -41
- package/src/models/modelPickerDisplay.tsx +0 -132
- package/src/models/modelPickerHfFlow.ts +0 -192
- package/src/models/modelPickerLocalRunnerFlow.ts +0 -115
- package/src/models/modelPickerOptions.ts +0 -457
- package/src/models/modelPickerTypes.ts +0 -69
- package/src/models/modelPickerUninstallFlow.ts +0 -48
- package/src/models/modelPickerViewHelpers.ts +0 -174
- package/src/models/modelRecommendation.ts +0 -139
- package/src/models/providerDisplay.ts +0 -16
- package/src/models/runtimeDetection.ts +0 -81
- package/src/models/uncensoredCatalog.ts +0 -86
- package/src/providers/anthropic.ts +0 -290
- package/src/providers/contracts.ts +0 -71
- package/src/providers/errors.ts +0 -80
- package/src/providers/gemini.ts +0 -391
- package/src/providers/openai-chat.ts +0 -474
- package/src/providers/openai-responses-format.ts +0 -177
- package/src/providers/openai-responses.ts +0 -306
- package/src/providers/openaiChatWire.ts +0 -124
- package/src/providers/registry.ts +0 -120
- package/src/providers/retry.ts +0 -58
- package/src/providers/sse.ts +0 -93
- package/src/runtime/compaction.ts +0 -395
- package/src/runtime/cwd.ts +0 -43
- package/src/runtime/providerTurn.ts +0 -38
- package/src/runtime/sessionMode.ts +0 -55
- package/src/runtime/systemPrompt.ts +0 -213
- package/src/runtime/textToolParser.ts +0 -161
- package/src/runtime/toolClaimGuards.ts +0 -143
- package/src/runtime/toolExecution.ts +0 -304
- package/src/runtime/toolIntent.ts +0 -143
- package/src/runtime/turn.ts +0 -369
- package/src/runtime/turnNudges.ts +0 -223
- package/src/runtime/turnTypes.ts +0 -86
- package/src/storage/factoryReset.ts +0 -127
- package/src/storage/history.ts +0 -58
- package/src/storage/permissions.ts +0 -76
- package/src/storage/rewind.ts +0 -266
- package/src/storage/sessionExport.ts +0 -49
- package/src/storage/sessions.ts +0 -495
- package/src/tools/bashSafety.ts +0 -186
- package/src/tools/bashTool.ts +0 -140
- package/src/tools/changeDirectoryTool.ts +0 -213
- package/src/tools/contracts.ts +0 -192
- package/src/tools/deleteFileTool.ts +0 -116
- package/src/tools/editTool.ts +0 -165
- package/src/tools/editUtils.ts +0 -170
- package/src/tools/fileDiff.ts +0 -261
- package/src/tools/listDirectoryTool.ts +0 -55
- package/src/tools/listSkillFilesTool.ts +0 -77
- package/src/tools/listSkillsTool.ts +0 -68
- package/src/tools/mcpResourceTools.ts +0 -95
- package/src/tools/permissionRules.ts +0 -85
- package/src/tools/privateContinuityEditTool.ts +0 -187
- package/src/tools/privateContinuityReadTool.ts +0 -106
- package/src/tools/readSkillTool.ts +0 -107
- package/src/tools/readTool.ts +0 -85
- package/src/tools/registry.ts +0 -103
- package/src/tools/writeFileTool.ts +0 -167
- package/src/ui/BrandSplash.tsx +0 -133
- package/src/ui/terminalTitle.ts +0 -30
- package/src/utils/images.ts +0 -140
- package/src/utils/markdownSegments.ts +0 -51
- package/src/utils/messages.ts +0 -37
- package/src/utils/withRetry.ts +0 -324
- /package/src/identity/{hub → manager}/continuity/state.ts +0 -0
- /package/src/identity/{hub → manager}/custody/helpers.ts +0 -0
- /package/src/identity/{hub → manager}/custody/preflight.ts +0 -0
- /package/src/identity/{hub → manager}/custody/state.ts +0 -0
- /package/src/identity/{hub → manager}/custody/useCustodyFlow.tsx +0 -0
- /package/src/identity/{hub → manager}/ens/EnsEditFlow.tsx +0 -0
- /package/src/identity/{hub → manager}/ens/advancedEnsValidation.ts +0 -0
- /package/src/identity/{hub → manager}/ens/state.ts +0 -0
- /package/src/identity/{hub → manager}/ens/transactions.ts +0 -0
- /package/src/identity/{hub → manager}/ens/types.ts +0 -0
- /package/src/identity/{hub → manager}/profile/identity.ts +0 -0
- /package/src/identity/{hub → manager}/restore/envelopes.ts +0 -0
- /package/src/identity/{hub → manager}/restore/helpers.ts +0 -0
- /package/src/identity/{hub → manager}/restore/recovery.ts +0 -0
- /package/src/identity/{hub → manager}/restore/resolve.ts +0 -0
- /package/src/identity/{hub → manager}/shared/components/BusyScreen.tsx +0 -0
- /package/src/identity/{hub → manager}/shared/components/NetworkScreen.tsx +0 -0
- /package/src/identity/{hub → manager}/shared/components/PinataJwtInput.tsx +0 -0
- /package/src/identity/{hub → manager}/shared/effects/receipts.ts +0 -0
- /package/src/identity/{hub → manager}/shared/effects/sync.ts +0 -0
- /package/src/identity/{hub → manager}/shared/model/format.ts +0 -0
- /package/src/identity/{hub → manager}/shared/operatorWallets.ts +0 -0
- /package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/ownership.ts +0 -0
- /package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/types.ts +0 -0
- /package/src/identity/{hub → manager}/shared/reconciliation/walletSetup.ts +0 -0
- /package/src/identity/{hub → manager}/shared/txGuard.ts +0 -0
- /package/src/identity/{hub → manager}/transfer/progress.ts +0 -0
- /package/src/identity/{hub → manager}/transfer/state.ts +0 -0
package/src/storage/sessions.ts
DELETED
|
@@ -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
|
-
}
|
package/src/tools/bashSafety.ts
DELETED
|
@@ -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
|
-
}
|