@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
|
@@ -61,3 +61,50 @@
|
|
|
61
61
|
position: relative;
|
|
62
62
|
top: -1px;
|
|
63
63
|
}
|
|
64
|
+
|
|
65
|
+
/* --- agent cost (AgentCostPanel) ---------------------------------------------------------- */
|
|
66
|
+
.usage-section {
|
|
67
|
+
margin: 8px 0 0;
|
|
68
|
+
font: 600 15px var(--sans);
|
|
69
|
+
color: var(--text-2);
|
|
70
|
+
}
|
|
71
|
+
.cost-rows {
|
|
72
|
+
list-style: none;
|
|
73
|
+
margin: 0;
|
|
74
|
+
padding: 0;
|
|
75
|
+
display: flex;
|
|
76
|
+
flex-direction: column;
|
|
77
|
+
gap: 8px;
|
|
78
|
+
}
|
|
79
|
+
.cost-rows li {
|
|
80
|
+
display: grid;
|
|
81
|
+
grid-template-columns: minmax(0, 1fr) auto auto;
|
|
82
|
+
column-gap: 10px;
|
|
83
|
+
align-items: baseline;
|
|
84
|
+
font-size: 13px;
|
|
85
|
+
}
|
|
86
|
+
.cost-rows .cost-name {
|
|
87
|
+
min-width: 0;
|
|
88
|
+
overflow: hidden;
|
|
89
|
+
text-overflow: ellipsis;
|
|
90
|
+
white-space: nowrap;
|
|
91
|
+
}
|
|
92
|
+
.cost-rows .bar {
|
|
93
|
+
grid-column: 1 / -1;
|
|
94
|
+
grid-row: 2;
|
|
95
|
+
margin-top: 3px;
|
|
96
|
+
}
|
|
97
|
+
.cost-rows .cost-value {
|
|
98
|
+
font-variant-numeric: tabular-nums;
|
|
99
|
+
font-weight: 600;
|
|
100
|
+
}
|
|
101
|
+
.cost-rows .cost-pct {
|
|
102
|
+
min-width: 34px;
|
|
103
|
+
text-align: right;
|
|
104
|
+
font-variant-numeric: tabular-nums;
|
|
105
|
+
}
|
|
106
|
+
.cost-sub {
|
|
107
|
+
margin: 14px 0 8px;
|
|
108
|
+
font: 600 12.5px var(--sans);
|
|
109
|
+
color: var(--text-3);
|
|
110
|
+
}
|
|
@@ -1,15 +1,13 @@
|
|
|
1
|
-
/* StudioOffline.tsx: the screen
|
|
2
|
-
|
|
1
|
+
/* StudioOffline.tsx and FirstConnection.tsx: the screen inside the app (the menu stays) while the studio isn't
|
|
2
|
+
connected. A computer that was there and is away: the 404's look, a centered column (not-found.css). The first
|
|
3
|
+
time: the welcome, how it works, the plugin to install and the setup's steps. */
|
|
4
|
+
|
|
5
|
+
/* --- a computer that was there and is away ---------------------------------------------------- */
|
|
3
6
|
.studio-offline {
|
|
4
|
-
position: fixed;
|
|
5
|
-
inset: 0;
|
|
6
|
-
z-index: 100;
|
|
7
7
|
display: flex;
|
|
8
8
|
align-items: center;
|
|
9
9
|
justify-content: center;
|
|
10
|
-
|
|
11
|
-
overflow-y: auto;
|
|
12
|
-
background: var(--bg);
|
|
10
|
+
min-height: calc(var(--content-height) - var(--top-offset) - 88px);
|
|
13
11
|
}
|
|
14
12
|
.studio-offline-body {
|
|
15
13
|
display: flex;
|
|
@@ -44,28 +42,11 @@
|
|
|
44
42
|
line-height: 20px;
|
|
45
43
|
color: var(--text-2);
|
|
46
44
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
border-radius: var(--el-radius-control);
|
|
52
|
-
background: var(--muted);
|
|
45
|
+
/* the stepper under the line: a card, left aligned inside the centered column */
|
|
46
|
+
.studio-offline-card {
|
|
47
|
+
width: 100%;
|
|
48
|
+
margin-top: 8px;
|
|
53
49
|
text-align: left;
|
|
54
|
-
font-size: var(--el-text-ui);
|
|
55
|
-
line-height: 22px;
|
|
56
|
-
color: var(--text-2);
|
|
57
|
-
}
|
|
58
|
-
.studio-offline-steps li + li {
|
|
59
|
-
margin-top: 6px;
|
|
60
|
-
}
|
|
61
|
-
.studio-offline-steps code {
|
|
62
|
-
padding: 1px 6px;
|
|
63
|
-
border: 1px solid var(--border-strong);
|
|
64
|
-
border-radius: 6px;
|
|
65
|
-
background: var(--bg);
|
|
66
|
-
font: 400 var(--el-text-small) / 20px var(--mono);
|
|
67
|
-
color: var(--text);
|
|
68
|
-
overflow-wrap: anywhere;
|
|
69
50
|
}
|
|
70
51
|
.studio-offline-actions {
|
|
71
52
|
justify-content: center;
|
|
@@ -80,26 +61,231 @@
|
|
|
80
61
|
padding: 0 16px;
|
|
81
62
|
border-radius: var(--el-radius-control);
|
|
82
63
|
font: 500 var(--el-text-ui) / 20px var(--sans);
|
|
64
|
+
text-decoration: none;
|
|
83
65
|
}
|
|
84
66
|
.studio-offline-actions .button .ic {
|
|
85
67
|
width: 16px;
|
|
86
68
|
height: 16px;
|
|
87
69
|
}
|
|
88
70
|
.studio-offline-actions .ic.spin {
|
|
89
|
-
animation:
|
|
71
|
+
animation: spin 0.9s linear infinite;
|
|
90
72
|
}
|
|
91
|
-
|
|
73
|
+
|
|
74
|
+
/* --- the first time ---------------------------------------------------------------------------- */
|
|
75
|
+
.connect {
|
|
76
|
+
max-width: 760px;
|
|
77
|
+
margin: 0 auto;
|
|
78
|
+
display: flex;
|
|
79
|
+
flex-direction: column;
|
|
80
|
+
gap: 16px;
|
|
81
|
+
}
|
|
82
|
+
.connect .header {
|
|
83
|
+
margin-bottom: 0;
|
|
84
|
+
}
|
|
85
|
+
/* the welcome: three short lines with an icon each */
|
|
86
|
+
.connect-welcome {
|
|
87
|
+
list-style: none;
|
|
92
88
|
margin: 0;
|
|
89
|
+
padding: 0;
|
|
90
|
+
display: grid;
|
|
91
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
92
|
+
gap: 12px;
|
|
93
|
+
}
|
|
94
|
+
.connect-welcome li {
|
|
95
|
+
display: flex;
|
|
96
|
+
flex-direction: column;
|
|
97
|
+
gap: 2px;
|
|
98
|
+
padding: 14px;
|
|
99
|
+
border: 1px solid var(--border);
|
|
100
|
+
border-radius: var(--radius);
|
|
101
|
+
background: var(--surface);
|
|
93
102
|
font-size: var(--el-text-small);
|
|
103
|
+
line-height: 18px;
|
|
94
104
|
color: var(--text-3);
|
|
95
105
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
106
|
+
.connect-welcome li b {
|
|
107
|
+
font-size: var(--el-text-ui);
|
|
108
|
+
font-weight: 500;
|
|
109
|
+
line-height: 20px;
|
|
110
|
+
color: var(--text);
|
|
111
|
+
}
|
|
112
|
+
.connect-welcome-icon {
|
|
113
|
+
display: grid;
|
|
114
|
+
place-items: center;
|
|
115
|
+
width: 32px;
|
|
116
|
+
height: 32px;
|
|
117
|
+
margin-bottom: 8px;
|
|
118
|
+
border-radius: var(--el-radius-nav);
|
|
119
|
+
background: var(--muted);
|
|
120
|
+
color: var(--text);
|
|
121
|
+
font-size: 18px;
|
|
122
|
+
}
|
|
123
|
+
/* the steps before the computer reports: numbered circles on the same line as the setup's */
|
|
124
|
+
.install-steps .setup-step.todo .setup-mark {
|
|
125
|
+
border-color: var(--text-2);
|
|
126
|
+
color: var(--text);
|
|
127
|
+
font-size: 13px;
|
|
128
|
+
font-weight: 600;
|
|
129
|
+
}
|
|
130
|
+
.install-steps .setup-text {
|
|
131
|
+
gap: 6px;
|
|
132
|
+
}
|
|
133
|
+
.install-steps .setup-step {
|
|
134
|
+
padding-bottom: 24px;
|
|
135
|
+
}
|
|
136
|
+
.install-steps .setup-step:last-child {
|
|
137
|
+
padding-bottom: 0;
|
|
138
|
+
}
|
|
139
|
+
.install-steps .setup-step:not(:last-child)::before {
|
|
140
|
+
background: var(--border-strong);
|
|
141
|
+
}
|
|
142
|
+
.connect-command {
|
|
143
|
+
display: flex;
|
|
144
|
+
align-items: center;
|
|
145
|
+
gap: 10px;
|
|
146
|
+
margin-top: 10px;
|
|
147
|
+
padding: 8px 8px 8px 14px;
|
|
148
|
+
border: 1px solid var(--border-strong);
|
|
149
|
+
border-radius: var(--el-radius-control);
|
|
150
|
+
background: var(--muted);
|
|
151
|
+
}
|
|
152
|
+
.connect-command code {
|
|
153
|
+
flex: 1 1 auto;
|
|
154
|
+
min-width: 0;
|
|
155
|
+
font: 400 var(--el-text-small) / 20px var(--mono);
|
|
156
|
+
color: var(--text);
|
|
157
|
+
overflow-wrap: anywhere;
|
|
158
|
+
}
|
|
159
|
+
.connect-command .button {
|
|
160
|
+
flex: none;
|
|
161
|
+
height: 32px;
|
|
162
|
+
padding: 0 12px;
|
|
163
|
+
}
|
|
164
|
+
.connect-does-title {
|
|
165
|
+
margin-top: 4px;
|
|
166
|
+
font-size: 12px;
|
|
167
|
+
font-weight: 500;
|
|
168
|
+
letter-spacing: 0.02em;
|
|
169
|
+
color: var(--text-3);
|
|
170
|
+
}
|
|
171
|
+
.connect-does {
|
|
172
|
+
list-style: none;
|
|
173
|
+
margin: 0;
|
|
174
|
+
padding: 0;
|
|
175
|
+
display: flex;
|
|
176
|
+
flex-direction: column;
|
|
177
|
+
gap: 4px;
|
|
178
|
+
}
|
|
179
|
+
.connect-does li {
|
|
180
|
+
display: flex;
|
|
181
|
+
align-items: center;
|
|
182
|
+
gap: 8px;
|
|
183
|
+
font-size: var(--el-text-small);
|
|
184
|
+
line-height: 18px;
|
|
185
|
+
color: var(--text-2);
|
|
186
|
+
}
|
|
187
|
+
.connect-does li svg {
|
|
188
|
+
flex: none;
|
|
189
|
+
color: var(--emerald);
|
|
190
|
+
}
|
|
191
|
+
/* the approval page, in miniature */
|
|
192
|
+
.connect-mock {
|
|
193
|
+
width: 100%;
|
|
194
|
+
max-width: 300px;
|
|
195
|
+
margin-top: 4px;
|
|
196
|
+
border: 1px solid var(--border-strong);
|
|
197
|
+
border-radius: var(--el-radius-control);
|
|
198
|
+
background: var(--bg);
|
|
199
|
+
overflow: hidden;
|
|
200
|
+
}
|
|
201
|
+
.connect-mock-bar {
|
|
202
|
+
display: flex;
|
|
203
|
+
gap: 5px;
|
|
204
|
+
padding: 8px 10px;
|
|
205
|
+
border-bottom: 1px solid var(--border);
|
|
206
|
+
background: var(--muted);
|
|
207
|
+
}
|
|
208
|
+
.connect-mock-bar i {
|
|
209
|
+
width: 8px;
|
|
210
|
+
height: 8px;
|
|
211
|
+
border-radius: 50%;
|
|
212
|
+
background: var(--border-strong);
|
|
213
|
+
}
|
|
214
|
+
.connect-mock-body {
|
|
215
|
+
display: flex;
|
|
216
|
+
flex-direction: column;
|
|
217
|
+
align-items: center;
|
|
218
|
+
gap: 8px;
|
|
219
|
+
padding: 14px 16px 16px;
|
|
220
|
+
text-align: center;
|
|
221
|
+
}
|
|
222
|
+
.connect-mock-body b {
|
|
223
|
+
font-size: 13px;
|
|
224
|
+
font-weight: 500;
|
|
225
|
+
color: var(--text);
|
|
226
|
+
}
|
|
227
|
+
.connect-mock-lines {
|
|
228
|
+
display: flex;
|
|
229
|
+
flex-direction: column;
|
|
230
|
+
gap: 4px;
|
|
231
|
+
width: 80%;
|
|
232
|
+
}
|
|
233
|
+
.connect-mock-lines i {
|
|
234
|
+
height: 5px;
|
|
235
|
+
border-radius: 3px;
|
|
236
|
+
background: var(--muted-hover);
|
|
237
|
+
}
|
|
238
|
+
.connect-mock-lines i:last-child {
|
|
239
|
+
width: 60%;
|
|
240
|
+
align-self: center;
|
|
241
|
+
}
|
|
242
|
+
.connect-mock-buttons {
|
|
243
|
+
display: flex;
|
|
244
|
+
gap: 6px;
|
|
245
|
+
width: 100%;
|
|
246
|
+
}
|
|
247
|
+
.connect-mock-buttons em {
|
|
248
|
+
flex: 1;
|
|
249
|
+
padding: 5px 0;
|
|
250
|
+
border: 1px solid transparent;
|
|
251
|
+
border-radius: 8px;
|
|
252
|
+
background: var(--action);
|
|
253
|
+
font-size: 11px;
|
|
254
|
+
font-style: normal;
|
|
255
|
+
font-weight: 500;
|
|
256
|
+
color: var(--action-text);
|
|
257
|
+
}
|
|
258
|
+
.connect-mock-buttons em.outline {
|
|
259
|
+
background: none;
|
|
260
|
+
border-color: var(--border-strong);
|
|
261
|
+
color: var(--text);
|
|
262
|
+
}
|
|
263
|
+
/* one line of status under a stepper */
|
|
264
|
+
.connect-status {
|
|
265
|
+
display: flex;
|
|
266
|
+
align-items: center;
|
|
267
|
+
gap: 10px;
|
|
268
|
+
margin: 20px 0 0;
|
|
269
|
+
padding: 10px 12px;
|
|
270
|
+
border-radius: var(--el-radius-control);
|
|
271
|
+
background: var(--muted);
|
|
272
|
+
font-size: var(--el-text-small);
|
|
273
|
+
line-height: 18px;
|
|
274
|
+
color: var(--text-2);
|
|
275
|
+
}
|
|
276
|
+
.connect-status.warning {
|
|
277
|
+
background: var(--warning-bg);
|
|
278
|
+
color: var(--warning);
|
|
279
|
+
}
|
|
280
|
+
.connect-status svg {
|
|
281
|
+
flex: none;
|
|
282
|
+
font-size: 16px;
|
|
283
|
+
}
|
|
284
|
+
.connect .panel-top {
|
|
285
|
+
margin-bottom: 16px;
|
|
100
286
|
}
|
|
101
|
-
@media (
|
|
102
|
-
.
|
|
103
|
-
|
|
287
|
+
@media (max-width: 720px) {
|
|
288
|
+
.connect-welcome {
|
|
289
|
+
grid-template-columns: 1fr;
|
|
104
290
|
}
|
|
105
291
|
}
|
package/tools/cost.mjs
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* What the agents spent on the videos: per video, stage, fork and model.
|
|
3
|
+
|
|
4
|
+
cost.mjs summary of every session (--days N for the last N days)
|
|
5
|
+
cost.mjs video <slug> one video: stages, forks, turns
|
|
6
|
+
cost.mjs turns [--n 20] the latest turns, one line each
|
|
7
|
+
cost.mjs ... --json the same as data (what the studio reads)
|
|
8
|
+
|
|
9
|
+
Read-only: it reads .sessions/ (the studio's claude stream-json) and keeps a cache in .cost/.
|
|
10
|
+
The values are the API list price Claude Code itself reports (`total_cost_usd`), which is what
|
|
11
|
+
the usage costs even on a subscription. The logic is tools/lib/cost.mjs. */
|
|
12
|
+
|
|
13
|
+
import { readTurns, summarize } from "./lib/cost.mjs";
|
|
14
|
+
|
|
15
|
+
const args = process.argv.slice(2);
|
|
16
|
+
const flag = name => {
|
|
17
|
+
const i = args.indexOf(name);
|
|
18
|
+
if (i < 0) return null;
|
|
19
|
+
const [v] = args.splice(i, args[i + 1]?.startsWith("--") || args[i + 1] == null ? 1 : 2).slice(1);
|
|
20
|
+
return v ?? true;
|
|
21
|
+
};
|
|
22
|
+
const json = !!flag("--json");
|
|
23
|
+
const days = Number(flag("--days")) || null;
|
|
24
|
+
const nTurns = Number(flag("--n")) || 20;
|
|
25
|
+
const cmd = args[0] && !args[0].startsWith("--") ? args.shift() : "summary";
|
|
26
|
+
|
|
27
|
+
const STAGE = {
|
|
28
|
+
orchestrator: "orquestrador",
|
|
29
|
+
"av-script": "roteiro",
|
|
30
|
+
"av-motion": "motion (roteiro + base)",
|
|
31
|
+
compose: "composição (1 fork por cena)",
|
|
32
|
+
wireframe: "wireframes (1 fork por cena)",
|
|
33
|
+
"av-narration": "narração",
|
|
34
|
+
"av-voice": "voz",
|
|
35
|
+
"av-render": "render",
|
|
36
|
+
"av-captions": "legendas",
|
|
37
|
+
"av-assemble": "montagem",
|
|
38
|
+
"av-editor": "editor",
|
|
39
|
+
"av-reference": "estilo visual",
|
|
40
|
+
"av-thumbnail": "thumbnail",
|
|
41
|
+
"av-preview": "prévia (aposentada)",
|
|
42
|
+
other: "outros",
|
|
43
|
+
};
|
|
44
|
+
const KIND = { video: "vídeos", edit: "editor", reference: "estilos visuais", analysis: "análise", omni: "Omni" };
|
|
45
|
+
const usd = n => `US$ ${n.toLocaleString("pt-BR", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
|
46
|
+
const pad = (s, n) => String(s).padEnd(n);
|
|
47
|
+
const lpad = (s, n) => String(s).padStart(n);
|
|
48
|
+
const pct = (n, t) => (t ? `${Math.round((n / t) * 100)}%` : "—");
|
|
49
|
+
const sorted = o => Object.entries(o).sort((a, b) => b[1] - a[1]);
|
|
50
|
+
|
|
51
|
+
function table(title, entries, total, label = k => k) {
|
|
52
|
+
console.log(`\n${title}`);
|
|
53
|
+
for (const [k, v] of entries) console.log(` ${pad(label(k), 34)} ${lpad(usd(v), 13)} ${lpad(pct(v, total), 4)}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const rows = await readTurns({ since: days ? Date.now() - days * 86_400_000 : null });
|
|
57
|
+
|
|
58
|
+
if (cmd === "turns") {
|
|
59
|
+
const last = rows.slice(-nTurns);
|
|
60
|
+
if (json) console.log(JSON.stringify(last, null, 2));
|
|
61
|
+
else
|
|
62
|
+
for (const r of last)
|
|
63
|
+
console.log(
|
|
64
|
+
`${r.at.slice(0, 16).replace("T", " ")} ${pad(r.type, 9)} ${pad(r.target.slice(0, 38), 38)} t${r.n} ${lpad(r.total == null ? "sem custo (Codex)" : usd(r.total) + (r.partial ? " (parcial)" : ""), 22)} ${r.subagents} forks`,
|
|
65
|
+
);
|
|
66
|
+
} else if (cmd === "video") {
|
|
67
|
+
const slug = args[0];
|
|
68
|
+
if (!slug) {
|
|
69
|
+
console.error("erro: diga o vídeo — cost.mjs video <slug>");
|
|
70
|
+
process.exit(2);
|
|
71
|
+
}
|
|
72
|
+
const mine = rows.filter(r => r.target === slug || r.target === slug.replace(/^videos\//, "").replace(/\/$/, ""));
|
|
73
|
+
if (!mine.length) {
|
|
74
|
+
console.error(`erro: nenhuma sessão registrada para ${slug}`);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
const s = summarize(mine);
|
|
78
|
+
if (json) console.log(JSON.stringify({ ...s, turns_list: mine.map(({ groups, ...r }) => r) }, null, 2));
|
|
79
|
+
else {
|
|
80
|
+
console.log(
|
|
81
|
+
`${slug}: ${usd(s.total)} em ${s.turns} turnos${s.partial ? ` (${s.partial} em andamento ou interrompido, estimado)` : ""}`,
|
|
82
|
+
);
|
|
83
|
+
table("por etapa", sorted(s.by_stage), s.total, k => STAGE[k] ?? k);
|
|
84
|
+
table("por modelo", sorted(s.by_model), s.total);
|
|
85
|
+
console.log("\nmaiores forks");
|
|
86
|
+
for (const f of s.top_forks.slice(0, 8))
|
|
87
|
+
console.log(` ${pad(f.label.slice(0, 34), 34)} ${lpad(usd(f.cost), 13)} ${f.messages} mensagens`);
|
|
88
|
+
console.log("\nturnos");
|
|
89
|
+
for (const r of mine)
|
|
90
|
+
console.log(
|
|
91
|
+
` t${r.n} ${r.at.slice(0, 16).replace("T", " ")} ${pad(r.origin ?? "", 8)} ${lpad(r.total == null ? "—" : usd(r.total), 13)} ${r.subagents} forks`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
const s = summarize(rows);
|
|
96
|
+
if (json) console.log(JSON.stringify(s, null, 2));
|
|
97
|
+
else {
|
|
98
|
+
console.log(
|
|
99
|
+
`${usd(s.total)} em ${s.turns} turnos de ${s.sessions} sessões${days ? ` (últimos ${days} dias)` : ""}`,
|
|
100
|
+
);
|
|
101
|
+
if (s.partial) console.log(`aviso: ${s.partial} turno(s) sem fechamento, valor estimado`);
|
|
102
|
+
if (s.unmeasured) console.log(`aviso: ${s.unmeasured} turno(s) do Codex sem custo em dólar`);
|
|
103
|
+
table("por tipo de sessão", sorted(s.by_kind), s.total, k => KIND[k] ?? k);
|
|
104
|
+
table("por etapa", sorted(s.by_stage), s.total, k => STAGE[k] ?? k);
|
|
105
|
+
table("por modelo", sorted(s.by_model), s.total);
|
|
106
|
+
console.log("\npor vídeo");
|
|
107
|
+
for (const v of s.by_video.slice(0, 15))
|
|
108
|
+
console.log(
|
|
109
|
+
` ${pad(v.video.slice(0, 44), 44)} ${lpad(usd(v.total), 13)} ${lpad(v.turns, 3)} turnos ${pad(v.type, 9)}`,
|
|
110
|
+
);
|
|
111
|
+
console.log("\npor dia");
|
|
112
|
+
for (const d of s.by_day.slice(-10)) console.log(` ${d.day} ${lpad(usd(d.cost), 13)}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
package/tools/lib/agents.mjs
CHANGED
|
@@ -160,7 +160,8 @@ const jwtPayload = token => {
|
|
|
160
160
|
|
|
161
161
|
/**
|
|
162
162
|
* Who is logged in to an account, read from its own files (no process, no network).
|
|
163
|
-
* @returns {{ email: string|null, detail: string|null } | null}
|
|
163
|
+
* @returns {{ email: string|null, detail: string|null, name?: string|null } | null} `name` is the account's own
|
|
164
|
+
* display name, when its files carry one (Claude's)
|
|
164
165
|
*/
|
|
165
166
|
export function loginOf(a) {
|
|
166
167
|
if (a.provider === "claude") {
|
|
@@ -169,7 +170,7 @@ export function loginOf(a) {
|
|
|
169
170
|
if (!o) return null;
|
|
170
171
|
const org =
|
|
171
172
|
o.organizationName && o.organizationName !== `${o.emailAddress}'s Organization` ? o.organizationName : null;
|
|
172
|
-
return { email: o.emailAddress ?? null, detail: org };
|
|
173
|
+
return { email: o.emailAddress ?? null, detail: org, name: o.displayName ?? null };
|
|
173
174
|
}
|
|
174
175
|
const auth = readJson(join(a.dir, "auth.json"));
|
|
175
176
|
if (!auth) return null;
|
package/tools/lib/channel.mjs
CHANGED
|
@@ -122,7 +122,7 @@ export class Peer {
|
|
|
122
122
|
this.waiting.delete(id);
|
|
123
123
|
this.outbound.get(`q${id}`)?.cancel();
|
|
124
124
|
this.frame({ t: "abort", id });
|
|
125
|
-
reject(new ChannelError("
|
|
125
|
+
reject(new ChannelError("a máquina não respondeu a tempo", "timeout"));
|
|
126
126
|
}, timeoutMs);
|
|
127
127
|
this.waiting.set(id, { resolve, reject, timer });
|
|
128
128
|
this.frame({
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
The machine's side of the user's own cloud (docs/specs/phase-3-per-user-cloud.md, step 3).
|
|
3
|
+
|
|
4
|
+
The CLIs (omnichat.mjs, metrics.mjs) talk to "the cloud" through cloudApi (tools/platforms.mjs) with the paths
|
|
5
|
+
and bodies of the omnichat Worker they were written against. With `cloud.user` on, the calls of the omnichat,
|
|
6
|
+
the Instagram account and the metrics go to the user's own cloud instead (the studio's UserCloud, signed in with
|
|
7
|
+
the Omni login): this file is the one place that knows both vocabularies, so the callers don't change.
|
|
8
|
+
|
|
9
|
+
What still goes to the omnichat Worker (`userRoute` answers null): the schedule, the media published now and
|
|
10
|
+
the YouTube/TikTok credentials. Publishing is the last step of the migration (the Worker keeps doing it).
|
|
11
|
+
|
|
12
|
+
Pure: no network, no disk. The tests are in cloud-user.test.mjs.
|
|
13
|
+
============================================================================ */
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {{ method: string, path: string, body?: unknown }} Call
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** a config key that holds a flag (the jobs' on/off switches are config keys of the user's cloud) */
|
|
20
|
+
const flag = (/** @type {string} */ key, /** @type {unknown} */ on) => ({
|
|
21
|
+
method: "PUT",
|
|
22
|
+
path: "/api/cloud/config",
|
|
23
|
+
body: { [key]: on === true },
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The call in the user's cloud that does what an old Worker call did, or null when the old call is not one the
|
|
28
|
+
* user's cloud takes yet.
|
|
29
|
+
* @param {string} method @param {string} path with its query, as the old Worker had it @param {unknown} [body]
|
|
30
|
+
* @returns {Call | null}
|
|
31
|
+
*/
|
|
32
|
+
export function userRoute(method, path, body) {
|
|
33
|
+
const [pathname, query] = path.split("?");
|
|
34
|
+
const q = query ? `?${query}` : "";
|
|
35
|
+
const is = (/** @type {string} */ m, /** @type {string} */ p) => method === m && pathname === p;
|
|
36
|
+
|
|
37
|
+
/* what only the user's own cloud has (its status with the webhook address, the secrets): as they are */
|
|
38
|
+
if (pathname.startsWith("/api/cloud/")) return { method, path, body };
|
|
39
|
+
|
|
40
|
+
/* the switches and the omnichat config: config keys */
|
|
41
|
+
if (is("PUT", "/api/active")) return flag("omnichat.active", /** @type {any} */ (body)?.active);
|
|
42
|
+
if (is("PUT", "/api/metrics/active")) return flag("metrics.active", /** @type {any} */ (body)?.active);
|
|
43
|
+
if (is("PUT", "/api/config")) return { method, path: "/api/cloud/config", body: { "omnichat.config": body } };
|
|
44
|
+
|
|
45
|
+
if (is("GET", "/api/status")) return { method, path: "/api/cloud/omnichat/status" };
|
|
46
|
+
if (is("PUT", "/api/account")) return { method, path: "/api/cloud/instagram/account", body };
|
|
47
|
+
|
|
48
|
+
/* a post's rule and record, and the passes on it */
|
|
49
|
+
const post = /^\/api\/posts\/(\d+)(\/(?:record|retry|run|send))?$/.exec(pathname);
|
|
50
|
+
if (post) return { method, path: `/api/cloud/omnichat/posts/${post[1]}${post[2] ?? ""}${q}`, body };
|
|
51
|
+
|
|
52
|
+
/* the metrics */
|
|
53
|
+
if (is("GET", "/api/metrics")) return { method, path: "/api/cloud/metrics" };
|
|
54
|
+
const metrics = /^\/api\/metrics\/(videos\/\d+|account|run)$/.exec(pathname);
|
|
55
|
+
if (metrics) return { method, path: `/api/cloud/metrics/${metrics[1]}`, body };
|
|
56
|
+
|
|
57
|
+
return null;
|
|
58
|
+
}
|