@crievo/omni 0.1.2 → 0.1.3
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 +1 -1
- package/reference/omnichat.md +5 -4
- package/reference/publish.md +9 -9
- package/reference/studio.md +1 -1
- package/studio/api/services/onboarding.ts +242 -21
- package/studio/api/services/settings.ts +4 -2
- package/studio/shared/types/api.ts +26 -1
- package/studio/web/hooks/useOnboarding.ts +24 -6
- package/studio/web/lib/alerts.ts +10 -0
- package/studio/web/pages/GettingStartedPage/components/ComputerCard.tsx +102 -0
- package/studio/web/pages/GettingStartedPage/index.tsx +84 -58
- package/studio/web/pages/SettingsPage/components/AgentAccounts.tsx +30 -3
- package/studio/web/styles/pages/getting-started.css +46 -0
- package/studio/web/styles/pages/settings/shell.css +4 -0
- 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
|
@@ -1,40 +1,247 @@
|
|
|
1
1
|
/* Getting started (/getting-started): the guide in docs/onboarding.md, one step per section
|
|
2
|
-
(`<!-- step: id -->`), with
|
|
3
|
-
|
|
2
|
+
(`<!-- step: id -->`), each with its state measured here. The guide owns the text: the UI repeats
|
|
3
|
+
none of it.
|
|
4
4
|
|
|
5
|
+
The first step is this computer: the four programs the CLI (`omni up`) downloads into ~/.omni in the
|
|
6
|
+
background (checkDeps, from tools/lib/deps.mjs) and the Claude Code permission rules the plugin's
|
|
7
|
+
skills need (missingRules, from tools/lib/claude-settings.mjs; Claude writes them through the
|
|
8
|
+
plugin's MCP, never the studio). The page polls while something is missing, so checkDeps is cached:
|
|
9
|
+
briefly while the download runs, longer once everything answers. The other steps read the
|
|
10
|
+
workspace (.env, config, styles library, videos/): nothing there runs a program.
|
|
11
|
+
|
|
12
|
+
`tools/setup.mjs check` stays the development repository's terminal check; the studio no longer
|
|
13
|
+
runs it. */
|
|
14
|
+
|
|
15
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
5
16
|
import { join, resolve, sep } from "node:path";
|
|
6
|
-
import { CODE_ROOT,
|
|
17
|
+
import { CODE_ROOT, readText } from "./root";
|
|
18
|
+
import { providerKey, readConfig, readEnv } from "./settings";
|
|
19
|
+
import { LIBRARY_DIR } from "./references";
|
|
20
|
+
import { videoDirs } from "./videos";
|
|
21
|
+
import { checkDeps } from "../../../tools/lib/deps.mjs";
|
|
22
|
+
import { missingRules } from "../../../tools/lib/claude-settings.mjs";
|
|
23
|
+
import { accountsOf, loginOf, tildify } from "../../../tools/lib/agents.mjs";
|
|
7
24
|
|
|
8
25
|
export const DOCS_DIR = join(CODE_ROOT, "docs");
|
|
9
26
|
const GUIDE = join(DOCS_DIR, "onboarding.md");
|
|
10
27
|
|
|
11
|
-
type
|
|
12
|
-
type
|
|
13
|
-
|
|
14
|
-
steps: { id: string; state: Check["state"]; optional?: boolean; items: Check[] }[];
|
|
15
|
-
};
|
|
28
|
+
type State = "ok" | "missing" | "warning";
|
|
29
|
+
type Check = { id: string; title: string; state: State; detail: string };
|
|
30
|
+
type StepCheck = { optional?: boolean; items: Check[] };
|
|
16
31
|
export type OnboardingStep = {
|
|
17
32
|
id: string;
|
|
18
33
|
title: string;
|
|
19
34
|
markdown: string;
|
|
20
|
-
state:
|
|
35
|
+
state: State | null;
|
|
21
36
|
optional: boolean;
|
|
22
37
|
items: Check[];
|
|
23
38
|
};
|
|
39
|
+
export type Computer = {
|
|
40
|
+
ffmpeg: string | null;
|
|
41
|
+
ffprobe: string | null;
|
|
42
|
+
chrome: string | null;
|
|
43
|
+
python: string | null;
|
|
44
|
+
/** only the visual style from a link needs it: not part of `ready` */
|
|
45
|
+
ytdlp: string | null;
|
|
46
|
+
/** the four programs answer */
|
|
47
|
+
ready: boolean;
|
|
48
|
+
/** the permission rules still missing in the Claude Code settings (empty = ok) */
|
|
49
|
+
missing_rules: string[];
|
|
50
|
+
/** a development checkout (the code root has .git, as `omni up` tells it): Claude runs the repository's
|
|
51
|
+
own skills, so the plugin's rules don't block anything */
|
|
52
|
+
dev: boolean;
|
|
53
|
+
/** the agent account the sessions run on: logged in (the e-mail) or where the login was looked for */
|
|
54
|
+
agent: { ok: boolean; detail: string };
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const item = (id: string, title: string, state: State, detail: string): Check => ({ id, title, state, detail });
|
|
58
|
+
const stateOf = (items: Check[]): State =>
|
|
59
|
+
items.some(i => i.state === "missing") ? "missing" : items.some(i => i.state === "warning") ? "warning" : "ok";
|
|
60
|
+
|
|
61
|
+
/* --- this computer ------------------------------------------------------------------- */
|
|
24
62
|
|
|
25
|
-
/*
|
|
26
|
-
let
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
63
|
+
/* checkDeps runs each program (spawnSync): one run serves every poll for a while */
|
|
64
|
+
let deps: { at: number; value: ReturnType<typeof checkDeps> } | null = null;
|
|
65
|
+
const DEPS_TTL_PREPARING = 4_000;
|
|
66
|
+
const DEPS_TTL_READY = 5 * 60_000;
|
|
67
|
+
const DEV = existsSync(join(CODE_ROOT, ".git"));
|
|
68
|
+
|
|
69
|
+
export function getComputer(fresh = false): Computer {
|
|
70
|
+
const ready = (d: ReturnType<typeof checkDeps>) => !!(d.ffmpeg && d.ffprobe && d.chrome && d.python);
|
|
71
|
+
const ttl = deps && ready(deps.value) ? DEPS_TTL_READY : DEPS_TTL_PREPARING;
|
|
72
|
+
if (fresh || !deps || Date.now() - deps.at > ttl) deps = { at: Date.now(), value: checkDeps(CODE_ROOT) };
|
|
73
|
+
const d = deps.value;
|
|
74
|
+
let rules: string[];
|
|
30
75
|
try {
|
|
31
|
-
|
|
32
|
-
return cached.r;
|
|
76
|
+
rules = missingRules();
|
|
33
77
|
} catch {
|
|
34
|
-
|
|
78
|
+
rules = [];
|
|
35
79
|
}
|
|
80
|
+
return {
|
|
81
|
+
agent: agentAccount(),
|
|
82
|
+
ffmpeg: d.ffmpeg || null,
|
|
83
|
+
ffprobe: d.ffprobe || null,
|
|
84
|
+
chrome: d.chrome || null,
|
|
85
|
+
python: d.python || null,
|
|
86
|
+
ytdlp: d.ytdlp || null,
|
|
87
|
+
ready: ready(d),
|
|
88
|
+
missing_rules: rules,
|
|
89
|
+
dev: DEV,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* the studio's sessions need one enabled account with a login (tools/lib/agents.mjs): with no list, the
|
|
94
|
+
Claude Code of this machine, which may live in another folder than the one looked at */
|
|
95
|
+
function agentAccount(): Computer["agent"] {
|
|
96
|
+
const on = accountsOf(readConfig()).filter(a => a.enabled);
|
|
97
|
+
const logged = on.find(a => loginOf(a));
|
|
98
|
+
if (logged) return { ok: true, detail: loginOf(logged)?.email ?? tildify(logged.dir) };
|
|
99
|
+
return {
|
|
100
|
+
ok: false,
|
|
101
|
+
detail: on.length ? `sem login em ${on.map(a => tildify(a.dir)).join(", ")}` : "nenhuma conta ligada",
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const PREPARING = "preparando…";
|
|
106
|
+
|
|
107
|
+
function computerStep(c: Computer): StepCheck {
|
|
108
|
+
const program = (id: keyof Computer, title: string) => {
|
|
109
|
+
const v = c[id] as string | null;
|
|
110
|
+
return item(id, title, v ? "ok" : "missing", v ?? PREPARING);
|
|
111
|
+
};
|
|
112
|
+
return {
|
|
113
|
+
items: [
|
|
114
|
+
program("ffmpeg", "ffmpeg"),
|
|
115
|
+
program("ffprobe", "ffprobe"),
|
|
116
|
+
program("chrome", "Chrome sem janela"),
|
|
117
|
+
program("python", "Python das ferramentas"),
|
|
118
|
+
/* optional: a warning while missing, so it never holds up the computer step */
|
|
119
|
+
item("ytdlp", "yt-dlp", c.ytdlp ? "ok" : "warning", c.ytdlp ?? PREPARING),
|
|
120
|
+
item("agent", "Conta do Claude Code", c.agent.ok ? "ok" : "missing", c.agent.detail),
|
|
121
|
+
item(
|
|
122
|
+
"skills",
|
|
123
|
+
"Permissão das skills no Claude Code",
|
|
124
|
+
c.missing_rules.length && !c.dev ? "missing" : "ok",
|
|
125
|
+
!c.missing_rules.length
|
|
126
|
+
? "liberadas"
|
|
127
|
+
: c.dev
|
|
128
|
+
? "repositório de desenvolvimento: as skills são as dele"
|
|
129
|
+
: `faltam ${c.missing_rules.length} regras`,
|
|
130
|
+
),
|
|
131
|
+
],
|
|
132
|
+
};
|
|
36
133
|
}
|
|
37
134
|
|
|
135
|
+
/* --- the steps that keep the user busy meanwhile -------------------------------------- */
|
|
136
|
+
|
|
137
|
+
const PROVIDERS = [
|
|
138
|
+
["fish", "Fish Audio"],
|
|
139
|
+
["cartesia", "Cartesia"],
|
|
140
|
+
["elevenlabs", "ElevenLabs"],
|
|
141
|
+
] as const;
|
|
142
|
+
|
|
143
|
+
function voiceSteps(): Record<"voice-key" | "voice-clone" | "voice-default", StepCheck> {
|
|
144
|
+
const voice = readConfig()?.voice ?? {};
|
|
145
|
+
const withKey = PROVIDERS.filter(([id]) => providerKey(id));
|
|
146
|
+
const profiles: { id: string; name?: string; [provider: string]: unknown }[] = voice.profiles ?? [];
|
|
147
|
+
const nameOf = (p: (typeof profiles)[number]) => String(p.name ?? p.id);
|
|
148
|
+
const usable = profiles.filter(p => withKey.some(([id]) => p[id]));
|
|
149
|
+
const active = profiles.find(p => p.id === voice.default_profile);
|
|
150
|
+
return {
|
|
151
|
+
"voice-key": {
|
|
152
|
+
items: [
|
|
153
|
+
item(
|
|
154
|
+
"voice-key",
|
|
155
|
+
"Chave de um provider de voz",
|
|
156
|
+
withKey.length ? "ok" : "missing",
|
|
157
|
+
withKey.length ? withKey.map(([, name]) => name).join(", ") : "nenhuma chave da Fish Audio ou da Cartesia",
|
|
158
|
+
),
|
|
159
|
+
],
|
|
160
|
+
},
|
|
161
|
+
"voice-clone": {
|
|
162
|
+
items: [
|
|
163
|
+
item(
|
|
164
|
+
"voice-profile",
|
|
165
|
+
"Uma voz clonada",
|
|
166
|
+
usable.length ? "ok" : "missing",
|
|
167
|
+
usable.length
|
|
168
|
+
? usable.map(nameOf).join(", ")
|
|
169
|
+
: profiles.length
|
|
170
|
+
? "a voz criada não tem o ID de um provider com chave"
|
|
171
|
+
: "nenhuma voz ainda",
|
|
172
|
+
),
|
|
173
|
+
],
|
|
174
|
+
},
|
|
175
|
+
"voice-default": {
|
|
176
|
+
items: [
|
|
177
|
+
item(
|
|
178
|
+
"voice-active",
|
|
179
|
+
"Voz padrão do vídeo novo",
|
|
180
|
+
active ? "ok" : "missing",
|
|
181
|
+
active ? nameOf(active) : profiles.length ? "nenhuma voz marcada como padrão" : "clone uma voz antes",
|
|
182
|
+
),
|
|
183
|
+
],
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function platformsStep(): StepCheck {
|
|
189
|
+
const env = readEnv();
|
|
190
|
+
/* the same tokens Configurações → Plataformas reads */
|
|
191
|
+
const on = (id: string, title: string, connected: boolean) =>
|
|
192
|
+
item(id, title, connected ? "ok" : "warning", connected ? "conectado" : "não conectado");
|
|
193
|
+
return {
|
|
194
|
+
optional: true,
|
|
195
|
+
items: [
|
|
196
|
+
on("youtube", "YouTube", !!env.YOUTUBE_REFRESH_TOKEN),
|
|
197
|
+
on("instagram", "Instagram", !!env.INSTAGRAM_TOKEN),
|
|
198
|
+
on("tiktok", "TikTok", !!env.TIKTOK_REFRESH_TOKEN),
|
|
199
|
+
],
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function styleStep(): StepCheck {
|
|
204
|
+
let n = 0;
|
|
205
|
+
try {
|
|
206
|
+
n = readdirSync(LIBRARY_DIR).filter(name => {
|
|
207
|
+
if (name.startsWith(".")) return false;
|
|
208
|
+
const dir = join(LIBRARY_DIR, name);
|
|
209
|
+
return statSync(dir).isDirectory() && !existsSync(join(dir, ".disabled"));
|
|
210
|
+
}).length;
|
|
211
|
+
} catch {
|
|
212
|
+
/* no library yet */
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
items: [
|
|
216
|
+
item(
|
|
217
|
+
"style",
|
|
218
|
+
"Um estilo visual",
|
|
219
|
+
n ? "ok" : "missing",
|
|
220
|
+
n ? `${n} na biblioteca de estilos visuais` : "a biblioteca está vazia",
|
|
221
|
+
),
|
|
222
|
+
],
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function firstVideoStep(): StepCheck {
|
|
227
|
+
let done: string | null = null;
|
|
228
|
+
for (const [slug, dir] of videoDirs()) {
|
|
229
|
+
try {
|
|
230
|
+
if (readdirSync(join(dir, "export")).some(f => f.endsWith(".mp4"))) {
|
|
231
|
+
done = slug;
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
} catch {
|
|
235
|
+
/* not exported */
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
items: [item("video", "Um vídeo exportado", done ? "ok" : "missing", done ?? "nenhum ainda")],
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/* --- the guide --------------------------------------------------------------------------- */
|
|
244
|
+
|
|
38
245
|
/* an image relative to the guide (onboarding/x.png) becomes the studio's /docs/… route; a link to another
|
|
39
246
|
repo file becomes the path in code (in the studio it has nowhere to go) */
|
|
40
247
|
const withImages = (md: string) =>
|
|
@@ -44,10 +251,21 @@ const withImages = (md: string) =>
|
|
|
44
251
|
text.includes("`") ? text : `${text} (\`${target}\`)`,
|
|
45
252
|
);
|
|
46
253
|
|
|
254
|
+
function checks(computer: Computer): Record<string, StepCheck> {
|
|
255
|
+
return {
|
|
256
|
+
computer: computerStep(computer),
|
|
257
|
+
...voiceSteps(),
|
|
258
|
+
platforms: platformsStep(),
|
|
259
|
+
style: styleStep(),
|
|
260
|
+
"first-video": firstVideoStep(),
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
47
264
|
export async function getOnboarding(fresh = false) {
|
|
48
265
|
const text = readText(GUIDE);
|
|
49
266
|
if (!text) return { error: "docs/onboarding.md não existe" };
|
|
50
|
-
const
|
|
267
|
+
const computer = getComputer(fresh);
|
|
268
|
+
const all = checks(computer);
|
|
51
269
|
const [head, ...sections] = text.split(/<!--\s*step:\s*([\w-]+)\s*-->/);
|
|
52
270
|
/* split interleaves: id, body, id, body… */
|
|
53
271
|
const steps: OnboardingStep[] = [];
|
|
@@ -55,18 +273,21 @@ export async function getOnboarding(fresh = false) {
|
|
|
55
273
|
const id = sections[i];
|
|
56
274
|
const body = sections[i + 1].trim();
|
|
57
275
|
const m = body.match(/^##\s+(.+)\n/);
|
|
58
|
-
const c =
|
|
276
|
+
const c = all[id];
|
|
59
277
|
steps.push({
|
|
60
278
|
id,
|
|
61
279
|
title: m ? m[1].trim() : id,
|
|
62
280
|
markdown: withImages(m ? body.slice(m[0].length) : body).trim(),
|
|
63
|
-
state: c
|
|
281
|
+
state: c ? stateOf(c.items) : null,
|
|
64
282
|
optional: !!c?.optional,
|
|
65
283
|
items: c?.items ?? [],
|
|
66
284
|
});
|
|
67
285
|
}
|
|
286
|
+
/* ready for the first video: every required step before it with nothing missing */
|
|
287
|
+
const upTo = steps.findIndex(s => s.id === "first-video");
|
|
288
|
+
const ready = steps.slice(0, upTo < 0 ? steps.length : upTo).every(s => s.optional || s.state !== "missing");
|
|
68
289
|
const intro = withImages(head.replace(/^#\s+.+\n/, "")).trim();
|
|
69
|
-
return { ready
|
|
290
|
+
return { ready, checked: true, intro, steps, computer };
|
|
70
291
|
}
|
|
71
292
|
|
|
72
293
|
/* /docs/<path>: the guide's images */
|
|
@@ -78,7 +78,7 @@ export const ENV_KEYS = [
|
|
|
78
78
|
...PUBLISH_ENV_KEYS,
|
|
79
79
|
];
|
|
80
80
|
|
|
81
|
-
function readEnv(): Record<string, string> {
|
|
81
|
+
export function readEnv(): Record<string, string> {
|
|
82
82
|
const vals: Record<string, string> = {};
|
|
83
83
|
for (const line of (readText(ENV_FILE) ?? "").split("\n")) {
|
|
84
84
|
const l = line.trim();
|
|
@@ -562,7 +562,8 @@ export function setCartesiaPlan(plan: unknown, quota: unknown, day: unknown): Re
|
|
|
562
562
|
|
|
563
563
|
/* --- agent accounts --------------------------------------------------------------------
|
|
564
564
|
`agents.accounts` (N Claude config dirs, N Codex homes) and `agents.codex` (model, effort,
|
|
565
|
-
sandbox); the rotation and the limits live in tools/lib/agents.mjs.
|
|
565
|
+
sandbox); the rotation and the limits live in tools/lib/agents.mjs. With no list, the one account is the
|
|
566
|
+
Claude Code this machine uses (~/.claude, or the folder the plugin's hook recorded). */
|
|
566
567
|
const SANDBOXES = ["auto", "full"];
|
|
567
568
|
|
|
568
569
|
function agentsSettings(cfg: any) {
|
|
@@ -579,6 +580,7 @@ function agentsSettings(cfg: any) {
|
|
|
579
580
|
limited_until: limitedUntil(ROOT, a.id) || null,
|
|
580
581
|
login_command: loginCommand(a, cfg),
|
|
581
582
|
logging_in: logins[a.id] ?? null,
|
|
583
|
+
implicit: a.implicit,
|
|
582
584
|
})),
|
|
583
585
|
/* who takes the next new session */
|
|
584
586
|
next: pickAccount(ROOT, cfg, { advance: false })?.id ?? null,
|
|
@@ -1042,6 +1042,8 @@ export type AgentAccount = {
|
|
|
1042
1042
|
limited_until: number | null;
|
|
1043
1043
|
login_command: string;
|
|
1044
1044
|
logging_in: { state: "waiting" | "ok" | "error"; url: string | null; output: string; at: number } | null;
|
|
1045
|
+
/* the default account (no list in the config): the Claude Code this machine uses */
|
|
1046
|
+
implicit: boolean;
|
|
1045
1047
|
};
|
|
1046
1048
|
export type AgentsSettings = {
|
|
1047
1049
|
accounts: AgentAccount[];
|
|
@@ -1067,7 +1069,30 @@ export type OnboardingStep = {
|
|
|
1067
1069
|
optional: boolean;
|
|
1068
1070
|
items: OnboardingCheck[];
|
|
1069
1071
|
};
|
|
1070
|
-
|
|
1072
|
+
/* this computer: what the CLI downloads into ~/.omni (tools/lib/deps.mjs checkDeps, a version line or
|
|
1073
|
+
path, null while it isn't there yet) and the skills' permission rules (tools/lib/claude-settings.mjs) */
|
|
1074
|
+
export type OnboardingComputer = {
|
|
1075
|
+
ffmpeg: string | null;
|
|
1076
|
+
ffprobe: string | null;
|
|
1077
|
+
chrome: string | null;
|
|
1078
|
+
python: string | null;
|
|
1079
|
+
/* only the visual style from a link needs it: not part of `ready` */
|
|
1080
|
+
ytdlp: string | null;
|
|
1081
|
+
ready: boolean;
|
|
1082
|
+
missing_rules: string[];
|
|
1083
|
+
/* a development checkout: the repository's own skills, the plugin's rules don't block */
|
|
1084
|
+
dev: boolean;
|
|
1085
|
+
/* the agent account the sessions run on: its e-mail, or where no login was found */
|
|
1086
|
+
agent: { ok: boolean; detail: string };
|
|
1087
|
+
};
|
|
1088
|
+
export type Onboarding = {
|
|
1089
|
+
ready: boolean;
|
|
1090
|
+
checked: boolean;
|
|
1091
|
+
intro: string;
|
|
1092
|
+
steps: OnboardingStep[];
|
|
1093
|
+
computer?: OnboardingComputer;
|
|
1094
|
+
error?: string;
|
|
1095
|
+
};
|
|
1071
1096
|
|
|
1072
1097
|
/* --- analytics (tools/analysis.mjs context; metrics from tools/metrics.mjs) ----------------- */
|
|
1073
1098
|
export type MetricsSnapshot = {
|
|
@@ -1,15 +1,27 @@
|
|
|
1
|
-
/* Getting started (/api/onboarding): the docs/onboarding.md guide in steps, each with
|
|
2
|
-
|
|
1
|
+
/* Getting started (/api/onboarding): the docs/onboarding.md guide in steps, each with its state, and
|
|
2
|
+
this computer (what the CLI is still downloading, the skills' permission). While the computer isn't
|
|
3
|
+
ready the page asks again every 5 s, so "preparando…" turns into "Computador pronto" on its own.
|
|
4
|
+
`reload(true)` asks for a fresh check (?fresh=1). */
|
|
3
5
|
|
|
4
6
|
import { useCallback, useState } from "react";
|
|
7
|
+
import { useQuery } from "@tanstack/react-query";
|
|
5
8
|
import { api, errorText, read } from "../lib/api";
|
|
6
9
|
import { queryClient, queryKeys } from "../lib/query-client";
|
|
7
10
|
import type { Onboarding } from "../types/api";
|
|
8
|
-
|
|
11
|
+
|
|
12
|
+
const POLL_MS = 5_000;
|
|
13
|
+
|
|
14
|
+
/** the computer still has something to answer for (a program downloading, the skills' rules) */
|
|
15
|
+
export const computerPending = (o: Onboarding | null | undefined) =>
|
|
16
|
+
!!o?.computer && (!o.computer.ready || (!o.computer.dev && o.computer.missing_rules.length > 0));
|
|
9
17
|
|
|
10
18
|
export function useOnboarding() {
|
|
11
|
-
const
|
|
12
|
-
|
|
19
|
+
const q = useQuery({
|
|
20
|
+
queryKey: queryKeys.onboarding,
|
|
21
|
+
queryFn: (): Promise<Onboarding> => read(api.onboarding.$get({ query: {} })),
|
|
22
|
+
refetchInterval: query => (computerPending(query.state.data) ? POLL_MS : false),
|
|
23
|
+
});
|
|
24
|
+
const onboarding: Onboarding | null = q.data ?? null;
|
|
13
25
|
const [freshError, setFreshError] = useState<string | null>(null);
|
|
14
26
|
const [checking, setChecking] = useState(false);
|
|
15
27
|
const reload = useCallback(async (fresh = false) => {
|
|
@@ -23,5 +35,11 @@ export function useOnboarding() {
|
|
|
23
35
|
}
|
|
24
36
|
setChecking(false);
|
|
25
37
|
}, []);
|
|
26
|
-
return {
|
|
38
|
+
return {
|
|
39
|
+
onboarding,
|
|
40
|
+
error: freshError ?? (q.error ? errorText(q.error) : null),
|
|
41
|
+
/* the 5 s poll doesn't count as "Conferindo…" */
|
|
42
|
+
checking,
|
|
43
|
+
reload,
|
|
44
|
+
};
|
|
27
45
|
}
|
package/studio/web/lib/alerts.ts
CHANGED
|
@@ -119,6 +119,16 @@ function agentAlerts(agents: AgentsSettings, now: number): Alert[] {
|
|
|
119
119
|
if (on.some(a => a.login && !limited(a))) return [];
|
|
120
120
|
const until = on.filter(a => a.login && limited(a)).map(a => a.limited_until!);
|
|
121
121
|
if (!until.length) {
|
|
122
|
+
/* the default account (~/.claude) without a login: the Claude Code may live in another folder */
|
|
123
|
+
if (on.length === 1 && on[0].implicit && on[0].provider === "claude")
|
|
124
|
+
return [
|
|
125
|
+
{
|
|
126
|
+
key: "agents:no-claude",
|
|
127
|
+
label: "Não achei o login do Claude Code",
|
|
128
|
+
message: `Em ${on[0].dir}: entre, ou coloque a pasta dele em Configurações`,
|
|
129
|
+
link,
|
|
130
|
+
},
|
|
131
|
+
];
|
|
122
132
|
return [
|
|
123
133
|
{
|
|
124
134
|
key: "agents:no-login",
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/* "Este computador", on top of Getting started: what `omni up` is still preparing on this Mac, live.
|
|
2
|
+
Being on the page already means the login and the studio connection work (the hosted studio only
|
|
3
|
+
answers through this computer's runner). The four programs come from checkDeps: while one is
|
|
4
|
+
missing it reads "preparando…" (the CLI is downloading it; nothing for the user to do). The skills'
|
|
5
|
+
permission rules are written by Claude, on the user's yes, through the plugin's MCP: the studio only
|
|
6
|
+
says what to ask for. The Claude Code account is looked for where the plugin's hook found it, else
|
|
7
|
+
~/.claude; without a login there, the user enters or gives the folder in Settings. */
|
|
8
|
+
|
|
9
|
+
import { Link } from "@tanstack/react-router";
|
|
10
|
+
import { Badge } from "../../../components/ui/badge";
|
|
11
|
+
import { Markdown } from "../../../components/Markdown";
|
|
12
|
+
import { Check, X } from "../../../components/icons";
|
|
13
|
+
import { useRunner } from "../../../hooks/useRunner";
|
|
14
|
+
import type { OnboardingComputer } from "../../../types/api";
|
|
15
|
+
|
|
16
|
+
type Row = { id: string; title: string; value: string | null; detail?: string };
|
|
17
|
+
|
|
18
|
+
export function ComputerCard({ computer: c, markdown }: { computer: OnboardingComputer; markdown: string }) {
|
|
19
|
+
const { hosted, name } = useRunner();
|
|
20
|
+
const rulesOk = c.dev || c.missing_rules.length === 0;
|
|
21
|
+
const programs: Row[] = [
|
|
22
|
+
{ id: "ffmpeg", title: "ffmpeg", value: c.ffmpeg },
|
|
23
|
+
{ id: "ffprobe", title: "ffprobe", value: c.ffprobe },
|
|
24
|
+
{ id: "chrome", title: "Chrome sem janela", value: c.chrome },
|
|
25
|
+
{ id: "python", title: "Python das ferramentas", value: c.python },
|
|
26
|
+
{ id: "ytdlp", title: "yt-dlp", value: c.ytdlp },
|
|
27
|
+
];
|
|
28
|
+
const state = !c.ready ? "preparing" : !c.agent.ok ? "agent" : rulesOk ? "ok" : "rules";
|
|
29
|
+
return (
|
|
30
|
+
<section className={`onboarding-computer ${state}`} aria-live="polite" aria-labelledby="computer-title">
|
|
31
|
+
<header>
|
|
32
|
+
<h2 id="computer-title">Este computador</h2>
|
|
33
|
+
{state === "ok" && <Badge variant="ok">Computador pronto</Badge>}
|
|
34
|
+
{state === "rules" && <Badge variant="warning">Falta liberar as skills</Badge>}
|
|
35
|
+
{state === "agent" && <Badge variant="warning">Falta a conta do Claude Code</Badge>}
|
|
36
|
+
{state === "preparing" && (
|
|
37
|
+
<Badge variant="running">
|
|
38
|
+
<span className="spinning" aria-hidden="true" /> Preparando…
|
|
39
|
+
</Badge>
|
|
40
|
+
)}
|
|
41
|
+
</header>
|
|
42
|
+
{state === "preparing" && (
|
|
43
|
+
<p className="subtle">
|
|
44
|
+
O OmniVideo está baixando o que falta. Leva alguns minutos e não precisa fazer nada: siga pelos passos abaixo
|
|
45
|
+
enquanto isso.
|
|
46
|
+
</p>
|
|
47
|
+
)}
|
|
48
|
+
<ul className="onboarding-items">
|
|
49
|
+
<li className="ok">
|
|
50
|
+
<span className="onboarding-item-mark">
|
|
51
|
+
<Check weight="bold" />
|
|
52
|
+
</span>
|
|
53
|
+
<b>Login e studio conectados</b>
|
|
54
|
+
<span className="subtle">
|
|
55
|
+
{hosted ? (name ? `conectado pelo ${name}` : "conectado") : "studio aberto neste computador"}
|
|
56
|
+
</span>
|
|
57
|
+
</li>
|
|
58
|
+
{programs.map(p => (
|
|
59
|
+
<li key={p.id} className={p.value ? "ok" : "preparing"}>
|
|
60
|
+
<span className="onboarding-item-mark">
|
|
61
|
+
{p.value ? <Check weight="bold" /> : <span className="spinning" aria-hidden="true" />}
|
|
62
|
+
</span>
|
|
63
|
+
<b>{p.title}</b>
|
|
64
|
+
<span className="subtle">{p.value ?? "preparando…"}</span>
|
|
65
|
+
</li>
|
|
66
|
+
))}
|
|
67
|
+
<li className={c.agent.ok ? "ok" : "missing"}>
|
|
68
|
+
<span className="onboarding-item-mark">{c.agent.ok ? <Check weight="bold" /> : <X weight="bold" />}</span>
|
|
69
|
+
<b>Conta do Claude Code</b>
|
|
70
|
+
<span className="subtle">{c.agent.detail}</span>
|
|
71
|
+
</li>
|
|
72
|
+
<li className={rulesOk ? "ok" : "missing"}>
|
|
73
|
+
<span className="onboarding-item-mark">{rulesOk ? <Check weight="bold" /> : <X weight="bold" />}</span>
|
|
74
|
+
<b>Permissão das skills no Claude Code</b>
|
|
75
|
+
<span className="subtle">
|
|
76
|
+
{c.missing_rules.length === 0
|
|
77
|
+
? "liberadas"
|
|
78
|
+
: c.dev
|
|
79
|
+
? "repositório de desenvolvimento: as skills são as dele"
|
|
80
|
+
: "ainda não liberadas"}
|
|
81
|
+
</span>
|
|
82
|
+
</li>
|
|
83
|
+
</ul>
|
|
84
|
+
{!c.agent.ok && (
|
|
85
|
+
<p className="onboarding-ask">
|
|
86
|
+
Não achei o login do seu Claude Code. Se ele usa outra pasta, coloque ela em{" "}
|
|
87
|
+
<Link to="/settings/$category" params={{ category: "agent" }}>
|
|
88
|
+
Configurações → Agente de IA
|
|
89
|
+
</Link>
|
|
90
|
+
; se é essa mesma, entre por lá.
|
|
91
|
+
</p>
|
|
92
|
+
)}
|
|
93
|
+
{!rulesOk && (
|
|
94
|
+
<p className="onboarding-ask">
|
|
95
|
+
No Claude Code, diga: <q>libere as skills do OmniVideo</q>. O Claude mostra as regras, pede o seu ok e grava.
|
|
96
|
+
Esta tela atualiza sozinha.
|
|
97
|
+
</p>
|
|
98
|
+
)}
|
|
99
|
+
{markdown && <Markdown text={markdown} />}
|
|
100
|
+
</section>
|
|
101
|
+
);
|
|
102
|
+
}
|