@crievo/omni 0.0.0-stage → 0.1.1
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/.env.example +41 -0
- package/README.md +2 -2
- package/bin/omni.mjs +18 -0
- package/docs/onboarding/clone-voice.png +0 -0
- package/docs/onboarding/getting-started.png +0 -0
- package/docs/onboarding/new-video.png +0 -0
- package/docs/onboarding/providers.png +0 -0
- package/docs/onboarding/publish.png +0 -0
- package/docs/onboarding/references.png +0 -0
- package/docs/onboarding.md +212 -0
- package/omnivideo.config.example.json +437 -0
- package/package.json +40 -4
- package/reference/captions.md +156 -0
- package/reference/carousel-contract.md +91 -0
- package/reference/cloud.md +96 -0
- package/reference/learnings.md +792 -0
- package/reference/omnichat.md +305 -0
- package/reference/premiere-mcp.md +91 -0
- package/reference/publish.md +305 -0
- package/reference/scenes-contract.md +109 -0
- package/reference/studio.md +666 -0
- package/reference/voicestudio.md +81 -0
- package/reference/writing-for-voice.md +67 -0
- package/requirements.txt +3 -0
- package/studio/api/app.ts +87 -0
- package/studio/api/lib/errors.ts +33 -0
- package/studio/api/lib/files.ts +41 -0
- package/studio/api/lib/hono.ts +9 -0
- package/studio/api/lib/result.ts +14 -0
- package/studio/api/lib/validate.ts +31 -0
- package/studio/api/lib/video.ts +10 -0
- package/studio/api/middleware/json-charset.ts +11 -0
- package/studio/api/middleware/local-only.ts +32 -0
- package/studio/api/middleware/no-timeout.ts +10 -0
- package/studio/api/routes/admin.ts +18 -0
- package/studio/api/routes/agent-cascade.ts +100 -0
- package/studio/api/routes/agent-history.ts +23 -0
- package/studio/api/routes/agent.ts +162 -0
- package/studio/api/routes/analytics.ts +31 -0
- package/studio/api/routes/approval.ts +60 -0
- package/studio/api/routes/archive.ts +12 -0
- package/studio/api/routes/captions.ts +28 -0
- package/studio/api/routes/carousel.ts +38 -0
- package/studio/api/routes/clone.ts +27 -0
- package/studio/api/routes/create.ts +19 -0
- package/studio/api/routes/events.ts +11 -0
- package/studio/api/routes/export.ts +14 -0
- package/studio/api/routes/media.ts +30 -0
- package/studio/api/routes/omnichat.ts +113 -0
- package/studio/api/routes/onboarding.ts +14 -0
- package/studio/api/routes/publish.ts +40 -0
- package/studio/api/routes/queue.ts +14 -0
- package/studio/api/routes/references.ts +40 -0
- package/studio/api/routes/runner.ts +17 -0
- package/studio/api/routes/schedule.ts +47 -0
- package/studio/api/routes/services.ts +11 -0
- package/studio/api/routes/sessions.ts +27 -0
- package/studio/api/routes/settings.ts +102 -0
- package/studio/api/routes/sound.ts +45 -0
- package/studio/api/routes/thumbnail.ts +18 -0
- package/studio/api/routes/usage.ts +13 -0
- package/studio/api/routes/videos.ts +41 -0
- package/studio/api/routes/voices.ts +71 -0
- package/studio/api/services/admin.ts +113 -0
- package/studio/api/services/agent-cascade.ts +975 -0
- package/studio/api/services/agent-claude.ts +47 -0
- package/studio/api/services/agent-history.ts +103 -0
- package/studio/api/services/agent-persona.ts +77 -0
- package/studio/api/services/agent-tools.ts +2237 -0
- package/studio/api/services/agent-ui-tools.ts +259 -0
- package/studio/api/services/agent-wait.ts +32 -0
- package/studio/api/services/agent.ts +553 -0
- package/studio/api/services/analytics.ts +92 -0
- package/studio/api/services/approval.ts +134 -0
- package/studio/api/services/archive.ts +38 -0
- package/studio/api/services/captions.ts +268 -0
- package/studio/api/services/carousel.ts +206 -0
- package/studio/api/services/classify.ts +196 -0
- package/studio/api/services/clone-phrases.ts +75 -0
- package/studio/api/services/clone.ts +722 -0
- package/studio/api/services/create.ts +421 -0
- package/studio/api/services/events.ts +215 -0
- package/studio/api/services/media.ts +32 -0
- package/studio/api/services/omnichat.ts +624 -0
- package/studio/api/services/onboarding.ts +77 -0
- package/studio/api/services/publish.ts +287 -0
- package/studio/api/services/queue.ts +150 -0
- package/studio/api/services/references.ts +269 -0
- package/studio/api/services/root.ts +60 -0
- package/studio/api/services/services.ts +114 -0
- package/studio/api/services/sessions.ts +1007 -0
- package/studio/api/services/settings.ts +983 -0
- package/studio/api/services/sound.ts +311 -0
- package/studio/api/services/thumbnail.ts +133 -0
- package/studio/api/services/usage.ts +27 -0
- package/studio/api/services/videos.ts +418 -0
- package/studio/api/services/voice-pause.ts +98 -0
- package/studio/api/services/voices.ts +324 -0
- package/studio/runner/runner.ts +244 -0
- package/studio/shared/omni-actions.ts +68 -0
- package/studio/shared/omni-admin.ts +52 -0
- package/studio/shared/omni-farewell.ts +48 -0
- package/studio/shared/omni-hotword.ts +20 -0
- package/studio/shared/omni-screens.ts +228 -0
- package/studio/shared/schemas/admin.ts +15 -0
- package/studio/shared/schemas/agent-cascade.ts +58 -0
- package/studio/shared/schemas/agent-history.ts +73 -0
- package/studio/shared/schemas/agent.ts +50 -0
- package/studio/shared/schemas/analytics.ts +6 -0
- package/studio/shared/schemas/approval.ts +22 -0
- package/studio/shared/schemas/captions.ts +19 -0
- package/studio/shared/schemas/carousel.ts +26 -0
- package/studio/shared/schemas/clone.ts +40 -0
- package/studio/shared/schemas/common.ts +16 -0
- package/studio/shared/schemas/omnichat.ts +54 -0
- package/studio/shared/schemas/onboarding.ts +5 -0
- package/studio/shared/schemas/publish.ts +45 -0
- package/studio/shared/schemas/references.ts +20 -0
- package/studio/shared/schemas/settings.ts +82 -0
- package/studio/shared/schemas/sound.ts +22 -0
- package/studio/shared/schemas/videos.ts +38 -0
- package/studio/shared/schemas/voices.ts +23 -0
- package/studio/shared/types/api.ts +1290 -0
- package/studio/studio.ts +148 -0
- package/studio/tsconfig.json +15 -0
- package/studio/web/assets/logos/cartesia.svg +4 -0
- package/studio/web/assets/logos/elevenlabs.svg +12 -0
- package/studio/web/assets/logos/fish.svg +24 -0
- package/studio/web/assets/logos/omni-black.png +0 -0
- package/studio/web/assets/logos/omni-white.png +0 -0
- package/studio/web/assets/logos/voicestudio.svg +21 -0
- package/studio/web/components/AgentMark.tsx +7 -0
- package/studio/web/components/AgentWalking.tsx +11 -0
- package/studio/web/components/ApiKeyField.tsx +104 -0
- package/studio/web/components/AppShell.tsx +64 -0
- package/studio/web/components/ApprovalNotice.tsx +65 -0
- package/studio/web/components/AvatarGroupIcon.tsx +22 -0
- package/studio/web/components/AvatarNameField.tsx +86 -0
- package/studio/web/components/AvatarPicker.tsx +84 -0
- package/studio/web/components/BrandLogo.tsx +16 -0
- package/studio/web/components/CartesiaPlanSetting.tsx +90 -0
- package/studio/web/components/ClaudeWalking.tsx +32 -0
- package/studio/web/components/CloneVoiceDialog/GuidedScript.tsx +39 -0
- package/studio/web/components/CloneVoiceDialog/LanguageBar.tsx +37 -0
- package/studio/web/components/CloneVoiceDialog/LevelMeter.tsx +31 -0
- package/studio/web/components/CloneVoiceDialog/ProviderChoice.tsx +81 -0
- package/studio/web/components/CloneVoiceDialog/QualityPanel.tsx +62 -0
- package/studio/web/components/CloneVoiceDialog/RecordStep.tsx +50 -0
- package/studio/web/components/CloneVoiceDialog/RecorderBar.tsx +79 -0
- package/studio/web/components/CloneVoiceDialog/RedoSummary.tsx +54 -0
- package/studio/web/components/CloneVoiceDialog/SaveStep.tsx +91 -0
- package/studio/web/components/CloneVoiceDialog/SourceBar.tsx +30 -0
- package/studio/web/components/CloneVoiceDialog/index.tsx +141 -0
- package/studio/web/components/CloneVoiceDialog/types.ts +28 -0
- package/studio/web/components/CloneVoiceDialog/useCloneVoice.ts +528 -0
- package/studio/web/components/ConnectionSignals.tsx +51 -0
- package/studio/web/components/EditVoiceDialog/EditVoiceFooter.tsx +72 -0
- package/studio/web/components/EditVoiceDialog/ProviderCards.tsx +81 -0
- package/studio/web/components/EditVoiceDialog/index.tsx +256 -0
- package/studio/web/components/ErrorBanner.tsx +115 -0
- package/studio/web/components/FinishedTask.tsx +41 -0
- package/studio/web/components/FishModelSetting.tsx +69 -0
- package/studio/web/components/FishPriceSetting.tsx +44 -0
- package/studio/web/components/HumanBanner.tsx +64 -0
- package/studio/web/components/Lightbox.tsx +41 -0
- package/studio/web/components/LogStream.tsx +42 -0
- package/studio/web/components/Markdown.tsx +21 -0
- package/studio/web/components/MissingDmsPanel/BatchSend.tsx +137 -0
- package/studio/web/components/MissingDmsPanel/CommentCard.tsx +168 -0
- package/studio/web/components/MissingDmsPanel/CommentFeed.tsx +45 -0
- package/studio/web/components/MissingDmsPanel/DmBubble.tsx +38 -0
- package/studio/web/components/MissingDmsPanel/ReplyComposer.tsx +63 -0
- package/studio/web/components/MissingDmsPanel/index.tsx +98 -0
- package/studio/web/components/NotFound.tsx +48 -0
- package/studio/web/components/Omni/OmniAvatarPicker.tsx +92 -0
- package/studio/web/components/Omni/OmniCards.tsx +202 -0
- package/studio/web/components/Omni/OmniGlow.tsx +183 -0
- package/studio/web/components/Omni/OmniHistory.tsx +119 -0
- package/studio/web/components/Omni/OmniHotword.tsx +40 -0
- package/studio/web/components/Omni/OmniMicSelect.tsx +92 -0
- package/studio/web/components/Omni/OmniVoiceSample.tsx +81 -0
- package/studio/web/components/Omni/OmniWidget.tsx +743 -0
- package/studio/web/components/Omni/PendingActions.tsx +200 -0
- package/studio/web/components/Omni/Transcript.tsx +127 -0
- package/studio/web/components/OmnichatAccountStatus.tsx +87 -0
- package/studio/web/components/OmnichatRulePanel/RuleConfig.tsx +150 -0
- package/studio/web/components/OmnichatRulePanel/RuleDryRun.tsx +15 -0
- package/studio/web/components/OmnichatRulePanel/RuleEnableConfirm.tsx +42 -0
- package/studio/web/components/OmnichatRulePanel/RuleSkeleton.tsx +18 -0
- package/studio/web/components/OmnichatRulePanel/RuleStatus.tsx +55 -0
- package/studio/web/components/OmnichatRulePanel/index.tsx +115 -0
- package/studio/web/components/OmnichatRulePanel/rule-steps.ts +67 -0
- package/studio/web/components/OmnichatRulePanel/steps/MessageStep.tsx +91 -0
- package/studio/web/components/OmnichatRulePanel/steps/RepliesStep.tsx +201 -0
- package/studio/web/components/OmnichatRulePanel/steps/TriggerStep.tsx +76 -0
- package/studio/web/components/OmnichatRulePanel/steps/types.ts +11 -0
- package/studio/web/components/Palette.tsx +17 -0
- package/studio/web/components/PlatformCard.tsx +109 -0
- package/studio/web/components/PlatformIcon.tsx +19 -0
- package/studio/web/components/ProviderCard.tsx +80 -0
- package/studio/web/components/ProviderDetail.tsx +367 -0
- package/studio/web/components/ProviderLogo.tsx +20 -0
- package/studio/web/components/QueuePanel.tsx +77 -0
- package/studio/web/components/ReferenceActions.tsx +89 -0
- package/studio/web/components/ReferencePicker.tsx +136 -0
- package/studio/web/components/ResultMessage.tsx +7 -0
- package/studio/web/components/RuleForm.tsx +138 -0
- package/studio/web/components/RuleLog.tsx +151 -0
- package/studio/web/components/RunningTask.tsx +23 -0
- package/studio/web/components/SampleList.tsx +55 -0
- package/studio/web/components/SessionPanel.tsx +352 -0
- package/studio/web/components/SettingRow.tsx +14 -0
- package/studio/web/components/SettingsCategorySkeleton.tsx +25 -0
- package/studio/web/components/SidePanel.tsx +77 -0
- package/studio/web/components/Sidebar.tsx +289 -0
- package/studio/web/components/StageStepper.tsx +87 -0
- package/studio/web/components/StudioOffline.tsx +80 -0
- package/studio/web/components/TaskName.tsx +17 -0
- package/studio/web/components/TopBanners.tsx +30 -0
- package/studio/web/components/VideoOmnichatPanel.tsx +287 -0
- package/studio/web/components/VoiceAvatar.tsx +33 -0
- package/studio/web/components/VoiceCarousel.tsx +196 -0
- package/studio/web/components/VoiceCategory.tsx +9 -0
- package/studio/web/components/VoiceModal.tsx +39 -0
- package/studio/web/components/VoiceProfileCard.tsx +75 -0
- package/studio/web/components/VoiceProfilePicker.tsx +107 -0
- package/studio/web/components/VoiceProfilesSettings.tsx +150 -0
- package/studio/web/components/VoiceProviderCard.tsx +118 -0
- package/studio/web/components/VoiceProvidersCard.tsx +26 -0
- package/studio/web/components/WaitList.tsx +21 -0
- package/studio/web/components/icons.tsx +247 -0
- package/studio/web/components/ui/accordion.tsx +47 -0
- package/studio/web/components/ui/badge.tsx +18 -0
- package/studio/web/components/ui/calendar.tsx +43 -0
- package/studio/web/components/ui/chart.tsx +229 -0
- package/studio/web/components/ui/checkbox.tsx +26 -0
- package/studio/web/components/ui/cx.ts +2 -0
- package/studio/web/components/ui/date-time-picker.tsx +84 -0
- package/studio/web/components/ui/panel.tsx +23 -0
- package/studio/web/components/ui/picker.tsx +58 -0
- package/studio/web/components/ui/popover.tsx +24 -0
- package/studio/web/components/ui/select.tsx +89 -0
- package/studio/web/components/ui/skeleton.tsx +10 -0
- package/studio/web/components/ui/slider.tsx +68 -0
- package/studio/web/components/ui/switch.tsx +13 -0
- package/studio/web/components/ui/tooltip.tsx +59 -0
- package/studio/web/hooks/useAccounts.ts +27 -0
- package/studio/web/hooks/useAdmin.ts +12 -0
- package/studio/web/hooks/useAnalytics.ts +21 -0
- package/studio/web/hooks/useAvatarAudioReactive.ts +87 -0
- package/studio/web/hooks/useBrowserConnect.ts +48 -0
- package/studio/web/hooks/useClonePlan.ts +14 -0
- package/studio/web/hooks/useConnected.ts +7 -0
- package/studio/web/hooks/useLiveQuery.ts +29 -0
- package/studio/web/hooks/useMenuCollapsed.ts +32 -0
- package/studio/web/hooks/useMicDevices.ts +53 -0
- package/studio/web/hooks/useMissingDms.ts +211 -0
- package/studio/web/hooks/useMissingSetupSteps.ts +26 -0
- package/studio/web/hooks/useNow.ts +11 -0
- package/studio/web/hooks/useOmniAgent.ts +278 -0
- package/studio/web/hooks/useOmniClaude.ts +124 -0
- package/studio/web/hooks/useOmniDock.ts +142 -0
- package/studio/web/hooks/useOmniGreeting.ts +60 -0
- package/studio/web/hooks/useOmniHotword.ts +128 -0
- package/studio/web/hooks/useOmnichat.ts +26 -0
- package/studio/web/hooks/useOnboarding.ts +27 -0
- package/studio/web/hooks/usePost.ts +23 -0
- package/studio/web/hooks/useQueue.ts +10 -0
- package/studio/web/hooks/useReferences.ts +20 -0
- package/studio/web/hooks/useRuleEditor.ts +101 -0
- package/studio/web/hooks/useRunner.ts +24 -0
- package/studio/web/hooks/useSaveAction.ts +23 -0
- package/studio/web/hooks/useSchedule.ts +11 -0
- package/studio/web/hooks/useServices.ts +10 -0
- package/studio/web/hooks/useSessions.ts +15 -0
- package/studio/web/hooks/useSettings.ts +11 -0
- package/studio/web/hooks/useSettingsSection.ts +30 -0
- package/studio/web/hooks/useSidePanel.ts +23 -0
- package/studio/web/hooks/useSound.ts +30 -0
- package/studio/web/hooks/useUsage.ts +11 -0
- package/studio/web/hooks/useVideoVoice.ts +36 -0
- package/studio/web/hooks/useVideos.ts +21 -0
- package/studio/web/hooks/useVoiceProfilePreview.ts +52 -0
- package/studio/web/hooks/useVoiceProfiles.ts +7 -0
- package/studio/web/hooks/useVoiceRunning.ts +10 -0
- package/studio/web/hooks/useVoiceStudioClosed.ts +8 -0
- package/studio/web/index.html +19 -0
- package/studio/web/lib/agent.ts +7 -0
- package/studio/web/lib/alerts.ts +210 -0
- package/studio/web/lib/analytics-metrics.ts +351 -0
- package/studio/web/lib/api.ts +70 -0
- package/studio/web/lib/audio.ts +39 -0
- package/studio/web/lib/avatars.ts +41 -0
- package/studio/web/lib/calendar.ts +218 -0
- package/studio/web/lib/caption-position.ts +249 -0
- package/studio/web/lib/carousel.ts +24 -0
- package/studio/web/lib/chime.ts +42 -0
- package/studio/web/lib/clone-voice.ts +55 -0
- package/studio/web/lib/export.ts +11 -0
- package/studio/web/lib/files.ts +8 -0
- package/studio/web/lib/format.ts +93 -0
- package/studio/web/lib/media.ts +29 -0
- package/studio/web/lib/mic-device.ts +72 -0
- package/studio/web/lib/missing-dms.ts +70 -0
- package/studio/web/lib/omni-cards.ts +42 -0
- package/studio/web/lib/omni-cascade.ts +1067 -0
- package/studio/web/lib/omni-greeting-audio.ts +24 -0
- package/studio/web/lib/omni-hotword.ts +231 -0
- package/studio/web/lib/omni-panel.ts +67 -0
- package/studio/web/lib/omni-realtime.ts +811 -0
- package/studio/web/lib/omni-ui.ts +402 -0
- package/studio/web/lib/omni-wait.ts +174 -0
- package/studio/web/lib/omnichat-api.ts +44 -0
- package/studio/web/lib/orb.ts +270 -0
- package/studio/web/lib/pcm-recorder.ts +115 -0
- package/studio/web/lib/platforms.ts +17 -0
- package/studio/web/lib/providers.ts +27 -0
- package/studio/web/lib/publish-draft.ts +52 -0
- package/studio/web/lib/publish-settings.ts +81 -0
- package/studio/web/lib/query-client.ts +122 -0
- package/studio/web/lib/references.ts +13 -0
- package/studio/web/lib/services.ts +40 -0
- package/studio/web/lib/sessions.ts +103 -0
- package/studio/web/lib/settings-categories.ts +58 -0
- package/studio/web/lib/sound-audio.ts +33 -0
- package/studio/web/lib/sound-timeline.ts +291 -0
- package/studio/web/lib/sse.ts +86 -0
- package/studio/web/lib/stage-history.ts +141 -0
- package/studio/web/lib/stages.ts +344 -0
- package/studio/web/lib/studio-offline.ts +60 -0
- package/studio/web/lib/upload.ts +24 -0
- package/studio/web/lib/video-tabs.ts +118 -0
- package/studio/web/lib/voice-profile-draft.ts +34 -0
- package/studio/web/lib/voice-profiles.ts +3 -0
- package/studio/web/main.tsx +14 -0
- package/studio/web/pages/AdminPage/index.tsx +123 -0
- package/studio/web/pages/AgentPage/components/Measurements.tsx +102 -0
- package/studio/web/pages/AgentPage/index.tsx +28 -0
- package/studio/web/pages/AnalyticsPage/components/About.tsx +52 -0
- package/studio/web/pages/AnalyticsPage/components/ChannelCards.tsx +106 -0
- package/studio/web/pages/AnalyticsPage/components/ChannelView.tsx +137 -0
- package/studio/web/pages/AnalyticsPage/components/CollectionStatus.tsx +46 -0
- package/studio/web/pages/AnalyticsPage/components/Comparison.tsx +68 -0
- package/studio/web/pages/AnalyticsPage/components/Diagnosis.tsx +50 -0
- package/studio/web/pages/AnalyticsPage/components/Highlights.tsx +49 -0
- package/studio/web/pages/AnalyticsPage/components/Hint.tsx +11 -0
- package/studio/web/pages/AnalyticsPage/components/HookVsBody.tsx +100 -0
- package/studio/web/pages/AnalyticsPage/components/HourlyWave.tsx +133 -0
- package/studio/web/pages/AnalyticsPage/components/Insights.tsx +70 -0
- package/studio/web/pages/AnalyticsPage/components/MetricCard.tsx +46 -0
- package/studio/web/pages/AnalyticsPage/components/PlanSection.tsx +40 -0
- package/studio/web/pages/AnalyticsPage/components/ProgressionChart.tsx +155 -0
- package/studio/web/pages/AnalyticsPage/components/ReelBarsChart.tsx +154 -0
- package/studio/web/pages/AnalyticsPage/components/ReelThumb.tsx +19 -0
- package/studio/web/pages/AnalyticsPage/components/ReelView.tsx +290 -0
- package/studio/web/pages/AnalyticsPage/components/ReelsTable.tsx +269 -0
- package/studio/web/pages/AnalyticsPage/components/RequestWithFocus.tsx +38 -0
- package/studio/web/pages/AnalyticsPage/components/SegmentedControl.tsx +35 -0
- package/studio/web/pages/AnalyticsPage/components/VsMedian.tsx +31 -0
- package/studio/web/pages/AnalyticsPage/index.tsx +17 -0
- package/studio/web/pages/GettingStartedPage/index.tsx +174 -0
- package/studio/web/pages/NewVideoPage/components/DeliverablesPicker.tsx +62 -0
- package/studio/web/pages/NewVideoPage/components/FormatPicker.tsx +33 -0
- package/studio/web/pages/NewVideoPage/index.tsx +133 -0
- package/studio/web/pages/NotFoundPage/index.tsx +9 -0
- package/studio/web/pages/OmnichatPage/components/NewFromLink.tsx +59 -0
- package/studio/web/pages/OmnichatPage/components/RuleListItem.tsx +64 -0
- package/studio/web/pages/OmnichatPage/components/RulePreview.tsx +14 -0
- package/studio/web/pages/OmnichatPage/components/RuleView.tsx +88 -0
- package/studio/web/pages/OmnichatPage/index.tsx +153 -0
- package/studio/web/pages/QueuePage/index.tsx +22 -0
- package/studio/web/pages/ReferencePage/components/ReferenceTexts.tsx +131 -0
- package/studio/web/pages/ReferencePage/index.tsx +216 -0
- package/studio/web/pages/ReferencesPage/components/NewReferenceForm.tsx +150 -0
- package/studio/web/pages/ReferencesPage/components/ReferenceCard.tsx +42 -0
- package/studio/web/pages/ReferencesPage/components/ReferenceGridSkeleton.tsx +31 -0
- package/studio/web/pages/ReferencesPage/index.tsx +67 -0
- package/studio/web/pages/SchedulePage/components/CalendarSkeleton.tsx +18 -0
- package/studio/web/pages/SchedulePage/components/DayView.tsx +177 -0
- package/studio/web/pages/SchedulePage/components/HourPicker.tsx +103 -0
- package/studio/web/pages/SchedulePage/components/MonthDayCell.tsx +111 -0
- package/studio/web/pages/SchedulePage/components/MonthView.tsx +47 -0
- package/studio/web/pages/SchedulePage/components/PlatformIcons.tsx +12 -0
- package/studio/web/pages/SchedulePage/components/PostThumb.tsx +15 -0
- package/studio/web/pages/SchedulePage/components/WeekColumn.tsx +75 -0
- package/studio/web/pages/SchedulePage/components/WeekView.tsx +35 -0
- package/studio/web/pages/SchedulePage/index.tsx +285 -0
- package/studio/web/pages/SettingsPage/components/AgentAccounts.tsx +263 -0
- package/studio/web/pages/SettingsPage/components/AgentCategory.tsx +20 -0
- package/studio/web/pages/SettingsPage/components/ModelPicker.tsx +49 -0
- package/studio/web/pages/SettingsPage/components/ModelsSettings.tsx +138 -0
- package/studio/web/pages/SettingsPage/components/PlatformsCategory.tsx +23 -0
- package/studio/web/pages/SettingsPage/components/PublishingCategory/AccountStatus.tsx +29 -0
- package/studio/web/pages/SettingsPage/components/PublishingCategory/AutoPublishPanel.tsx +90 -0
- package/studio/web/pages/SettingsPage/components/PublishingCategory/BrowserConnect.tsx +62 -0
- package/studio/web/pages/SettingsPage/components/PublishingCategory/CheckField.tsx +19 -0
- package/studio/web/pages/SettingsPage/components/PublishingCategory/InstagramPanel.tsx +113 -0
- package/studio/web/pages/SettingsPage/components/PublishingCategory/OmnichatSettingsPanel.tsx +146 -0
- package/studio/web/pages/SettingsPage/components/PublishingCategory/SaveBar.tsx +33 -0
- package/studio/web/pages/SettingsPage/components/PublishingCategory/SchedulePanel.tsx +132 -0
- package/studio/web/pages/SettingsPage/components/PublishingCategory/SettingsCard.tsx +47 -0
- package/studio/web/pages/SettingsPage/components/PublishingCategory/SettingsCardGroup.tsx +19 -0
- package/studio/web/pages/SettingsPage/components/PublishingCategory/TextPanel.tsx +55 -0
- package/studio/web/pages/SettingsPage/components/PublishingCategory/TiktokPanel.tsx +123 -0
- package/studio/web/pages/SettingsPage/components/PublishingCategory/YoutubePanel.tsx +100 -0
- package/studio/web/pages/SettingsPage/components/PublishingCategory/badges.tsx +15 -0
- package/studio/web/pages/SettingsPage/components/PublishingCategory/index.tsx +40 -0
- package/studio/web/pages/SettingsPage/components/PublishingCategory/types.ts +10 -0
- package/studio/web/pages/SettingsPage/components/UsageCategory.tsx +389 -0
- package/studio/web/pages/SettingsPage/components/VoiceProvidersCategory.tsx +97 -0
- package/studio/web/pages/SettingsPage/index.tsx +85 -0
- package/studio/web/pages/VideoPage/components/AudioPlayer.tsx +113 -0
- package/studio/web/pages/VideoPage/components/CaptionPositionEditor/AxisField.tsx +50 -0
- package/studio/web/pages/VideoPage/components/CaptionPositionEditor/CaptionList.tsx +56 -0
- package/studio/web/pages/VideoPage/components/CaptionPositionEditor/CaptionListItem.tsx +92 -0
- package/studio/web/pages/VideoPage/components/CaptionPositionEditor/CaptionMargins.tsx +57 -0
- package/studio/web/pages/VideoPage/components/CaptionPositionEditor/CaptionStyleFields.tsx +70 -0
- package/studio/web/pages/VideoPage/components/CaptionPositionEditor/CaptionTextForm.tsx +87 -0
- package/studio/web/pages/VideoPage/components/CaptionPositionEditor/ColorField.tsx +37 -0
- package/studio/web/pages/VideoPage/components/CaptionPositionEditor/MarginField.tsx +59 -0
- package/studio/web/pages/VideoPage/components/CaptionPositionEditor/NumberField.tsx +57 -0
- package/studio/web/pages/VideoPage/components/CaptionPositionEditor/PositionControls.tsx +140 -0
- package/studio/web/pages/VideoPage/components/CaptionPositionEditor/captionEditor.ts +215 -0
- package/studio/web/pages/VideoPage/components/CaptionPositionEditor/useCaptionPosition.ts +154 -0
- package/studio/web/pages/VideoPage/components/CarouselChangeRequest.tsx +72 -0
- package/studio/web/pages/VideoPage/components/CarouselCompositionStage.tsx +80 -0
- package/studio/web/pages/VideoPage/components/CarouselCreate.tsx +63 -0
- package/studio/web/pages/VideoPage/components/CarouselMissing.tsx +19 -0
- package/studio/web/pages/VideoPage/components/CarouselPublishStage.tsx +173 -0
- package/studio/web/pages/VideoPage/components/CarouselScriptStage.tsx +82 -0
- package/studio/web/pages/VideoPage/components/CarouselSlideCard.tsx +49 -0
- package/studio/web/pages/VideoPage/components/CarouselViewer.tsx +55 -0
- package/studio/web/pages/VideoPage/components/ChangedVoices.tsx +53 -0
- package/studio/web/pages/VideoPage/components/CompositionStage.tsx +78 -0
- package/studio/web/pages/VideoPage/components/CoverPicker.tsx +32 -0
- package/studio/web/pages/VideoPage/components/EditorStage.tsx +53 -0
- package/studio/web/pages/VideoPage/components/LineTakes.tsx +160 -0
- package/studio/web/pages/VideoPage/components/LineTextEditor.tsx +124 -0
- package/studio/web/pages/VideoPage/components/LineVoicePicker.tsx +37 -0
- package/studio/web/pages/VideoPage/components/MediaImage.tsx +25 -0
- package/studio/web/pages/VideoPage/components/MotionScriptStage.tsx +39 -0
- package/studio/web/pages/VideoPage/components/NarrationStage.tsx +70 -0
- package/studio/web/pages/VideoPage/components/PublishForm.tsx +387 -0
- package/studio/web/pages/VideoPage/components/PublishStage.tsx +76 -0
- package/studio/web/pages/VideoPage/components/PublishText.tsx +149 -0
- package/studio/web/pages/VideoPage/components/ScenesApprovalPanel.tsx +278 -0
- package/studio/web/pages/VideoPage/components/ScheduledInfo.tsx +72 -0
- package/studio/web/pages/VideoPage/components/ScriptApprovalPanel.tsx +189 -0
- package/studio/web/pages/VideoPage/components/ScriptStage.tsx +17 -0
- package/studio/web/pages/VideoPage/components/SequencePlayer.tsx +146 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/AddSound.tsx +117 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/AreaSelection.tsx +74 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/BedPanel.tsx +94 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/BedRow.tsx +45 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/CaptionOverlay.tsx +44 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/CaptionSaveBar.tsx +32 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/CaptionsPanel.tsx +22 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/ClipBlock.tsx +111 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/ClipPanel.tsx +76 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/CutsPanel.tsx +44 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/EditorTabBar.tsx +73 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/EffectChoice.tsx +46 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/EffectItem.tsx +89 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/EffectPanel.tsx +232 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/EffectsRow.tsx +21 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/Lane.tsx +39 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/MarkerRow.tsx +40 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/MuteButton.tsx +36 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/NumberInput.tsx +37 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/PlanWarnings.tsx +19 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/PlayerResizeHandle.tsx +50 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/PreviewControls.tsx +51 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/SceneRow.tsx +54 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/SettingsPanel.tsx +65 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/SoundLibrary.tsx +99 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/SoundPreview.tsx +82 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/SpeedPanel.tsx +128 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/TimelineDock.tsx +56 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/TimelineFooter.tsx +26 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/TimelineSkeleton.tsx +21 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/TimelineToolbar.tsx +160 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/TimelineTracks.tsx +121 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/TrackSidebar.tsx +30 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/VoiceRow.tsx +53 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/Waveform.tsx +97 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/clipActions.ts +50 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/effectActions.ts +161 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/index.tsx +161 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/timelineGeometry.ts +45 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/types.ts +43 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/useAutosave.ts +107 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/useEditorShortcuts.ts +168 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/useFitHeight.ts +40 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/useFollowPlayhead.ts +21 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/useListenMix.ts +43 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/usePlayback.ts +231 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/usePlayerWidth.ts +27 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/useSoundData.ts +124 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/useSoundTimeline.ts +511 -0
- package/studio/web/pages/VideoPage/components/StageDrawer/CompareVersions.tsx +111 -0
- package/studio/web/pages/VideoPage/components/StageDrawer/VersionCard.tsx +81 -0
- package/studio/web/pages/VideoPage/components/StageDrawer/VersionMedia.tsx +24 -0
- package/studio/web/pages/VideoPage/components/StageDrawer/VersionText.tsx +15 -0
- package/studio/web/pages/VideoPage/components/StageDrawer/index.tsx +109 -0
- package/studio/web/pages/VideoPage/components/StageProcess.tsx +126 -0
- package/studio/web/pages/VideoPage/components/StageResult.tsx +69 -0
- package/studio/web/pages/VideoPage/components/SwapReference.tsx +92 -0
- package/studio/web/pages/VideoPage/components/ThumbnailPanel.tsx +252 -0
- package/studio/web/pages/VideoPage/components/TimingPanel.tsx +89 -0
- package/studio/web/pages/VideoPage/components/VideoPlayer.tsx +18 -0
- package/studio/web/pages/VideoPage/components/VideoSkeleton.tsx +50 -0
- package/studio/web/pages/VideoPage/components/VideoVoiceInfo.tsx +33 -0
- package/studio/web/pages/VideoPage/components/VideoVoicePicker.tsx +165 -0
- package/studio/web/pages/VideoPage/components/VoicePauseBanner.tsx +56 -0
- package/studio/web/pages/VideoPage/components/WithLogo.tsx +10 -0
- package/studio/web/pages/VideoPage/index.tsx +295 -0
- package/studio/web/pages/VideosPage/components/PublishedOn.tsx +85 -0
- package/studio/web/pages/VideosPage/components/ServicesPanel.tsx +81 -0
- package/studio/web/pages/VideosPage/components/SessionsPanel.tsx +62 -0
- package/studio/web/pages/VideosPage/components/VideoListSkeleton.tsx +25 -0
- package/studio/web/pages/VideosPage/components/VideoRow.tsx +122 -0
- package/studio/web/pages/VideosPage/index.tsx +134 -0
- package/studio/web/pages/VoicePage/index.tsx +28 -0
- package/studio/web/router.tsx +201 -0
- package/studio/web/styles/base/elements.css +55 -0
- package/studio/web/styles/base/utilities.css +25 -0
- package/studio/web/styles/components/badge.css +33 -0
- package/studio/web/styles/components/claude-running.css +27 -0
- package/studio/web/styles/components/claude-session.css +212 -0
- package/studio/web/styles/components/claude-walking.css +92 -0
- package/studio/web/styles/components/clone-voice-save.css +136 -0
- package/studio/web/styles/components/clone-voice.css +379 -0
- package/studio/web/styles/components/controls.css +70 -0
- package/studio/web/styles/components/data-and-image.css +54 -0
- package/studio/web/styles/components/forms.css +132 -0
- package/studio/web/styles/components/human-banner.css +99 -0
- package/studio/web/styles/components/lightbox.css +50 -0
- package/studio/web/styles/components/markdown.css +68 -0
- package/studio/web/styles/components/modal.css +70 -0
- package/studio/web/styles/components/omni-cards.css +105 -0
- package/studio/web/styles/components/omni.css +859 -0
- package/studio/web/styles/components/omnichat.css +538 -0
- package/studio/web/styles/components/panel.css +115 -0
- package/studio/web/styles/components/platform-card.css +140 -0
- package/studio/web/styles/components/provider-card.css +132 -0
- package/studio/web/styles/components/queue.css +185 -0
- package/studio/web/styles/components/reference-actions.css +133 -0
- package/studio/web/styles/components/reference-picker.css +146 -0
- package/studio/web/styles/components/results-drawer.css +224 -0
- package/studio/web/styles/components/segmented.css +115 -0
- package/studio/web/styles/components/side-panel-tab.css +23 -0
- package/studio/web/styles/components/status-signals.css +66 -0
- package/studio/web/styles/components/stepper.css +208 -0
- package/studio/web/styles/components/table.css +19 -0
- package/studio/web/styles/components/tabs.css +24 -0
- package/studio/web/styles/components/ui/accordion.css +71 -0
- package/studio/web/styles/components/ui/chart.css +162 -0
- package/studio/web/styles/components/ui/checkbox.css +37 -0
- package/studio/web/styles/components/ui/date-picker.css +153 -0
- package/studio/web/styles/components/ui/select.css +110 -0
- package/studio/web/styles/components/ui/skeleton.css +25 -0
- package/studio/web/styles/components/ui/slider.css +81 -0
- package/studio/web/styles/components/ui/switch.css +41 -0
- package/studio/web/styles/components/ui/tooltip.css +24 -0
- package/studio/web/styles/components/voice-carousel.css +255 -0
- package/studio/web/styles/components/voice-editor.css +179 -0
- package/studio/web/styles/components/voice-grid.css +56 -0
- package/studio/web/styles/components/voice-profiles.css +192 -0
- package/studio/web/styles/design-system.md +136 -0
- package/studio/web/styles/index.css +116 -0
- package/studio/web/styles/layout/page.css +41 -0
- package/studio/web/styles/layout/sidebar-groups.css +39 -0
- package/studio/web/styles/layout/sidebar.css +111 -0
- package/studio/web/styles/pages/agent.css +26 -0
- package/studio/web/styles/pages/analytics/dashboard.css +614 -0
- package/studio/web/styles/pages/analytics/insights.css +48 -0
- package/studio/web/styles/pages/getting-started.css +166 -0
- package/studio/web/styles/pages/new-video/form.css +32 -0
- package/studio/web/styles/pages/not-found.css +59 -0
- package/studio/web/styles/pages/references.css +204 -0
- package/studio/web/styles/pages/schedule/calendar.css +541 -0
- package/studio/web/styles/pages/schedule/chip-states.css +60 -0
- package/studio/web/styles/pages/settings/models.css +72 -0
- package/studio/web/styles/pages/settings/shell.css +361 -0
- package/studio/web/styles/pages/settings/usage.css +63 -0
- package/studio/web/styles/pages/studio-offline.css +105 -0
- package/studio/web/styles/pages/video/caption-controls.css +194 -0
- package/studio/web/styles/pages/video/captions-grid.css +70 -0
- package/studio/web/styles/pages/video/captions.css +79 -0
- package/studio/web/styles/pages/video/editor-chat.css +103 -0
- package/studio/web/styles/pages/video/export.css +50 -0
- package/studio/web/styles/pages/video/frames.css +29 -0
- package/studio/web/styles/pages/video/fullscreen-stage.css +400 -0
- package/studio/web/styles/pages/video/instagram-carousel.css +309 -0
- package/studio/web/styles/pages/video/layout.css +285 -0
- package/studio/web/styles/pages/video/narration.css +138 -0
- package/studio/web/styles/pages/video/preview-overview.css +11 -0
- package/studio/web/styles/pages/video/processing-notice.css +34 -0
- package/studio/web/styles/pages/video/publish-when.css +76 -0
- package/studio/web/styles/pages/video/publish.css +97 -0
- package/studio/web/styles/pages/video/result.css +96 -0
- package/studio/web/styles/pages/video/scenes.css +167 -0
- package/studio/web/styles/pages/video/sound-timeline.css +672 -0
- package/studio/web/styles/pages/video/sound-toolbar.css +149 -0
- package/studio/web/styles/pages/video/take-player.css +100 -0
- package/studio/web/styles/pages/video/thumbnail.css +157 -0
- package/studio/web/styles/pages/video/timeline.css +117 -0
- package/studio/web/styles/pages/video/voice-picker.css +14 -0
- package/studio/web/styles/pages/video-list/active-row.css +27 -0
- package/studio/web/styles/pages/video-list/card-link.css +49 -0
- package/studio/web/styles/pages/video-list/list.css +86 -0
- package/studio/web/styles/theme/identity/details.css +204 -0
- package/studio/web/styles/theme/identity/shell.css +249 -0
- package/studio/web/styles/theme/identity/surfaces.css +183 -0
- package/studio/web/styles/theme/identity/titles-and-buttons.css +120 -0
- package/studio/web/styles/theme/no-borders/blocks.css +208 -0
- package/studio/web/styles/theme/no-borders/controls.css +256 -0
- package/studio/web/styles/tokens.css +245 -0
- package/studio/web/types/api.ts +2 -0
- package/studio/web/types/assets.d.ts +9 -0
- package/templates/brief.md +27 -0
- package/templates/captions.html +204 -0
- package/templates/carousel-composition.md +47 -0
- package/templates/carousel-script.md +60 -0
- package/templates/components/voiceavatar/README.md +87 -0
- package/templates/components/voiceavatar/demo.html +50 -0
- package/templates/components/voiceavatar/voiceavatar.js +415 -0
- package/templates/motion.md +80 -0
- package/templates/playbook.md +43 -0
- package/templates/reference.md +123 -0
- package/templates/scene.html +112 -0
- package/templates/scenes.example.json +96 -0
- package/templates/script.md +63 -0
- package/templates/slide.html +71 -0
- package/templates/wireframe.html +91 -0
- package/tools/analysis.mjs +300 -0
- package/tools/approval.mjs +442 -0
- package/tools/assemble.mjs +942 -0
- package/tools/build-motion.mjs +437 -0
- package/tools/captions.mjs +246 -0
- package/tools/carousel.mjs +558 -0
- package/tools/compose.mjs +307 -0
- package/tools/cuts.mjs +74 -0
- package/tools/editor.mjs +419 -0
- package/tools/epidemic-capture.js +54 -0
- package/tools/fanout.mjs +69 -0
- package/tools/history.mjs +70 -0
- package/tools/history.py +61 -0
- package/tools/job-queue.mjs +180 -0
- package/tools/job_queue.py +430 -0
- package/tools/lib/agents.mjs +412 -0
- package/tools/lib/channel.mjs +399 -0
- package/tools/lib/claude-settings.mjs +67 -0
- package/tools/lib/cli.mjs +9 -0
- package/tools/lib/fanout.mjs +234 -0
- package/tools/lib/http.mjs +94 -0
- package/tools/lib/index.js +14 -0
- package/tools/lib/mcp-server.mjs +69 -0
- package/tools/lib/metrics/metrics.mjs +204 -0
- package/tools/lib/narration.mjs +15 -0
- package/tools/lib/oauth-client.mjs +295 -0
- package/tools/lib/omnichat/config.mjs +139 -0
- package/tools/lib/omnichat/errors.mjs +42 -0
- package/tools/lib/omnichat/extract.mjs +82 -0
- package/tools/lib/omnichat/gate.mjs +151 -0
- package/tools/lib/omnichat/manual.mjs +166 -0
- package/tools/lib/omnichat/match.mjs +78 -0
- package/tools/lib/omnichat/pass.mjs +393 -0
- package/tools/lib/omnichat/record.mjs +208 -0
- package/tools/lib/omnichat/text.mjs +315 -0
- package/tools/lib/omnichat/webhook.mjs +166 -0
- package/tools/lib/pairing.mjs +99 -0
- package/tools/lib/paths.mjs +41 -0
- package/tools/lib/platforms/instagram.mjs +469 -0
- package/tools/lib/platforms/tiktok.mjs +343 -0
- package/tools/lib/platforms/youtube.mjs +228 -0
- package/tools/lib/publish/publish.mjs +244 -0
- package/tools/lib/schedule/schedule.mjs +191 -0
- package/tools/lib/service.mjs +134 -0
- package/tools/lib/skill-client.mjs +126 -0
- package/tools/lib/skills-release.mjs +196 -0
- package/tools/lib/testing.mjs +52 -0
- package/tools/lib/up.mjs +187 -0
- package/tools/metrics.mjs +415 -0
- package/tools/models.mjs +134 -0
- package/tools/narrate.mjs +257 -0
- package/tools/omni.mjs +298 -0
- package/tools/omni_paths.py +12 -0
- package/tools/omnichat.mjs +1577 -0
- package/tools/omnivideo.py +1122 -0
- package/tools/package.mjs +130 -0
- package/tools/platforms.mjs +236 -0
- package/tools/premiere.mjs +533 -0
- package/tools/publish.mjs +1496 -0
- package/tools/reference.mjs +1408 -0
- package/tools/render.mjs +426 -0
- package/tools/runner.mjs +156 -0
- package/tools/setup.mjs +466 -0
- package/tools/skills-release.mjs +93 -0
- package/tools/sound.mjs +1323 -0
- package/tools/soundtrack.mjs +217 -0
- package/tools/thumbnail.mjs +271 -0
- package/tools/timing.mjs +108 -0
- package/tools/usage.py +628 -0
- package/tools/voice-envelope.mjs +185 -0
- package/tools/voice.py +448 -0
- package/tools/voice_sample.py +75 -0
- package/tools/wireframe.mjs +230 -0
|
@@ -0,0 +1,1007 @@
|
|
|
1
|
+
/* Agent sessions opened from the UI. Each turn is a `claude -p` (or a `codex exec`) without a
|
|
2
|
+
terminal, at the omnivideo root and with the project's skills; the next
|
|
3
|
+
reply resumes the same session (--resume). It is the same work as the chat, only
|
|
4
|
+
the UI is the one reading and replying.
|
|
5
|
+
|
|
6
|
+
Accounts (tools/lib/agents.mjs): a new session goes to the next account of the rotation and
|
|
7
|
+
stays there while it is free. A turn that hits the account's limit sets it aside and the
|
|
8
|
+
session carries on in the next free one: Claude to Claude with the transcript copied over
|
|
9
|
+
(same conversation), anything else as a new conversation that gets the story so far.
|
|
10
|
+
|
|
11
|
+
The studio keeps no state in memory: each session is a dir
|
|
12
|
+
.sessions/<id>/ with session.json (who, what, the turns) and one
|
|
13
|
+
turn-<n>.jsonl per turn, claude's stream-json output (codex's --json events). State comes from
|
|
14
|
+
there and from the pid: a turn with `result` has ended; without `result` and with a live pid
|
|
15
|
+
it is running; with neither it was interrupted. claude runs in its own process
|
|
16
|
+
group and writes straight to the file, so the studio can go down and come back in
|
|
17
|
+
the middle of a turn without losing anything. */
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
mkdirSync,
|
|
21
|
+
readdirSync,
|
|
22
|
+
writeFileSync,
|
|
23
|
+
renameSync,
|
|
24
|
+
openSync,
|
|
25
|
+
closeSync,
|
|
26
|
+
readSync,
|
|
27
|
+
statSync,
|
|
28
|
+
existsSync,
|
|
29
|
+
rmSync,
|
|
30
|
+
} from "node:fs";
|
|
31
|
+
import { join, relative } from "node:path";
|
|
32
|
+
import { ROOT, CONFIG, PYTHON, CLAUDE_PERSONAL_DIR, readJson } from "./root";
|
|
33
|
+
import {
|
|
34
|
+
PROVIDER_LABEL,
|
|
35
|
+
accountsOf,
|
|
36
|
+
codexBin,
|
|
37
|
+
codexResetOf,
|
|
38
|
+
codexSettings,
|
|
39
|
+
copyClaudeSession,
|
|
40
|
+
envFor,
|
|
41
|
+
isLimitText,
|
|
42
|
+
isReady,
|
|
43
|
+
markLimited,
|
|
44
|
+
noAccountReason,
|
|
45
|
+
pickAccount,
|
|
46
|
+
} from "../../../tools/lib/agents.mjs";
|
|
47
|
+
import { isAlive } from "./queue";
|
|
48
|
+
import { sessionModel } from "./settings";
|
|
49
|
+
import { LIBRARY_DIR } from "./references";
|
|
50
|
+
|
|
51
|
+
export const SESSIONS_DIR = join(ROOT, ".sessions");
|
|
52
|
+
|
|
53
|
+
/* "analysis": the Analytics page session (target "channel"), which runs the av-analysis skill;
|
|
54
|
+
"edit": a video's editor chat (target = the slug), which runs the av-editor skill;
|
|
55
|
+
"omni": Omni in Claude mode (target "omni"), after the day's voice quota */
|
|
56
|
+
export type SessionType = "video" | "reference" | "analysis" | "edit" | "omni";
|
|
57
|
+
type Provider = "claude" | "codex";
|
|
58
|
+
type Account = ReturnType<typeof accountsOf>[number];
|
|
59
|
+
type Turn = {
|
|
60
|
+
n: number;
|
|
61
|
+
origin: string;
|
|
62
|
+
prompt: string;
|
|
63
|
+
pid: number;
|
|
64
|
+
started_at: string;
|
|
65
|
+
file: string;
|
|
66
|
+
/* the account (agents.accounts) and the agent the turn ran on; absent before the accounts,
|
|
67
|
+
when every turn was Claude on the personal dir */
|
|
68
|
+
account?: string;
|
|
69
|
+
provider?: Provider;
|
|
70
|
+
/* Claude's session id (r.id, or a new one after a handoff); Codex's thread is read from the file */
|
|
71
|
+
engine_id?: string;
|
|
72
|
+
/* the turn started a new conversation on another account, carrying the story so far */
|
|
73
|
+
handoff?: boolean;
|
|
74
|
+
/* a failover turn: the request it carries on */
|
|
75
|
+
request?: string;
|
|
76
|
+
};
|
|
77
|
+
/* config_dir: the Claude dir (CLAUDE_CONFIG_DIR) of a session from before the accounts; null = ~/.claude */
|
|
78
|
+
type SessionRecord = {
|
|
79
|
+
id: string;
|
|
80
|
+
type: SessionType;
|
|
81
|
+
target: string;
|
|
82
|
+
dir: string;
|
|
83
|
+
title: string;
|
|
84
|
+
created_at: string;
|
|
85
|
+
config_dir?: string | null;
|
|
86
|
+
turns: Turn[];
|
|
87
|
+
};
|
|
88
|
+
type TurnResult = {
|
|
89
|
+
duration_ms: number | null;
|
|
90
|
+
cost: number | null;
|
|
91
|
+
models: Record<string, number>;
|
|
92
|
+
text: string;
|
|
93
|
+
error: boolean;
|
|
94
|
+
denied: string[];
|
|
95
|
+
/* the account hit its usage limit in this turn: when it resets (ms), null when unknown */
|
|
96
|
+
limited: { until: number | null } | null;
|
|
97
|
+
/* Codex reports tokens, not dollars */
|
|
98
|
+
tokens?: number | null;
|
|
99
|
+
};
|
|
100
|
+
export type SessionState = "running" | "stopped" | "error" | "interrupted";
|
|
101
|
+
|
|
102
|
+
const CLAUDE = CONFIG.studio?.claude ?? {};
|
|
103
|
+
const BASH_MS: number = CLAUDE.bash_timeout_ms ?? 7_200_000;
|
|
104
|
+
|
|
105
|
+
/* --- reading ------------------------------------------------------------------ */
|
|
106
|
+
const isValid = (id: string) => /^[0-9a-f-]{36}$/.test(id);
|
|
107
|
+
|
|
108
|
+
function readRecord(id: string): SessionRecord | null {
|
|
109
|
+
return isValid(id) ? readJson<SessionRecord>(join(SESSIONS_DIR, id, "session.json")) : null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function writeRecord(r: SessionRecord) {
|
|
113
|
+
const file = join(SESSIONS_DIR, r.id, "session.json");
|
|
114
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
115
|
+
writeFileSync(tmp, JSON.stringify(r, null, 2) + "\n");
|
|
116
|
+
renameSync(tmp, file);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* The end of the turn and what Claude was doing last, read from the tail of the jsonl.
|
|
120
|
+
An image Read puts megabytes of base64 on a single line: the tail is enough,
|
|
121
|
+
because `result` is always the last thing in the turn. */
|
|
122
|
+
type TurnInfo = { result: TurnResult | null; now: string | null };
|
|
123
|
+
const readCache = new Map<string, { size: number; mtime: number; info: TurnInfo }>();
|
|
124
|
+
|
|
125
|
+
const providerOf = (t: Turn): Provider => t.provider ?? "claude";
|
|
126
|
+
const turnInfo = (r: SessionRecord, t: Turn) => readTurn(join(SESSIONS_DIR, r.id, t.file), providerOf(t), t.started_at);
|
|
127
|
+
|
|
128
|
+
function readTurn(file: string, provider: Provider = "claude", startedAt?: string): TurnInfo {
|
|
129
|
+
let st: ReturnType<typeof statSync>;
|
|
130
|
+
try {
|
|
131
|
+
st = statSync(file);
|
|
132
|
+
} catch {
|
|
133
|
+
return { result: null, now: null };
|
|
134
|
+
}
|
|
135
|
+
const c = readCache.get(file);
|
|
136
|
+
if (c && c.size === st.size && c.mtime === st.mtimeMs) return c.info;
|
|
137
|
+
const from = Math.max(0, st.size - 1_000_000);
|
|
138
|
+
const buf = Buffer.alloc(st.size - from);
|
|
139
|
+
const fd = openSync(file, "r");
|
|
140
|
+
try {
|
|
141
|
+
readSync(fd, buf, 0, buf.length, from);
|
|
142
|
+
} finally {
|
|
143
|
+
closeSync(fd);
|
|
144
|
+
}
|
|
145
|
+
const lines = buf.toString("utf8").split("\n");
|
|
146
|
+
if (provider === "codex") {
|
|
147
|
+
const info = codexTurn(lines, st.mtimeMs, startedAt);
|
|
148
|
+
readCache.set(file, { size: st.size, mtime: st.mtimeMs, info });
|
|
149
|
+
return info;
|
|
150
|
+
}
|
|
151
|
+
const info: TurnInfo = { result: null, now: null };
|
|
152
|
+
/* While a long tool runs (a forked stage is a Skill), claude
|
|
153
|
+
writes a heartbeat every 30 s. Those after the last action tell
|
|
154
|
+
how long the UI has been still, and whether the action's result is back
|
|
155
|
+
(the model is thinking) or not (the tool is still running). Without this, an
|
|
156
|
+
Opus thinking for five minutes looks stuck. */
|
|
157
|
+
const beats: number[] = [];
|
|
158
|
+
let answered = false;
|
|
159
|
+
for (let i = lines.length - 1; i >= 0 && !info.now; i--) {
|
|
160
|
+
let o: any;
|
|
161
|
+
try {
|
|
162
|
+
o = JSON.parse(lines[i]);
|
|
163
|
+
} catch {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (o.type === "result" && !info.result) info.result = turnResult(o);
|
|
167
|
+
if (o.type === "tool_progress" && o.heartbeat) {
|
|
168
|
+
beats.push(o.elapsed_time_seconds);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (
|
|
172
|
+
o.type === "user" &&
|
|
173
|
+
Array.isArray(o.message?.content) &&
|
|
174
|
+
o.message.content.some((c: any) => c.type === "tool_result")
|
|
175
|
+
)
|
|
176
|
+
answered = true;
|
|
177
|
+
if (o.type === "assistant") {
|
|
178
|
+
/* forked stage: what the subagent does, with its model */
|
|
179
|
+
const sub = o.parent_tool_use_id ? `↳ ${shortModel(o.message?.model)} ` : "";
|
|
180
|
+
const last = (o.message?.content ?? [])
|
|
181
|
+
.filter((x: any) => x.type === "tool_use" || (x.type === "text" && x.text.trim()))
|
|
182
|
+
.at(-1);
|
|
183
|
+
if (last)
|
|
184
|
+
info.now =
|
|
185
|
+
sub +
|
|
186
|
+
(last.type === "text"
|
|
187
|
+
? last.text.trim().split("\n")[0].slice(0, 200)
|
|
188
|
+
: `▸ ${last.name} ${toolSummary(last.name, last.input ?? {})}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/* the limit: a 429 at the end, and the rejected rate_limit_event a few lines before it says when it resets */
|
|
192
|
+
if (info.result?.error && info.result.limited) {
|
|
193
|
+
for (let i = lines.length - 1; i >= Math.max(0, lines.length - 80); i--) {
|
|
194
|
+
if (!lines[i].includes('"rate_limit_event"')) continue;
|
|
195
|
+
try {
|
|
196
|
+
const e = JSON.parse(lines[i]).rate_limit_info;
|
|
197
|
+
if (e?.status === "rejected" && e.resetsAt) {
|
|
198
|
+
info.result.limited = { until: e.resetsAt * 1000 };
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
} catch {
|
|
202
|
+
/* a cut line */
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (info.now && !info.result && beats.length >= 2) {
|
|
207
|
+
const s = beats[0] - beats.at(-1)! + 30;
|
|
208
|
+
const time = s < 60 ? `${s} s` : `${Math.floor(s / 60)} min${s % 60 ? ` ${s % 60} s` : ""}`;
|
|
209
|
+
info.now += ` — ${answered ? "Pensando" : "esperando a ferramenta"} há ${time}, processo vivo (sinal a cada 30 s)`;
|
|
210
|
+
}
|
|
211
|
+
readCache.set(file, { size: st.size, mtime: st.mtimeMs, info });
|
|
212
|
+
return info;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function turnResult(o: any): TurnResult {
|
|
216
|
+
return {
|
|
217
|
+
duration_ms: o.duration_ms ?? null,
|
|
218
|
+
cost: o.total_cost_usd ?? null,
|
|
219
|
+
/* includes the spend of forked stages, each on its own model */
|
|
220
|
+
models: Object.fromEntries(
|
|
221
|
+
Object.entries(o.modelUsage ?? {}).map(([m, u]: [string, any]) => [shortModel(m), +(u.costUSD ?? 0).toFixed(4)]),
|
|
222
|
+
),
|
|
223
|
+
text:
|
|
224
|
+
typeof o.result === "string" && o.result.trim()
|
|
225
|
+
? o.result
|
|
226
|
+
: `(o turno terminou sem resposta: ${o.subtype ?? "?"})`,
|
|
227
|
+
error: !!o.is_error,
|
|
228
|
+
limited: o.is_error && (o.api_error_status === 429 || isLimitText(o.result)) ? { until: null } : null,
|
|
229
|
+
denied: (o.permission_denials ?? []).map((d: any) => {
|
|
230
|
+
const e = d.tool_input ?? {};
|
|
231
|
+
const target = e.command ?? e.file_path ?? e.url ?? "";
|
|
232
|
+
return target ? `${d.tool_name}: ${String(target).split("\n")[0].slice(0, 200)}` : d.tool_name;
|
|
233
|
+
}),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/* Codex's --json events: item.started/completed (command_execution, agent_message, file_change,
|
|
238
|
+
mcp_tool_call, web_search, todo_list, reasoning, error), then turn.completed or turn.failed */
|
|
239
|
+
function codexItemLine(it: any): string | null {
|
|
240
|
+
switch (it?.type) {
|
|
241
|
+
case "command_execution":
|
|
242
|
+
return `▸ Bash $ ${shellOf(it.command)}`;
|
|
243
|
+
case "file_change":
|
|
244
|
+
return `▸ Edit ${(it.changes ?? []).map((c: any) => rel(String(c.path ?? ""))).join(", ")}`;
|
|
245
|
+
case "mcp_tool_call":
|
|
246
|
+
return `▸ ${it.server ?? "mcp"}.${it.tool ?? "?"}`;
|
|
247
|
+
case "web_search":
|
|
248
|
+
return `▸ WebSearch ${it.query ?? ""}`;
|
|
249
|
+
case "todo_list":
|
|
250
|
+
return `▸ TodoWrite ${(it.items ?? []).length} tarefas`;
|
|
251
|
+
case "agent_message":
|
|
252
|
+
case "reasoning":
|
|
253
|
+
case "error":
|
|
254
|
+
return null;
|
|
255
|
+
default:
|
|
256
|
+
return it?.type ? `▸ ${it.type}` : null;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/* `/bin/zsh -lc 'cmd'` → cmd */
|
|
261
|
+
const shellOf = (c: unknown) =>
|
|
262
|
+
String(c ?? "")
|
|
263
|
+
.replace(/^\S*sh -lc '([\s\S]*)'$/, "$1")
|
|
264
|
+
.split("\n")[0]
|
|
265
|
+
.slice(0, 240);
|
|
266
|
+
|
|
267
|
+
function codexTurn(lines: string[], mtime: number, startedAt?: string): TurnInfo {
|
|
268
|
+
const info: TurnInfo = { result: null, now: null };
|
|
269
|
+
let said = "";
|
|
270
|
+
let lastError = "";
|
|
271
|
+
for (const line of lines) {
|
|
272
|
+
let o: any;
|
|
273
|
+
try {
|
|
274
|
+
o = JSON.parse(line);
|
|
275
|
+
} catch {
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
const it = o.item;
|
|
279
|
+
if (o.type === "item.started" || o.type === "item.completed") {
|
|
280
|
+
if (it?.type === "agent_message" && it.text?.trim()) {
|
|
281
|
+
said = it.text;
|
|
282
|
+
info.now = it.text.trim().split("\n")[0].slice(0, 200);
|
|
283
|
+
} else if (it?.type === "error") lastError = it.message ?? lastError;
|
|
284
|
+
else if (it?.type === "reasoning") info.now = "Pensando";
|
|
285
|
+
else info.now = codexItemLine(it) ?? info.now;
|
|
286
|
+
}
|
|
287
|
+
if (o.type === "error") lastError = o.message ?? lastError;
|
|
288
|
+
const duration_ms = startedAt ? Math.max(0, Math.round(mtime - Date.parse(startedAt))) : null;
|
|
289
|
+
if (o.type === "turn.completed") {
|
|
290
|
+
const u = o.usage ?? {};
|
|
291
|
+
info.result = {
|
|
292
|
+
duration_ms,
|
|
293
|
+
cost: null,
|
|
294
|
+
models: {},
|
|
295
|
+
tokens: (u.input_tokens ?? 0) + (u.output_tokens ?? 0) || null,
|
|
296
|
+
text: said.trim() ? said : "(o turno terminou sem resposta)",
|
|
297
|
+
error: false,
|
|
298
|
+
denied: [],
|
|
299
|
+
limited: null,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
if (o.type === "turn.failed") {
|
|
303
|
+
const message = String(o.error?.message ?? lastError ?? "erro");
|
|
304
|
+
info.result = {
|
|
305
|
+
duration_ms,
|
|
306
|
+
cost: null,
|
|
307
|
+
models: {},
|
|
308
|
+
text: `Erro do Codex: ${readableError(message)}`,
|
|
309
|
+
error: true,
|
|
310
|
+
denied: [],
|
|
311
|
+
limited: isLimitText(message) ? { until: codexResetOf(message) } : null,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return info;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/* the API error inside Codex's message, when it is one: `{"error":{"message":…}}` */
|
|
319
|
+
function readableError(message: string): string {
|
|
320
|
+
try {
|
|
321
|
+
const j = JSON.parse(message);
|
|
322
|
+
return String(j?.error?.message ?? j?.message ?? message);
|
|
323
|
+
} catch {
|
|
324
|
+
return message;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function stateOf(r: SessionRecord): { state: SessionState; result: TurnResult | null; now: string | null } {
|
|
329
|
+
const t = r.turns.at(-1);
|
|
330
|
+
if (!t) return { state: "interrupted", result: null, now: null };
|
|
331
|
+
const { result, now } = turnInfo(r, t);
|
|
332
|
+
if (result) return { state: result.error ? "error" : "stopped", result, now };
|
|
333
|
+
return { state: isAlive(t.pid) ? "running" : "interrupted", result: null, now };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export function summarizeSession(r: SessionRecord) {
|
|
337
|
+
const { state, result, now } = stateOf(r);
|
|
338
|
+
const t = r.turns.at(-1);
|
|
339
|
+
const costs = r.turns.map(x => turnInfo(r, x).result?.cost ?? 0);
|
|
340
|
+
const account = t ? accountOfTurn(r, t) : null;
|
|
341
|
+
let activity = Date.parse(r.created_at);
|
|
342
|
+
try {
|
|
343
|
+
activity = statSync(join(SESSIONS_DIR, r.id, t!.file)).mtimeMs;
|
|
344
|
+
} catch {
|
|
345
|
+
/* turn has no output yet */
|
|
346
|
+
}
|
|
347
|
+
return {
|
|
348
|
+
id: r.id,
|
|
349
|
+
type: r.type,
|
|
350
|
+
target: r.target,
|
|
351
|
+
title: r.title,
|
|
352
|
+
created_at: r.created_at,
|
|
353
|
+
state,
|
|
354
|
+
activity: Math.round(activity),
|
|
355
|
+
turns: r.turns.length,
|
|
356
|
+
turn: t ? { n: t.n, origin: t.origin, prompt: t.prompt, started_at: t.started_at, pid: t.pid, result } : null,
|
|
357
|
+
now,
|
|
358
|
+
total_cost: +costs.reduce((a, b) => a + b, 0).toFixed(4),
|
|
359
|
+
agent: t ? { provider: providerOf(t), account: account?.id ?? null, label: account?.label ?? null } : null,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
export type SessionSummary = ReturnType<typeof summarizeSession>;
|
|
363
|
+
|
|
364
|
+
export function listSessions(): SessionSummary[] {
|
|
365
|
+
if (!existsSync(SESSIONS_DIR)) return [];
|
|
366
|
+
const all: SessionSummary[] = [];
|
|
367
|
+
for (const id of readdirSync(SESSIONS_DIR)) {
|
|
368
|
+
const r = readRecord(id);
|
|
369
|
+
if (r) all.push(summarizeSession(r));
|
|
370
|
+
}
|
|
371
|
+
return all.sort((a, b) => b.activity - a.activity);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export function getSession(id: string) {
|
|
375
|
+
const r = readRecord(id);
|
|
376
|
+
return r
|
|
377
|
+
? {
|
|
378
|
+
...summarizeSession(r),
|
|
379
|
+
history: r.turns.map(t => ({ ...t, result: turnInfo(r, t).result })),
|
|
380
|
+
}
|
|
381
|
+
: null;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/* the most recent session of a video or a reference */
|
|
385
|
+
export function latestSessionFor(type: SessionType, target: string): SessionSummary | null {
|
|
386
|
+
return listSessions().find(s => s.type === type && s.target === target) ?? null;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/* --- running ---------------------------------------------------------------------- */
|
|
390
|
+
const systemContext =
|
|
391
|
+
() => `Sessão aberta pelo omnivideo studio (http://127.0.0.1:5432), sem terminal: o usuário lê o que você escreve e responde pela tela, e ninguém aprova permissão. O modo de permissão é auto: o comum roda sozinho e o arriscado é negado na hora, sem volta nesta sessão.
|
|
392
|
+
- A pasta de trabalho já é a raiz do omnivideo; o Python do projeto é ${PYTHON}.
|
|
393
|
+
- Ferramentas pesadas (voz, legenda, render, som, montagem) em primeiro plano, não em segundo plano: o limite do Bash aqui é de ${Math.round(BASH_MS / 60_000)} min, e a fila da máquina espera a vez sozinha.
|
|
394
|
+
- Não confira serviços com curl: o studio mostra VoiceStudio e Premiere (o Premiere é opcional: a etapa 8 é o assemble.mjs, por ffmpeg). Se uma ferramenta falhar por serviço fora do ar, pare e diga o que abrir.
|
|
395
|
+
- Aprovação do roteiro (approval.mjs wait --script, logo depois do av-script) e das cenas (approval.mjs wait) em primeiro plano: ela segura até o prazo de aprovação automática (a contagem pausa enquanto o usuário edita) e segue sozinha; saindo com 3, chame a etapa de novo com o pedido (no roteiro, av-script com \`ajustar roteiro: «…»\`); saindo com 4, pare: o studio retoma esta sessão quando a rodada terminar.
|
|
396
|
+
Quando parar (na aprovação do roteiro ou das cenas com saída 4, num bloqueio ou na entrega), termine com uma mensagem curta: o que foi feito, o que você precisa do usuário e onde olhar no studio.`;
|
|
397
|
+
|
|
398
|
+
/* what Codex needs on top of the same context: it runs skills written for Claude Code */
|
|
399
|
+
const CODEX_NOTES = `Você é o Codex rodando as skills do omnivideo, escritas para o Claude Code:
|
|
400
|
+
- As skills do projeto estão em .agents/skills (as mesmas de .claude/skills). Os campos model, effort e context do frontmatter são do Claude: ignore.
|
|
401
|
+
- Onde uma skill ou o CLAUDE.md manda abrir um fork, um subagente ou a ferramenta Agent, use sua ferramenta de subagentes se houver; senão, faça a etapa você mesmo, em sequência.
|
|
402
|
+
- A sandbox não deixa o Chrome abrir: os comandos que renderizam (tools/render.mjs, tools/build-motion.mjs, tools/wireframe.mjs, tools/thumbnail.mjs, tools/carousel.mjs, tools/reference.mjs) pedem para rodar fora da sandbox já na primeira tentativa; o mesmo para um comando que falhar por permissão.
|
|
403
|
+
- Onde a skill fala do Claude in Chrome (sons do Epidemic), use o navegador que você tiver; se não houver, pare e diga.`;
|
|
404
|
+
|
|
405
|
+
const liveConfig = () => readJson<any>(join(ROOT, "omnivideo.config.json")) ?? CONFIG;
|
|
406
|
+
const engineArgs = (plan: Plan) =>
|
|
407
|
+
plan.engine.kind === "new" ? (plan.engine.id ? ["--session-id", plan.engine.id] : []) : ["--resume", plan.engine.id];
|
|
408
|
+
|
|
409
|
+
function claudeArgs(r: SessionRecord, plan: Plan): string[] {
|
|
410
|
+
const allow: string[] = (CLAUDE.allow ?? []).map((p: string) => p.replaceAll("{python}", PYTHON));
|
|
411
|
+
return [
|
|
412
|
+
Bun.which(CLAUDE.bin ?? "claude") ?? CLAUDE.bin ?? "claude",
|
|
413
|
+
"-p",
|
|
414
|
+
plan.prompt,
|
|
415
|
+
...engineArgs(plan),
|
|
416
|
+
"--output-format",
|
|
417
|
+
"stream-json",
|
|
418
|
+
"--verbose",
|
|
419
|
+
/* reread every turn; `auto`: the classifier approves the usual and denies the risky,
|
|
420
|
+
and with --permission-prompts none nobody needs to answer */
|
|
421
|
+
"--permission-mode",
|
|
422
|
+
liveConfig()?.studio?.claude?.permission_mode ?? "auto",
|
|
423
|
+
"--permission-prompts",
|
|
424
|
+
"none",
|
|
425
|
+
"--add-dir",
|
|
426
|
+
LIBRARY_DIR,
|
|
427
|
+
/* sound effects only from Epidemic, captured in Chrome (skill av-motion, Sound) */
|
|
428
|
+
"--chrome",
|
|
429
|
+
"--append-system-prompt",
|
|
430
|
+
systemContext(),
|
|
431
|
+
/* orchestrator or reference: `models` in the config, reread every turn; forked
|
|
432
|
+
stages bring their own model in the skill frontmatter */
|
|
433
|
+
...(m => [...(m?.model ? ["--model", m.model] : []), ...(m?.effort ? ["--effort", m.effort] : [])])(
|
|
434
|
+
sessionModel(r.type),
|
|
435
|
+
),
|
|
436
|
+
...(CLAUDE.budget_usd ? ["--max-budget-usd", String(CLAUDE.budget_usd)] : []),
|
|
437
|
+
/* variadic: goes last */
|
|
438
|
+
...(allow.length ? ["--allowedTools", ...allow] : []),
|
|
439
|
+
];
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/* `codex exec` (or `exec resume <thread>`): the same context as developer instructions, CLAUDE.md as
|
|
443
|
+
the project doc, and a sandbox that writes the project and the library, with network. "auto": what
|
|
444
|
+
needs to leave the sandbox (Chrome) asks, and Codex's reviewer decides, like Claude's auto mode;
|
|
445
|
+
"full": no sandbox at all (agents.codex.sandbox). */
|
|
446
|
+
function codexArgs(plan: Plan): string[] {
|
|
447
|
+
const cfg = liveConfig();
|
|
448
|
+
const c = codexSettings(cfg);
|
|
449
|
+
const sandbox =
|
|
450
|
+
c.sandbox === "full"
|
|
451
|
+
? ["--dangerously-bypass-approvals-and-sandbox"]
|
|
452
|
+
: [
|
|
453
|
+
"-c",
|
|
454
|
+
'approval_policy="on-request"',
|
|
455
|
+
"-c",
|
|
456
|
+
'approvals_reviewer="auto_review"',
|
|
457
|
+
"-c",
|
|
458
|
+
'sandbox_mode="workspace-write"',
|
|
459
|
+
"-c",
|
|
460
|
+
"sandbox_workspace_write.network_access=true",
|
|
461
|
+
"-c",
|
|
462
|
+
`sandbox_workspace_write.writable_roots=${JSON.stringify([LIBRARY_DIR])}`,
|
|
463
|
+
];
|
|
464
|
+
const opts = [
|
|
465
|
+
"--json",
|
|
466
|
+
"--skip-git-repo-check",
|
|
467
|
+
"-m",
|
|
468
|
+
c.model,
|
|
469
|
+
...(c.effort ? ["-c", `model_reasoning_effort="${c.effort}"`] : []),
|
|
470
|
+
"-c",
|
|
471
|
+
'project_doc_fallback_filenames=["CLAUDE.md"]',
|
|
472
|
+
"-c",
|
|
473
|
+
`developer_instructions=${JSON.stringify(`${systemContext()}\n${CODEX_NOTES}`)}`,
|
|
474
|
+
...sandbox,
|
|
475
|
+
];
|
|
476
|
+
/* a prompt that starts with "-" would read as a flag */
|
|
477
|
+
const prompt = plan.prompt.startsWith("-") ? ` ${plan.prompt}` : plan.prompt;
|
|
478
|
+
return [
|
|
479
|
+
codexBin(cfg),
|
|
480
|
+
"exec",
|
|
481
|
+
...(plan.engine.kind === "resume" ? ["resume", ...opts, plan.engine.id] : opts),
|
|
482
|
+
prompt,
|
|
483
|
+
];
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/* a studio opened from inside a chat inherits that session's variables: the account's dir is set
|
|
487
|
+
here and nothing else is kept (tools/lib/agents.mjs envFor) */
|
|
488
|
+
const agentEnv = (a: Account) =>
|
|
489
|
+
envFor(a, process.env, { BASH_MAX_TIMEOUT_MS: String(BASH_MS), BASH_DEFAULT_TIMEOUT_MS: String(BASH_MS) });
|
|
490
|
+
|
|
491
|
+
/* the account a turn ran on; a turn from before the accounts ran on the session's config_dir */
|
|
492
|
+
function accountOfTurn(r: SessionRecord, t: Turn): Account | null {
|
|
493
|
+
const all = accountsOf(liveConfig());
|
|
494
|
+
if (t.account) return all.find(a => a.id === t.account) ?? null;
|
|
495
|
+
const dir = r.config_dir === undefined ? CLAUDE_PERSONAL_DIR : r.config_dir;
|
|
496
|
+
return all.find(a => a.provider === "claude" && a.dir === dir) ?? null;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/* Codex's thread id: the first event of the turn's file */
|
|
500
|
+
function codexThread(r: SessionRecord, t: Turn): string | null {
|
|
501
|
+
const file = join(SESSIONS_DIR, r.id, t.file);
|
|
502
|
+
try {
|
|
503
|
+
const fd = openSync(file, "r");
|
|
504
|
+
const buf = Buffer.alloc(4096);
|
|
505
|
+
try {
|
|
506
|
+
readSync(fd, buf, 0, buf.length, 0);
|
|
507
|
+
} finally {
|
|
508
|
+
closeSync(fd);
|
|
509
|
+
}
|
|
510
|
+
return /"thread_id":"([^"]+)"/.exec(buf.toString("utf8"))?.[1] ?? null;
|
|
511
|
+
} catch {
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const engineOf = (r: SessionRecord, t: Turn) =>
|
|
517
|
+
providerOf(t) === "claude" ? (t.engine_id ?? r.id) : codexThread(r, t);
|
|
518
|
+
|
|
519
|
+
type Plan = {
|
|
520
|
+
account: Account;
|
|
521
|
+
engine: { kind: "new"; id?: string } | { kind: "resume"; id: string };
|
|
522
|
+
prompt: string;
|
|
523
|
+
handoff: boolean;
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
/* Where the next turn runs. The account of the last turn while it is free; otherwise the next one of
|
|
527
|
+
the rotation: Claude after Claude resumes the same conversation (its transcript copied over),
|
|
528
|
+
anything else opens a new one that gets the story so far. */
|
|
529
|
+
function planTurn(r: SessionRecord, prompt: string, exclude: string[] = []): Plan {
|
|
530
|
+
const cfg = liveConfig();
|
|
531
|
+
const last = r.turns.at(-1);
|
|
532
|
+
const prev = last ? accountOfTurn(r, last) : null;
|
|
533
|
+
const prevEngine = last ? engineOf(r, last) : null;
|
|
534
|
+
if (prev && prevEngine && !exclude.includes(prev.id) && isReady(ROOT, prev))
|
|
535
|
+
return { account: prev, engine: { kind: "resume", id: prevEngine }, prompt, handoff: false };
|
|
536
|
+
const a: Account | null = pickAccount(ROOT, cfg, { exclude });
|
|
537
|
+
if (!a) throw new Error(noAccountReason(ROOT, cfg));
|
|
538
|
+
const fresh = (p: string, handoff: boolean): Plan => ({
|
|
539
|
+
account: a,
|
|
540
|
+
engine: { kind: "new", id: a.provider === "claude" ? (last ? crypto.randomUUID() : r.id) : undefined },
|
|
541
|
+
prompt: p,
|
|
542
|
+
handoff,
|
|
543
|
+
});
|
|
544
|
+
if (!last) return fresh(prompt, false);
|
|
545
|
+
if (a.provider === "claude" && providerOf(last) === "claude" && prev && prevEngine) {
|
|
546
|
+
if (copyClaudeSession(prev, a, ROOT, prevEngine))
|
|
547
|
+
return { account: a, engine: { kind: "resume", id: prevEngine }, prompt, handoff: false };
|
|
548
|
+
}
|
|
549
|
+
return fresh(handoffPrompt(r, prompt, prev), true);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const clip = (s: string, n: number) => (s.length > n ? `${s.slice(0, n)}…` : s);
|
|
553
|
+
|
|
554
|
+
/* the story so far, for a new conversation on another account (prose to the agent: Portuguese) */
|
|
555
|
+
function handoffPrompt(r: SessionRecord, prompt: string, prev: Account | null): string {
|
|
556
|
+
const first = r.turns[0];
|
|
557
|
+
const recent = r.turns
|
|
558
|
+
.slice(-4)
|
|
559
|
+
.map(t => {
|
|
560
|
+
const res = turnInfo(r, t).result;
|
|
561
|
+
return `- ${ORIGIN_LABEL[t.origin] ?? t.origin}: «${clip(t.request ?? t.prompt, 1200)}»\n ${
|
|
562
|
+
res ? `resposta: «${clip(res.text, 1500)}»` : "(turno interrompido)"
|
|
563
|
+
}`;
|
|
564
|
+
})
|
|
565
|
+
.join("\n");
|
|
566
|
+
const where = r.type === "video" ? ` (rode ${PYTHON} tools/omnivideo.py status ${rel(r.dir)})` : "";
|
|
567
|
+
return `Esta sessão do omnivideo vinha rodando em outra conta de agente${
|
|
568
|
+
prev ? ` (${PROVIDER_LABEL[prev.provider]}, ${prev.label})` : ""
|
|
569
|
+
}, que não pode seguir: bateu o limite de uso ou saiu da lista. Você continua o trabalho numa conversa nova, sem o histórico dela.
|
|
570
|
+
Pedido que abriu a sessão: «${clip(first?.request ?? first?.prompt ?? "", 3000)}»
|
|
571
|
+
Últimos turnos:
|
|
572
|
+
${recent}
|
|
573
|
+
O estado real está nos arquivos${where}: confira antes de agir e não refaça o que já está pronto.
|
|
574
|
+
|
|
575
|
+
Agora: ${prompt}`;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
let onChange: () => void = () => {};
|
|
579
|
+
export function onSessionsChange(fn: () => void) {
|
|
580
|
+
onChange = fn;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/* what to check when a turn ends (the scenes approval decided in the middle of it) */
|
|
584
|
+
const afterTurn: ((type: SessionType, target: string) => void)[] = [];
|
|
585
|
+
export function onTurnEnd(fn: (type: SessionType, target: string) => void) {
|
|
586
|
+
afterTurn.push(fn);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function startTurn(
|
|
590
|
+
r: SessionRecord,
|
|
591
|
+
prompt: string,
|
|
592
|
+
origin: string,
|
|
593
|
+
opts: { request?: string; exclude?: string[] } = {},
|
|
594
|
+
) {
|
|
595
|
+
const plan = planTurn(r, prompt, opts.exclude);
|
|
596
|
+
const n = r.turns.length + 1;
|
|
597
|
+
const file = `turn-${n}.jsonl`;
|
|
598
|
+
const fd = openSync(join(SESSIONS_DIR, r.id, file), "a");
|
|
599
|
+
let p: ReturnType<typeof Bun.spawn>;
|
|
600
|
+
try {
|
|
601
|
+
p = Bun.spawn(plan.account.provider === "claude" ? claudeArgs(r, plan) : codexArgs(plan), {
|
|
602
|
+
cwd: ROOT,
|
|
603
|
+
stdin: "ignore",
|
|
604
|
+
stdout: fd,
|
|
605
|
+
stderr: fd,
|
|
606
|
+
env: agentEnv(plan.account),
|
|
607
|
+
detached: true,
|
|
608
|
+
});
|
|
609
|
+
} finally {
|
|
610
|
+
closeSync(fd);
|
|
611
|
+
}
|
|
612
|
+
p.unref();
|
|
613
|
+
r.turns.push({
|
|
614
|
+
n,
|
|
615
|
+
origin,
|
|
616
|
+
prompt,
|
|
617
|
+
pid: p.pid,
|
|
618
|
+
started_at: new Date().toISOString(),
|
|
619
|
+
file,
|
|
620
|
+
account: plan.account.id,
|
|
621
|
+
provider: plan.account.provider,
|
|
622
|
+
...(plan.account.provider === "claude" ? { engine_id: plan.engine.id } : {}),
|
|
623
|
+
...(plan.handoff ? { handoff: true } : {}),
|
|
624
|
+
...(opts.request ? { request: opts.request } : {}),
|
|
625
|
+
});
|
|
626
|
+
writeRecord(r);
|
|
627
|
+
p.exited.then(() => {
|
|
628
|
+
failover(r.id, n);
|
|
629
|
+
onChange();
|
|
630
|
+
for (const fn of afterTurn) {
|
|
631
|
+
try {
|
|
632
|
+
fn(r.type, r.target);
|
|
633
|
+
} catch {
|
|
634
|
+
/* one listener does not take the studio down */
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/* The turn ended on the account's limit: the account is set aside until it resets and the same
|
|
641
|
+
request carries on in the next free one. A chain of failovers stops once every account had its
|
|
642
|
+
turn; with none free the session stays stopped on the limit message. */
|
|
643
|
+
function failover(id: string, n: number) {
|
|
644
|
+
const r = readRecord(id);
|
|
645
|
+
const t = r?.turns.at(-1);
|
|
646
|
+
if (!r || !t || t.n !== n) return;
|
|
647
|
+
const res = turnInfo(r, t).result;
|
|
648
|
+
if (!res?.limited) return;
|
|
649
|
+
const acc = accountOfTurn(r, t);
|
|
650
|
+
if (acc) markLimited(ROOT, acc.id, res.limited.until, res.text.slice(0, 200));
|
|
651
|
+
let chain = 0;
|
|
652
|
+
for (let i = r.turns.length - 1; i >= 0 && r.turns[i].origin === "failover"; i--) chain++;
|
|
653
|
+
if (chain >= accountsOf(liveConfig()).length) return;
|
|
654
|
+
const request = t.request ?? t.prompt;
|
|
655
|
+
try {
|
|
656
|
+
startTurn(
|
|
657
|
+
r,
|
|
658
|
+
`A conta anterior bateu o limite de uso no meio deste pedido. Continue de onde parou, sem refazer o que já ficou pronto: «${request}»`,
|
|
659
|
+
"failover",
|
|
660
|
+
{ request, exclude: acc ? [acc.id] : [] },
|
|
661
|
+
);
|
|
662
|
+
} catch {
|
|
663
|
+
/* no account free: the turn's own answer says the limit */
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
export type ApiResult = { ok: boolean; output?: string; error?: string; session?: string };
|
|
668
|
+
|
|
669
|
+
export function openSession(
|
|
670
|
+
type: SessionType,
|
|
671
|
+
target: string,
|
|
672
|
+
dir: string,
|
|
673
|
+
title: string,
|
|
674
|
+
prompt: string,
|
|
675
|
+
origin = "request",
|
|
676
|
+
): ApiResult {
|
|
677
|
+
const active = latestSessionFor(type, target);
|
|
678
|
+
if (active?.state === "running")
|
|
679
|
+
return {
|
|
680
|
+
ok: false,
|
|
681
|
+
error: "o agente já está trabalhando aqui: espere o turno terminar ou pare a sessão",
|
|
682
|
+
session: active.id,
|
|
683
|
+
};
|
|
684
|
+
const r: SessionRecord = {
|
|
685
|
+
id: crypto.randomUUID(),
|
|
686
|
+
type,
|
|
687
|
+
target,
|
|
688
|
+
dir,
|
|
689
|
+
title,
|
|
690
|
+
created_at: new Date().toISOString(),
|
|
691
|
+
turns: [],
|
|
692
|
+
};
|
|
693
|
+
mkdirSync(join(SESSIONS_DIR, r.id), { recursive: true });
|
|
694
|
+
try {
|
|
695
|
+
startTurn(r, prompt, origin);
|
|
696
|
+
} catch (e: any) {
|
|
697
|
+
rmSync(join(SESSIONS_DIR, r.id), { recursive: true, force: true });
|
|
698
|
+
return { ok: false, error: `não deu para abrir o agente: ${e.message}` };
|
|
699
|
+
}
|
|
700
|
+
return { ok: true, output: "o agente começou", session: r.id };
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
export function replyToSession(id: string, message: string, origin = "user"): ApiResult {
|
|
704
|
+
const r = readRecord(id);
|
|
705
|
+
if (!r) return { ok: false, error: "sessão não encontrada" };
|
|
706
|
+
const text = message.trim();
|
|
707
|
+
if (!text) return { ok: false, error: "escreva a mensagem" };
|
|
708
|
+
if (stateOf(r).state === "running")
|
|
709
|
+
return { ok: false, error: "o agente ainda está no meio do turno: espere ele parar ou pare a sessão", session: id };
|
|
710
|
+
try {
|
|
711
|
+
startTurn(r, text, origin);
|
|
712
|
+
} catch (e: any) {
|
|
713
|
+
return { ok: false, error: `não deu para retomar o agente: ${e.message}` };
|
|
714
|
+
}
|
|
715
|
+
return { ok: true, output: "o agente retomou a sessão", session: id };
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/* the whole group: claude and the tools it opened */
|
|
719
|
+
export function stopSession(id: string): ApiResult {
|
|
720
|
+
const r = readRecord(id);
|
|
721
|
+
const t = r?.turns.at(-1);
|
|
722
|
+
if (!r || !t) return { ok: false, error: "sessão não encontrada" };
|
|
723
|
+
if (stateOf(r).state !== "running") return { ok: false, error: "a sessão não está rodando" };
|
|
724
|
+
try {
|
|
725
|
+
process.kill(-t.pid, "SIGTERM");
|
|
726
|
+
} catch {
|
|
727
|
+
try {
|
|
728
|
+
process.kill(t.pid, "SIGTERM");
|
|
729
|
+
} catch {
|
|
730
|
+
/* already gone */
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
return { ok: true, output: "sessão parada: o que já estava gravado continua lá" };
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/* --- transcript ------------------------------------------------------------------
|
|
737
|
+
The stream-json becomes readable text: what Claude wrote, each tool on
|
|
738
|
+
one line, tool errors and denied permissions. Successful tool results
|
|
739
|
+
are left out (they are the whole file it read). */
|
|
740
|
+
const rel = (p: string) => {
|
|
741
|
+
const r = relative(ROOT, p);
|
|
742
|
+
return r.startsWith("..") ? p : r;
|
|
743
|
+
};
|
|
744
|
+
|
|
745
|
+
function toolSummary(name: string, e: any): string {
|
|
746
|
+
switch (name) {
|
|
747
|
+
case "Bash":
|
|
748
|
+
return `$ ${String(e.command ?? "")
|
|
749
|
+
.split("\n")[0]
|
|
750
|
+
.slice(0, 240)}`;
|
|
751
|
+
case "Read":
|
|
752
|
+
case "Write":
|
|
753
|
+
case "Edit":
|
|
754
|
+
case "NotebookEdit":
|
|
755
|
+
return rel(String(e.file_path ?? e.notebook_path ?? ""));
|
|
756
|
+
case "Skill":
|
|
757
|
+
return `${e.skill ?? e.name ?? ""}${e.args ? ` ${e.args}` : ""}`;
|
|
758
|
+
case "Glob":
|
|
759
|
+
case "Grep":
|
|
760
|
+
return String(e.pattern ?? "");
|
|
761
|
+
case "WebFetch":
|
|
762
|
+
return String(e.url ?? "");
|
|
763
|
+
case "WebSearch":
|
|
764
|
+
return String(e.query ?? "");
|
|
765
|
+
case "Task":
|
|
766
|
+
case "Agent":
|
|
767
|
+
return String(e.description ?? "");
|
|
768
|
+
case "TodoWrite":
|
|
769
|
+
return `${(e.todos ?? []).length} tarefas`;
|
|
770
|
+
default:
|
|
771
|
+
return "";
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
const textOf = (c: unknown): string =>
|
|
776
|
+
typeof c === "string" ? c : Array.isArray(c) ? c.map(x => (x?.type === "text" ? x.text : "")).join("") : "";
|
|
777
|
+
|
|
778
|
+
/* claude-sonnet-5-5 → sonnet 5.5; claude-haiku-4-5-20251001 → haiku 4.5 */
|
|
779
|
+
const shortModel = (m: unknown) => {
|
|
780
|
+
const r = /^claude-([a-z]+)-(\d+)(?:-(\d{1,2}))?(?:-\d{8})?$/.exec(String(m ?? ""));
|
|
781
|
+
return r ? `${r[1]} ${r[2]}${r[3] ? `.${r[3]}` : ""}` : String(m ?? "?");
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
/* one Codex event as the same readable text */
|
|
785
|
+
function transcribeCodex(o: any): string {
|
|
786
|
+
const it = o.item;
|
|
787
|
+
if (o.type === "item.completed" && it?.type === "agent_message" && it.text?.trim()) return `\n${it.text.trim()}\n\n`;
|
|
788
|
+
if (o.type === "item.started" && it) {
|
|
789
|
+
const line = codexItemLine(it);
|
|
790
|
+
return line ? `${line}\n` : "";
|
|
791
|
+
}
|
|
792
|
+
if (o.type === "item.completed" && it?.type === "command_execution" && it.exit_code && it.exit_code !== 0)
|
|
793
|
+
return ` ✕ ${
|
|
794
|
+
String(it.aggregated_output ?? "")
|
|
795
|
+
.trim()
|
|
796
|
+
.split("\n")
|
|
797
|
+
.slice(-8)
|
|
798
|
+
.join("\n ")
|
|
799
|
+
.slice(0, 900) || `saiu com ${it.exit_code}`
|
|
800
|
+
}\n`;
|
|
801
|
+
if (
|
|
802
|
+
o.type === "item.completed" &&
|
|
803
|
+
(it?.type === "file_change" || it?.type === "web_search") &&
|
|
804
|
+
it.status === "failed"
|
|
805
|
+
)
|
|
806
|
+
return ` ✕ ${it.type} falhou\n`;
|
|
807
|
+
if (o.type === "item.completed" && it?.type === "error") return ` ⚠ ${it.message}\n`;
|
|
808
|
+
if (o.type === "error") return ` ✕ ${readableError(String(o.message ?? ""))}\n`;
|
|
809
|
+
if (o.type === "turn.completed") {
|
|
810
|
+
const u = o.usage ?? {};
|
|
811
|
+
const tokens = (u.input_tokens ?? 0) + (u.output_tokens ?? 0);
|
|
812
|
+
return `── turno terminou${tokens ? ` · ${Math.round(tokens / 1000)} mil tokens` : ""}\n`;
|
|
813
|
+
}
|
|
814
|
+
if (o.type === "turn.failed") return `── turno terminou com erro: ${readableError(String(o.error?.message ?? ""))}\n`;
|
|
815
|
+
return "";
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
export function transcribeLine(line: string, provider: Provider = "claude"): string {
|
|
819
|
+
if (!line.trim()) return "";
|
|
820
|
+
let o: any;
|
|
821
|
+
try {
|
|
822
|
+
o = JSON.parse(line);
|
|
823
|
+
} catch {
|
|
824
|
+
/* codex notes on stderr that it reads stdin (the studio gives it none) */
|
|
825
|
+
return provider === "codex" && line.startsWith("Reading additional input from stdin") ? "" : `${line}\n`;
|
|
826
|
+
}
|
|
827
|
+
if (provider === "codex") return transcribeCodex(o);
|
|
828
|
+
/* forked stage: only tools and errors, indented, with the subagent's model */
|
|
829
|
+
if (o.parent_tool_use_id) {
|
|
830
|
+
if (o.type === "assistant") {
|
|
831
|
+
return (o.message?.content ?? [])
|
|
832
|
+
.filter((c: any) => c.type === "tool_use")
|
|
833
|
+
.map((c: any) => ` ↳ ${shortModel(o.message?.model)} ▸ ${c.name} ${toolSummary(c.name, c.input ?? {})}\n`)
|
|
834
|
+
.join("");
|
|
835
|
+
}
|
|
836
|
+
if (o.type === "user" && Array.isArray(o.message?.content)) {
|
|
837
|
+
return o.message.content
|
|
838
|
+
.filter(
|
|
839
|
+
(c: any) =>
|
|
840
|
+
c.type === "tool_result" &&
|
|
841
|
+
c.is_error &&
|
|
842
|
+
!textOf(c.content).startsWith("Permission for this tool use was denied"),
|
|
843
|
+
)
|
|
844
|
+
.map(
|
|
845
|
+
(c: any) => ` ✕ ${textOf(c.content).trim().split("\n").slice(0, 4).join("\n ").slice(0, 500)}\n`,
|
|
846
|
+
)
|
|
847
|
+
.join("");
|
|
848
|
+
}
|
|
849
|
+
return "";
|
|
850
|
+
}
|
|
851
|
+
if (o.type === "assistant") {
|
|
852
|
+
return (o.message?.content ?? [])
|
|
853
|
+
.map((c: any) =>
|
|
854
|
+
c.type === "text" && c.text.trim()
|
|
855
|
+
? `\n${c.text.trim()}\n\n`
|
|
856
|
+
: c.type === "tool_use"
|
|
857
|
+
? `▸ ${c.name} ${toolSummary(c.name, c.input ?? {})}\n`
|
|
858
|
+
: "",
|
|
859
|
+
)
|
|
860
|
+
.join("");
|
|
861
|
+
}
|
|
862
|
+
if (o.type === "user" && Array.isArray(o.message?.content)) {
|
|
863
|
+
return o.message.content
|
|
864
|
+
.filter(
|
|
865
|
+
(c: any) =>
|
|
866
|
+
c.type === "tool_result" &&
|
|
867
|
+
c.is_error &&
|
|
868
|
+
!textOf(c.content).startsWith("Permission for this tool use was denied"),
|
|
869
|
+
)
|
|
870
|
+
.map((c: any) => ` ✕ ${textOf(c.content).trim().split("\n").slice(0, 8).join("\n ").slice(0, 900)}\n`)
|
|
871
|
+
.join("");
|
|
872
|
+
}
|
|
873
|
+
if (o.type === "system" && o.subtype === "init") return `· ${o.model} · permissão ${o.permissionMode}\n`;
|
|
874
|
+
if (o.type === "system" && o.subtype === "permission_denied")
|
|
875
|
+
return ` ⊘ negado: ${o.tool_name} pedia aprovação, e esta sessão não tem quem aprove\n`;
|
|
876
|
+
if (o.type === "system" && o.subtype === "compact_boundary") return "· contexto compactado\n";
|
|
877
|
+
if (o.type === "result") {
|
|
878
|
+
const f = turnResult(o);
|
|
879
|
+
const dur = f.duration_ms != null ? `${Math.round(f.duration_ms / 60_000)} min` : "";
|
|
880
|
+
const byModel = Object.entries(f.models)
|
|
881
|
+
.filter(([, c]) => c >= 0.005)
|
|
882
|
+
.map(([m, c]) => `${m} ${c.toFixed(2)}`)
|
|
883
|
+
.join(" · ");
|
|
884
|
+
const cost = f.cost != null ? `US$ ${f.cost.toFixed(2)}${byModel ? ` (${byModel})` : ""}` : "";
|
|
885
|
+
return `── ${f.error ? `turno terminou com erro (${o.subtype})` : "turno terminou"}${dur ? ` · ${dur}` : ""}${cost ? ` · ${cost}` : ""}\n`;
|
|
886
|
+
}
|
|
887
|
+
return "";
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
const ORIGIN_LABEL: Record<string, string> = {
|
|
891
|
+
request: "pedido feito na tela",
|
|
892
|
+
user: "você",
|
|
893
|
+
resume: "você, retomando",
|
|
894
|
+
/* sessions from before the preview stage was dropped: nothing produces these anymore */
|
|
895
|
+
"preview-approved": "prévia aprovada na tela",
|
|
896
|
+
"preview-rejected": "prévia reprovada na tela",
|
|
897
|
+
"scenes-approval": "cenas decididas na tela",
|
|
898
|
+
"script-approval": "roteiro decidido na tela",
|
|
899
|
+
/* the carousel requests (api/services/carousel.ts): the live log header showed the raw PT origin */
|
|
900
|
+
carousel: "carrossel",
|
|
901
|
+
/* the account hit its limit: the same request in the next one (failover) */
|
|
902
|
+
failover: "limite da conta: segue em outra",
|
|
903
|
+
};
|
|
904
|
+
const turnHeader = (r: SessionRecord, t: Turn) =>
|
|
905
|
+
`\n▶ ${ORIGIN_LABEL[t.origin] ?? t.origin} · ${new Date(t.started_at).toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" })} · ${
|
|
906
|
+
PROVIDER_LABEL[providerOf(t)]
|
|
907
|
+
}${(a => (a ? ` (${a.label})` : ""))(accountOfTurn(r, t))}${t.handoff ? " · conversa nova, com o resumo" : ""}\n` +
|
|
908
|
+
`${t.prompt.length > 900 ? t.prompt.slice(0, 900) + "…" : t.prompt}\n\n`;
|
|
909
|
+
|
|
910
|
+
/* SSE: the whole session so far, turn by turn, and what arrives in the last one.
|
|
911
|
+
Ends when the last turn stops; a new turn needs another connection. */
|
|
912
|
+
export function streamSessionLog(id: string, req: Request): Response {
|
|
913
|
+
if (!readRecord(id)) return new Response("sessão não encontrada", { status: 404 });
|
|
914
|
+
const encoder = new TextEncoder();
|
|
915
|
+
const decoder = new TextDecoder();
|
|
916
|
+
let i = 0;
|
|
917
|
+
let pos = 0;
|
|
918
|
+
let rest = new Uint8Array(0);
|
|
919
|
+
let headerSent = false;
|
|
920
|
+
let busy = false;
|
|
921
|
+
let closed = false;
|
|
922
|
+
let timer: ReturnType<typeof setInterval>;
|
|
923
|
+
const stop = () => {
|
|
924
|
+
closed = true;
|
|
925
|
+
clearInterval(timer);
|
|
926
|
+
};
|
|
927
|
+
const body = new ReadableStream({
|
|
928
|
+
start(ctrl) {
|
|
929
|
+
const send = (event: string, data: unknown) =>
|
|
930
|
+
ctrl.enqueue(encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`));
|
|
931
|
+
const check = () => {
|
|
932
|
+
if (busy || closed) return;
|
|
933
|
+
busy = true;
|
|
934
|
+
try {
|
|
935
|
+
const r = readRecord(id);
|
|
936
|
+
if (!r) {
|
|
937
|
+
stop();
|
|
938
|
+
ctrl.close();
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
let output = "";
|
|
942
|
+
while (i < r.turns.length) {
|
|
943
|
+
const t = r.turns[i];
|
|
944
|
+
if (!headerSent) {
|
|
945
|
+
output += turnHeader(r, t);
|
|
946
|
+
headerSent = true;
|
|
947
|
+
}
|
|
948
|
+
const file = join(SESSIONS_DIR, id, t.file);
|
|
949
|
+
/* state before size: a turn that has already ended here has written everything */
|
|
950
|
+
const isLast = i === r.turns.length - 1;
|
|
951
|
+
const ended = !isLast || stateOf(r).state !== "running";
|
|
952
|
+
let size = 0;
|
|
953
|
+
try {
|
|
954
|
+
size = statSync(file).size;
|
|
955
|
+
} catch {
|
|
956
|
+
/* no output yet */
|
|
957
|
+
}
|
|
958
|
+
if (size > pos) {
|
|
959
|
+
const chunk = Buffer.alloc(size - pos);
|
|
960
|
+
const fd = openSync(file, "r");
|
|
961
|
+
try {
|
|
962
|
+
readSync(fd, chunk, 0, chunk.length, pos);
|
|
963
|
+
} finally {
|
|
964
|
+
closeSync(fd);
|
|
965
|
+
}
|
|
966
|
+
pos = size;
|
|
967
|
+
const joined = new Uint8Array(rest.length + chunk.length);
|
|
968
|
+
joined.set(rest);
|
|
969
|
+
joined.set(chunk, rest.length);
|
|
970
|
+
const cut = joined.lastIndexOf(10);
|
|
971
|
+
rest = joined.slice(cut + 1);
|
|
972
|
+
if (cut >= 0) {
|
|
973
|
+
for (const line of decoder.decode(joined.slice(0, cut)).split("\n"))
|
|
974
|
+
output += transcribeLine(line, providerOf(t));
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
if (!ended) break;
|
|
978
|
+
if (rest.length) {
|
|
979
|
+
output += transcribeLine(decoder.decode(rest), providerOf(t));
|
|
980
|
+
rest = new Uint8Array(0);
|
|
981
|
+
}
|
|
982
|
+
if (!turnInfo(r, t).result) output += "── turno interrompido antes de terminar\n";
|
|
983
|
+
i++;
|
|
984
|
+
pos = 0;
|
|
985
|
+
headerSent = false;
|
|
986
|
+
}
|
|
987
|
+
if (output) send("log", output);
|
|
988
|
+
if (i >= r.turns.length) {
|
|
989
|
+
send("end", { state: stateOf(r).state });
|
|
990
|
+
stop();
|
|
991
|
+
ctrl.close();
|
|
992
|
+
}
|
|
993
|
+
} catch {
|
|
994
|
+
/* client left in the middle of a read */
|
|
995
|
+
stop();
|
|
996
|
+
} finally {
|
|
997
|
+
busy = false;
|
|
998
|
+
}
|
|
999
|
+
};
|
|
1000
|
+
timer = setInterval(check, 500);
|
|
1001
|
+
check();
|
|
1002
|
+
req.signal.addEventListener("abort", stop);
|
|
1003
|
+
},
|
|
1004
|
+
cancel: stop,
|
|
1005
|
+
});
|
|
1006
|
+
return new Response(body, { headers: { "content-type": "text/event-stream", "cache-control": "no-cache" } });
|
|
1007
|
+
}
|