better-codex 0.1.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/README.md +26 -0
- package/apps/backend/README.md +46 -0
- package/apps/backend/bun.lock +64 -0
- package/apps/backend/package.json +18 -0
- package/apps/backend/scripts/generate-protocol.ts +32 -0
- package/apps/backend/src/analytics/service.ts +219 -0
- package/apps/backend/src/analytics/store.ts +284 -0
- package/apps/backend/src/config.ts +98 -0
- package/apps/backend/src/core/app-server.ts +131 -0
- package/apps/backend/src/core/jsonrpc.ts +166 -0
- package/apps/backend/src/protocol/AbsolutePathBuf.ts +14 -0
- package/apps/backend/src/protocol/AddConversationListenerParams.ts +6 -0
- package/apps/backend/src/protocol/AddConversationSubscriptionResponse.ts +5 -0
- package/apps/backend/src/protocol/AgentMessageContent.ts +5 -0
- package/apps/backend/src/protocol/AgentMessageContentDeltaEvent.ts +5 -0
- package/apps/backend/src/protocol/AgentMessageDeltaEvent.ts +5 -0
- package/apps/backend/src/protocol/AgentMessageEvent.ts +5 -0
- package/apps/backend/src/protocol/AgentMessageItem.ts +6 -0
- package/apps/backend/src/protocol/AgentReasoningDeltaEvent.ts +5 -0
- package/apps/backend/src/protocol/AgentReasoningEvent.ts +5 -0
- package/apps/backend/src/protocol/AgentReasoningRawContentDeltaEvent.ts +5 -0
- package/apps/backend/src/protocol/AgentReasoningRawContentEvent.ts +5 -0
- package/apps/backend/src/protocol/AgentReasoningSectionBreakEvent.ts +5 -0
- package/apps/backend/src/protocol/Annotations.ts +9 -0
- package/apps/backend/src/protocol/ApplyPatchApprovalParams.ts +21 -0
- package/apps/backend/src/protocol/ApplyPatchApprovalRequestEvent.ts +23 -0
- package/apps/backend/src/protocol/ApplyPatchApprovalResponse.ts +6 -0
- package/apps/backend/src/protocol/ArchiveConversationParams.ts +6 -0
- package/apps/backend/src/protocol/ArchiveConversationResponse.ts +5 -0
- package/apps/backend/src/protocol/AskForApproval.ts +9 -0
- package/apps/backend/src/protocol/AudioContent.ts +9 -0
- package/apps/backend/src/protocol/AuthMode.ts +5 -0
- package/apps/backend/src/protocol/AuthStatusChangeNotification.ts +9 -0
- package/apps/backend/src/protocol/BackgroundEventEvent.ts +5 -0
- package/apps/backend/src/protocol/BlobResourceContents.ts +5 -0
- package/apps/backend/src/protocol/CallToolResult.ts +10 -0
- package/apps/backend/src/protocol/CancelLoginChatGptParams.ts +5 -0
- package/apps/backend/src/protocol/CancelLoginChatGptResponse.ts +5 -0
- package/apps/backend/src/protocol/ClientInfo.ts +5 -0
- package/apps/backend/src/protocol/ClientNotification.ts +5 -0
- package/apps/backend/src/protocol/ClientRequest.ts +46 -0
- package/apps/backend/src/protocol/CodexErrorInfo.ts +8 -0
- package/apps/backend/src/protocol/ContentBlock.ts +10 -0
- package/apps/backend/src/protocol/ContentItem.ts +5 -0
- package/apps/backend/src/protocol/ContextCompactedEvent.ts +5 -0
- package/apps/backend/src/protocol/ConversationGitInfo.ts +5 -0
- package/apps/backend/src/protocol/ConversationId.ts +5 -0
- package/apps/backend/src/protocol/ConversationSummary.ts +8 -0
- package/apps/backend/src/protocol/CreditsSnapshot.ts +5 -0
- package/apps/backend/src/protocol/CustomPrompt.ts +5 -0
- package/apps/backend/src/protocol/DeprecationNoticeEvent.ts +13 -0
- package/apps/backend/src/protocol/ElicitationRequestEvent.ts +6 -0
- package/apps/backend/src/protocol/EmbeddedResource.ts +13 -0
- package/apps/backend/src/protocol/EmbeddedResourceResource.ts +7 -0
- package/apps/backend/src/protocol/ErrorEvent.ts +6 -0
- package/apps/backend/src/protocol/EventMsg.ts +60 -0
- package/apps/backend/src/protocol/ExecApprovalRequestEvent.ts +32 -0
- package/apps/backend/src/protocol/ExecCommandApprovalParams.ts +12 -0
- package/apps/backend/src/protocol/ExecCommandApprovalResponse.ts +6 -0
- package/apps/backend/src/protocol/ExecCommandBeginEvent.ts +35 -0
- package/apps/backend/src/protocol/ExecCommandEndEvent.ts +59 -0
- package/apps/backend/src/protocol/ExecCommandOutputDeltaEvent.ts +18 -0
- package/apps/backend/src/protocol/ExecCommandSource.ts +5 -0
- package/apps/backend/src/protocol/ExecOneOffCommandParams.ts +6 -0
- package/apps/backend/src/protocol/ExecOneOffCommandResponse.ts +5 -0
- package/apps/backend/src/protocol/ExecOutputStream.ts +5 -0
- package/apps/backend/src/protocol/ExecPolicyAmendment.ts +12 -0
- package/apps/backend/src/protocol/ExitedReviewModeEvent.ts +6 -0
- package/apps/backend/src/protocol/FileChange.ts +5 -0
- package/apps/backend/src/protocol/ForcedLoginMethod.ts +5 -0
- package/apps/backend/src/protocol/FunctionCallOutputContentItem.ts +9 -0
- package/apps/backend/src/protocol/FunctionCallOutputPayload.ts +15 -0
- package/apps/backend/src/protocol/FuzzyFileSearchParams.ts +5 -0
- package/apps/backend/src/protocol/FuzzyFileSearchResponse.ts +6 -0
- package/apps/backend/src/protocol/FuzzyFileSearchResult.ts +8 -0
- package/apps/backend/src/protocol/GetAuthStatusParams.ts +5 -0
- package/apps/backend/src/protocol/GetAuthStatusResponse.ts +6 -0
- package/apps/backend/src/protocol/GetConversationSummaryParams.ts +6 -0
- package/apps/backend/src/protocol/GetConversationSummaryResponse.ts +6 -0
- package/apps/backend/src/protocol/GetHistoryEntryResponseEvent.ts +10 -0
- package/apps/backend/src/protocol/GetUserAgentResponse.ts +5 -0
- package/apps/backend/src/protocol/GetUserSavedConfigResponse.ts +6 -0
- package/apps/backend/src/protocol/GhostCommit.ts +8 -0
- package/apps/backend/src/protocol/GitDiffToRemoteParams.ts +5 -0
- package/apps/backend/src/protocol/GitDiffToRemoteResponse.ts +6 -0
- package/apps/backend/src/protocol/GitSha.ts +5 -0
- package/apps/backend/src/protocol/HistoryEntry.ts +5 -0
- package/apps/backend/src/protocol/ImageContent.ts +9 -0
- package/apps/backend/src/protocol/InitializeParams.ts +6 -0
- package/apps/backend/src/protocol/InitializeResponse.ts +5 -0
- package/apps/backend/src/protocol/InputItem.ts +5 -0
- package/apps/backend/src/protocol/InterruptConversationParams.ts +6 -0
- package/apps/backend/src/protocol/InterruptConversationResponse.ts +6 -0
- package/apps/backend/src/protocol/ItemCompletedEvent.ts +7 -0
- package/apps/backend/src/protocol/ItemStartedEvent.ts +7 -0
- package/apps/backend/src/protocol/ListConversationsParams.ts +5 -0
- package/apps/backend/src/protocol/ListConversationsResponse.ts +6 -0
- package/apps/backend/src/protocol/ListCustomPromptsResponseEvent.ts +9 -0
- package/apps/backend/src/protocol/ListSkillsResponseEvent.ts +9 -0
- package/apps/backend/src/protocol/LocalShellAction.ts +6 -0
- package/apps/backend/src/protocol/LocalShellExecAction.ts +5 -0
- package/apps/backend/src/protocol/LocalShellStatus.ts +5 -0
- package/apps/backend/src/protocol/LoginApiKeyParams.ts +5 -0
- package/apps/backend/src/protocol/LoginApiKeyResponse.ts +5 -0
- package/apps/backend/src/protocol/LoginChatGptCompleteNotification.ts +8 -0
- package/apps/backend/src/protocol/LoginChatGptResponse.ts +5 -0
- package/apps/backend/src/protocol/LogoutChatGptResponse.ts +5 -0
- package/apps/backend/src/protocol/McpAuthStatus.ts +5 -0
- package/apps/backend/src/protocol/McpInvocation.ts +18 -0
- package/apps/backend/src/protocol/McpListToolsResponseEvent.ts +25 -0
- package/apps/backend/src/protocol/McpStartupCompleteEvent.ts +6 -0
- package/apps/backend/src/protocol/McpStartupFailure.ts +5 -0
- package/apps/backend/src/protocol/McpStartupStatus.ts +5 -0
- package/apps/backend/src/protocol/McpStartupUpdateEvent.ts +14 -0
- package/apps/backend/src/protocol/McpToolCallBeginEvent.ts +10 -0
- package/apps/backend/src/protocol/McpToolCallEndEvent.ts +15 -0
- package/apps/backend/src/protocol/NetworkAccess.ts +8 -0
- package/apps/backend/src/protocol/NewConversationParams.ts +8 -0
- package/apps/backend/src/protocol/NewConversationResponse.ts +7 -0
- package/apps/backend/src/protocol/ParsedCommand.ts +12 -0
- package/apps/backend/src/protocol/PatchApplyBeginEvent.ts +23 -0
- package/apps/backend/src/protocol/PatchApplyEndEvent.ts +31 -0
- package/apps/backend/src/protocol/PlanItemArg.ts +6 -0
- package/apps/backend/src/protocol/PlanType.ts +5 -0
- package/apps/backend/src/protocol/Profile.ts +9 -0
- package/apps/backend/src/protocol/README.md +11 -0
- package/apps/backend/src/protocol/RateLimitSnapshot.ts +8 -0
- package/apps/backend/src/protocol/RateLimitWindow.ts +17 -0
- package/apps/backend/src/protocol/RawResponseItemEvent.ts +6 -0
- package/apps/backend/src/protocol/ReasoningContentDeltaEvent.ts +5 -0
- package/apps/backend/src/protocol/ReasoningEffort.ts +8 -0
- package/apps/backend/src/protocol/ReasoningItem.ts +5 -0
- package/apps/backend/src/protocol/ReasoningItemContent.ts +5 -0
- package/apps/backend/src/protocol/ReasoningItemReasoningSummary.ts +5 -0
- package/apps/backend/src/protocol/ReasoningRawContentDeltaEvent.ts +5 -0
- package/apps/backend/src/protocol/ReasoningSummary.ts +10 -0
- package/apps/backend/src/protocol/RemoveConversationListenerParams.ts +5 -0
- package/apps/backend/src/protocol/RemoveConversationSubscriptionResponse.ts +5 -0
- package/apps/backend/src/protocol/RequestId.ts +5 -0
- package/apps/backend/src/protocol/Resource.ts +9 -0
- package/apps/backend/src/protocol/ResourceLink.ts +11 -0
- package/apps/backend/src/protocol/ResourceTemplate.ts +9 -0
- package/apps/backend/src/protocol/ResponseItem.ts +17 -0
- package/apps/backend/src/protocol/ResumeConversationParams.ts +8 -0
- package/apps/backend/src/protocol/ResumeConversationResponse.ts +7 -0
- package/apps/backend/src/protocol/ReviewCodeLocation.ts +9 -0
- package/apps/backend/src/protocol/ReviewDecision.ts +9 -0
- package/apps/backend/src/protocol/ReviewFinding.ts +9 -0
- package/apps/backend/src/protocol/ReviewLineRange.ts +8 -0
- package/apps/backend/src/protocol/ReviewOutputEvent.ts +9 -0
- package/apps/backend/src/protocol/ReviewRequest.ts +9 -0
- package/apps/backend/src/protocol/ReviewTarget.ts +9 -0
- package/apps/backend/src/protocol/Role.ts +8 -0
- package/apps/backend/src/protocol/SandboxMode.ts +5 -0
- package/apps/backend/src/protocol/SandboxPolicy.ts +35 -0
- package/apps/backend/src/protocol/SandboxSettings.ts +6 -0
- package/apps/backend/src/protocol/SendUserMessageParams.ts +7 -0
- package/apps/backend/src/protocol/SendUserMessageResponse.ts +5 -0
- package/apps/backend/src/protocol/SendUserTurnParams.ts +11 -0
- package/apps/backend/src/protocol/SendUserTurnResponse.ts +5 -0
- package/apps/backend/src/protocol/ServerNotification.ts +36 -0
- package/apps/backend/src/protocol/ServerRequest.ts +13 -0
- package/apps/backend/src/protocol/SessionConfiguredEvent.ts +48 -0
- package/apps/backend/src/protocol/SessionConfiguredNotification.ts +8 -0
- package/apps/backend/src/protocol/SessionSource.ts +6 -0
- package/apps/backend/src/protocol/SetDefaultModelParams.ts +6 -0
- package/apps/backend/src/protocol/SetDefaultModelResponse.ts +5 -0
- package/apps/backend/src/protocol/SkillErrorInfo.ts +5 -0
- package/apps/backend/src/protocol/SkillMetadata.ts +6 -0
- package/apps/backend/src/protocol/SkillScope.ts +5 -0
- package/apps/backend/src/protocol/SkillsListEntry.ts +7 -0
- package/apps/backend/src/protocol/StepStatus.ts +5 -0
- package/apps/backend/src/protocol/StreamErrorEvent.ts +6 -0
- package/apps/backend/src/protocol/SubAgentSource.ts +5 -0
- package/apps/backend/src/protocol/TaskCompleteEvent.ts +5 -0
- package/apps/backend/src/protocol/TaskStartedEvent.ts +5 -0
- package/apps/backend/src/protocol/TerminalInteractionEvent.ts +17 -0
- package/apps/backend/src/protocol/TextContent.ts +9 -0
- package/apps/backend/src/protocol/TextResourceContents.ts +5 -0
- package/apps/backend/src/protocol/TokenCountEvent.ts +7 -0
- package/apps/backend/src/protocol/TokenUsage.ts +5 -0
- package/apps/backend/src/protocol/TokenUsageInfo.ts +6 -0
- package/apps/backend/src/protocol/Tool.ts +11 -0
- package/apps/backend/src/protocol/ToolAnnotations.ts +15 -0
- package/apps/backend/src/protocol/ToolInputSchema.ts +9 -0
- package/apps/backend/src/protocol/ToolOutputSchema.ts +10 -0
- package/apps/backend/src/protocol/Tools.ts +5 -0
- package/apps/backend/src/protocol/TurnAbortReason.ts +5 -0
- package/apps/backend/src/protocol/TurnAbortedEvent.ts +6 -0
- package/apps/backend/src/protocol/TurnDiffEvent.ts +5 -0
- package/apps/backend/src/protocol/TurnItem.ts +9 -0
- package/apps/backend/src/protocol/UndoCompletedEvent.ts +5 -0
- package/apps/backend/src/protocol/UndoStartedEvent.ts +5 -0
- package/apps/backend/src/protocol/UpdatePlanArgs.ts +6 -0
- package/apps/backend/src/protocol/UserInfoResponse.ts +5 -0
- package/apps/backend/src/protocol/UserInput.ts +8 -0
- package/apps/backend/src/protocol/UserMessageEvent.ts +5 -0
- package/apps/backend/src/protocol/UserMessageItem.ts +6 -0
- package/apps/backend/src/protocol/UserSavedConfig.ts +14 -0
- package/apps/backend/src/protocol/Verbosity.ts +9 -0
- package/apps/backend/src/protocol/ViewImageToolCallEvent.ts +13 -0
- package/apps/backend/src/protocol/WarningEvent.ts +5 -0
- package/apps/backend/src/protocol/WebSearchAction.ts +5 -0
- package/apps/backend/src/protocol/WebSearchBeginEvent.ts +5 -0
- package/apps/backend/src/protocol/WebSearchEndEvent.ts +5 -0
- package/apps/backend/src/protocol/WebSearchItem.ts +5 -0
- package/apps/backend/src/protocol/index.ts +198 -0
- package/apps/backend/src/protocol/serde_json/JsonValue.ts +5 -0
- package/apps/backend/src/protocol/v2/Account.ts +6 -0
- package/apps/backend/src/protocol/v2/AccountLoginCompletedNotification.ts +5 -0
- package/apps/backend/src/protocol/v2/AccountRateLimitsUpdatedNotification.ts +6 -0
- package/apps/backend/src/protocol/v2/AccountUpdatedNotification.ts +6 -0
- package/apps/backend/src/protocol/v2/AgentMessageDeltaNotification.ts +5 -0
- package/apps/backend/src/protocol/v2/ApprovalDecision.ts +6 -0
- package/apps/backend/src/protocol/v2/AskForApproval.ts +5 -0
- package/apps/backend/src/protocol/v2/CancelLoginAccountParams.ts +5 -0
- package/apps/backend/src/protocol/v2/CancelLoginAccountResponse.ts +6 -0
- package/apps/backend/src/protocol/v2/CancelLoginAccountStatus.ts +5 -0
- package/apps/backend/src/protocol/v2/CodexErrorInfo.ts +11 -0
- package/apps/backend/src/protocol/v2/CommandAction.ts +5 -0
- package/apps/backend/src/protocol/v2/CommandExecParams.ts +6 -0
- package/apps/backend/src/protocol/v2/CommandExecResponse.ts +5 -0
- package/apps/backend/src/protocol/v2/CommandExecutionOutputDeltaNotification.ts +5 -0
- package/apps/backend/src/protocol/v2/CommandExecutionRequestApprovalParams.ts +14 -0
- package/apps/backend/src/protocol/v2/CommandExecutionRequestApprovalResponse.ts +6 -0
- package/apps/backend/src/protocol/v2/CommandExecutionStatus.ts +5 -0
- package/apps/backend/src/protocol/v2/Config.ts +15 -0
- package/apps/backend/src/protocol/v2/ConfigBatchWriteParams.ts +10 -0
- package/apps/backend/src/protocol/v2/ConfigEdit.ts +7 -0
- package/apps/backend/src/protocol/v2/ConfigLayer.ts +7 -0
- package/apps/backend/src/protocol/v2/ConfigLayerMetadata.ts +6 -0
- package/apps/backend/src/protocol/v2/ConfigLayerSource.ts +6 -0
- package/apps/backend/src/protocol/v2/ConfigReadParams.ts +5 -0
- package/apps/backend/src/protocol/v2/ConfigReadResponse.ts +8 -0
- package/apps/backend/src/protocol/v2/ConfigValueWriteParams.ts +11 -0
- package/apps/backend/src/protocol/v2/ConfigWriteResponse.ts +12 -0
- package/apps/backend/src/protocol/v2/ContextCompactedNotification.ts +5 -0
- package/apps/backend/src/protocol/v2/CreditsSnapshot.ts +5 -0
- package/apps/backend/src/protocol/v2/DeprecationNoticeNotification.ts +13 -0
- package/apps/backend/src/protocol/v2/ErrorNotification.ts +6 -0
- package/apps/backend/src/protocol/v2/ExecPolicyAmendment.ts +5 -0
- package/apps/backend/src/protocol/v2/FeedbackUploadParams.ts +5 -0
- package/apps/backend/src/protocol/v2/FeedbackUploadResponse.ts +5 -0
- package/apps/backend/src/protocol/v2/FileChangeOutputDeltaNotification.ts +5 -0
- package/apps/backend/src/protocol/v2/FileChangeRequestApprovalParams.ts +14 -0
- package/apps/backend/src/protocol/v2/FileChangeRequestApprovalResponse.ts +6 -0
- package/apps/backend/src/protocol/v2/FileUpdateChange.ts +6 -0
- package/apps/backend/src/protocol/v2/GetAccountParams.ts +5 -0
- package/apps/backend/src/protocol/v2/GetAccountRateLimitsResponse.ts +6 -0
- package/apps/backend/src/protocol/v2/GetAccountResponse.ts +6 -0
- package/apps/backend/src/protocol/v2/GitInfo.ts +5 -0
- package/apps/backend/src/protocol/v2/ItemCompletedNotification.ts +6 -0
- package/apps/backend/src/protocol/v2/ItemStartedNotification.ts +6 -0
- package/apps/backend/src/protocol/v2/ListMcpServerStatusParams.ts +13 -0
- package/apps/backend/src/protocol/v2/ListMcpServerStatusResponse.ts +11 -0
- package/apps/backend/src/protocol/v2/LoginAccountParams.ts +5 -0
- package/apps/backend/src/protocol/v2/LoginAccountResponse.ts +9 -0
- package/apps/backend/src/protocol/v2/LogoutAccountResponse.ts +5 -0
- package/apps/backend/src/protocol/v2/McpAuthStatus.ts +5 -0
- package/apps/backend/src/protocol/v2/McpServerOauthLoginCompletedNotification.ts +5 -0
- package/apps/backend/src/protocol/v2/McpServerOauthLoginParams.ts +5 -0
- package/apps/backend/src/protocol/v2/McpServerOauthLoginResponse.ts +5 -0
- package/apps/backend/src/protocol/v2/McpServerStatus.ts +9 -0
- package/apps/backend/src/protocol/v2/McpToolCallError.ts +5 -0
- package/apps/backend/src/protocol/v2/McpToolCallProgressNotification.ts +5 -0
- package/apps/backend/src/protocol/v2/McpToolCallResult.ts +7 -0
- package/apps/backend/src/protocol/v2/McpToolCallStatus.ts +5 -0
- package/apps/backend/src/protocol/v2/MergeStrategy.ts +5 -0
- package/apps/backend/src/protocol/v2/Model.ts +7 -0
- package/apps/backend/src/protocol/v2/ModelListParams.ts +13 -0
- package/apps/backend/src/protocol/v2/ModelListResponse.ts +11 -0
- package/apps/backend/src/protocol/v2/NetworkAccess.ts +5 -0
- package/apps/backend/src/protocol/v2/OverriddenMetadata.ts +7 -0
- package/apps/backend/src/protocol/v2/PatchApplyStatus.ts +5 -0
- package/apps/backend/src/protocol/v2/PatchChangeKind.ts +5 -0
- package/apps/backend/src/protocol/v2/ProfileV2.ts +10 -0
- package/apps/backend/src/protocol/v2/RateLimitSnapshot.ts +8 -0
- package/apps/backend/src/protocol/v2/RateLimitWindow.ts +5 -0
- package/apps/backend/src/protocol/v2/RawResponseItemCompletedNotification.ts +6 -0
- package/apps/backend/src/protocol/v2/ReasoningEffortOption.ts +6 -0
- package/apps/backend/src/protocol/v2/ReasoningSummaryPartAddedNotification.ts +5 -0
- package/apps/backend/src/protocol/v2/ReasoningSummaryTextDeltaNotification.ts +5 -0
- package/apps/backend/src/protocol/v2/ReasoningTextDeltaNotification.ts +5 -0
- package/apps/backend/src/protocol/v2/ReviewDelivery.ts +5 -0
- package/apps/backend/src/protocol/v2/ReviewStartParams.ts +12 -0
- package/apps/backend/src/protocol/v2/ReviewStartResponse.ts +13 -0
- package/apps/backend/src/protocol/v2/ReviewTarget.ts +9 -0
- package/apps/backend/src/protocol/v2/SandboxMode.ts +5 -0
- package/apps/backend/src/protocol/v2/SandboxPolicy.ts +7 -0
- package/apps/backend/src/protocol/v2/SandboxWorkspaceWrite.ts +5 -0
- package/apps/backend/src/protocol/v2/SessionSource.ts +5 -0
- package/apps/backend/src/protocol/v2/SkillErrorInfo.ts +5 -0
- package/apps/backend/src/protocol/v2/SkillMetadata.ts +6 -0
- package/apps/backend/src/protocol/v2/SkillScope.ts +5 -0
- package/apps/backend/src/protocol/v2/SkillsListEntry.ts +7 -0
- package/apps/backend/src/protocol/v2/SkillsListParams.ts +13 -0
- package/apps/backend/src/protocol/v2/SkillsListResponse.ts +6 -0
- package/apps/backend/src/protocol/v2/TerminalInteractionNotification.ts +5 -0
- package/apps/backend/src/protocol/v2/Thread.ts +46 -0
- package/apps/backend/src/protocol/v2/ThreadArchiveParams.ts +5 -0
- package/apps/backend/src/protocol/v2/ThreadArchiveResponse.ts +5 -0
- package/apps/backend/src/protocol/v2/ThreadItem.ts +48 -0
- package/apps/backend/src/protocol/v2/ThreadListParams.ts +18 -0
- package/apps/backend/src/protocol/v2/ThreadListResponse.ts +11 -0
- package/apps/backend/src/protocol/v2/ThreadResumeParams.ts +35 -0
- package/apps/backend/src/protocol/v2/ThreadResumeResponse.ts +9 -0
- package/apps/backend/src/protocol/v2/ThreadStartParams.ts +15 -0
- package/apps/backend/src/protocol/v2/ThreadStartResponse.ts +9 -0
- package/apps/backend/src/protocol/v2/ThreadStartedNotification.ts +6 -0
- package/apps/backend/src/protocol/v2/ThreadTokenUsage.ts +6 -0
- package/apps/backend/src/protocol/v2/ThreadTokenUsageUpdatedNotification.ts +6 -0
- package/apps/backend/src/protocol/v2/TokenUsageBreakdown.ts +5 -0
- package/apps/backend/src/protocol/v2/ToolsV2.ts +5 -0
- package/apps/backend/src/protocol/v2/Turn.ts +18 -0
- package/apps/backend/src/protocol/v2/TurnCompletedNotification.ts +6 -0
- package/apps/backend/src/protocol/v2/TurnDiffUpdatedNotification.ts +9 -0
- package/apps/backend/src/protocol/v2/TurnError.ts +6 -0
- package/apps/backend/src/protocol/v2/TurnInterruptParams.ts +5 -0
- package/apps/backend/src/protocol/v2/TurnInterruptResponse.ts +5 -0
- package/apps/backend/src/protocol/v2/TurnPlanStep.ts +6 -0
- package/apps/backend/src/protocol/v2/TurnPlanStepStatus.ts +5 -0
- package/apps/backend/src/protocol/v2/TurnPlanUpdatedNotification.ts +6 -0
- package/apps/backend/src/protocol/v2/TurnStartParams.ts +34 -0
- package/apps/backend/src/protocol/v2/TurnStartResponse.ts +6 -0
- package/apps/backend/src/protocol/v2/TurnStartedNotification.ts +6 -0
- package/apps/backend/src/protocol/v2/TurnStatus.ts +5 -0
- package/apps/backend/src/protocol/v2/UserInput.ts +5 -0
- package/apps/backend/src/protocol/v2/WindowsWorldWritableWarningNotification.ts +5 -0
- package/apps/backend/src/protocol/v2/WriteStatus.ts +5 -0
- package/apps/backend/src/protocol/v2/index.ts +123 -0
- package/apps/backend/src/reviews/service.ts +27 -0
- package/apps/backend/src/reviews/store.ts +124 -0
- package/apps/backend/src/server.ts +531 -0
- package/apps/backend/src/services/profile-store.ts +114 -0
- package/apps/backend/src/services/supervisor.ts +102 -0
- package/apps/backend/src/thread-index/service.ts +75 -0
- package/apps/backend/src/thread-index/store.ts +195 -0
- package/apps/backend/src/ws/messages.ts +73 -0
- package/apps/backend/tsconfig.json +20 -0
- package/apps/web/README.md +24 -0
- package/apps/web/bun.lock +1062 -0
- package/apps/web/eslint.config.js +23 -0
- package/apps/web/index.html +16 -0
- package/apps/web/package.json +38 -0
- package/apps/web/src/app.tsx +83 -0
- package/apps/web/src/components/composer/slash-command-menu.tsx +47 -0
- package/apps/web/src/components/index.ts +2 -0
- package/apps/web/src/components/layout/account-usage-panel.tsx +167 -0
- package/apps/web/src/components/layout/analytics-view.tsx +296 -0
- package/apps/web/src/components/layout/index.ts +7 -0
- package/apps/web/src/components/layout/mobile-header.tsx +56 -0
- package/apps/web/src/components/layout/reviews-view.tsx +848 -0
- package/apps/web/src/components/layout/session-view.tsx +1374 -0
- package/apps/web/src/components/layout/settings-dialog.tsx +322 -0
- package/apps/web/src/components/layout/side-bar.tsx +417 -0
- package/apps/web/src/components/layout/thread-list.tsx +488 -0
- package/apps/web/src/components/layout/virtualized-message-list.tsx +748 -0
- package/apps/web/src/components/loading/startup-ascii.ts +652 -0
- package/apps/web/src/components/loading/startup-loader.tsx +37 -0
- package/apps/web/src/components/session-view/file-mention-menu.tsx +46 -0
- package/apps/web/src/components/session-view/session-auth-banner.tsx +61 -0
- package/apps/web/src/components/session-view/session-composer.tsx +328 -0
- package/apps/web/src/components/session-view/session-dialogs.tsx +280 -0
- package/apps/web/src/components/session-view/session-empty.tsx +47 -0
- package/apps/web/src/components/session-view/session-header.tsx +49 -0
- package/apps/web/src/components/ui/avatar.tsx +19 -0
- package/apps/web/src/components/ui/badge.tsx +21 -0
- package/apps/web/src/components/ui/button.tsx +47 -0
- package/apps/web/src/components/ui/collapsible-content.tsx +114 -0
- package/apps/web/src/components/ui/contribution-graph.tsx +182 -0
- package/apps/web/src/components/ui/dialog-box.tsx +203 -0
- package/apps/web/src/components/ui/icon-button.tsx +32 -0
- package/apps/web/src/components/ui/icons.tsx +187 -0
- package/apps/web/src/components/ui/index.tsx +15 -0
- package/apps/web/src/components/ui/input.tsx +43 -0
- package/apps/web/src/components/ui/markdown-stream.tsx +21 -0
- package/apps/web/src/components/ui/mobile-drawer.tsx +124 -0
- package/apps/web/src/components/ui/section-header.tsx +13 -0
- package/apps/web/src/components/ui/select.tsx +217 -0
- package/apps/web/src/components/ui/shimmer.tsx +138 -0
- package/apps/web/src/components/ui/status-dot.tsx +24 -0
- package/apps/web/src/config.ts +5 -0
- package/apps/web/src/hooks/index.ts +3 -0
- package/apps/web/src/hooks/use-analytics.ts +122 -0
- package/apps/web/src/hooks/use-hub-connection.ts +587 -0
- package/apps/web/src/hooks/use-mobile.ts +76 -0
- package/apps/web/src/hooks/use-thread-history.ts +210 -0
- package/apps/web/src/index.css +269 -0
- package/apps/web/src/main.tsx +10 -0
- package/apps/web/src/services/hub-client.ts +358 -0
- package/apps/web/src/store/index.ts +528 -0
- package/apps/web/src/types/index.ts +119 -0
- package/apps/web/src/utils/account-refresh.ts +168 -0
- package/apps/web/src/utils/approval-policy.ts +53 -0
- package/apps/web/src/utils/init-prompt.ts +41 -0
- package/apps/web/src/utils/item-format.ts +170 -0
- package/apps/web/src/utils/prompt-expander.ts +62 -0
- package/apps/web/src/utils/reasoning-summary.ts +48 -0
- package/apps/web/src/utils/slash-commands.ts +98 -0
- package/apps/web/tsconfig.app.json +28 -0
- package/apps/web/tsconfig.json +7 -0
- package/apps/web/tsconfig.node.json +26 -0
- package/apps/web/vite.config.ts +8 -0
- package/bin/better-codex.cjs +199 -0
- package/package.json +20 -0
package/README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# better-codex
|
|
2
|
+
|
|
3
|
+
Consumer-friendly launcher for the Codex Hub web UI.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm i -g better-codex
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires Bun: https://bun.sh
|
|
12
|
+
|
|
13
|
+
## Run
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
better-codex
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`better-codex web` works too (same command).
|
|
20
|
+
|
|
21
|
+
Options:
|
|
22
|
+
- `--root PATH` set the repo root (defaults to current dir or parent lookup).
|
|
23
|
+
- `--host 127.0.0.1` host for backend + UI.
|
|
24
|
+
- `--backend-port 7711` backend port.
|
|
25
|
+
- `--web-port 5173` UI port.
|
|
26
|
+
- `--open` open the UI in your browser after startup.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# codex-backend
|
|
2
|
+
|
|
3
|
+
Local backend that supervises multiple `codex app-server` processes and exposes a
|
|
4
|
+
WebSocket bridge for the web UI.
|
|
5
|
+
|
|
6
|
+
## Setup
|
|
7
|
+
|
|
8
|
+
1. Install dependencies
|
|
9
|
+
```bash
|
|
10
|
+
bun install
|
|
11
|
+
```
|
|
12
|
+
2. Run the server
|
|
13
|
+
```bash
|
|
14
|
+
bun run dev
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Protocol types
|
|
18
|
+
|
|
19
|
+
Generate TypeScript bindings from the installed Codex CLI:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
bun run generate:protocol
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Environment variables
|
|
26
|
+
|
|
27
|
+
- `CODEX_HUB_HOST` (default: `127.0.0.1`)
|
|
28
|
+
- `CODEX_HUB_PORT` (default: `7711`)
|
|
29
|
+
- `CODEX_HUB_TOKEN` (default: auto-generated at boot)
|
|
30
|
+
- `CODEX_HUB_DATA_DIR` (default: `~/.codex-hub`)
|
|
31
|
+
- `CODEX_HUB_PROFILES_DIR` (default: `~/.codex/profiles`)
|
|
32
|
+
- `CODEX_HUB_DEFAULT_CODEX_HOME` (default: `~/.codex`)
|
|
33
|
+
- `CODEX_BIN` (default: `codex`)
|
|
34
|
+
- `CODEX_FLAGS` (space-delimited)
|
|
35
|
+
- `CODEX_FLAGS_JSON` (JSON array, preferred)
|
|
36
|
+
- `CODEX_APP_SERVER_FLAGS`
|
|
37
|
+
- `CODEX_APP_SERVER_FLAGS_JSON`
|
|
38
|
+
|
|
39
|
+
## Endpoints
|
|
40
|
+
|
|
41
|
+
- `GET /health`
|
|
42
|
+
- `GET /profiles`
|
|
43
|
+
- `POST /profiles`
|
|
44
|
+
- `POST /profiles/:profileId/start`
|
|
45
|
+
- `POST /profiles/:profileId/stop`
|
|
46
|
+
- `WS /ws?token=...`
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lockfileVersion": 1,
|
|
3
|
+
"configVersion": 1,
|
|
4
|
+
"workspaces": {
|
|
5
|
+
"": {
|
|
6
|
+
"name": "codex-backend",
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"@elysiajs/cors": "^1.4.1",
|
|
9
|
+
"elysia": "1",
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"@types/bun": "1",
|
|
13
|
+
"typescript": "5",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
"packages": {
|
|
18
|
+
"@borewit/text-codec": ["@borewit/text-codec@0.2.1", "", {}, "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw=="],
|
|
19
|
+
|
|
20
|
+
"@elysiajs/cors": ["@elysiajs/cors@1.4.1", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-lQfad+F3r4mNwsxRKbXyJB8Jg43oAOXjRwn7sKUL6bcOW3KjUqUimTS+woNpO97efpzjtDE0tEjGk9DTw8lqTQ=="],
|
|
21
|
+
|
|
22
|
+
"@sinclair/typebox": ["@sinclair/typebox@0.34.46", "", {}, "sha512-kiW7CtS/NkdvTUjkjUJo7d5JsFfbJ14YjdhDk9KoEgK6nFjKNXZPrX0jfLA8ZlET4cFLHxOZ/0vFKOP+bOxIOQ=="],
|
|
23
|
+
|
|
24
|
+
"@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="],
|
|
25
|
+
|
|
26
|
+
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
|
|
27
|
+
|
|
28
|
+
"@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="],
|
|
29
|
+
|
|
30
|
+
"@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
|
|
31
|
+
|
|
32
|
+
"bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="],
|
|
33
|
+
|
|
34
|
+
"cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
|
|
35
|
+
|
|
36
|
+
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
|
37
|
+
|
|
38
|
+
"elysia": ["elysia@1.4.19", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "0.2.5", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-DZb9y8FnWyX5IuqY44SvqAV0DjJ15NeCWHrLdgXrKgTPDPsl3VNwWHqrEr9bmnOCpg1vh6QUvAX/tcxNj88jLA=="],
|
|
39
|
+
|
|
40
|
+
"exact-mirror": ["exact-mirror@0.2.5", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-u8Wu2lO8nio5lKSJubOydsdNtQmH8ENba5m0nbQYmTvsjksXKYIS1nSShdDlO8Uem+kbo+N6eD5I03cpZ+QsRQ=="],
|
|
41
|
+
|
|
42
|
+
"fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="],
|
|
43
|
+
|
|
44
|
+
"file-type": ["file-type@21.2.0", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.4", "token-types": "^6.1.1", "uint8array-extras": "^1.4.0" } }, "sha512-vCYBgFOrJQLoTzDyAXAL/RFfKnXXpUYt4+tipVy26nJJhT7ftgGETf2tAQF59EEL61i3MrorV/PG6tf7LJK7eg=="],
|
|
45
|
+
|
|
46
|
+
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
|
47
|
+
|
|
48
|
+
"memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="],
|
|
49
|
+
|
|
50
|
+
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
|
51
|
+
|
|
52
|
+
"openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
|
|
53
|
+
|
|
54
|
+
"strtok3": ["strtok3@10.3.4", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg=="],
|
|
55
|
+
|
|
56
|
+
"token-types": ["token-types@6.1.2", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="],
|
|
57
|
+
|
|
58
|
+
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
|
59
|
+
|
|
60
|
+
"uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="],
|
|
61
|
+
|
|
62
|
+
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "codex-backend",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"scripts": {
|
|
5
|
+
"dev": "bun --watch src/server.ts",
|
|
6
|
+
"generate:protocol": "bun scripts/generate-protocol.ts",
|
|
7
|
+
"start": "bun src/server.ts",
|
|
8
|
+
"typecheck": "tsc"
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"@types/bun": "1",
|
|
12
|
+
"typescript": "5"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@elysiajs/cors": "^1.4.1",
|
|
16
|
+
"elysia": "1"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
import { mkdir } from 'node:fs/promises'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { loadConfig } from '../src/config'
|
|
5
|
+
|
|
6
|
+
const config = loadConfig()
|
|
7
|
+
const outDir = join(process.cwd(), 'src', 'protocol')
|
|
8
|
+
|
|
9
|
+
await mkdir(outDir, { recursive: true })
|
|
10
|
+
|
|
11
|
+
const args = [
|
|
12
|
+
...config.codexArgs,
|
|
13
|
+
'app-server',
|
|
14
|
+
'generate-ts',
|
|
15
|
+
'--out',
|
|
16
|
+
outDir,
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
const child = spawn(config.codexBin, args, {
|
|
20
|
+
stdio: 'inherit',
|
|
21
|
+
env: {
|
|
22
|
+
...process.env,
|
|
23
|
+
},
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const exitCode = await new Promise<number>((resolve) => {
|
|
27
|
+
child.on('exit', (code) => resolve(code ?? 1))
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
if (exitCode !== 0) {
|
|
31
|
+
process.exit(exitCode)
|
|
32
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { AnalyticsStore, type AnalyticsEvent, deriveDateKey } from './store'
|
|
2
|
+
|
|
3
|
+
type RpcEvent = {
|
|
4
|
+
profileId: string
|
|
5
|
+
method: string
|
|
6
|
+
params?: unknown
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
type RpcRequest = {
|
|
10
|
+
profileId: string
|
|
11
|
+
method: string
|
|
12
|
+
params?: unknown
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type RpcResponse = {
|
|
16
|
+
profileId: string
|
|
17
|
+
method?: string
|
|
18
|
+
result?: unknown
|
|
19
|
+
error?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type ServerRequest = {
|
|
23
|
+
profileId: string
|
|
24
|
+
id: number
|
|
25
|
+
method: string
|
|
26
|
+
params?: unknown
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type ApprovalDecision = {
|
|
30
|
+
decision?: string
|
|
31
|
+
acceptSettings?: { forSession?: boolean }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const now = () => Date.now()
|
|
35
|
+
|
|
36
|
+
export class AnalyticsService {
|
|
37
|
+
private readonly store: AnalyticsStore
|
|
38
|
+
private readonly pendingApprovals = new Map<number, { type: string }>()
|
|
39
|
+
|
|
40
|
+
constructor(store: AnalyticsStore) {
|
|
41
|
+
this.store = store
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
init(): void {
|
|
45
|
+
this.store.init()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
getDailySeries(metric: string, profileId?: string, model?: string, days = 365) {
|
|
49
|
+
return this.store.getDailySeries(metric, profileId, model, days)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
trackRpcEvent(event: RpcEvent): void {
|
|
53
|
+
const timestamp = now()
|
|
54
|
+
const base: AnalyticsEvent = {
|
|
55
|
+
occurredAt: timestamp,
|
|
56
|
+
dateKey: deriveDateKey(timestamp),
|
|
57
|
+
profileId: event.profileId,
|
|
58
|
+
eventType: `rpc.event:${event.method}`,
|
|
59
|
+
payload: event.params,
|
|
60
|
+
}
|
|
61
|
+
this.store.recordEvent(base)
|
|
62
|
+
|
|
63
|
+
if (event.method === 'thread/started' && event.params && typeof event.params === 'object') {
|
|
64
|
+
const { thread } = event.params as { thread?: { id?: string; modelProvider?: string; cwd?: string; createdAt?: number } }
|
|
65
|
+
if (thread?.id) {
|
|
66
|
+
this.store.upsertThreadMeta(thread.id, event.profileId, thread.modelProvider, thread.cwd, thread.createdAt ? thread.createdAt * 1000 : undefined)
|
|
67
|
+
this.store.incrementDaily('threads_started', event.profileId, thread.modelProvider, timestamp)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (event.method === 'turn/started' && event.params && typeof event.params === 'object') {
|
|
72
|
+
const { threadId, turn } = event.params as { threadId?: string; turn?: { id?: string } }
|
|
73
|
+
if (threadId && turn?.id) {
|
|
74
|
+
this.store.upsertTurnMeta(turn.id, threadId, event.profileId, undefined, timestamp)
|
|
75
|
+
this.store.incrementDaily('turns_started', event.profileId, undefined, timestamp)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (event.method === 'turn/completed' && event.params && typeof event.params === 'object') {
|
|
80
|
+
const { threadId, turn } = event.params as { threadId?: string; turn?: { id?: string; status?: string } }
|
|
81
|
+
if (threadId && turn?.id) {
|
|
82
|
+
this.store.upsertTurnMeta(turn.id, threadId, event.profileId, undefined, undefined, timestamp, turn.status)
|
|
83
|
+
this.store.incrementDaily('turns_completed', event.profileId, undefined, timestamp)
|
|
84
|
+
if (turn.status) {
|
|
85
|
+
this.store.incrementDaily(`turns_${turn.status}`, event.profileId, undefined, timestamp)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (event.method === 'item/started' && event.params && typeof event.params === 'object') {
|
|
91
|
+
const { item } = event.params as { item?: { type?: string; id?: string } }
|
|
92
|
+
const itemType = item?.type
|
|
93
|
+
if (itemType) {
|
|
94
|
+
this.store.incrementDaily(`items_${itemType}`, event.profileId, undefined, timestamp)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (event.method === 'item/completed' && event.params && typeof event.params === 'object') {
|
|
99
|
+
const { item } = event.params as { item?: { type?: string; id?: string } }
|
|
100
|
+
const itemType = item?.type
|
|
101
|
+
if (itemType) {
|
|
102
|
+
this.store.incrementDaily(`items_completed_${itemType}`, event.profileId, undefined, timestamp)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (event.method === 'thread/tokenUsage/updated' && event.params && typeof event.params === 'object') {
|
|
107
|
+
const { threadId } = event.params as { threadId?: string }
|
|
108
|
+
if (threadId) {
|
|
109
|
+
this.store.recordTokenUsage(event.profileId, threadId, event.params, timestamp)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
trackRpcRequest(request: RpcRequest): void {
|
|
115
|
+
const timestamp = now()
|
|
116
|
+
const base: AnalyticsEvent = {
|
|
117
|
+
occurredAt: timestamp,
|
|
118
|
+
dateKey: deriveDateKey(timestamp),
|
|
119
|
+
profileId: request.profileId,
|
|
120
|
+
eventType: `rpc.request:${request.method}`,
|
|
121
|
+
payload: request.params,
|
|
122
|
+
}
|
|
123
|
+
this.store.recordEvent(base)
|
|
124
|
+
|
|
125
|
+
if (request.method === 'turn/start') {
|
|
126
|
+
const params = (request.params ?? {}) as { model?: string; threadId?: string }
|
|
127
|
+
if (params.threadId && params.model) {
|
|
128
|
+
this.store.upsertThreadMeta(params.threadId, request.profileId, params.model)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (request.method === 'command/exec') {
|
|
133
|
+
this.store.incrementDaily('command_exec', request.profileId, undefined, timestamp)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (request.method === 'review/start') {
|
|
137
|
+
this.store.incrementDaily('reviews_started', request.profileId, undefined, timestamp)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (request.method === 'account/login/start') {
|
|
141
|
+
const params = (request.params ?? {}) as { type?: string }
|
|
142
|
+
if (params.type) {
|
|
143
|
+
this.store.incrementDaily(`login_started_${params.type}`, request.profileId, undefined, timestamp)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
trackRpcResponse(response: RpcResponse): void {
|
|
149
|
+
const timestamp = now()
|
|
150
|
+
const base: AnalyticsEvent = {
|
|
151
|
+
occurredAt: timestamp,
|
|
152
|
+
dateKey: deriveDateKey(timestamp),
|
|
153
|
+
profileId: response.profileId,
|
|
154
|
+
eventType: `rpc.response:${response.method ?? 'unknown'}`,
|
|
155
|
+
status: response.error ? 'error' : 'ok',
|
|
156
|
+
payload: response.error ?? response.result,
|
|
157
|
+
}
|
|
158
|
+
this.store.recordEvent(base)
|
|
159
|
+
|
|
160
|
+
if (response.method === 'thread/start' && response.result && typeof response.result === 'object') {
|
|
161
|
+
const { thread } = response.result as { thread?: { id?: string; modelProvider?: string; cwd?: string; createdAt?: number } }
|
|
162
|
+
if (thread?.id) {
|
|
163
|
+
this.store.upsertThreadMeta(thread.id, response.profileId, thread.modelProvider, thread.cwd, thread.createdAt ? thread.createdAt * 1000 : undefined)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (response.method === 'thread/resume' && response.result && typeof response.result === 'object') {
|
|
168
|
+
const result = response.result as { thread?: { id?: string; modelProvider?: string; cwd?: string } }
|
|
169
|
+
if (result.thread?.id) {
|
|
170
|
+
this.store.upsertThreadMeta(result.thread.id, response.profileId, result.thread.modelProvider, result.thread.cwd)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
trackServerRequest(request: ServerRequest): void {
|
|
176
|
+
const timestamp = now()
|
|
177
|
+
const base: AnalyticsEvent = {
|
|
178
|
+
occurredAt: timestamp,
|
|
179
|
+
dateKey: deriveDateKey(timestamp),
|
|
180
|
+
profileId: request.profileId,
|
|
181
|
+
eventType: `rpc.serverRequest:${request.method}`,
|
|
182
|
+
payload: request.params,
|
|
183
|
+
}
|
|
184
|
+
this.store.recordEvent(base)
|
|
185
|
+
|
|
186
|
+
if (request.method === 'item/commandExecution/requestApproval') {
|
|
187
|
+
this.pendingApprovals.set(request.id, { type: 'command' })
|
|
188
|
+
this.store.recordApprovalRequest(request.id, request.profileId, 'command', (request.params as { threadId?: string; itemId?: string })?.threadId, (request.params as { itemId?: string })?.itemId)
|
|
189
|
+
this.store.incrementDaily('approvals_requested_command', request.profileId, undefined, timestamp)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (request.method === 'item/fileChange/requestApproval') {
|
|
193
|
+
this.pendingApprovals.set(request.id, { type: 'file' })
|
|
194
|
+
this.store.recordApprovalRequest(request.id, request.profileId, 'file', (request.params as { threadId?: string; itemId?: string })?.threadId, (request.params as { itemId?: string })?.itemId)
|
|
195
|
+
this.store.incrementDaily('approvals_requested_file', request.profileId, undefined, timestamp)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
trackApprovalDecision(profileId: string, requestId: number, result?: unknown, error?: { message: string } | null): void {
|
|
200
|
+
const timestamp = now()
|
|
201
|
+
const decisionPayload = result as ApprovalDecision | undefined
|
|
202
|
+
const decision = error ? 'error' : decisionPayload?.decision ?? 'unknown'
|
|
203
|
+
|
|
204
|
+
this.store.recordEvent({
|
|
205
|
+
occurredAt: timestamp,
|
|
206
|
+
dateKey: deriveDateKey(timestamp),
|
|
207
|
+
profileId,
|
|
208
|
+
eventType: 'approval.decision',
|
|
209
|
+
status: decision,
|
|
210
|
+
payload: result ?? error,
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
if (this.pendingApprovals.has(requestId)) {
|
|
214
|
+
this.pendingApprovals.delete(requestId)
|
|
215
|
+
this.store.recordApprovalDecision(requestId, decision)
|
|
216
|
+
this.store.incrementDaily(`approvals_${decision}`, profileId, undefined, timestamp)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { Database } from 'bun:sqlite'
|
|
2
|
+
|
|
3
|
+
export type AnalyticsEvent = {
|
|
4
|
+
occurredAt: number
|
|
5
|
+
dateKey: string
|
|
6
|
+
profileId: string
|
|
7
|
+
eventType: string
|
|
8
|
+
threadId?: string
|
|
9
|
+
turnId?: string
|
|
10
|
+
itemId?: string
|
|
11
|
+
model?: string
|
|
12
|
+
status?: string
|
|
13
|
+
payload?: unknown
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const toDateKey = (timestampMs: number): string => {
|
|
17
|
+
const date = new Date(timestampMs)
|
|
18
|
+
const year = date.getUTCFullYear()
|
|
19
|
+
const month = String(date.getUTCMonth() + 1).padStart(2, '0')
|
|
20
|
+
const day = String(date.getUTCDate()).padStart(2, '0')
|
|
21
|
+
return `${year}-${month}-${day}`
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class AnalyticsStore {
|
|
25
|
+
private readonly db: Database
|
|
26
|
+
|
|
27
|
+
constructor(private readonly dbPath: string) {
|
|
28
|
+
this.db = new Database(dbPath)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
init(): void {
|
|
32
|
+
this.db.exec(`
|
|
33
|
+
pragma journal_mode = wal;
|
|
34
|
+
create table if not exists analytics_events (
|
|
35
|
+
id integer primary key autoincrement,
|
|
36
|
+
occurred_at integer not null,
|
|
37
|
+
date_key text not null,
|
|
38
|
+
profile_id text not null,
|
|
39
|
+
thread_id text,
|
|
40
|
+
turn_id text,
|
|
41
|
+
item_id text,
|
|
42
|
+
model text,
|
|
43
|
+
event_type text not null,
|
|
44
|
+
status text,
|
|
45
|
+
payload text
|
|
46
|
+
);
|
|
47
|
+
create index if not exists analytics_events_date on analytics_events(date_key);
|
|
48
|
+
create index if not exists analytics_events_profile on analytics_events(profile_id);
|
|
49
|
+
create index if not exists analytics_events_thread on analytics_events(thread_id);
|
|
50
|
+
create index if not exists analytics_events_type on analytics_events(event_type);
|
|
51
|
+
|
|
52
|
+
create table if not exists analytics_daily (
|
|
53
|
+
date_key text not null,
|
|
54
|
+
metric text not null,
|
|
55
|
+
profile_id text not null,
|
|
56
|
+
model text,
|
|
57
|
+
count integer not null default 0,
|
|
58
|
+
primary key (date_key, metric, profile_id, model)
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
create table if not exists analytics_thread_meta (
|
|
62
|
+
thread_id text primary key,
|
|
63
|
+
profile_id text not null,
|
|
64
|
+
model text,
|
|
65
|
+
cwd text,
|
|
66
|
+
created_at integer
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
create table if not exists analytics_turn_meta (
|
|
70
|
+
turn_id text primary key,
|
|
71
|
+
thread_id text not null,
|
|
72
|
+
profile_id text not null,
|
|
73
|
+
model text,
|
|
74
|
+
started_at integer,
|
|
75
|
+
completed_at integer,
|
|
76
|
+
status text
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
create table if not exists analytics_token_usage (
|
|
80
|
+
id integer primary key autoincrement,
|
|
81
|
+
occurred_at integer not null,
|
|
82
|
+
date_key text not null,
|
|
83
|
+
profile_id text not null,
|
|
84
|
+
thread_id text not null,
|
|
85
|
+
payload text
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
create table if not exists analytics_approvals (
|
|
89
|
+
request_id integer primary key,
|
|
90
|
+
profile_id text not null,
|
|
91
|
+
thread_id text,
|
|
92
|
+
item_id text,
|
|
93
|
+
approval_type text not null,
|
|
94
|
+
requested_at integer not null,
|
|
95
|
+
decision text,
|
|
96
|
+
decided_at integer
|
|
97
|
+
);
|
|
98
|
+
`)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
recordEvent(event: AnalyticsEvent): void {
|
|
102
|
+
const payload = event.payload ? safeStringify(event.payload) : null
|
|
103
|
+
this.db
|
|
104
|
+
.prepare(
|
|
105
|
+
`insert into analytics_events
|
|
106
|
+
(occurred_at, date_key, profile_id, thread_id, turn_id, item_id, model, event_type, status, payload)
|
|
107
|
+
values
|
|
108
|
+
($occurred_at, $date_key, $profile_id, $thread_id, $turn_id, $item_id, $model, $event_type, $status, $payload)
|
|
109
|
+
`
|
|
110
|
+
)
|
|
111
|
+
.run({
|
|
112
|
+
$occurred_at: event.occurredAt,
|
|
113
|
+
$date_key: event.dateKey,
|
|
114
|
+
$profile_id: event.profileId,
|
|
115
|
+
$thread_id: event.threadId ?? null,
|
|
116
|
+
$turn_id: event.turnId ?? null,
|
|
117
|
+
$item_id: event.itemId ?? null,
|
|
118
|
+
$model: event.model ?? null,
|
|
119
|
+
$event_type: event.eventType,
|
|
120
|
+
$status: event.status ?? null,
|
|
121
|
+
$payload: payload,
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
incrementDaily(metric: string, profileId: string, model?: string, occurredAt = Date.now()): void {
|
|
126
|
+
const dateKey = toDateKey(occurredAt)
|
|
127
|
+
this.db
|
|
128
|
+
.prepare(
|
|
129
|
+
`insert into analytics_daily (date_key, metric, profile_id, model, count)
|
|
130
|
+
values ($date_key, $metric, $profile_id, $model, 0)
|
|
131
|
+
on conflict(date_key, metric, profile_id, model) do nothing
|
|
132
|
+
`
|
|
133
|
+
)
|
|
134
|
+
.run({
|
|
135
|
+
$date_key: dateKey,
|
|
136
|
+
$metric: metric,
|
|
137
|
+
$profile_id: profileId,
|
|
138
|
+
$model: model ?? null,
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
this.db
|
|
142
|
+
.prepare(
|
|
143
|
+
`update analytics_daily
|
|
144
|
+
set count = count + 1
|
|
145
|
+
where date_key = $date_key and metric = $metric and profile_id = $profile_id and model is $model
|
|
146
|
+
`
|
|
147
|
+
)
|
|
148
|
+
.run({
|
|
149
|
+
$date_key: dateKey,
|
|
150
|
+
$metric: metric,
|
|
151
|
+
$profile_id: profileId,
|
|
152
|
+
$model: model ?? null,
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
upsertThreadMeta(threadId: string, profileId: string, model?: string, cwd?: string, createdAt?: number): void {
|
|
157
|
+
this.db
|
|
158
|
+
.prepare(
|
|
159
|
+
`insert into analytics_thread_meta (thread_id, profile_id, model, cwd, created_at)
|
|
160
|
+
values ($thread_id, $profile_id, $model, $cwd, $created_at)
|
|
161
|
+
on conflict(thread_id) do update set
|
|
162
|
+
profile_id = excluded.profile_id,
|
|
163
|
+
model = coalesce(excluded.model, analytics_thread_meta.model),
|
|
164
|
+
cwd = coalesce(excluded.cwd, analytics_thread_meta.cwd),
|
|
165
|
+
created_at = coalesce(excluded.created_at, analytics_thread_meta.created_at)
|
|
166
|
+
`
|
|
167
|
+
)
|
|
168
|
+
.run({
|
|
169
|
+
$thread_id: threadId,
|
|
170
|
+
$profile_id: profileId,
|
|
171
|
+
$model: model ?? null,
|
|
172
|
+
$cwd: cwd ?? null,
|
|
173
|
+
$created_at: createdAt ?? null,
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
upsertTurnMeta(turnId: string, threadId: string, profileId: string, model?: string, startedAt?: number, completedAt?: number, status?: string): void {
|
|
178
|
+
this.db
|
|
179
|
+
.prepare(
|
|
180
|
+
`insert into analytics_turn_meta (turn_id, thread_id, profile_id, model, started_at, completed_at, status)
|
|
181
|
+
values ($turn_id, $thread_id, $profile_id, $model, $started_at, $completed_at, $status)
|
|
182
|
+
on conflict(turn_id) do update set
|
|
183
|
+
model = coalesce(excluded.model, analytics_turn_meta.model),
|
|
184
|
+
started_at = coalesce(excluded.started_at, analytics_turn_meta.started_at),
|
|
185
|
+
completed_at = coalesce(excluded.completed_at, analytics_turn_meta.completed_at),
|
|
186
|
+
status = coalesce(excluded.status, analytics_turn_meta.status)
|
|
187
|
+
`
|
|
188
|
+
)
|
|
189
|
+
.run({
|
|
190
|
+
$turn_id: turnId,
|
|
191
|
+
$thread_id: threadId,
|
|
192
|
+
$profile_id: profileId,
|
|
193
|
+
$model: model ?? null,
|
|
194
|
+
$started_at: startedAt ?? null,
|
|
195
|
+
$completed_at: completedAt ?? null,
|
|
196
|
+
$status: status ?? null,
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
recordTokenUsage(profileId: string, threadId: string, payload: unknown, occurredAt = Date.now()): void {
|
|
201
|
+
this.db
|
|
202
|
+
.prepare(
|
|
203
|
+
`insert into analytics_token_usage (occurred_at, date_key, profile_id, thread_id, payload)
|
|
204
|
+
values ($occurred_at, $date_key, $profile_id, $thread_id, $payload)
|
|
205
|
+
`
|
|
206
|
+
)
|
|
207
|
+
.run({
|
|
208
|
+
$occurred_at: occurredAt,
|
|
209
|
+
$date_key: toDateKey(occurredAt),
|
|
210
|
+
$profile_id: profileId,
|
|
211
|
+
$thread_id: threadId,
|
|
212
|
+
$payload: safeStringify(payload),
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
recordApprovalRequest(requestId: number, profileId: string, approvalType: string, threadId?: string, itemId?: string): void {
|
|
217
|
+
this.db
|
|
218
|
+
.prepare(
|
|
219
|
+
`insert into analytics_approvals (request_id, profile_id, thread_id, item_id, approval_type, requested_at)
|
|
220
|
+
values ($request_id, $profile_id, $thread_id, $item_id, $approval_type, $requested_at)
|
|
221
|
+
on conflict(request_id) do nothing
|
|
222
|
+
`
|
|
223
|
+
)
|
|
224
|
+
.run({
|
|
225
|
+
$request_id: requestId,
|
|
226
|
+
$profile_id: profileId,
|
|
227
|
+
$thread_id: threadId ?? null,
|
|
228
|
+
$item_id: itemId ?? null,
|
|
229
|
+
$approval_type: approvalType,
|
|
230
|
+
$requested_at: Date.now(),
|
|
231
|
+
})
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
recordApprovalDecision(requestId: number, decision: string): void {
|
|
235
|
+
this.db
|
|
236
|
+
.prepare(
|
|
237
|
+
`update analytics_approvals
|
|
238
|
+
set decision = $decision, decided_at = $decided_at
|
|
239
|
+
where request_id = $request_id
|
|
240
|
+
`
|
|
241
|
+
)
|
|
242
|
+
.run({
|
|
243
|
+
$decision: decision,
|
|
244
|
+
$decided_at: Date.now(),
|
|
245
|
+
$request_id: requestId,
|
|
246
|
+
})
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
getDailySeries(metric: string, profileId?: string, model?: string, days = 365): Array<{ date: string; count: number }> {
|
|
250
|
+
const cutoff = new Date()
|
|
251
|
+
cutoff.setUTCDate(cutoff.getUTCDate() - days + 1)
|
|
252
|
+
const cutoffKey = toDateKey(cutoff.getTime())
|
|
253
|
+
|
|
254
|
+
const rows = this.db
|
|
255
|
+
.prepare(
|
|
256
|
+
`select date_key as date, count
|
|
257
|
+
from analytics_daily
|
|
258
|
+
where metric = $metric
|
|
259
|
+
and date_key >= $cutoff
|
|
260
|
+
and ($profile_id is null or profile_id = $profile_id)
|
|
261
|
+
and ($model is null or model = $model)
|
|
262
|
+
order by date_key asc
|
|
263
|
+
`
|
|
264
|
+
)
|
|
265
|
+
.all({
|
|
266
|
+
$metric: metric,
|
|
267
|
+
$cutoff: cutoffKey,
|
|
268
|
+
$profile_id: profileId ?? null,
|
|
269
|
+
$model: model ?? null,
|
|
270
|
+
}) as Array<{ date: string; count: number }>
|
|
271
|
+
|
|
272
|
+
return rows
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const safeStringify = (value: unknown): string => {
|
|
277
|
+
try {
|
|
278
|
+
return JSON.stringify(value)
|
|
279
|
+
} catch {
|
|
280
|
+
return String(value)
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export const deriveDateKey = toDateKey
|