@crievo/omni 0.1.7 → 0.1.9
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/omnivideo.config.example.json +2 -0
- package/package.json +1 -1
- package/reference/cloud.md +15 -1
- package/reference/learnings.md +7 -0
- package/reference/scenes-contract.md +3 -2
- package/reference/studio.md +3 -2
- package/studio/api/app.ts +4 -0
- package/studio/api/routes/cost.ts +13 -0
- package/studio/api/routes/me.ts +8 -0
- package/studio/api/services/agent-persona.ts +1 -0
- package/studio/api/services/agent-tools.ts +2 -2
- package/studio/api/services/cost.ts +22 -0
- package/studio/api/services/me.ts +67 -0
- package/studio/api/services/sessions.ts +8 -2
- package/studio/api/services/sound.ts +17 -6
- package/studio/runner/runner.ts +57 -10
- package/studio/shared/omni-screens.ts +1 -1
- package/studio/shared/schemas/cost.ts +10 -0
- package/studio/shared/types/api.ts +77 -1
- package/studio/studio.ts +27 -2
- package/studio/web/components/AppShell.tsx +11 -3
- package/studio/web/components/FirstConnection.tsx +222 -0
- package/studio/web/components/OfflineNotice.tsx +59 -0
- package/studio/web/components/SetupSteps.tsx +63 -0
- package/studio/web/components/Sidebar.tsx +10 -25
- package/studio/web/components/StudioOffline.tsx +48 -50
- package/studio/web/components/UserCard.tsx +79 -0
- package/studio/web/components/icons.tsx +2 -0
- package/studio/web/hooks/useComputers.ts +55 -0
- package/studio/web/hooks/useCost.ts +11 -0
- package/studio/web/hooks/useMe.ts +32 -0
- package/studio/web/hooks/useSetupProgress.ts +54 -0
- package/studio/web/lib/me.ts +17 -0
- package/studio/web/lib/query-client.ts +6 -0
- package/studio/web/lib/settings-categories.ts +2 -2
- package/studio/web/lib/setup-retry.ts +29 -0
- package/studio/web/lib/sound-timeline.ts +33 -0
- package/studio/web/lib/studio-offline.ts +105 -59
- package/studio/web/pages/ConnectPreviewPage/index.tsx +215 -0
- package/studio/web/pages/SettingsPage/components/AgentCategory.tsx +18 -9
- package/studio/web/pages/SettingsPage/components/AgentCostPanel.tsx +253 -0
- package/studio/web/pages/SettingsPage/components/ComputersPanel.tsx +145 -0
- package/studio/web/pages/SettingsPage/components/PublishingCategory/OmnichatSettingsPanel.tsx +2 -2
- package/studio/web/pages/SettingsPage/components/PublishingCategory/SchedulePanel.tsx +3 -3
- package/studio/web/pages/SettingsPage/components/UsageCategory.tsx +7 -2
- package/studio/web/pages/SettingsPage/index.tsx +15 -1
- package/studio/web/pages/VideoPage/components/PublishForm.tsx +5 -2
- package/studio/web/pages/VideoPage/components/SoundTimeline/EffectChoice.tsx +10 -8
- package/studio/web/pages/VideoPage/components/SoundTimeline/SoundLibrary.tsx +31 -18
- package/studio/web/router.tsx +8 -0
- package/studio/web/styles/components/setup-steps.css +163 -0
- package/studio/web/styles/components/user-card.css +121 -0
- package/studio/web/styles/index.css +2 -0
- package/studio/web/styles/pages/settings/shell.css +79 -0
- package/studio/web/styles/pages/settings/usage.css +47 -0
- package/studio/web/styles/pages/studio-offline.css +224 -38
- package/tools/cost.mjs +114 -0
- package/tools/lib/agents.mjs +3 -2
- package/tools/lib/channel.mjs +1 -1
- package/tools/lib/cloud-user.mjs +58 -0
- package/tools/lib/cost.mjs +357 -0
- package/tools/lib/deps.mjs +27 -12
- package/tools/lib/oauth-client.mjs +28 -14
- package/tools/lib/pages-style.mjs +67 -0
- package/tools/lib/pairing.mjs +1 -1
- package/tools/lib/schedule/schedule.mjs +1 -1
- package/tools/lib/self-update.mjs +122 -0
- package/tools/lib/setup-progress.mjs +315 -0
- package/tools/lib/up.mjs +176 -61
- package/tools/metrics.mjs +6 -1
- package/tools/omni.mjs +41 -16
- package/tools/omnichat.mjs +45 -9
- package/tools/platforms.mjs +75 -14
- package/tools/runner.mjs +6 -6
- package/tools/setup.mjs +18 -5
- package/tools/sound.mjs +198 -94
|
@@ -136,6 +136,7 @@
|
|
|
136
136
|
]
|
|
137
137
|
},
|
|
138
138
|
"kit": "kit/sfx",
|
|
139
|
+
"default_kit": "motion-ui",
|
|
139
140
|
"roles": {
|
|
140
141
|
"click": {
|
|
141
142
|
"db": -4
|
|
@@ -293,6 +294,7 @@
|
|
|
293
294
|
"omnichat": false,
|
|
294
295
|
"metrics": false,
|
|
295
296
|
"schedule": false,
|
|
297
|
+
"user": false,
|
|
296
298
|
"studio": false,
|
|
297
299
|
"studio_url": ""
|
|
298
300
|
},
|
package/package.json
CHANGED
package/reference/cloud.md
CHANGED
|
@@ -39,7 +39,7 @@ and the studio talk to the Worker with `OMNICHAT_CLOUD_SECRET` (the Worker's `SY
|
|
|
39
39
|
back if it stops, log in `.runner/studio.log`; `service restart` after a `git pull`). Setup:
|
|
40
40
|
`node cloud/studio/setup-auth.mjs` (tables and secrets), then `npx wrangler deploy` in `cloud/studio`.
|
|
41
41
|
Every write that goes through to the Mac is logged (email, method, path, status; the last 2000, at
|
|
42
|
-
`<studio>/
|
|
42
|
+
Configurações → Agente de IA, from `<studio>/session/audit`) and limited to 120 a minute per user (429 beyond it); a runner and a hub on
|
|
43
43
|
different protocol numbers refuse each other and say which side is old.
|
|
44
44
|
Protocol in `tools/lib/channel.mjs`, plan in `docs/specs/phase-1-split-api.md`.
|
|
45
45
|
- **Each job has an off switch in the database** (`settings`: `active`, `metrics_active`,
|
|
@@ -55,6 +55,20 @@ and the studio talk to the Worker with `OMNICHAT_CLOUD_SECRET` (the Worker's `SY
|
|
|
55
55
|
|
|
56
56
|
Turning back: the flag to `false` and the push again (the cloud stops), and the local agent on.
|
|
57
57
|
|
|
58
|
+
## The per-user cloud (Phase 3, in progress)
|
|
59
|
+
|
|
60
|
+
The jobs above are moving from the shared `omnichat` Worker into one Durable Object per user, `UserCloud` in the studio
|
|
61
|
+
Worker (`cloud/studio/src/cloud-*.mjs`), with their own config, secrets, webhook (`/hook/<cloudId>`), media
|
|
62
|
+
(`u/<cloudId>/` in R2) and one alarm in place of the crons and the KV gate. Step 2 (the jobs inside the object) is built and
|
|
63
|
+
tested but not deployed or used by a machine yet; the `omnichat` Worker above still does the real work until steps 3–4.
|
|
64
|
+
Routes, flags and what differs from the sketch: `docs/specs/phase-3-per-user-cloud.md`, "Step 2, as built".
|
|
65
|
+
|
|
66
|
+
On the machine, `cloud.user: true` (with `cloud.studio_url`, signed in with the Omni login) points the omnichat, the
|
|
67
|
+
Instagram account and the metrics at that cloud instead of the Worker; publishing (the schedule, the media published
|
|
68
|
+
now, the YouTube/TikTok credentials) stays on the Worker until the last step. Off by default: turning it on points the
|
|
69
|
+
CLIs at an empty cloud, so see "Step 3, as built so far" in the spec for the order (push first, and the Worker stops
|
|
70
|
+
answering before the new cloud starts).
|
|
71
|
+
|
|
58
72
|
## Free plans and their limits
|
|
59
73
|
|
|
60
74
|
- **Workers Free:** 10 ms of CPU and 50 subrequests per run. That is why a cloud pass sends at most 5 DMs
|
package/reference/learnings.md
CHANGED
|
@@ -814,3 +814,10 @@ these voices).
|
|
|
814
814
|
- **Symptom:** none yet; found reading the queue for a Windows port.
|
|
815
815
|
- **Cause:** on Windows, Python's `os.kill(pid, sig)` treats `sig == 0` as `CTRL_C_EVENT` and sends it to the process group of `pid`; any other number is a `TerminateProcess`. The Node `process.kill(pid, 0)` is a real probe on both systems.
|
|
816
816
|
- **Fixed:** `tools/job_queue.py` asks the kernel (`OpenProcess` + `GetExitCodeProcess`, `STILL_ACTIVE`) on Windows and keeps `os.kill` plus the `ps` zombie check elsewhere. Also on Windows: `select()` works on sockets only (the guardian reads the Node pipe in a thread), an atomic replace fails while a reader has the file open (retry), and a text file is written in the code page unless `encoding="utf-8"` says otherwise.
|
|
817
|
+
|
|
818
|
+
## A turn's `total_cost_usd` is the session's running total, not the turn's (18/10/2026)
|
|
819
|
+
|
|
820
|
+
- **Symptom:** the studio's session list showed US$ 220,93 for the Grok video, whose last turn closed at US$ 53,79. Measuring cost by hand from the transcripts had given a different number each time.
|
|
821
|
+
- **Cause:** `claude -p --resume` keeps adding to the conversation's totals. The `result` of turn N carries `total_cost_usd`, `modelUsage[].costUSD` and `duration_api_ms` since the conversation began (same `session_id`, only growing: 4,01 → 10,07 → 22,98 → … → 53,79). Summing the turns' costs counts the early turns again and again. The per-message `usage` in the stream-json is no substitute: it is a partial reading, with the output tokens undercounted (about 10 to 30 % under the real cost).
|
|
822
|
+
- **Fixed:** `tools/lib/cost.mjs` takes a turn's cost as the difference to the previous `result` of the same `session_id`, and uses the messages' `usage` only to split that cost between forks (scaled per model to what the `result` says). A turn that never closed (running, interrupted) is an estimate, and the next `result` gives that estimate back so the session still adds up to its last total. `summarizeSession` counts each conversation once, by its last total. Rule: to measure what an agent spent, run `node tools/cost.mjs`; do not add up `result` costs by hand, and count messages by `message.id`, never by transcript line.
|
|
823
|
+
|
|
@@ -45,7 +45,7 @@ the others only read it. A full example is in `templates/scenes.example.json`.
|
|
|
45
45
|
| `element` | 3 | name of what enters; becomes the CSS variable `--b-<element>` |
|
|
46
46
|
| `t` | 3 | provisional time in seconds, used until a voice exists |
|
|
47
47
|
| `enters_with` | 4 | reference to the line: `c01f2`, `c01f2:end`, `c01f2:palavra`, `c01f2:palavra#2`, with optional `+0.3`/`-0.3`. The word is the spoken (PT) word; `:fim` is still accepted as a legacy alias of `:end` |
|
|
48
|
-
| `sound` | 3, optional | effect on the beat: a file name in
|
|
48
|
+
| `sound` | 3, optional | effect on the beat: a file name in a sound kit (`kit/sfx/<kit>/`, `"clique-seco.mp3"`) or in `<video>/sfx/` (captured from Epidemic or sent by the user, registered in `sfx/sources.json`), an object with adjustments, or a list of them (layers at the same instant). Fields below |
|
|
49
49
|
|
|
50
50
|
`tools/timing.mjs` → `beatTime(scene, beat)` resolves it: `enters_with` if
|
|
51
51
|
present, otherwise `t`.
|
|
@@ -54,7 +54,8 @@ present, otherwise `t`.
|
|
|
54
54
|
|
|
55
55
|
| field | default | what it is |
|
|
56
56
|
|---|---|---|
|
|
57
|
-
| `name` | required | file in the video's `sfx/` or in
|
|
57
|
+
| `name` | required | file in the video's `sfx/` or in a kit; the video's one wins. From a kit, the role's `db` is added (`sound.roles`) |
|
|
58
|
+
| `kit` | the default kit (`sound.default_kit`, `motion-ui`) | which kit the file comes from when it is not in the video's `sfx/`: `"epidemic-sounds"` for the Epidemic effects. A sound never falls back to another kit: a name only in another kit fails and says which one to name |
|
|
58
59
|
| `anchor` | `"peak"` | which point of the effect lands on the instant: `"peak"` (the loudest sample) or `"start"` |
|
|
59
60
|
| `offset` | 0 | seconds after the instant (negative: before) |
|
|
60
61
|
| `db` | 0 | adjustment over the default level: `sound.level_db` below the voice's integrated loudness |
|
package/reference/studio.md
CHANGED
|
@@ -589,11 +589,12 @@ comes, that it be the studio's own.
|
|
|
589
589
|
| `GET /api/videos/<slug>/captions/page` | the video's `captions.html` (`?preview=1` marks the safe area and accepts `postMessage({ position, t })`) |
|
|
590
590
|
| `GET /api/videos/<slug>/captions/frame?t=` | JPEG of the motion at second `t` of the video, from the scene's MP4 |
|
|
591
591
|
| `POST /api/videos/<slug>/captions/position` | `{ scope: "video" \| "scene" \| "caption", target?, value }` → `captions` in `scenes.json`; `value: null` removes a scene's or caption's adjustment |
|
|
592
|
-
| `GET /api/videos/<slug>/sound` | sound timeline: total, scenes, lines (video time), sound places (each beat with its instant and each scene change, with the sounds), the bed, the catalog (
|
|
592
|
+
| `GET /api/videos/<slug>/sound` | sound timeline: total, scenes, lines (video time), sound places (each beat with its instant and each scene change, with the sounds), the bed, the catalog (the kits by role, each effect named `kit/name.mp3`, and the video's `sfx/`, measured) and the export's base MP4 |
|
|
593
593
|
| `POST /api/videos/<slug>/sound/plan` | `{ places, bed }` (draft) → the `sound.mjs` plan with the URL of each finished WAV under `/sound-draft/<slug>/…`; nothing is written to the video |
|
|
594
594
|
| `POST /api/videos/<slug>/sound` | `{ places, bed }` → `sound` on the beats, `transition.sound` and `bed` in `scenes.json` (validated; defaults are dropped) |
|
|
595
595
|
| `POST /api/videos/<slug>/sound/reassemble` | runs `assemble.mjs` (waits for its turn in the queue) |
|
|
596
|
-
| `GET /kit/<name>` | MP3 from
|
|
596
|
+
| `GET /kit/<kit>%2F<name>` | MP3 from a sound kit (`kit/sfx/<kit>/`), by the catalog's name `kit/name.mp3`, for the waveform |
|
|
597
|
+
| `GET /api/cost` | `tools/lib/cost.mjs`: what the Claude sessions spent, by video, stage, fork, model and day (`?days=N`); reads `.sessions/`, cache in `.cost/` |
|
|
597
598
|
| `GET /api/usage` | `tools/usage.py summary --json`: Fish balance, Cartesia quota, usage per video and latest generations (only reads the local log) |
|
|
598
599
|
| `POST /api/usage/refresh` | the same with `--live`: asks the platforms now and writes the reading to `.usage/` |
|
|
599
600
|
| `GET /api/analytics` | `tools/analysis.mjs context` (account, medians at the current reading and at the 24 h and 72 h milestones, also per thousand views, the channel's hour-by-hour `progression` and the `publications` in it, the playbook's `before` range, each Reel with metrics, strategy and hook, the agent) plus the text of the latest channel analysis |
|
package/studio/api/app.ts
CHANGED
|
@@ -28,6 +28,7 @@ import { onboarding } from "./routes/onboarding";
|
|
|
28
28
|
import { publish } from "./routes/publish";
|
|
29
29
|
import { queue } from "./routes/queue";
|
|
30
30
|
import { references } from "./routes/references";
|
|
31
|
+
import { me } from "./routes/me";
|
|
31
32
|
import { runner } from "./routes/runner";
|
|
32
33
|
import { schedule } from "./routes/schedule";
|
|
33
34
|
import { services } from "./routes/services";
|
|
@@ -36,6 +37,7 @@ import { settings } from "./routes/settings";
|
|
|
36
37
|
import { sound, SOUND_UPLOAD_PATH } from "./routes/sound";
|
|
37
38
|
import { thumbnail } from "./routes/thumbnail";
|
|
38
39
|
import { usage } from "./routes/usage";
|
|
40
|
+
import { cost } from "./routes/cost";
|
|
39
41
|
import { videos } from "./routes/videos";
|
|
40
42
|
import { voices } from "./routes/voices";
|
|
41
43
|
|
|
@@ -63,8 +65,10 @@ export function createApp({ port }: AppOptions) {
|
|
|
63
65
|
.route("/", references)
|
|
64
66
|
.route("/", queue)
|
|
65
67
|
.route("/", runner)
|
|
68
|
+
.route("/", me)
|
|
66
69
|
.route("/", services)
|
|
67
70
|
.route("/", usage)
|
|
71
|
+
.route("/", cost)
|
|
68
72
|
.route("/", analytics)
|
|
69
73
|
.route("/", settings)
|
|
70
74
|
.route("/", clone)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/* Agent cost (settings Uso e custo): what the Claude sessions spent, by video, stage and model. */
|
|
2
|
+
|
|
3
|
+
import { router } from "../lib/hono";
|
|
4
|
+
import { shaped } from "../lib/result";
|
|
5
|
+
import { query } from "../lib/validate";
|
|
6
|
+
import { getCost } from "../services/cost";
|
|
7
|
+
import { costQuery } from "../../shared/schemas/cost";
|
|
8
|
+
import type { AgentCost } from "../../shared/types/api";
|
|
9
|
+
|
|
10
|
+
export const cost = router().get("/cost", query(costQuery), async c => {
|
|
11
|
+
const days = Number(c.req.valid("query").days) || null;
|
|
12
|
+
return c.json(shaped<AgentCost>(await getCost(days)));
|
|
13
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/* Who is using the studio (services/me.ts), for the menu's user card at 127.0.0.1, and its sign-out. */
|
|
2
|
+
|
|
3
|
+
import { router } from "../lib/hono";
|
|
4
|
+
import { getMe, signOutOmni } from "../services/me";
|
|
5
|
+
|
|
6
|
+
export const me = router()
|
|
7
|
+
.get("/me", async c => c.json({ me: await getMe() }))
|
|
8
|
+
.post("/me/logout", async c => c.json(await signOutOmni()));
|
|
@@ -27,6 +27,7 @@ O que você faz, e só isso:
|
|
|
27
27
|
- Olhar a composição e o resultado (inspect_content): o quadro de uma cena (ou o do segundo at), a folha de contato dela (o movimento), o corte que a abre, o wireframe, várias cenas lado a lado, o vídeo exportado e os slides do carrossel. Para "essa cena está boa?", pergunte algo objetivo (o elemento principal aparece? o texto é legível?).
|
|
28
28
|
- Mostrar as coisas abrindo a tela delas, não no seu painel: "mostra as thumbs" é navigate com stage publish e tab thumbnail; "o Omnichat desse vídeo", navigate com screen omnichat_rule e o slug (content carousel para o do carrossel); o vídeo exportado, stage publish; o carrossel, stage carousel-publish; os números, video_metrics. Um pedido (thumbnail, publicação, Omnichat, aprovação, alteração) já abre sozinho a tela onde o resultado aparece: não chame navigate para ela de novo. O card no seu painel (show_card) é só quando o usuário pedir para ver ali, sem sair da tela onde está.
|
|
29
29
|
- No editor de um vídeo já montado: tocar e pausar (control_editor) e adicionar uma música ou um efeito da Epidemic Sound (add_epidemic_sound, com a busca em inglês, como "calm piano"), só quando o usuário pedir. Avise que o Claude do editor baixa a faixa em um ou dois minutos e que ela entra nos sons do vídeo.
|
|
30
|
+
- Os efeitos vêm de kits (pastas de sons). O padrão é o kit motion-ui (sons de interface sintetizados, do bencho.dev): quando o usuário pede um som sem dizer de onde, é desse kit. O kit epidemic-sounds tem os efeitos capturados da Epidemic. Quando o usuário pedir para editar um som, diga de qual kit ele é, e não troque o kit de um som sem pedido: o Claude do editor é quem muda o som, então passe o kit no pedido. add_epidemic_sound baixa só para este vídeo, não para um kit.
|
|
30
31
|
- A agenda (get_schedule): o que está agendado e o que foi publicado em cada dia, com a hora, as plataformas e o Omnichat de cada um. Para "o que sai amanhã", "o que tem essa semana", "o que publiquei ontem".
|
|
31
32
|
- O Omnichat de um conteúdo publicado ou agendado (omnichat_status, set_omnichat): você configura sozinha, sem card. Leia a regra (omnichat_status), decida e grave com set_omnichat: palavras, link, o texto da DM, as respostas públicas e as fases da DM (botão; só para quem segue, com a mensagem de quem não segue e o "Receber sem seguir"; o link num botão). O Omnichat de um conteúdo se configura sempre na tela dele, /omnichat/video/<slug> (screen omnichat_rule), nunca na etapa Publicar: ela abre sozinha com o set_omnichat e mostra a regra mudando. Depois diga numa frase o que gravou e, se faltar algo para ligar (missing_to_enable), o que falta. Ligar as respostas automáticas responde em nome da conta: só quando o usuário pedir para ligar, com essas palavras; desligar, quando ele pedir.
|
|
32
33
|
- Quando o pedido depende de um link ou de um texto que o usuário precisa colar (um vídeo para virar estilo visual, uma página, um roteiro pronto), chame ask_in_chat e diga numa frase o que colar no chat. O chat abre com o campo pronto; se o usuário responder por voz que não quer ou mudar de assunto, ele fecha sozinho. Não peça para ele ditar um link.
|
|
@@ -853,7 +853,7 @@ const SCHEDULE_STATE: Record<string, string> = {
|
|
|
853
853
|
scheduled: "agendado",
|
|
854
854
|
publishing: "publicando agora",
|
|
855
855
|
failed: "falhou",
|
|
856
|
-
missed: "perdido (
|
|
856
|
+
missed: "perdido (a máquina estava desligada)",
|
|
857
857
|
};
|
|
858
858
|
|
|
859
859
|
export async function toolSchedule(from?: string, to?: string, now = new Date()): Promise<ToolOutput> {
|
|
@@ -1858,7 +1858,7 @@ async function toolOmnichatStatus(slug?: string, content?: "video" | "carousel")
|
|
|
1858
1858
|
account: st.account?.ready ? { username: st.account.username ?? null } : { problem: st.account?.reason ?? null },
|
|
1859
1859
|
comments_permission: st.comments?.ready ?? null,
|
|
1860
1860
|
/* who answers the comments: the cloud Worker, or this Mac's launchd agent */
|
|
1861
|
-
answered_by: st.agent?.cloud?.ready ? "nuvem" : st.agent?.loaded ? "
|
|
1861
|
+
answered_by: st.agent?.cloud?.ready ? "nuvem" : st.agent?.loaded ? "esta máquina" : "ninguém (agente parado)",
|
|
1862
1862
|
pause: st.pause,
|
|
1863
1863
|
rules: (st.rules ?? []).map((r: any) => ({
|
|
1864
1864
|
content: r.type,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/* What the agents spent on the videos. The accounting lives in tools/lib/cost.mjs, which
|
|
2
|
+
tools/cost.mjs (the terminal) also uses; the studio only shows what it returns.
|
|
3
|
+
A finished turn is cached on disk, but the open ones are read again, so the answer is kept for
|
|
4
|
+
a few seconds: the UI reloads on every queue change. */
|
|
5
|
+
|
|
6
|
+
import { readTurns, summarize } from "../../../tools/lib/cost.mjs";
|
|
7
|
+
|
|
8
|
+
const cached = new Map<string, { at: number; data: Promise<unknown> }>();
|
|
9
|
+
|
|
10
|
+
export function getCost(days: number | null) {
|
|
11
|
+
const key = String(days ?? "all");
|
|
12
|
+
const hit = cached.get(key);
|
|
13
|
+
if (hit && Date.now() - hit.at < 5000) return hit.data;
|
|
14
|
+
const data = readTurns({ since: days ? Date.now() - days * 86_400_000 : null }).then(rows => ({
|
|
15
|
+
days,
|
|
16
|
+
...summarize(rows),
|
|
17
|
+
}));
|
|
18
|
+
cached.set(key, { at: Date.now(), data });
|
|
19
|
+
/* a failed reading is not kept */
|
|
20
|
+
data.catch(() => cached.delete(key));
|
|
21
|
+
return data;
|
|
22
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/* Who is using the studio, for the menu's user card (GET /api/me). The hosted page doesn't ask this: the Worker
|
|
2
|
+
answers /session/me from the signed-in session, even while this computer is away. Here, at 127.0.0.1, it is
|
|
3
|
+
the account signed in to Omni on this computer (the plugin's login: the Worker's /account), else the Claude
|
|
4
|
+
Code account that runs the agent (its name and email, read from its own files), which also fills in a name the
|
|
5
|
+
Omni account doesn't carry yet. Without either, nobody:
|
|
6
|
+
the card hides. Cached a minute: the menu asks on every page and the Worker needn't hear that often. */
|
|
7
|
+
|
|
8
|
+
import { accountsOf, loginOf } from "../../../tools/lib/agents.mjs";
|
|
9
|
+
import { accessToken, logout, readTokens } from "../../../tools/lib/oauth-client.mjs";
|
|
10
|
+
import { cloudSettings } from "../../../tools/platforms.mjs";
|
|
11
|
+
import type { Me } from "../../shared/types/api";
|
|
12
|
+
import { readConfig } from "./settings";
|
|
13
|
+
|
|
14
|
+
const TTL_MS = 60_000;
|
|
15
|
+
let cached: { at: number; me: Me | null } | null = null;
|
|
16
|
+
|
|
17
|
+
/* the Omni account of this computer: its login's token trades for the account at the Worker */
|
|
18
|
+
async function omniAccount(): Promise<Me | null> {
|
|
19
|
+
const url = cloudSettings().studio.url;
|
|
20
|
+
if (!url) return null;
|
|
21
|
+
try {
|
|
22
|
+
const token = await accessToken(url);
|
|
23
|
+
if (!token) return null;
|
|
24
|
+
const r = await fetch(`${url}/account`, {
|
|
25
|
+
headers: { authorization: `Bearer ${token}` },
|
|
26
|
+
signal: AbortSignal.timeout(4000),
|
|
27
|
+
});
|
|
28
|
+
if (!r.ok) return null;
|
|
29
|
+
const a = (await r.json()) as { email?: string | null; name?: string | null; image?: string | null };
|
|
30
|
+
return a.email ? { name: a.name ?? null, email: a.email, image: a.image ?? null, source: "omni" } : null;
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* the Claude Code account the agent runs on */
|
|
37
|
+
function claudeAccount(): Me | null {
|
|
38
|
+
for (const a of accountsOf(readConfig() ?? {}) as { provider: string }[]) {
|
|
39
|
+
if (a.provider !== "claude") continue;
|
|
40
|
+
const login = loginOf(a) as { email: string | null; name?: string | null } | null;
|
|
41
|
+
if (login?.email) return { name: login.name ?? null, email: login.email, image: null, source: "claude" };
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function getMe(now = Date.now()): Promise<Me | null> {
|
|
47
|
+
if (cached && now - cached.at < TTL_MS) return cached.me;
|
|
48
|
+
const claude = claudeAccount();
|
|
49
|
+
const omni = await omniAccount();
|
|
50
|
+
/* the Worker may not know the name yet: the Claude Code account of the same email does */
|
|
51
|
+
const me = omni ? { ...omni, name: omni.name ?? (claude?.email === omni.email ? claude.name : null) } : claude;
|
|
52
|
+
cached = { at: now, me };
|
|
53
|
+
return me;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* Sair da conta Omni deste computador: the same as `omni logout`. The Worker is told to end this computer's login
|
|
57
|
+
and the tokens here are forgotten; the card then falls back to the Claude Code account, or hides. */
|
|
58
|
+
export async function signOutOmni(): Promise<{ ok: boolean; output?: string; error?: string }> {
|
|
59
|
+
const url = cloudSettings().studio.url;
|
|
60
|
+
if (!url || !readTokens(url)) return { ok: false, error: "Este computador não está logado na conta Omni." };
|
|
61
|
+
const confirmed = await logout(url);
|
|
62
|
+
cached = null;
|
|
63
|
+
return {
|
|
64
|
+
ok: true,
|
|
65
|
+
output: confirmed ? "Saiu da conta Omni." : "Saiu da conta Omni aqui; o studio hospedado não confirmou.",
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -336,7 +336,13 @@ function stateOf(r: SessionRecord): { state: SessionState; result: TurnResult |
|
|
|
336
336
|
export function summarizeSession(r: SessionRecord) {
|
|
337
337
|
const { state, result, now } = stateOf(r);
|
|
338
338
|
const t = r.turns.at(-1);
|
|
339
|
-
|
|
339
|
+
/* a turn's `cost` is the conversation's running total (--resume keeps adding to it), so a
|
|
340
|
+
conversation counts once, by its last total; a handoff to another account is a new one */
|
|
341
|
+
const running = new Map<string, number>();
|
|
342
|
+
for (const x of r.turns) {
|
|
343
|
+
const cost = turnInfo(r, x).result?.cost;
|
|
344
|
+
if (cost != null) running.set(x.engine_id ?? r.id, Math.max(running.get(x.engine_id ?? r.id) ?? 0, cost));
|
|
345
|
+
}
|
|
340
346
|
const account = t ? accountOfTurn(r, t) : null;
|
|
341
347
|
let activity = Date.parse(r.created_at);
|
|
342
348
|
try {
|
|
@@ -355,7 +361,7 @@ export function summarizeSession(r: SessionRecord) {
|
|
|
355
361
|
turns: r.turns.length,
|
|
356
362
|
turn: t ? { n: t.n, origin: t.origin, prompt: t.prompt, started_at: t.started_at, pid: t.pid, result } : null,
|
|
357
363
|
now,
|
|
358
|
-
total_cost: +
|
|
364
|
+
total_cost: +[...running.values()].reduce((a, b) => a + b, 0).toFixed(4),
|
|
359
365
|
agent: t ? { provider: providerOf(t), account: account?.id ?? null, label: account?.label ?? null } : null,
|
|
360
366
|
};
|
|
361
367
|
}
|
|
@@ -18,7 +18,8 @@ import {
|
|
|
18
18
|
sanitizeBed,
|
|
19
19
|
sanitizeSpeed,
|
|
20
20
|
finalClock,
|
|
21
|
-
|
|
21
|
+
kitDirOf,
|
|
22
|
+
kitNames,
|
|
22
23
|
} from "../../../tools/sound.mjs";
|
|
23
24
|
import { videoClips, lineClips, lineDuration } from "../../../tools/soundtrack.mjs";
|
|
24
25
|
import { normalizeCuts } from "../../../tools/cuts.mjs";
|
|
@@ -28,7 +29,12 @@ import type { ApiResult } from "./sessions";
|
|
|
28
29
|
type Result = { ok: true; output: string } | { ok: false; error: string };
|
|
29
30
|
const DRAFT_DIR = join(ROOT, ".sound-draft");
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
/* a sound from scenes.json: its kit's effect is shown by the ref "kit/name.mp3", as the catalog names it */
|
|
33
|
+
const normalizeSound = (s: any) => {
|
|
34
|
+
const o = typeof s === "string" ? { name: s } : { ...s };
|
|
35
|
+
if (o.kit && typeof o.name === "string" && !o.name.includes("/")) o.name = `${o.kit}/${o.name}`;
|
|
36
|
+
return o;
|
|
37
|
+
};
|
|
32
38
|
const asList = (sound: any) => (sound == null ? [] : (Array.isArray(sound) ? sound : [sound]).map(normalizeSound));
|
|
33
39
|
|
|
34
40
|
/* the export's base MP4 (without the speed), for the timeline picture, and the cuts it already has */
|
|
@@ -164,10 +170,15 @@ export function draftFile(slug: string, rest: string): string | null {
|
|
|
164
170
|
return abs.startsWith(base + sep) && abs.endsWith(".wav") && existsSync(abs) ? abs : null;
|
|
165
171
|
}
|
|
166
172
|
|
|
167
|
-
/* a file
|
|
168
|
-
export function kitFile(
|
|
169
|
-
const
|
|
170
|
-
|
|
173
|
+
/* a file of a kit, for the waveform: the ref "kit/name.mp3" (the catalog's name for a kit effect) */
|
|
174
|
+
export function kitFile(ref: string): string | null {
|
|
175
|
+
const i = ref.indexOf("/");
|
|
176
|
+
const kit = i > 0 ? ref.slice(0, i) : "";
|
|
177
|
+
const name = i > 0 ? ref.slice(i + 1) : "";
|
|
178
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(kit) || !kitNames().includes(kit)) return null;
|
|
179
|
+
const dir = kitDirOf(kit);
|
|
180
|
+
const abs = resolve(dir, name);
|
|
181
|
+
return abs.startsWith(resolve(dir) + sep) && abs.endsWith(".mp3") && existsSync(abs) ? abs : null;
|
|
171
182
|
}
|
|
172
183
|
|
|
173
184
|
/* writes the draft to scenes.json: the sound of each beat, each transition, the bed and the cuts */
|
package/studio/runner/runner.ts
CHANGED
|
@@ -25,6 +25,9 @@ const DEAD_MS = 75_000;
|
|
|
25
25
|
const KEY_CHECK_MS = 30_000;
|
|
26
26
|
/* a refused key pairs again by itself at most this often (the approval page opens on this Mac) */
|
|
27
27
|
const PAIR_EVERY_MS = 60 * 60_000;
|
|
28
|
+
/* with no login (never entered, signed out, or disconnected at the studio) the approval page opens on this computer by
|
|
29
|
+
itself, at most this often: it asks for a click, and someone who closed it didn't want it every hour */
|
|
30
|
+
const LOGIN_EVERY_MS = 6 * 60 * 60_000;
|
|
28
31
|
|
|
29
32
|
export type RunnerOptions = {
|
|
30
33
|
/** the hosted studio, https://… */
|
|
@@ -42,6 +45,18 @@ export type RunnerOptions = {
|
|
|
42
45
|
/** pairs this Mac again (opens the approval page, waits, saves the new key): the key, or null. Called when
|
|
43
46
|
* the cloud refuses the key, never after a "Desconectar", which is the user's own decision */
|
|
44
47
|
pair?: () => Promise<string | null>;
|
|
48
|
+
/** signs this computer in again: opens the approval page in the browser and waits for the user to approve
|
|
49
|
+
* (tools/lib/oauth-client.mjs login). Called when there is no usable login, so nobody has to open a terminal or
|
|
50
|
+
* Claude Code to get back in; the user only approves */
|
|
51
|
+
login?: () => Promise<void>;
|
|
52
|
+
/** the hub says this machine and the hosted studio speak different protocols (UPGRADE). Which side is old isn't
|
|
53
|
+
* in the close reason, so whoever answers this looks (tools/lib/self-update.mjs: is there a newer version of
|
|
54
|
+
* the CLI?) and updates this machine when it is the one behind. Called on every retry, a minute apart. */
|
|
55
|
+
onOutdated?: () => void | Promise<void>;
|
|
56
|
+
/** the waits, in ms; the tests shorten them */
|
|
57
|
+
keyCheckMs?: number;
|
|
58
|
+
loginEveryMs?: number;
|
|
59
|
+
protocolRetryMs?: number;
|
|
45
60
|
};
|
|
46
61
|
|
|
47
62
|
/** Range on a media response: 206 from a Blob slice, 416 out of bounds; the rest as it is. */
|
|
@@ -107,6 +122,10 @@ export function startRunner(o: RunnerOptions) {
|
|
|
107
122
|
let hinted = false;
|
|
108
123
|
let pairing = false;
|
|
109
124
|
let pairedAt = 0;
|
|
125
|
+
const keyCheckMs = o.keyCheckMs ?? KEY_CHECK_MS;
|
|
126
|
+
const loginEveryMs = o.loginEveryMs ?? LOGIN_EVERY_MS;
|
|
127
|
+
let signingIn = false;
|
|
128
|
+
let signedInAt = 0;
|
|
110
129
|
|
|
111
130
|
const serve = async (request: Request) => withRange(request, await o.handle(asLocal(request, o.port)));
|
|
112
131
|
|
|
@@ -114,9 +133,10 @@ export function startRunner(o: RunnerOptions) {
|
|
|
114
133
|
/* an unreachable studio while refreshing says nothing about the login: try again later */
|
|
115
134
|
const token = await Promise.resolve(o.token()).catch(() => "");
|
|
116
135
|
if (!token || token === revoked) {
|
|
117
|
-
if (!hinted) log("aviso:
|
|
136
|
+
if (!hinted) log("aviso: esta máquina não está conectada ao studio hospedado");
|
|
118
137
|
hinted = true;
|
|
119
|
-
|
|
138
|
+
void signIn();
|
|
139
|
+
timer = setTimeout(connect, keyCheckMs);
|
|
120
140
|
return;
|
|
121
141
|
}
|
|
122
142
|
hinted = false;
|
|
@@ -171,17 +191,20 @@ export function startRunner(o: RunnerOptions) {
|
|
|
171
191
|
/* the key was deleted at /runner: wait for a new pairing instead of knocking with the old key */
|
|
172
192
|
if (e.code === REVOKED) {
|
|
173
193
|
revoked = token;
|
|
174
|
-
log("aviso:
|
|
194
|
+
log("aviso: esta máquina foi desconectada do studio hospedado");
|
|
175
195
|
hinted = true;
|
|
176
|
-
|
|
196
|
+
/* the next look finds no login and opens the approval page (once): the user can approve or refuse it */
|
|
197
|
+
timer = setTimeout(connect, keyCheckMs);
|
|
177
198
|
return;
|
|
178
199
|
}
|
|
179
200
|
/* the hub and this Mac speak different protocols: say which side is old, and look again in a minute
|
|
180
201
|
(the studio may be redeployed, or this Mac updated, without a restart) */
|
|
181
202
|
if (e.code === UPGRADE) {
|
|
182
|
-
if (!warned)
|
|
203
|
+
if (!warned)
|
|
204
|
+
log(`aviso: esta máquina e o studio hospedado não se entendem (${e.reason}); tento de novo em 1 min`);
|
|
183
205
|
warned = true;
|
|
184
|
-
|
|
206
|
+
void Promise.resolve(o.onOutdated?.()).catch(() => {});
|
|
207
|
+
timer = setTimeout(connect, o.protocolRetryMs ?? 60_000);
|
|
185
208
|
return;
|
|
186
209
|
}
|
|
187
210
|
if (was || !warned) {
|
|
@@ -200,6 +223,30 @@ export function startRunner(o: RunnerOptions) {
|
|
|
200
223
|
/* onclose follows */
|
|
201
224
|
};
|
|
202
225
|
};
|
|
226
|
+
/* No usable login: opens the approval page on this computer and waits for the user to approve, then connects with
|
|
227
|
+
the login it stored. At most once in a while, and never twice at the same time. */
|
|
228
|
+
const signIn = async () => {
|
|
229
|
+
if (!o.login || stopped || signingIn) return;
|
|
230
|
+
if (signedInAt && Date.now() - signedInAt < loginEveryMs) return;
|
|
231
|
+
signingIn = true;
|
|
232
|
+
signedInAt = Date.now();
|
|
233
|
+
log("aviso: abri a página de aprovação no navegador desta máquina: aprove para reconectar");
|
|
234
|
+
try {
|
|
235
|
+
await o.login();
|
|
236
|
+
log("cloud: login refeito");
|
|
237
|
+
delay = 1000;
|
|
238
|
+
hinted = false;
|
|
239
|
+
revoked = "";
|
|
240
|
+
if (!stopped) {
|
|
241
|
+
if (timer) clearTimeout(timer);
|
|
242
|
+
void connect();
|
|
243
|
+
}
|
|
244
|
+
} catch (e) {
|
|
245
|
+
log(`aviso: o login não terminou (${(e as Error).message})`);
|
|
246
|
+
} finally {
|
|
247
|
+
signingIn = false;
|
|
248
|
+
}
|
|
249
|
+
};
|
|
203
250
|
/* The key was refused (the login was redone, the key went missing in the cloud…): pair again, which opens
|
|
204
251
|
the approval page on this Mac, and connect with the new key. True when it took over the retry. */
|
|
205
252
|
const repair = async (token: string): Promise<boolean> => {
|
|
@@ -208,13 +255,13 @@ export function startRunner(o: RunnerOptions) {
|
|
|
208
255
|
if (k.valid !== false) return false;
|
|
209
256
|
revoked = token;
|
|
210
257
|
if (Date.now() - pairedAt < PAIR_EVERY_MS) {
|
|
211
|
-
timer = setTimeout(connect,
|
|
258
|
+
timer = setTimeout(connect, keyCheckMs);
|
|
212
259
|
return true;
|
|
213
260
|
}
|
|
214
261
|
pairing = true;
|
|
215
262
|
pairedAt = Date.now();
|
|
216
263
|
log(
|
|
217
|
-
"aviso: o studio hospedado recusou a chave
|
|
264
|
+
"aviso: o studio hospedado recusou a chave desta máquina; abri a página de aprovação no navegador: aprove para reconectar",
|
|
218
265
|
);
|
|
219
266
|
const key = await o.pair!().catch(e => {
|
|
220
267
|
log(
|
|
@@ -225,11 +272,11 @@ export function startRunner(o: RunnerOptions) {
|
|
|
225
272
|
pairing = false;
|
|
226
273
|
if (stopped) return true;
|
|
227
274
|
if (key) {
|
|
228
|
-
log("cloud:
|
|
275
|
+
log("cloud: máquina pareada de novo");
|
|
229
276
|
delay = 1000;
|
|
230
277
|
hinted = false;
|
|
231
278
|
void connect();
|
|
232
|
-
} else timer = setTimeout(connect,
|
|
279
|
+
} else timer = setTimeout(connect, keyCheckMs);
|
|
233
280
|
return true;
|
|
234
281
|
};
|
|
235
282
|
|
|
@@ -39,7 +39,7 @@ export const SCREENS: Record<ScreenId, { label: string; path: string }> = {
|
|
|
39
39
|
/* where a content's Omnichat is set up, always: /omnichat/video/<slug> (or carousel) */
|
|
40
40
|
omnichat_rule: { label: "Omnichat de um conteúdo", path: "/omnichat/$kind/$id" },
|
|
41
41
|
settings: { label: "Configurações", path: "/settings" },
|
|
42
|
-
usage: { label: "Uso
|
|
42
|
+
usage: { label: "Uso e custo", path: "/usage" },
|
|
43
43
|
omni_measures: { label: "Medições do Omni", path: "/agent" },
|
|
44
44
|
getting_started: { label: "Primeiros passos", path: "/getting-started" },
|
|
45
45
|
};
|
|
@@ -337,10 +337,11 @@ export type SoundBed = {
|
|
|
337
337
|
} | null;
|
|
338
338
|
/* an area cut (tools/cuts.mjs), in scenes.json time */
|
|
339
339
|
export type SoundCut = { from: number; to: number };
|
|
340
|
-
/* a sound of
|
|
340
|
+
/* a sound of a kit or of the video's sfx/ (a kit's effect is named "kit/name.mp3") */
|
|
341
341
|
export type SoundEffectInfo = {
|
|
342
342
|
name: string;
|
|
343
343
|
origin: "kit" | "video";
|
|
344
|
+
kit?: string | null;
|
|
344
345
|
role: string | null;
|
|
345
346
|
title: string;
|
|
346
347
|
music: boolean;
|
|
@@ -356,6 +357,7 @@ export type SoundClip = { from: number; to: number; offset: number; speed?: numb
|
|
|
356
357
|
export type SoundCatalog = {
|
|
357
358
|
kit: SoundEffectInfo[];
|
|
358
359
|
video: SoundEffectInfo[];
|
|
360
|
+
defaultKit?: string;
|
|
359
361
|
roles: Record<string, { db: number; stretchable?: boolean }>;
|
|
360
362
|
level_db: number;
|
|
361
363
|
ducking: { db: number };
|
|
@@ -939,6 +941,44 @@ export type UsageData = {
|
|
|
939
941
|
|
|
940
942
|
export type Usage = UsageData | { error: string };
|
|
941
943
|
|
|
944
|
+
/* what the agents spent (tools/lib/cost.mjs): the Claude turns in .sessions/, by video, stage and model */
|
|
945
|
+
export type AgentCostVideo = {
|
|
946
|
+
video: string;
|
|
947
|
+
type: string;
|
|
948
|
+
total: number;
|
|
949
|
+
turns: number;
|
|
950
|
+
first: string;
|
|
951
|
+
last: string;
|
|
952
|
+
stages: Record<string, number>;
|
|
953
|
+
models: Record<string, number>;
|
|
954
|
+
};
|
|
955
|
+
export type AgentCostFork = {
|
|
956
|
+
video: string;
|
|
957
|
+
session: string;
|
|
958
|
+
turn: number;
|
|
959
|
+
stage: string;
|
|
960
|
+
label: string;
|
|
961
|
+
cost: number;
|
|
962
|
+
messages: number;
|
|
963
|
+
};
|
|
964
|
+
export type AgentCost = {
|
|
965
|
+
/* the window in days; null = everything recorded */
|
|
966
|
+
days: number | null;
|
|
967
|
+
total: number;
|
|
968
|
+
turns: number;
|
|
969
|
+
sessions: number;
|
|
970
|
+
/* turns that never closed: their cost is an estimate */
|
|
971
|
+
partial: number;
|
|
972
|
+
/* turns with no dollar cost (Codex) */
|
|
973
|
+
unmeasured: number;
|
|
974
|
+
by_kind: Record<string, number>;
|
|
975
|
+
by_stage: Record<string, number>;
|
|
976
|
+
by_model: Record<string, number>;
|
|
977
|
+
by_day: { day: string; cost: number }[];
|
|
978
|
+
by_video: AgentCostVideo[];
|
|
979
|
+
top_forks: AgentCostFork[];
|
|
980
|
+
};
|
|
981
|
+
|
|
942
982
|
/* --- settings ---------------------------------------------------------------------------- */
|
|
943
983
|
export type KeyStatus = { name: string; masked: string | null; in_env: boolean };
|
|
944
984
|
|
|
@@ -1184,6 +1224,42 @@ export type RunnerProblem =
|
|
|
1184
1224
|
| { reason: "protocol"; at: string; side: "mac" | "cloud"; host?: string }
|
|
1185
1225
|
| { reason: "revoked"; at: string; name?: string };
|
|
1186
1226
|
|
|
1227
|
+
/* the first-time setup the CLI reports to the user's cloud while the studio isn't connected yet
|
|
1228
|
+
(tools/lib/setup-progress.mjs; read from GET /api/cloud/config, key `setup`) */
|
|
1229
|
+
export type SetupStepState = "pending" | "running" | "done" | "error";
|
|
1230
|
+
export type SetupStep = { id: string; label: string; detail: string; state: SetupStepState; error?: string };
|
|
1231
|
+
export type SetupRecord = {
|
|
1232
|
+
v: 1;
|
|
1233
|
+
state: "running" | "done" | "error";
|
|
1234
|
+
device: string;
|
|
1235
|
+
started_at: number;
|
|
1236
|
+
updated_at: number;
|
|
1237
|
+
steps: SetupStep[];
|
|
1238
|
+
/* the last time the process waiting for a retry looked (the page offers "Tentar de novo" while it is recent) and the
|
|
1239
|
+
id of the last retry request it answered */
|
|
1240
|
+
watching_at?: number;
|
|
1241
|
+
handled?: string;
|
|
1242
|
+
};
|
|
1243
|
+
/* what the page writes to ask for a retry (`setup_retry` of the cloud config): a step id, or "all" */
|
|
1244
|
+
export type SetupRetryRequest = { id: string; step: string; at: number };
|
|
1245
|
+
|
|
1246
|
+
/* who is using the studio: the menu's user card (GET /api/me here; the Worker's /session/me when hosted).
|
|
1247
|
+
`source` is where a local studio found them: the Omni login, or the Claude Code account */
|
|
1248
|
+
export type Me = { name: string | null; email: string; image: string | null; source: "omni" | "claude" | "session" };
|
|
1249
|
+
|
|
1250
|
+
/* the computers signed in to the hosted studio, and the actions that went through to them
|
|
1251
|
+
(the Worker's /session/computers and /session/audit: cloud/studio/src/account-routes.mjs) */
|
|
1252
|
+
export type Computer = {
|
|
1253
|
+
id: string;
|
|
1254
|
+
name: string;
|
|
1255
|
+
/* "login": entered with the plugin; "key": the old pairing key (tools/runner.mjs) */
|
|
1256
|
+
kind: "login" | "key";
|
|
1257
|
+
created_at: number;
|
|
1258
|
+
last_seen: number | null;
|
|
1259
|
+
online: boolean;
|
|
1260
|
+
};
|
|
1261
|
+
export type AuditEntry = { at: number; who: string; method: string; path: string; status: number; ms: number };
|
|
1262
|
+
|
|
1187
1263
|
/* --- the admin panel (GET /api/admin) ------------------------------------------------------------ */
|
|
1188
1264
|
export type OmniAdminSettings = {
|
|
1189
1265
|
enabled: boolean;
|