@crievo/omni 0.1.2 → 0.1.4
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/docs/onboarding.md +69 -161
- package/omnivideo.config.example.json +1 -1
- package/package.json +2 -1
- package/reference/learnings.md +12 -0
- package/reference/omnichat.md +5 -4
- package/reference/publish.md +9 -9
- package/reference/studio.md +1 -1
- package/studio/api/app.ts +2 -2
- package/studio/api/routes/sound.ts +22 -3
- package/studio/api/services/onboarding.ts +242 -21
- package/studio/api/services/settings.ts +4 -2
- package/studio/api/services/sound.ts +45 -0
- package/studio/shared/types/api.ts +26 -1
- package/studio/studio.ts +1 -0
- package/studio/web/components/AppShell.tsx +3 -1
- package/studio/web/components/CloneVoiceDialog/RecordStep.tsx +3 -0
- package/studio/web/components/CloneVoiceDialog/RecorderBar.tsx +37 -20
- package/studio/web/components/CloneVoiceDialog/useCloneVoice.ts +39 -12
- package/studio/web/components/MissingDmsPanel/BatchSend.tsx +13 -4
- package/studio/web/components/Omni/OmniWidget.tsx +14 -5
- package/studio/web/components/Omni/PendingActions.tsx +25 -6
- package/studio/web/components/OmnichatRulePanel/RuleConfig.tsx +13 -3
- package/studio/web/components/OmnichatRulePanel/RuleEnableConfirm.tsx +14 -4
- package/studio/web/components/ResultMessage.tsx +12 -3
- package/studio/web/components/Sidebar.tsx +63 -24
- package/studio/web/components/StageStepper.tsx +105 -34
- package/studio/web/components/Toaster.tsx +25 -0
- package/studio/web/components/VoiceCarousel.tsx +19 -16
- package/studio/web/components/VoiceProfilesSettings.tsx +8 -3
- package/studio/web/components/bencho/ActionNode.tsx +456 -0
- package/studio/web/components/bencho/AssigneePicker.tsx +246 -0
- package/studio/web/components/bencho/BenchoScope.tsx +8 -0
- package/studio/web/components/bencho/Checklist.tsx +421 -0
- package/studio/web/components/bencho/CreateMenu.tsx +258 -0
- package/studio/web/components/bencho/GenerateImage.tsx +290 -0
- package/studio/web/components/bencho/ImageAccordion.tsx +273 -0
- package/studio/web/components/bencho/LiquidToggle.tsx +289 -0
- package/studio/web/components/bencho/MagnifyDock.tsx +200 -0
- package/studio/web/components/bencho/NowPlaying.tsx +403 -0
- package/studio/web/components/bencho/OneTimeCode.tsx +374 -0
- package/studio/web/components/bencho/PullToRefresh.tsx +527 -0
- package/studio/web/components/bencho/SlideToConfirm.tsx +385 -0
- package/studio/web/components/bencho/SloshSlider.tsx +394 -0
- package/studio/web/components/bencho/StackedBars.tsx +342 -0
- package/studio/web/components/bencho/ToastStack.tsx +263 -0
- package/studio/web/components/bencho/UploadDropzone.tsx +394 -0
- package/studio/web/components/bencho/VoiceNote.tsx +705 -0
- package/studio/web/components/icons.tsx +27 -0
- package/studio/web/components/ui/slider.tsx +34 -1
- package/studio/web/components/ui/switch.tsx +18 -5
- package/studio/web/hooks/useOnboarding.ts +24 -6
- package/studio/web/index.html +1 -1
- package/studio/web/lib/alerts.ts +10 -0
- package/studio/web/lib/omni-panel.ts +4 -1
- package/studio/web/lib/toast.ts +196 -0
- package/studio/web/lib/upload.ts +24 -0
- package/studio/web/pages/BenchoPage/index.tsx +74 -0
- package/studio/web/pages/BenchoPage/registry.ts +174 -0
- package/studio/web/pages/GettingStartedPage/components/ComputerCard.tsx +102 -0
- package/studio/web/pages/GettingStartedPage/index.tsx +84 -58
- package/studio/web/pages/NewVideoPage/index.tsx +6 -2
- package/studio/web/pages/ReferencesPage/index.tsx +4 -1
- package/studio/web/pages/SettingsPage/components/AgentAccounts.tsx +30 -3
- package/studio/web/pages/VideoPage/components/ChangedVoices.tsx +14 -17
- package/studio/web/pages/VideoPage/components/LineTextEditor.tsx +1 -1
- package/studio/web/pages/VideoPage/components/PublishForm.tsx +24 -17
- package/studio/web/pages/VideoPage/components/ScheduledInfo.tsx +2 -1
- package/studio/web/pages/VideoPage/components/SoundTimeline/AddSound.tsx +12 -14
- package/studio/web/pages/VideoPage/components/SoundTimeline/BedRow.tsx +27 -11
- package/studio/web/pages/VideoPage/components/SoundTimeline/CaptionSaveBar.tsx +22 -18
- package/studio/web/pages/VideoPage/components/SoundTimeline/TimelineFooter.tsx +3 -4
- package/studio/web/pages/VideoPage/components/SoundTimeline/UploadTrack.tsx +59 -0
- package/studio/web/pages/VideoPage/components/SoundTimeline/useSoundTimeline.ts +1 -0
- package/studio/web/pages/VideoPage/components/VoicePauseBanner.tsx +4 -6
- package/studio/web/pages/VideoPage/index.tsx +4 -0
- package/studio/web/pages/VideosPage/components/VideoRow.tsx +2 -4
- package/studio/web/router.tsx +8 -0
- package/studio/web/styles/components/bencho/action-node.css +227 -0
- package/studio/web/styles/components/bencho/checklist.css +182 -0
- package/studio/web/styles/components/bencho/dock.css +188 -0
- package/studio/web/styles/components/bencho/gallery.css +109 -0
- package/studio/web/styles/components/bencho/generate.css +151 -0
- package/studio/web/styles/components/bencho/image-accordion.css +37 -0
- package/studio/web/styles/components/bencho/liq-create.css +279 -0
- package/studio/web/styles/components/bencho/liq-toggle.css +106 -0
- package/studio/web/styles/components/bencho/one-time-code.css +148 -0
- package/studio/web/styles/components/bencho/picker.css +204 -0
- package/studio/web/styles/components/bencho/pull.css +313 -0
- package/studio/web/styles/components/bencho/scope.css +42 -0
- package/studio/web/styles/components/bencho/slide-confirm.css +139 -0
- package/studio/web/styles/components/bencho/slosh.css +137 -0
- package/studio/web/styles/components/bencho/sound.css +260 -0
- package/studio/web/styles/components/bencho/stacked-bars.css +334 -0
- package/studio/web/styles/components/bencho/toast-stack.css +192 -0
- package/studio/web/styles/components/bencho/tokens.css +243 -0
- package/studio/web/styles/components/bencho/upload-dropzone.css +352 -0
- package/studio/web/styles/components/bencho/voice-note.css +337 -0
- package/studio/web/styles/components/clone-voice.css +12 -2
- package/studio/web/styles/components/omni.css +6 -1
- package/studio/web/styles/components/stepper.css +123 -7
- package/studio/web/styles/components/toaster.css +10 -0
- package/studio/web/styles/components/ui/slider.css +6 -16
- package/studio/web/styles/components/ui/switch.css +2 -41
- package/studio/web/styles/components/voice-carousel.css +46 -4
- package/studio/web/styles/index.css +23 -0
- package/studio/web/styles/pages/getting-started.css +46 -0
- package/studio/web/styles/pages/settings/shell.css +4 -0
- package/studio/web/styles/pages/video/layout.css +1 -4
- package/studio/web/styles/pages/video/sound-timeline.css +8 -0
- package/studio/web/styles/theme/identity/shell.css +10 -26
- package/tools/lib/agents.mjs +24 -13
- package/tools/lib/claude-settings.mjs +32 -3
- package/tools/lib/deps.mjs +24 -0
- package/tools/lib/platforms/instagram.mjs +1 -1
- package/tools/lib/up.mjs +5 -2
- package/tools/omni.mjs +7 -5
- package/tools/omnichat.mjs +5 -7
- package/tools/package.mjs +5 -3
- package/tools/publish.mjs +40 -137
- package/tools/reference.mjs +1 -1
- package/tools/setup.mjs +16 -15
- package/tools/sound.mjs +67 -3
|
@@ -114,3 +114,26 @@
|
|
|
114
114
|
@import "./pages/agent.css";
|
|
115
115
|
@import "./components/omni.css";
|
|
116
116
|
@import "./components/omni-cards.css";
|
|
117
|
+
|
|
118
|
+
/* Bencho ports (the /bencho gallery): its own palette, the page, then one file per component */
|
|
119
|
+
@import "./components/bencho/tokens.css";
|
|
120
|
+
@import "./components/bencho/scope.css";
|
|
121
|
+
@import "./components/bencho/gallery.css";
|
|
122
|
+
@import "./components/bencho/pull.css";
|
|
123
|
+
@import "./components/bencho/liq-toggle.css";
|
|
124
|
+
@import "./components/bencho/checklist.css";
|
|
125
|
+
@import "./components/bencho/sound.css";
|
|
126
|
+
@import "./components/bencho/dock.css";
|
|
127
|
+
@import "./components/bencho/slosh.css";
|
|
128
|
+
@import "./components/bencho/liq-create.css";
|
|
129
|
+
@import "./components/bencho/picker.css";
|
|
130
|
+
@import "./components/bencho/slide-confirm.css";
|
|
131
|
+
@import "./components/bencho/image-accordion.css";
|
|
132
|
+
@import "./components/bencho/action-node.css";
|
|
133
|
+
@import "./components/bencho/generate.css";
|
|
134
|
+
@import "./components/bencho/one-time-code.css";
|
|
135
|
+
@import "./components/bencho/upload-dropzone.css";
|
|
136
|
+
@import "./components/bencho/voice-note.css";
|
|
137
|
+
@import "./components/bencho/toast-stack.css";
|
|
138
|
+
@import "./components/toaster.css";
|
|
139
|
+
@import "./components/bencho/stacked-bars.css";
|
|
@@ -164,3 +164,49 @@
|
|
|
164
164
|
padding-left: 16px;
|
|
165
165
|
}
|
|
166
166
|
}
|
|
167
|
+
|
|
168
|
+
/* --- "Este computador": what the CLI is still preparing, live -------------------------------- */
|
|
169
|
+
.onboarding-computer {
|
|
170
|
+
margin: 16px 0 4px;
|
|
171
|
+
padding: 14px 16px 16px;
|
|
172
|
+
border-radius: var(--radius);
|
|
173
|
+
background: var(--surface);
|
|
174
|
+
}
|
|
175
|
+
.onboarding-computer.ok {
|
|
176
|
+
background: var(--ok-bg);
|
|
177
|
+
}
|
|
178
|
+
.onboarding-computer > header {
|
|
179
|
+
display: flex;
|
|
180
|
+
align-items: center;
|
|
181
|
+
gap: 10px;
|
|
182
|
+
margin-bottom: 10px;
|
|
183
|
+
}
|
|
184
|
+
.onboarding-computer h2 {
|
|
185
|
+
margin: 0;
|
|
186
|
+
font-size: 15px;
|
|
187
|
+
font-weight: 600;
|
|
188
|
+
}
|
|
189
|
+
.onboarding-computer > p {
|
|
190
|
+
margin: 0 0 12px;
|
|
191
|
+
font-size: 13px;
|
|
192
|
+
}
|
|
193
|
+
.onboarding-computer .badge .spinning {
|
|
194
|
+
width: 10px;
|
|
195
|
+
height: 10px;
|
|
196
|
+
border-width: 1.5px;
|
|
197
|
+
}
|
|
198
|
+
.onboarding-computer .onboarding-items li.preparing .onboarding-item-mark {
|
|
199
|
+
background: none;
|
|
200
|
+
}
|
|
201
|
+
.onboarding-computer .onboarding-items li.preparing .spinning {
|
|
202
|
+
width: 12px;
|
|
203
|
+
height: 12px;
|
|
204
|
+
}
|
|
205
|
+
.onboarding-ask {
|
|
206
|
+
padding: 10px 12px;
|
|
207
|
+
border-radius: var(--radius-sm);
|
|
208
|
+
background: var(--warning-bg);
|
|
209
|
+
}
|
|
210
|
+
.onboarding-ask q {
|
|
211
|
+
font-weight: 600;
|
|
212
|
+
}
|
|
@@ -41,10 +41,7 @@
|
|
|
41
41
|
padding: 4px 24px;
|
|
42
42
|
background: none;
|
|
43
43
|
border-bottom: 1px solid var(--border);
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
.rail .stepper {
|
|
47
|
-
min-width: 900px;
|
|
44
|
+
/* no scroll box: the stage cards' fans hang below the rail and a scroller would clip them; the cards wrap instead */
|
|
48
45
|
}
|
|
49
46
|
/* sentinel at the rail's natural position (takes no space): when it leaves through the top, the rail
|
|
50
47
|
is stuck and gets the panel background */
|
|
@@ -244,6 +244,9 @@
|
|
|
244
244
|
position: absolute;
|
|
245
245
|
left: 6px;
|
|
246
246
|
top: 4px;
|
|
247
|
+
display: flex;
|
|
248
|
+
align-items: center;
|
|
249
|
+
gap: 6px;
|
|
247
250
|
}
|
|
248
251
|
.sound-header {
|
|
249
252
|
position: absolute;
|
|
@@ -670,3 +673,8 @@
|
|
|
670
673
|
border-top-color: transparent;
|
|
671
674
|
animation: spin 0.8s linear infinite;
|
|
672
675
|
}
|
|
676
|
+
.sound-add-buttons {
|
|
677
|
+
display: flex;
|
|
678
|
+
flex-wrap: wrap;
|
|
679
|
+
gap: 6px;
|
|
680
|
+
}
|
|
@@ -89,44 +89,28 @@ main.with-sidebar {
|
|
|
89
89
|
background: var(--el-nav-active-bg);
|
|
90
90
|
color: var(--el-nav-active-text);
|
|
91
91
|
}
|
|
92
|
-
/* the "Criar
|
|
92
|
+
/* the "Criar" pill (components/bencho/CreateMenu) inside the Conteúdos row: it sits over the link at the right edge, as a
|
|
93
|
+
sibling (a button inside <a> is not valid HTML), and its menu hangs from the pill's top right corner. Out of flow,
|
|
94
|
+
centred on the row by margin rather than a transform: a transformed box would be a stacking context of its own. */
|
|
93
95
|
.menu .menu-item-with-action {
|
|
94
96
|
position: relative;
|
|
95
97
|
display: flex;
|
|
96
98
|
flex-direction: column;
|
|
97
99
|
}
|
|
98
100
|
.menu .menu-item-with-action > a {
|
|
99
|
-
padding-right:
|
|
101
|
+
padding-right: 100px;
|
|
100
102
|
}
|
|
101
|
-
.menu .menu-
|
|
103
|
+
.menu .menu-create {
|
|
102
104
|
position: absolute;
|
|
103
105
|
top: 50%;
|
|
104
|
-
right:
|
|
105
|
-
|
|
106
|
-
display: inline-flex;
|
|
107
|
-
align-items: center;
|
|
108
|
-
gap: 4px;
|
|
109
|
-
height: 24px;
|
|
110
|
-
padding: 0 8px 0 6px;
|
|
111
|
-
font: 500 12px / 1 var(--sans);
|
|
112
|
-
border: 0;
|
|
113
|
-
border-radius: var(--el-radius-nav);
|
|
114
|
-
background: transparent;
|
|
115
|
-
color: var(--text-2);
|
|
116
|
-
cursor: pointer;
|
|
117
|
-
}
|
|
118
|
-
.menu .menu-action svg {
|
|
119
|
-
width: 12px;
|
|
120
|
-
height: 12px;
|
|
121
|
-
}
|
|
122
|
-
.menu .menu-action:hover,
|
|
123
|
-
.menu .menu-action:focus-visible {
|
|
124
|
-
background: var(--surface-2);
|
|
125
|
-
color: var(--text);
|
|
106
|
+
right: 3px;
|
|
107
|
+
margin-top: calc(var(--pill-h) / -2);
|
|
126
108
|
}
|
|
127
109
|
@media (max-width: 900px) {
|
|
128
110
|
.menu {
|
|
129
111
|
width: auto;
|
|
112
|
+
/* the bar has no fixed height to scroll in, and the create menu hangs below it */
|
|
113
|
+
overflow: visible;
|
|
130
114
|
}
|
|
131
115
|
.brand {
|
|
132
116
|
margin-bottom: 0;
|
|
@@ -213,7 +197,7 @@ main.with-sidebar {
|
|
|
213
197
|
}
|
|
214
198
|
.menu-collapsed .brand-omni,
|
|
215
199
|
.menu-collapsed .menu-label,
|
|
216
|
-
.menu-collapsed .menu-
|
|
200
|
+
.menu-collapsed .menu-create {
|
|
217
201
|
display: none;
|
|
218
202
|
}
|
|
219
203
|
.menu-collapsed .menu-group-name {
|
package/tools/lib/agents.mjs
CHANGED
|
@@ -8,20 +8,28 @@
|
|
|
8
8
|
a session stays on the account it runs on while that one is free.
|
|
9
9
|
- Limit: an account that hits its usage limit is set aside until it resets
|
|
10
10
|
(`.accounts/state.json`), and the work goes on in the next free one.
|
|
11
|
-
-
|
|
12
|
-
|
|
11
|
+
- Empty list: the Claude Code this machine uses, the folder the plugin's hook
|
|
12
|
+
recorded (claude-settings.mjs), else ~/.claude. Without a login there the
|
|
13
|
+
studio says so and the user puts the right folder in Settings. A machine
|
|
14
|
+
that must stay off ~/.claude (a work account) lists its accounts.
|
|
15
|
+
- ~/.claude is Claude Code's own folder: its login is ~/.claude.json and the
|
|
16
|
+
child runs without CLAUDE_CONFIG_DIR (set to it, Claude Code looks for
|
|
17
|
+
another keychain entry and finds no login).
|
|
13
18
|
============================================================================ */
|
|
14
19
|
|
|
15
20
|
import { cpSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
16
21
|
import { homedir } from "node:os";
|
|
17
22
|
import { delimiter, join, resolve } from "node:path";
|
|
23
|
+
import { DEFAULT_CLAUDE_DIR, recordedClaudeDir } from "./claude-settings.mjs";
|
|
18
24
|
|
|
19
25
|
export const PROVIDERS = ["claude", "codex"];
|
|
20
26
|
export const PROVIDER_LABEL = { claude: "Claude", codex: "Codex" };
|
|
21
|
-
/* the
|
|
22
|
-
export const
|
|
23
|
-
|
|
24
|
-
|
|
27
|
+
/* with no list: the Claude Code this machine uses */
|
|
28
|
+
export const defaultAccounts = () => [
|
|
29
|
+
{ id: "claude", provider: "claude", dir: recordedClaudeDir() ?? DEFAULT_CLAUDE_DIR },
|
|
30
|
+
];
|
|
31
|
+
/* Claude Code's own folder, which runs without CLAUDE_CONFIG_DIR */
|
|
32
|
+
const isDefaultClaude = a => a.provider === "claude" && expandHome(a.dir) === DEFAULT_CLAUDE_DIR;
|
|
25
33
|
/* the Codex CLI the ChatGPT app ships, when there is no `codex` on the PATH */
|
|
26
34
|
export const CODEX_APP_BIN = "/Applications/ChatGPT.app/Contents/Resources/codex-cli/bin/codex";
|
|
27
35
|
export const CODEX_DEFAULTS = { model: "gpt-6-luna", effort: "medium", sandbox: "auto" };
|
|
@@ -70,8 +78,6 @@ export function accountProblem(a) {
|
|
|
70
78
|
if (!PROVIDERS.includes(a?.provider)) return `provedor desconhecido: ${a?.provider}`;
|
|
71
79
|
if (!String(a.dir ?? "").trim()) return "falta a pasta da conta";
|
|
72
80
|
const dir = expandHome(a.dir);
|
|
73
|
-
if (dir === FORBIDDEN_DIR || dir.startsWith(`${FORBIDDEN_DIR}/`))
|
|
74
|
-
return "o ~/.claude é a conta padrão do Claude Code (pode ser a do trabalho): o omnivideo não roda nela";
|
|
75
81
|
if (dir === homedir()) return "a pasta da conta não pode ser a home";
|
|
76
82
|
return null;
|
|
77
83
|
}
|
|
@@ -84,12 +90,13 @@ const slug = s =>
|
|
|
84
90
|
.replace(/^-|-$/g, "") || "conta";
|
|
85
91
|
|
|
86
92
|
/**
|
|
87
|
-
* The accounts in order, normalized: `dir` absolute, `id` unique, the
|
|
88
|
-
*
|
|
93
|
+
* The accounts in order, normalized: `dir` absolute, `id` unique, the invalid ones left out. `implicit`:
|
|
94
|
+
* the default account, from an empty list.
|
|
95
|
+
* @returns {{ id: string, provider: "claude"|"codex", dir: string, label: string, enabled: boolean, implicit: boolean }[]}
|
|
89
96
|
*/
|
|
90
97
|
export function accountsOf(cfg) {
|
|
91
|
-
const
|
|
92
|
-
|
|
98
|
+
const listed = Array.isArray(cfg?.agents?.accounts) && cfg.agents.accounts.length > 0;
|
|
99
|
+
const raw = listed ? cfg.agents.accounts : defaultAccounts();
|
|
93
100
|
const seen = new Set();
|
|
94
101
|
const out = [];
|
|
95
102
|
for (const a of raw) {
|
|
@@ -105,6 +112,7 @@ export function accountsOf(cfg) {
|
|
|
105
112
|
dir,
|
|
106
113
|
label: String(a.label ?? "").trim() || tildify(dir),
|
|
107
114
|
enabled: a.enabled !== false,
|
|
115
|
+
implicit: !listed,
|
|
108
116
|
});
|
|
109
117
|
}
|
|
110
118
|
return out;
|
|
@@ -151,7 +159,8 @@ const jwtPayload = token => {
|
|
|
151
159
|
*/
|
|
152
160
|
export function loginOf(a) {
|
|
153
161
|
if (a.provider === "claude") {
|
|
154
|
-
const
|
|
162
|
+
const file = isDefaultClaude(a) ? join(homedir(), ".claude.json") : join(a.dir, ".claude.json");
|
|
163
|
+
const o = readJson(file)?.oauthAccount;
|
|
155
164
|
if (!o) return null;
|
|
156
165
|
const org =
|
|
157
166
|
o.organizationName && o.organizationName !== `${o.emailAddress}'s Organization` ? o.organizationName : null;
|
|
@@ -171,6 +180,7 @@ export function loginOf(a) {
|
|
|
171
180
|
/* the command that logs an account in, for the screen and the CLI hints */
|
|
172
181
|
export function loginCommand(a, cfg) {
|
|
173
182
|
const dir = tildify(a.dir);
|
|
183
|
+
if (isDefaultClaude(a)) return "claude auth login";
|
|
174
184
|
return a.provider === "claude"
|
|
175
185
|
? `CLAUDE_CONFIG_DIR=${dir} claude auth login`
|
|
176
186
|
: `CODEX_HOME=${dir} ${codexBin(cfg) === "codex" ? "codex" : `"${codexBin(cfg)}"`} login`;
|
|
@@ -194,6 +204,7 @@ export const binOf = (a, cfg) => (a.provider === "claude" ? claudeBin(cfg) : cod
|
|
|
194
204
|
export function envFor(a, base = process.env, extra = {}) {
|
|
195
205
|
const env = { ...base, ...extra };
|
|
196
206
|
for (const k of INHERITED) delete env[k];
|
|
207
|
+
if (isDefaultClaude(a)) return env;
|
|
197
208
|
if (a.provider === "claude") env.CLAUDE_CONFIG_DIR = a.dir;
|
|
198
209
|
else env.CODEX_HOME = a.dir;
|
|
199
210
|
return env;
|
|
@@ -3,15 +3,44 @@
|
|
|
3
3
|
call `<cli> run …` through Bash, and their sibling files sit in a temporary folder outside the working
|
|
4
4
|
one. Written only with the user's yes (the MCP's omni_allow_skills, after Claude asks, or `omni setup`).
|
|
5
5
|
|
|
6
|
-
The settings file is
|
|
7
|
-
|
|
6
|
+
The settings file is settings.json in the Claude Code folder (claudeConfigDir). The rules are added to
|
|
7
|
+
permissions.allow; nothing else in the file changes.
|
|
8
|
+
|
|
9
|
+
The Claude Code folder is $CLAUDE_CONFIG_DIR, else the one `omni up --hook` recorded in ~/.omni/claude.json,
|
|
10
|
+
else ~/.claude. The hook runs inside the user's session, with its CLAUDE_CONFIG_DIR; the studio service
|
|
11
|
+
runs outside it and only knows the folder through that record. */
|
|
8
12
|
|
|
9
13
|
import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, writeFileSync } from "node:fs";
|
|
10
14
|
import { homedir, tmpdir } from "node:os";
|
|
11
15
|
import { dirname, join } from "node:path";
|
|
12
16
|
import { CLI_COMMAND } from "./cli.mjs";
|
|
17
|
+
import { OMNI_DIR } from "./deps.mjs";
|
|
18
|
+
|
|
19
|
+
/* Claude Code's own folder when CLAUDE_CONFIG_DIR is unset (its login then sits in ~/.claude.json) */
|
|
20
|
+
export const DEFAULT_CLAUDE_DIR = join(homedir(), ".claude");
|
|
21
|
+
const RECORD = join(OMNI_DIR, "claude.json");
|
|
22
|
+
|
|
23
|
+
/** the folder the hook recorded, or null @returns {string | null} */
|
|
24
|
+
export function recordedClaudeDir() {
|
|
25
|
+
try {
|
|
26
|
+
const dir = JSON.parse(readFileSync(RECORD, "utf8")).config_dir;
|
|
27
|
+
return typeof dir === "string" && dir ? dir : null;
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** records the session's Claude Code folder (on the hook only: a terminal may lack the variable)
|
|
34
|
+
* @returns {boolean} whether it changed */
|
|
35
|
+
export function recordClaudeDir(dir = process.env.CLAUDE_CONFIG_DIR || DEFAULT_CLAUDE_DIR) {
|
|
36
|
+
if (recordedClaudeDir() === dir) return false;
|
|
37
|
+
mkdirSync(OMNI_DIR, { recursive: true });
|
|
38
|
+
writeFileSync(RECORD, `${JSON.stringify({ config_dir: dir }, null, 2)}\n`);
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
13
41
|
|
|
14
|
-
export const
|
|
42
|
+
export const claudeConfigDir = () => process.env.CLAUDE_CONFIG_DIR || recordedClaudeDir() || DEFAULT_CLAUDE_DIR;
|
|
43
|
+
export const settingsFile = () => join(claudeConfigDir(), "settings.json");
|
|
15
44
|
|
|
16
45
|
/** the rules, for this CLI command and this machine's temporary folder (both spellings of it on macOS) */
|
|
17
46
|
export function skillRules(cli = CLI_COMMAND) {
|
package/tools/lib/deps.mjs
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
takes it from CHROME_PATH
|
|
8
8
|
python uv (UV_VERSION, in ~/.omni/bin) makes the app's .venv with a Python of its own and
|
|
9
9
|
installs requirements.txt into it
|
|
10
|
+
yt-dlp the project's standalone build (YTDLP_RELEASE), in ~/.omni/bin: a visual style from a
|
|
11
|
+
link (tools/reference.mjs). Sites change often, so bump the release when a download breaks
|
|
10
12
|
|
|
11
13
|
Each piece is checked by running it, and only fetched when it doesn't answer, so `omni up` can call
|
|
12
14
|
ensureDeps on every session. What was installed goes in ~/.omni/deps.json; depsEnv() is the PATH and
|
|
@@ -34,6 +36,7 @@ export const BIN_DIR = join(OMNI_DIR, "bin");
|
|
|
34
36
|
const STATE = join(OMNI_DIR, "deps.json");
|
|
35
37
|
export const FFMPEG_RELEASE = "b6.1.1";
|
|
36
38
|
export const UV_VERSION = "0.12.21";
|
|
39
|
+
export const YTDLP_RELEASE = "2026.08.19";
|
|
37
40
|
const PYTHON_VERSION = "3.12";
|
|
38
41
|
|
|
39
42
|
const win = process.platform === "win32";
|
|
@@ -128,6 +131,25 @@ export async function ensureChrome() {
|
|
|
128
131
|
return got.executablePath;
|
|
129
132
|
}
|
|
130
133
|
|
|
134
|
+
/* --- yt-dlp ----------------------------------------------------------------------------- */
|
|
135
|
+
|
|
136
|
+
/** the release asset for this machine: one universal macOS build, Linux per arch, Windows per arch */
|
|
137
|
+
export function ytdlpAsset(platform = process.platform, cpu = process.arch) {
|
|
138
|
+
if (platform === "darwin") return "yt-dlp_macos";
|
|
139
|
+
if (platform === "win32") return cpu === "arm64" ? "yt-dlp_arm64.exe" : "yt-dlp.exe";
|
|
140
|
+
return cpu === "arm64" ? "yt-dlp_linux_aarch64" : "yt-dlp_linux";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function ensureYtdlp() {
|
|
144
|
+
/* one already on the PATH (a development checkout's Homebrew) serves as well */
|
|
145
|
+
if (answers("yt-dlp", ["--version"])) return;
|
|
146
|
+
const file = join(BIN_DIR, exe("yt-dlp"));
|
|
147
|
+
if (existsSync(file) && answers(file, ["--version"])) return;
|
|
148
|
+
const url = `https://github.com/yt-dlp/yt-dlp/releases/download/${YTDLP_RELEASE}/${ytdlpAsset()}`;
|
|
149
|
+
install(file, await fetchBytes(url));
|
|
150
|
+
if (!answers(file, ["--version"])) throw new Error("o yt-dlp baixado não roda neste computador");
|
|
151
|
+
}
|
|
152
|
+
|
|
131
153
|
/* --- uv and the Python venv -------------------------------------------------------------- */
|
|
132
154
|
|
|
133
155
|
async function ensureUv() {
|
|
@@ -204,6 +226,7 @@ export function checkDeps(codeRoot) {
|
|
|
204
226
|
python: existsSync(python)
|
|
205
227
|
? answers(python, ["-c", "import numpy, soundfile, requests; print('ok')"]) && python
|
|
206
228
|
: null,
|
|
229
|
+
ytdlp: on("yt-dlp", ["--version"]),
|
|
207
230
|
};
|
|
208
231
|
}
|
|
209
232
|
|
|
@@ -219,6 +242,7 @@ export async function ensureDeps(codeRoot, { log = () => {} } = {}) {
|
|
|
219
242
|
["ffmpeg", () => ensureFfmpeg()],
|
|
220
243
|
["Chrome sem janela", () => ensureChrome()],
|
|
221
244
|
["Python", () => ensurePython(codeRoot)],
|
|
245
|
+
["yt-dlp", () => ensureYtdlp()],
|
|
222
246
|
]);
|
|
223
247
|
for (const [name, run] of steps) {
|
|
224
248
|
try {
|
|
@@ -355,7 +355,7 @@ export function instagramClient({ token, apiVersion, ...deps }) {
|
|
|
355
355
|
/**
|
|
356
356
|
* Publishes a Reel from a public video URL (and an optional public cover URL).
|
|
357
357
|
* With Instagram login Meta downloads the file itself: the caller provides the
|
|
358
|
-
* URLs (
|
|
358
|
+
* URLs (R2 behind the cloud Worker's /media, for the CLI and the cloud alike) and `release`,
|
|
359
359
|
* called once Instagram has finished downloading.
|
|
360
360
|
* @param {{ client: InstagramClient, userId: string, videoUrl: string, coverUrl?: string | null,
|
|
361
361
|
* cover?: { version: string } | null, caption: string, shareToFeed: boolean, coverAtS?: number | null,
|
package/tools/lib/up.mjs
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
tools ffmpeg, the Chrome without a window and Python in ~/.omni (tools/lib/deps.mjs; in the
|
|
9
9
|
background on a hook)
|
|
10
10
|
login none yet: the browser opens on the approval page (at most once an hour)
|
|
11
|
+
claude on a hook, the session's Claude Code folder into ~/.omni/claude.json (claude-settings.mjs)
|
|
11
12
|
service the studio as a launchd agent, from the app, so the hosted studio reaches this Mac
|
|
12
13
|
|
|
13
14
|
In this repo (a git checkout) the app, the tools and the service are left to `tools/setup.mjs` and
|
|
@@ -16,7 +17,7 @@
|
|
|
16
17
|
import { spawn, spawnSync } from "node:child_process";
|
|
17
18
|
import { copyFileSync, existsSync, mkdirSync, openSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
18
19
|
import { join } from "node:path";
|
|
19
|
-
import { missingRules } from "./claude-settings.mjs";
|
|
20
|
+
import { missingRules, recordClaudeDir } from "./claude-settings.mjs";
|
|
20
21
|
import { CLI_COMMAND, PACKAGE, STUDIO_URL } from "./cli.mjs";
|
|
21
22
|
import { OMNI_DIR, checkDeps, ensureDeps } from "./deps.mjs";
|
|
22
23
|
import { readTokens } from "./oauth-client.mjs";
|
|
@@ -145,6 +146,8 @@ function devServiceRoot() {
|
|
|
145
146
|
*/
|
|
146
147
|
export async function up({ hook = false, log = () => {} } = {}) {
|
|
147
148
|
const notes = [];
|
|
149
|
+
/* the session's Claude Code folder, for the studio service: its settings and its default agent account */
|
|
150
|
+
if (hook) recordClaudeDir();
|
|
148
151
|
if (ensureWorkspace()) notes.push(`workspace criado em ${DATA_ROOT}`);
|
|
149
152
|
const codeRoot = ensureApp(log);
|
|
150
153
|
const dev = isDev(codeRoot);
|
|
@@ -155,7 +158,7 @@ export async function up({ hook = false, log = () => {} } = {}) {
|
|
|
155
158
|
if (hook) {
|
|
156
159
|
const tools = ensureTools(codeRoot, { background: true });
|
|
157
160
|
if (tools === "started" || tools === "running")
|
|
158
|
-
notes.push("baixando o ffmpeg, o Chrome sem janela e o
|
|
161
|
+
notes.push("baixando o ffmpeg, o Chrome sem janela, o Python e o yt-dlp (alguns minutos)");
|
|
159
162
|
} else {
|
|
160
163
|
const r = await ensureDeps(codeRoot, { log });
|
|
161
164
|
if (!r.ok) notes.push(`faltou preparar ${r.missing.join(", ")}: ${r.errors.join("; ")}`);
|
package/tools/omni.mjs
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
omni logout signs this computer out in the cloud and here
|
|
13
13
|
omni allow [--yes] the Claude Code permission rules the skills need (asks first)
|
|
14
14
|
omni setup login, the rules and up, for whoever starts from a terminal
|
|
15
|
-
omni doctor [--json] whether ffmpeg, ffprobe, the Chrome without a window and
|
|
15
|
+
omni doctor [--json] whether ffmpeg, ffprobe, the Chrome without a window, Python and yt-dlp answer
|
|
16
16
|
omni deps install fetches whichever of them doesn't answer into ~/.omni (tools/lib/deps.mjs)
|
|
17
17
|
omni skill <nome> [--v <versão>] [--part i/n]
|
|
18
18
|
one part of a skill's body from the registry (tools/lib/skill-client.mjs)
|
|
@@ -93,7 +93,7 @@ async function statusText(url) {
|
|
|
93
93
|
lines.push(
|
|
94
94
|
missing.length
|
|
95
95
|
? `Ainda preparando os programas do pipeline (faltam: ${missing.join(", ")}); o omni up baixa sozinho, ou chame-o de novo se demorar mais de 10 minutos.`
|
|
96
|
-
: "O computador está pronto: ffmpeg, ffprobe, o Chrome sem janela e o
|
|
96
|
+
: "O computador está pronto: ffmpeg, ffprobe, o Chrome sem janela, o Python e o yt-dlp respondem.",
|
|
97
97
|
);
|
|
98
98
|
const rules = missingRules();
|
|
99
99
|
lines.push(
|
|
@@ -105,11 +105,12 @@ async function statusText(url) {
|
|
|
105
105
|
return lines.join("\n");
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
/** which of the pipeline's programs don't answer (tools/lib/deps.mjs) */
|
|
108
|
+
/** which of the pipeline's programs don't answer (tools/lib/deps.mjs), by the name the user knows */
|
|
109
|
+
const DEP_NAMES = { chrome: "Chrome sem janela", python: "Python", ytdlp: "yt-dlp" };
|
|
109
110
|
const depsMissing = () =>
|
|
110
111
|
Object.entries(checkDeps(CODE_ROOT))
|
|
111
112
|
.filter(([, v]) => !v)
|
|
112
|
-
.map(([k]) => k);
|
|
113
|
+
.map(([k]) => DEP_NAMES[/** @type {keyof typeof DEP_NAMES} */ (k)] ?? k);
|
|
113
114
|
|
|
114
115
|
/** the plugin's MCP tools @param {string} url */
|
|
115
116
|
const mcpTools = url => [
|
|
@@ -221,6 +222,7 @@ export async function main(args) {
|
|
|
221
222
|
ffprobe: "ffprobe",
|
|
222
223
|
chrome: "Chrome sem janela",
|
|
223
224
|
python: "Python das ferramentas",
|
|
225
|
+
ytdlp: "yt-dlp (estilo visual por link)",
|
|
224
226
|
};
|
|
225
227
|
for (const [k, v] of Object.entries(c))
|
|
226
228
|
console.log(`${v ? "ok " : "falta"} ${names[/** @type {keyof typeof names} */ (k)]}${v ? `: ${v}` : ""}`);
|
|
@@ -234,7 +236,7 @@ export async function main(args) {
|
|
|
234
236
|
if (args.includes("--unlock")) rmSync(join(OMNI_DIR, "deps.lock"), { force: true });
|
|
235
237
|
console.log(
|
|
236
238
|
r.ok
|
|
237
|
-
? "Tudo pronto: ffmpeg, ffprobe, o Chrome sem janela e o
|
|
239
|
+
? "Tudo pronto: ffmpeg, ffprobe, o Chrome sem janela, o Python e o yt-dlp respondem."
|
|
238
240
|
: `Faltou: ${r.errors.join("; ")}`,
|
|
239
241
|
);
|
|
240
242
|
if (!r.ok) process.exit(1);
|
package/tools/omnichat.mjs
CHANGED
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
node tools/omnichat.mjs webhook setup | subscribe | status | serve [--port 5434]
|
|
73
73
|
the messaging webhook that the button and the follow gate need: `setup` creates the verify token and says
|
|
74
74
|
what to paste in the Meta panel, `subscribe` signs the account up to messages and postbacks, `serve` listens
|
|
75
|
-
|
|
75
|
+
here for a test (the Callback URL is the cloud Worker's /webhook)
|
|
76
76
|
node tools/omnichat.mjs cloud status | push [videos/<slug>] [--records] [--force] | pull videos/<slug>
|
|
77
77
|
the Worker in cloud/omnichat (cloud.url in the config, OMNICHAT_CLOUD_SECRET in the .env):
|
|
78
78
|
`push` sends the config, the Instagram account and the rules (with --records, the local records too,
|
|
@@ -1284,9 +1284,8 @@ async function cloudCmd(sub) {
|
|
|
1284
1284
|
/* --- the messaging webhook: the button's tap and the follow gate ------------------------------
|
|
1285
1285
|
The comment's DM opens with a button (rule.button / rule.require_follow). The tap reaches here as a
|
|
1286
1286
|
`messaging_postbacks` event; handleTap (tools/lib/omnichat/gate.mjs) checks the criterion and sends the
|
|
1287
|
-
link, or asks to follow. Meta needs a public HTTPS callback URL:
|
|
1288
|
-
|
|
1289
|
-
uses the same handleWebhook. The record is written under the pass lock, so the agent and the tap
|
|
1287
|
+
link, or asks to follow. Meta needs a public HTTPS callback URL: the cloud Worker's /webhook
|
|
1288
|
+
(cloud/omnichat), which runs the same handleWebhook; `serve` listens on this machine for a test. The record is written under the pass lock, so the agent and the tap
|
|
1290
1289
|
don't overwrite each other. */
|
|
1291
1290
|
const WEBHOOK_PORT = 5434;
|
|
1292
1291
|
const gatedRules = () => videosWithRule().filter(d => gateOn(ruleOf(d) ?? {}));
|
|
@@ -1337,10 +1336,9 @@ async function webhook(sub) {
|
|
|
1337
1336
|
console.log("✓ INSTAGRAM_WEBHOOK_VERIFY_TOKEN no .env");
|
|
1338
1337
|
console.log(`
|
|
1339
1338
|
Faltam estes passos, uma vez só:
|
|
1340
|
-
1.
|
|
1341
|
-
cloudflared tunnel --url http://127.0.0.1:${port} (anote a URL https://….trycloudflare.com)
|
|
1339
|
+
1. A cloud no ar (cloud/omnichat, reference/omnichat.md): é ela que recebe o webhook.
|
|
1342
1340
|
2. developers.facebook.com/apps → seu app → Instagram → API setup with Instagram login → Configure webhooks:
|
|
1343
|
-
Callback URL:
|
|
1341
|
+
Callback URL: ${cloudSettings().url || "https://omnichat.<você>.workers.dev"}/webhook
|
|
1344
1342
|
Verify token: ${token}
|
|
1345
1343
|
e ligue os campos messages e messaging_postbacks.
|
|
1346
1344
|
3. node tools/omnichat.mjs webhook subscribe (inscreve a conta nesses campos)
|
package/tools/package.mjs
CHANGED
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
not Bun; ffmpeg, the Chrome without a window and Python are fetched by `omni up` (tools/lib/deps.mjs).
|
|
8
8
|
|
|
9
9
|
node tools/package.mjs [--version <x.y.z>] builds and packs; prints the .tgz path
|
|
10
|
-
node tools/package.mjs publish [--version …] builds, packs and publishes to npm
|
|
11
|
-
|
|
10
|
+
node tools/package.mjs publish [--version …] builds, packs and publishes to npm: in CI through npm's
|
|
11
|
+
trusted publishing (.github/workflows/deploy.yml), by
|
|
12
|
+
hand with the account in NPM_CONFIG_USERCONFIG
|
|
12
13
|
|
|
13
14
|
Try it before publishing: npm install --prefix /tmp/omni-test <the .tgz>, then
|
|
14
15
|
/tmp/omni-test/node_modules/.bin/omni status
|
|
@@ -120,7 +121,8 @@ const tgz = join(OUT, packed.stdout.trim().split("\n").at(-1) ?? "");
|
|
|
120
121
|
console.log(`${PACKAGE}@${v}: ${(size(PKG) / 1e6).toFixed(1)} MB de arquivos, pacote em ${tgz}`);
|
|
121
122
|
|
|
122
123
|
if (args[0] === "publish") {
|
|
123
|
-
|
|
124
|
+
/* CI publishes through npm's trusted publishing (OIDC); by hand, with the account's own login file */
|
|
125
|
+
if (!process.env.NPM_CONFIG_USERCONFIG && !process.env.GITHUB_ACTIONS)
|
|
124
126
|
fail("defina NPM_CONFIG_USERCONFIG com o login da conta que publica (ex.: ~/.npmrc-pedroflp)");
|
|
125
127
|
const r = spawnSync("npm", ["publish", tgz, "--access", "public", ...(v.includes("-") ? ["--tag", "next"] : [])], {
|
|
126
128
|
stdio: "inherit",
|