@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.
Files changed (122) hide show
  1. package/docs/onboarding.md +69 -161
  2. package/omnivideo.config.example.json +1 -1
  3. package/package.json +2 -1
  4. package/reference/learnings.md +12 -0
  5. package/reference/omnichat.md +5 -4
  6. package/reference/publish.md +9 -9
  7. package/reference/studio.md +1 -1
  8. package/studio/api/app.ts +2 -2
  9. package/studio/api/routes/sound.ts +22 -3
  10. package/studio/api/services/onboarding.ts +242 -21
  11. package/studio/api/services/settings.ts +4 -2
  12. package/studio/api/services/sound.ts +45 -0
  13. package/studio/shared/types/api.ts +26 -1
  14. package/studio/studio.ts +1 -0
  15. package/studio/web/components/AppShell.tsx +3 -1
  16. package/studio/web/components/CloneVoiceDialog/RecordStep.tsx +3 -0
  17. package/studio/web/components/CloneVoiceDialog/RecorderBar.tsx +37 -20
  18. package/studio/web/components/CloneVoiceDialog/useCloneVoice.ts +39 -12
  19. package/studio/web/components/MissingDmsPanel/BatchSend.tsx +13 -4
  20. package/studio/web/components/Omni/OmniWidget.tsx +14 -5
  21. package/studio/web/components/Omni/PendingActions.tsx +25 -6
  22. package/studio/web/components/OmnichatRulePanel/RuleConfig.tsx +13 -3
  23. package/studio/web/components/OmnichatRulePanel/RuleEnableConfirm.tsx +14 -4
  24. package/studio/web/components/ResultMessage.tsx +12 -3
  25. package/studio/web/components/Sidebar.tsx +63 -24
  26. package/studio/web/components/StageStepper.tsx +105 -34
  27. package/studio/web/components/Toaster.tsx +25 -0
  28. package/studio/web/components/VoiceCarousel.tsx +19 -16
  29. package/studio/web/components/VoiceProfilesSettings.tsx +8 -3
  30. package/studio/web/components/bencho/ActionNode.tsx +456 -0
  31. package/studio/web/components/bencho/AssigneePicker.tsx +246 -0
  32. package/studio/web/components/bencho/BenchoScope.tsx +8 -0
  33. package/studio/web/components/bencho/Checklist.tsx +421 -0
  34. package/studio/web/components/bencho/CreateMenu.tsx +258 -0
  35. package/studio/web/components/bencho/GenerateImage.tsx +290 -0
  36. package/studio/web/components/bencho/ImageAccordion.tsx +273 -0
  37. package/studio/web/components/bencho/LiquidToggle.tsx +289 -0
  38. package/studio/web/components/bencho/MagnifyDock.tsx +200 -0
  39. package/studio/web/components/bencho/NowPlaying.tsx +403 -0
  40. package/studio/web/components/bencho/OneTimeCode.tsx +374 -0
  41. package/studio/web/components/bencho/PullToRefresh.tsx +527 -0
  42. package/studio/web/components/bencho/SlideToConfirm.tsx +385 -0
  43. package/studio/web/components/bencho/SloshSlider.tsx +394 -0
  44. package/studio/web/components/bencho/StackedBars.tsx +342 -0
  45. package/studio/web/components/bencho/ToastStack.tsx +263 -0
  46. package/studio/web/components/bencho/UploadDropzone.tsx +394 -0
  47. package/studio/web/components/bencho/VoiceNote.tsx +705 -0
  48. package/studio/web/components/icons.tsx +27 -0
  49. package/studio/web/components/ui/slider.tsx +34 -1
  50. package/studio/web/components/ui/switch.tsx +18 -5
  51. package/studio/web/hooks/useOnboarding.ts +24 -6
  52. package/studio/web/index.html +1 -1
  53. package/studio/web/lib/alerts.ts +10 -0
  54. package/studio/web/lib/omni-panel.ts +4 -1
  55. package/studio/web/lib/toast.ts +196 -0
  56. package/studio/web/lib/upload.ts +24 -0
  57. package/studio/web/pages/BenchoPage/index.tsx +74 -0
  58. package/studio/web/pages/BenchoPage/registry.ts +174 -0
  59. package/studio/web/pages/GettingStartedPage/components/ComputerCard.tsx +102 -0
  60. package/studio/web/pages/GettingStartedPage/index.tsx +84 -58
  61. package/studio/web/pages/NewVideoPage/index.tsx +6 -2
  62. package/studio/web/pages/ReferencesPage/index.tsx +4 -1
  63. package/studio/web/pages/SettingsPage/components/AgentAccounts.tsx +30 -3
  64. package/studio/web/pages/VideoPage/components/ChangedVoices.tsx +14 -17
  65. package/studio/web/pages/VideoPage/components/LineTextEditor.tsx +1 -1
  66. package/studio/web/pages/VideoPage/components/PublishForm.tsx +24 -17
  67. package/studio/web/pages/VideoPage/components/ScheduledInfo.tsx +2 -1
  68. package/studio/web/pages/VideoPage/components/SoundTimeline/AddSound.tsx +12 -14
  69. package/studio/web/pages/VideoPage/components/SoundTimeline/BedRow.tsx +27 -11
  70. package/studio/web/pages/VideoPage/components/SoundTimeline/CaptionSaveBar.tsx +22 -18
  71. package/studio/web/pages/VideoPage/components/SoundTimeline/TimelineFooter.tsx +3 -4
  72. package/studio/web/pages/VideoPage/components/SoundTimeline/UploadTrack.tsx +59 -0
  73. package/studio/web/pages/VideoPage/components/SoundTimeline/useSoundTimeline.ts +1 -0
  74. package/studio/web/pages/VideoPage/components/VoicePauseBanner.tsx +4 -6
  75. package/studio/web/pages/VideoPage/index.tsx +4 -0
  76. package/studio/web/pages/VideosPage/components/VideoRow.tsx +2 -4
  77. package/studio/web/router.tsx +8 -0
  78. package/studio/web/styles/components/bencho/action-node.css +227 -0
  79. package/studio/web/styles/components/bencho/checklist.css +182 -0
  80. package/studio/web/styles/components/bencho/dock.css +188 -0
  81. package/studio/web/styles/components/bencho/gallery.css +109 -0
  82. package/studio/web/styles/components/bencho/generate.css +151 -0
  83. package/studio/web/styles/components/bencho/image-accordion.css +37 -0
  84. package/studio/web/styles/components/bencho/liq-create.css +279 -0
  85. package/studio/web/styles/components/bencho/liq-toggle.css +106 -0
  86. package/studio/web/styles/components/bencho/one-time-code.css +148 -0
  87. package/studio/web/styles/components/bencho/picker.css +204 -0
  88. package/studio/web/styles/components/bencho/pull.css +313 -0
  89. package/studio/web/styles/components/bencho/scope.css +42 -0
  90. package/studio/web/styles/components/bencho/slide-confirm.css +139 -0
  91. package/studio/web/styles/components/bencho/slosh.css +137 -0
  92. package/studio/web/styles/components/bencho/sound.css +260 -0
  93. package/studio/web/styles/components/bencho/stacked-bars.css +334 -0
  94. package/studio/web/styles/components/bencho/toast-stack.css +192 -0
  95. package/studio/web/styles/components/bencho/tokens.css +243 -0
  96. package/studio/web/styles/components/bencho/upload-dropzone.css +352 -0
  97. package/studio/web/styles/components/bencho/voice-note.css +337 -0
  98. package/studio/web/styles/components/clone-voice.css +12 -2
  99. package/studio/web/styles/components/omni.css +6 -1
  100. package/studio/web/styles/components/stepper.css +123 -7
  101. package/studio/web/styles/components/toaster.css +10 -0
  102. package/studio/web/styles/components/ui/slider.css +6 -16
  103. package/studio/web/styles/components/ui/switch.css +2 -41
  104. package/studio/web/styles/components/voice-carousel.css +46 -4
  105. package/studio/web/styles/index.css +23 -0
  106. package/studio/web/styles/pages/getting-started.css +46 -0
  107. package/studio/web/styles/pages/settings/shell.css +4 -0
  108. package/studio/web/styles/pages/video/layout.css +1 -4
  109. package/studio/web/styles/pages/video/sound-timeline.css +8 -0
  110. package/studio/web/styles/theme/identity/shell.css +10 -26
  111. package/tools/lib/agents.mjs +24 -13
  112. package/tools/lib/claude-settings.mjs +32 -3
  113. package/tools/lib/deps.mjs +24 -0
  114. package/tools/lib/platforms/instagram.mjs +1 -1
  115. package/tools/lib/up.mjs +5 -2
  116. package/tools/omni.mjs +7 -5
  117. package/tools/omnichat.mjs +5 -7
  118. package/tools/package.mjs +5 -3
  119. package/tools/publish.mjs +40 -137
  120. package/tools/reference.mjs +1 -1
  121. package/tools/setup.mjs +16 -15
  122. 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
+ }
@@ -359,3 +359,7 @@
359
359
  flex: 1;
360
360
  min-width: 0;
361
361
  }
362
+ /* the default Claude Code without a login: the sentence above the folder field */
363
+ .agent-missing small {
364
+ flex-basis: 100%;
365
+ }
@@ -41,10 +41,7 @@
41
41
  padding: 4px 24px;
42
42
  background: none;
43
43
  border-bottom: 1px solid var(--border);
44
- overflow-x: auto;
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 vídeo" + inside the Videos row */
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: 36px;
101
+ padding-right: 100px;
100
102
  }
101
- .menu .menu-action {
103
+ .menu .menu-create {
102
104
  position: absolute;
103
105
  top: 50%;
104
- right: 4px;
105
- transform: translateY(-50%);
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-action {
200
+ .menu-collapsed .menu-create {
217
201
  display: none;
218
202
  }
219
203
  .menu-collapsed .menu-group-name {
@@ -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
- - ~/.claude (Claude Code's default account, which may be the work one) is
12
- refused here, whatever the config says.
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 one dir no account may use */
22
- export const FORBIDDEN_DIR = join(homedir(), ".claude");
23
- /* before `agents` existed, the studio only ran here */
24
- export const DEFAULT_ACCOUNTS = [{ id: "claude-personal", provider: "claude", dir: "~/.claude-personal" }];
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 forbidden ones left out.
88
- * @returns {{ id: string, provider: "claude"|"codex", dir: string, label: string, enabled: boolean }[]}
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 raw =
92
- Array.isArray(cfg?.agents?.accounts) && cfg.agents.accounts.length ? cfg.agents.accounts : DEFAULT_ACCOUNTS;
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 o = readJson(join(a.dir, ".claude.json"))?.oauthAccount;
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 $CLAUDE_CONFIG_DIR/settings.json, else ~/.claude/settings.json. The rules are added
7
- to permissions.allow; nothing else in the file changes. */
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 settingsFile = () => join(process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude"), "settings.json");
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) {
@@ -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 (a local tunnel on the CLI, object storage in the cloud) and `release`,
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 Python (alguns minutos)");
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 Python answer
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 Python respondem.",
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 Python respondem."
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);
@@ -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
- (behind a tunnel: cloudflared tunnel --url http://127.0.0.1:5434)
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: `serve` listens on this machine and a
1288
- tunnel (cloudflared) exposes it while the Mac is awake. A webhook handled elsewhere (a cloud function)
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. Expor a porta ${port}: node tools/omnichat.mjs webhook serve e, em outro terminal,
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: <a URL do túnel>/webhook
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 with the account in
11
- NPM_CONFIG_USERCONFIG (e.g. ~/.npmrc-pedroflp)
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
- if (!process.env.NPM_CONFIG_USERCONFIG)
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",