videobook 0.1.0 → 0.2.1
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/CHANGELOG.md +45 -0
- package/README.md +80 -18
- package/dist/cli/args.js +53 -3
- package/dist/cli/config-command.js +103 -0
- package/dist/cli/doctor-command.js +51 -17
- package/dist/cli/key-report.js +16 -0
- package/dist/cli/main.js +14 -1
- package/dist/cli/mcp-command.js +6 -0
- package/dist/cli/run-command.js +60 -16
- package/dist/cli/serve-command.js +5 -0
- package/dist/cli/setup-command.js +60 -0
- package/dist/dancing-workflow.js +2 -1
- package/dist/env-file.js +91 -0
- package/dist/env-secrets.js +38 -74
- package/dist/ffprobe-binary.js +29 -7
- package/dist/hydration.js +56 -0
- package/dist/jobs/archive-clip.js +2 -1
- package/dist/jobs/download-video.js +2 -1
- package/dist/load-env.js +5 -2
- package/dist/one-password.js +200 -0
- package/dist/queue.js +4 -0
- package/dist/serve.js +5 -30
- package/dist/tools/videobook-domain.js +24 -9
- package/dist/ytdlp-binary.js +65 -0
- package/package.json +4 -12
- package/ui/assets/{alive-captions-DPEr1nKM.js → alive-captions-e-n3xmCd.js} +1 -1
- package/ui/assets/{confirm-bodies-D8ouHXKy.js → confirm-bodies-DS6jh0Kb.js} +1 -1
- package/ui/assets/{fullscreen-B7nw4YbE.js → fullscreen-VY7f4mJ-.js} +1 -1
- package/ui/assets/{goal-ui-DK-xTCK0.js → goal-ui-B__NgZYz.js} +1 -1
- package/ui/assets/{index-kS1905s5.js → index-BPco0HoT.js} +2 -2
- package/ui/assets/{inspector-MlzQbTVo.js → inspector-BBuZIaqg.js} +1 -1
- package/ui/assets/{jobs-ui-CoinOWx0.js → jobs-ui-vmEH4zBT.js} +1 -1
- package/ui/assets/{music-video-Cbe80kmQ.js → music-video-CklLKD1R.js} +1 -1
- package/ui/assets/{ui-xo_Y1yQg.js → ui-CtYXY_po.js} +1 -1
- package/ui/assets/{vendor-DfTCWgKm.js → vendor-OClqq_Da.js} +2 -2
- package/ui/assets/{vendor-flow-BXbeWxW7.js → vendor-flow-DY81i6dC.js} +1 -1
- package/ui/assets/{vendor-mcp-BTlEJcHV.js → vendor-mcp-BsGNAD4I.js} +1 -1
- package/ui/assets/{videobook-Qlp9HQFu.js → videobook-lhLK6e6Q.js} +1 -1
- package/ui/index.html +11 -11
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,51 @@ and the package uses [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.2.1] - 2026-10-20
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Upgrade to videobook-engine 5.7.1 with official DoltLite 0.51.1 for
|
|
14
|
+
concurrency, reopened-catalog allocation, schema-conflict recovery, and
|
|
15
|
+
ignored AUTOINCREMENT sequence fixes. Existing 0.50.9 books retain their
|
|
16
|
+
format and engine schema.
|
|
17
|
+
|
|
18
|
+
## [0.2.0] - 2026-10-19
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- `videobook config` lists where every provider key comes from, with secret
|
|
23
|
+
values masked; `config get`, `config set` and `config unset` edit the
|
|
24
|
+
data-home `.env` atomically.
|
|
25
|
+
- 1Password support: `op://` references in the environment or `.env` resolve
|
|
26
|
+
through the 1Password CLI when a server starts, and
|
|
27
|
+
`videobook setup 1password --op-vault <vault>` reads every key from a vault
|
|
28
|
+
whose items are named after the keys. `videobook doctor` reports the setup
|
|
29
|
+
and which keys 1Password supplied.
|
|
30
|
+
- `VIDEOBOOK_YTDLP_PATH` selects the yt-dlp binary.
|
|
31
|
+
- `list_jobs` accepts `job_keys`, the `job_id` strings other tools return.
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- ffprobe now comes from `@ffprobe-installer/ffprobe`, which installs one
|
|
36
|
+
native build for the current platform instead of `ffprobe-static`'s builds
|
|
37
|
+
for every platform. Apple Silicon and Linux arm64 get a bundled ffprobe, and
|
|
38
|
+
the install is about 300 MB smaller. When npm skips the package's chmod
|
|
39
|
+
script, videobook marks the binary executable itself.
|
|
40
|
+
- The npm page no longer links to the private source repository, which
|
|
41
|
+
anonymous visitors cannot open; its homepage is the npm package page.
|
|
42
|
+
- The README states that Windows is not supported yet, and why, instead of
|
|
43
|
+
calling it untested.
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
|
|
47
|
+
- Downloading videos from URLs works when npm blocked the yt-dlp download:
|
|
48
|
+
videobook falls back to `yt-dlp` on `PATH`, passes yt-dlp the resolved
|
|
49
|
+
ffmpeg so format merges work, and fails with install instructions when no
|
|
50
|
+
yt-dlp exists. `videobook doctor` shows which yt-dlp is used.
|
|
51
|
+
- `videobook run` waits for queued jobs on an already running server too,
|
|
52
|
+
instead of returning while the job runs, and reports how the job ended.
|
|
53
|
+
|
|
9
54
|
## [0.1.0] - 2026-10-19
|
|
10
55
|
|
|
11
56
|
### Added
|
package/README.md
CHANGED
|
@@ -12,14 +12,14 @@ graph, and every change is recorded as a revision. This package installs one
|
|
|
12
12
|
## Requirements
|
|
13
13
|
|
|
14
14
|
- Node.js 22 or newer.
|
|
15
|
-
- macOS (Apple Silicon or Intel) or Linux (x64 or arm64). Windows
|
|
16
|
-
|
|
17
|
-
- About
|
|
15
|
+
- macOS (Apple Silicon or Intel) or Linux (x64 or arm64). Windows is not
|
|
16
|
+
supported yet; see the platform notes.
|
|
17
|
+
- About 800 MB of disk. Installing downloads the ffmpeg and yt-dlp binaries
|
|
18
18
|
from GitHub, and the first video render downloads Chrome Headless Shell
|
|
19
19
|
(about 150 MB) into `~/.cache/videobook/remotion`.
|
|
20
|
-
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
- Native ffmpeg, ffprobe and yt-dlp builds come with the package. Every
|
|
21
|
+
platform falls back to `ffmpeg`, `ffprobe` and `yt-dlp` on `PATH` when a
|
|
22
|
+
bundled binary is missing.
|
|
23
23
|
|
|
24
24
|
## Install
|
|
25
25
|
|
|
@@ -39,10 +39,16 @@ npm config set allow-scripts=ffmpeg-static,ytdlp-nodejs,@dolthub/doltlite,usearc
|
|
|
39
39
|
|
|
40
40
|
- `ffmpeg-static` downloads ffmpeg, which every media operation needs.
|
|
41
41
|
- `ytdlp-nodejs` downloads yt-dlp, which downloading videos from URLs needs.
|
|
42
|
+
Without it, videobook uses `yt-dlp` on `PATH` or `VIDEOBOOK_YTDLP_PATH`.
|
|
42
43
|
- `@dolthub/doltlite`, `usearch` and `onnxruntime-node` use prebuilt native
|
|
43
44
|
binaries and only compile or fetch extras on unusual platforms.
|
|
44
45
|
- `esbuild` checks its platform binary for the Remotion renderer.
|
|
45
46
|
|
|
47
|
+
npm also reports the install script of `@ffprobe-installer/<platform>`, for
|
|
48
|
+
example `@ffprobe-installer/darwin-arm64`. It only marks ffprobe executable,
|
|
49
|
+
which videobook does itself when it starts, so it can stay unapproved. With
|
|
50
|
+
`--strict-allow-scripts`, approve it too.
|
|
51
|
+
|
|
46
52
|
`videobook doctor` reports anything missing and prints the fix.
|
|
47
53
|
|
|
48
54
|
## Run
|
|
@@ -57,13 +63,19 @@ videobook run create_book name=demo
|
|
|
57
63
|
videobook tools # list tools and their parameters
|
|
58
64
|
videobook gc-books # reclaim catalog storage in every book
|
|
59
65
|
videobook doctor # check Node, binaries, native modules and keys
|
|
66
|
+
videobook config # show where each provider key comes from
|
|
67
|
+
videobook config set FAL_API_KEY op://Private/FAL_API_KEY/password
|
|
68
|
+
videobook setup 1password --op-vault Private
|
|
60
69
|
videobook --version
|
|
61
70
|
```
|
|
62
71
|
|
|
63
72
|
Every command accepts `--port <n>` (default 3001), `--library <dir>`,
|
|
64
73
|
`--home <dir>` and `--log-level <silent|error|warn|info|debug>`. Tool
|
|
65
|
-
arguments are `key=value`; values that parse as JSON are passed as JSON.
|
|
66
|
-
|
|
74
|
+
arguments are `key=value`; values that parse as JSON are passed as JSON. When
|
|
75
|
+
a tool queues a job, `run` waits for it to finish, up to `--timeout <s>`
|
|
76
|
+
(default 600), and exits 1 if the job fails. `--no-wait` returns at once,
|
|
77
|
+
except on a server that `run` started for itself, which stops when `run`
|
|
78
|
+
exits. Exit codes are 0 for success, 1 for a failure and 2 for a usage error.
|
|
67
79
|
|
|
68
80
|
## Agent hosts
|
|
69
81
|
|
|
@@ -101,26 +113,76 @@ overrides any of them inherited from your shell. Set `VIDEOBOOK_HOME` and
|
|
|
101
113
|
`VIDEOBOOK_ENV_PATH` in the environment, not in `.env`, because they decide
|
|
102
114
|
which `.env` is read.
|
|
103
115
|
|
|
104
|
-
|
|
116
|
+
Caches live in `~/.cache/videobook`. Other settings: `PORT`,
|
|
117
|
+
`VIDEOBOOK_FFMPEG_PATH`, `VIDEOBOOK_FFPROBE_PATH`, `VIDEOBOOK_YTDLP_PATH`,
|
|
118
|
+
`VIDEOBOOK_REMOTION_CACHE_DIR`, `VIDEOBOOK_MODEL_CACHE_DIR`,
|
|
119
|
+
`VIDEOBOOK_DOWNLOADS_DIR` and `VIDEOBOOK_LOG_LEVEL`.
|
|
120
|
+
|
|
121
|
+
## Provider keys
|
|
122
|
+
|
|
123
|
+
The keys are `OPENAI_API_KEY`, `KIE_API_KEY`, `XAI_API_KEY`, `FAL_API_KEY`,
|
|
105
124
|
`DASHSCOPE_API_KEY`, `ELEVENLABS_API_KEY`, `KIMI_CODING_API_KEY`,
|
|
106
125
|
`X_CLIENT_ID` and `X_CLIENT_SECRET`, `DROPBOX_CLIENT_ID` and
|
|
107
126
|
`DROPBOX_CLIENT_SECRET`, and the `B2_*` storage settings. The server starts
|
|
108
127
|
without any of them; tools that need one report which.
|
|
109
128
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
`
|
|
129
|
+
A key can come from four places. The first one that has it wins:
|
|
130
|
+
|
|
131
|
+
1. The environment of the videobook process.
|
|
132
|
+
2. A value in `.env`, set with `videobook config set <KEY> <value>` or from
|
|
133
|
+
Settings in the web UI.
|
|
134
|
+
3. A 1Password secret reference in `.env`, such as
|
|
135
|
+
`op://Private/FAL_API_KEY/password`.
|
|
136
|
+
4. A 1Password vault whose items are named after the keys.
|
|
137
|
+
|
|
138
|
+
`videobook config` lists every key with its source and masks secret values.
|
|
139
|
+
`config get`, `config set` and `config unset` work on one key and edit
|
|
140
|
+
`.env` atomically. `config set` refuses a key that your shell already
|
|
141
|
+
exports, because the exported value would win.
|
|
142
|
+
|
|
143
|
+
### 1Password
|
|
144
|
+
|
|
145
|
+
References and vaults need the [1Password CLI](https://developer.1password.com/docs/cli/)
|
|
146
|
+
(`op`). When a server starts, videobook resolves every `op://` value in the
|
|
147
|
+
environment and `.env`, and every key the vault has, in one `op inject` run.
|
|
148
|
+
`videobook run` and `videobook mcp` skip this when they relay to a running
|
|
149
|
+
server.
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
videobook setup 1password --op-vault Private # use op's own sign-in
|
|
153
|
+
videobook setup 1password --op-vault Apicity --op-token env:OP_SERVICE_ACCOUNT_TOKEN
|
|
154
|
+
videobook doctor # shows which keys came from 1Password
|
|
155
|
+
videobook setup 1password --remove
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
`setup 1password` saves `VIDEOBOOK_OP_VAULT` and `VIDEOBOOK_OP_SERVICE_TOKEN`
|
|
159
|
+
in `.env`, then lists the vault to report which keys it holds; `--no-verify`
|
|
160
|
+
skips the listing. A vault item supplies a key when its title is the key name,
|
|
161
|
+
and the value is read from its `password` field, for example
|
|
162
|
+
`op://Apicity/OPENAI_API_KEY/password`.
|
|
163
|
+
|
|
164
|
+
The token setting takes `env:VAR`, `$VAR` or a literal service-account
|
|
165
|
+
token. Prefer `env:VAR`, which keeps the token out of the file. When
|
|
166
|
+
`OP_SERVICE_ACCOUNT_TOKEN` is exported, setup records
|
|
167
|
+
`env:OP_SERVICE_ACCOUNT_TOKEN`. Without a token, `op` uses its own sign-in.
|
|
168
|
+
The token reaches `op` only through its environment, never its arguments.
|
|
169
|
+
|
|
170
|
+
A reference that does not resolve is left unset, so it is never sent to a
|
|
171
|
+
provider as a key. The server and `videobook doctor` report it. Keys that
|
|
172
|
+
come from 1Password are read-only in Settings.
|
|
114
173
|
|
|
115
174
|
## Platform notes
|
|
116
175
|
|
|
117
|
-
- macOS on Apple Silicon is the primary platform.
|
|
118
|
-
Intel build, so install `ffprobe` with `brew install ffmpeg`.
|
|
176
|
+
- macOS on Apple Silicon is the primary platform.
|
|
119
177
|
- macOS on Intel works, but ONNX Runtime has no Intel Mac build, so semantic
|
|
120
178
|
search is unavailable there.
|
|
121
|
-
- Linux x64
|
|
122
|
-
|
|
123
|
-
|
|
179
|
+
- Linux x64 and arm64 are supported.
|
|
180
|
+
- Windows is not supported yet. Every native dependency has a Windows x64
|
|
181
|
+
build, but the release smoke test has not passed on Windows. Windows has no
|
|
182
|
+
SIGTERM, so stopping `videobook serve` from another process ends it without
|
|
183
|
+
a clean shutdown, and the next start replaces the lock file it leaves. The
|
|
184
|
+
Release check workflow runs the smoke test on Windows for every release tag
|
|
185
|
+
without blocking the release.
|
|
124
186
|
|
|
125
187
|
## Security
|
|
126
188
|
|
package/dist/cli/args.js
CHANGED
|
@@ -7,7 +7,9 @@ export class CliUsageError extends Error {
|
|
|
7
7
|
}
|
|
8
8
|
}
|
|
9
9
|
const LOG_LEVELS = new Set(["silent", "error", "warn", "info", "debug"]);
|
|
10
|
-
const COMMANDS = new Set(["serve", "mcp", "run", "tools", "gc-books", "doctor", "version", "help"]);
|
|
10
|
+
const COMMANDS = new Set(["serve", "mcp", "run", "tools", "gc-books", "doctor", "setup", "config", "version", "help"]);
|
|
11
|
+
const CONFIG_ACTIONS = new Set(["list", "get", "set", "unset"]);
|
|
12
|
+
const POSITIONAL_COMMANDS = new Set(["run", "setup", "config"]);
|
|
11
13
|
const DEFAULT_RUN_TIMEOUT_SECONDS = 600;
|
|
12
14
|
const GLOBAL_OPTIONS = {
|
|
13
15
|
port: { type: "string" },
|
|
@@ -24,6 +26,8 @@ const COMMAND_OPTIONS = {
|
|
|
24
26
|
tools: {},
|
|
25
27
|
"gc-books": {},
|
|
26
28
|
doctor: { json: { type: "boolean" } },
|
|
29
|
+
setup: { "op-vault": { type: "string" }, "op-token": { type: "string" }, "no-verify": { type: "boolean" }, remove: { type: "boolean" } },
|
|
30
|
+
config: { json: { type: "boolean" } },
|
|
27
31
|
};
|
|
28
32
|
function splitCommand(argv) {
|
|
29
33
|
const [first, ...rest] = argv;
|
|
@@ -98,7 +102,7 @@ function parseCommandArgs(command, rest) {
|
|
|
98
102
|
const parsed = parseArgs({
|
|
99
103
|
args: rest,
|
|
100
104
|
options: { ...GLOBAL_OPTIONS, ...COMMAND_OPTIONS[command] },
|
|
101
|
-
allowPositionals: command
|
|
105
|
+
allowPositionals: POSITIONAL_COMMANDS.has(command),
|
|
102
106
|
strict: true,
|
|
103
107
|
});
|
|
104
108
|
return { values: parsed.values, positionals: parsed.positionals };
|
|
@@ -107,6 +111,42 @@ function parseCommandArgs(command, rest) {
|
|
|
107
111
|
throw new CliUsageError(error instanceof Error ? error.message : String(error));
|
|
108
112
|
}
|
|
109
113
|
}
|
|
114
|
+
function buildSetup(values, positionals, flags) {
|
|
115
|
+
const [target, ...extra] = positionals;
|
|
116
|
+
if (target !== "1password" || extra.length > 0)
|
|
117
|
+
throw new CliUsageError("setup needs a target: videobook setup 1password --op-vault <vault> [--op-token env:OP_SERVICE_ACCOUNT_TOKEN]");
|
|
118
|
+
const vault = stringValue(values["op-vault"]);
|
|
119
|
+
const token = stringValue(values["op-token"]);
|
|
120
|
+
return {
|
|
121
|
+
command: "setup",
|
|
122
|
+
target,
|
|
123
|
+
flags: {
|
|
124
|
+
...flags,
|
|
125
|
+
...(vault === undefined ? {} : { vault }),
|
|
126
|
+
...(token === undefined ? {} : { token }),
|
|
127
|
+
verify: values["no-verify"] !== true,
|
|
128
|
+
remove: values.remove === true,
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function buildConfig(values, positionals, flags) {
|
|
133
|
+
const [action = "list", key, value, ...extra] = positionals;
|
|
134
|
+
if (!CONFIG_ACTIONS.has(action))
|
|
135
|
+
throw new CliUsageError(`Unknown config action "${action}". Use list, get, set or unset.`);
|
|
136
|
+
const needsKey = action !== "list";
|
|
137
|
+
const needsValue = action === "set";
|
|
138
|
+
const expected = 1 + (needsKey ? 1 : 0) + (needsValue ? 1 : 0);
|
|
139
|
+
if (positionals.length > Math.max(expected, 1) || extra.length > 0 || (needsKey && !key) || (needsValue && value === undefined)) {
|
|
140
|
+
throw new CliUsageError("Usage: videobook config list | get <KEY> | set <KEY> <VALUE> | unset <KEY>");
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
command: "config",
|
|
144
|
+
action: action,
|
|
145
|
+
key: key ?? null,
|
|
146
|
+
value: value ?? null,
|
|
147
|
+
flags: { ...flags, json: values.json === true },
|
|
148
|
+
};
|
|
149
|
+
}
|
|
110
150
|
function buildCommand(command, values, positionals) {
|
|
111
151
|
const flags = globalFlags(values);
|
|
112
152
|
switch (command) {
|
|
@@ -120,6 +160,10 @@ function buildCommand(command, values, positionals) {
|
|
|
120
160
|
}
|
|
121
161
|
case "doctor":
|
|
122
162
|
return { command, flags: { ...flags, json: values.json === true } };
|
|
163
|
+
case "setup":
|
|
164
|
+
return buildSetup(values, positionals, flags);
|
|
165
|
+
case "config":
|
|
166
|
+
return buildConfig(values, positionals, flags);
|
|
123
167
|
default:
|
|
124
168
|
return { command, flags };
|
|
125
169
|
}
|
|
@@ -170,6 +214,8 @@ export function helpText() {
|
|
|
170
214
|
" tools List the MCP tools and their parameters",
|
|
171
215
|
" gc-books Reclaim catalog storage in every book",
|
|
172
216
|
" doctor Check Node, media binaries, native modules and keys",
|
|
217
|
+
" config [action] list | get <KEY> | set <KEY> <VALUE> | unset <KEY> in the data-home .env",
|
|
218
|
+
" setup 1password Read keys from a 1Password vault (--op-vault, --op-token)",
|
|
173
219
|
" version Print versions",
|
|
174
220
|
" help Print this help",
|
|
175
221
|
"",
|
|
@@ -182,7 +228,11 @@ export function helpText() {
|
|
|
182
228
|
" --open serve: open the web UI in a browser",
|
|
183
229
|
" --no-wait run: do not wait for queued jobs",
|
|
184
230
|
" --timeout <s> run: seconds to wait for jobs (default 600)",
|
|
185
|
-
" --json doctor: print machine-readable results",
|
|
231
|
+
" --json doctor, config: print machine-readable results",
|
|
232
|
+
" --op-vault <name> setup 1password: vault whose items are named after the keys",
|
|
233
|
+
" --op-token <tok> setup 1password: service-account token, best as env:VAR",
|
|
234
|
+
" --no-verify setup 1password: skip the op item list check",
|
|
235
|
+
" --remove setup 1password: remove the saved 1Password settings",
|
|
186
236
|
" -v, --version Print versions",
|
|
187
237
|
" -h, --help Print this help",
|
|
188
238
|
"",
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { hasControlCharacters, readEnvValues, updateEnvFile } from "../env-file.js";
|
|
2
|
+
import { MANAGED_ENV_KEYS, getDefaultSecretStore, isSecretEnvKey, maskEnvValue, } from "../env-secrets.js";
|
|
3
|
+
import { OP_TOKEN_SETTING, OP_VAULT_SETTING, isOpReference, vaultReference } from "../one-password.js";
|
|
4
|
+
const SETTING_KEYS = [OP_VAULT_SETTING, OP_TOKEN_SETTING];
|
|
5
|
+
const CONFIG_KEYS = [...MANAGED_ENV_KEYS, ...SETTING_KEYS];
|
|
6
|
+
const TOKEN_VARIABLE_FORM = /^(env:|\$)?[A-Z_][A-Z0-9_]*$/u;
|
|
7
|
+
function isSecret(key) {
|
|
8
|
+
if (key === OP_VAULT_SETTING)
|
|
9
|
+
return false;
|
|
10
|
+
if (key === OP_TOKEN_SETTING)
|
|
11
|
+
return true;
|
|
12
|
+
return isSecretEnvKey(key);
|
|
13
|
+
}
|
|
14
|
+
function displayValue(key, value) {
|
|
15
|
+
if (isOpReference(value))
|
|
16
|
+
return value.trim();
|
|
17
|
+
if (key === OP_TOKEN_SETTING && TOKEN_VARIABLE_FORM.test(value))
|
|
18
|
+
return value;
|
|
19
|
+
return isSecret(key) ? maskEnvValue(value) : value;
|
|
20
|
+
}
|
|
21
|
+
function configuredVault(file) {
|
|
22
|
+
return process.env[OP_VAULT_SETTING]?.trim() || file[OP_VAULT_SETTING]?.trim() || null;
|
|
23
|
+
}
|
|
24
|
+
function configRow(key, file) {
|
|
25
|
+
const shell = process.env[key]?.trim();
|
|
26
|
+
if (shell)
|
|
27
|
+
return { key, source: "environment", value: displayValue(key, shell) };
|
|
28
|
+
const stored = file[key]?.trim();
|
|
29
|
+
if (stored)
|
|
30
|
+
return { key, source: isOpReference(stored) ? "1password reference" : ".env", value: displayValue(key, stored) };
|
|
31
|
+
const vault = configuredVault(file);
|
|
32
|
+
if (vault && !SETTING_KEYS.includes(key))
|
|
33
|
+
return { key, source: "1password vault", value: vaultReference(vault, key) };
|
|
34
|
+
return { key, source: "missing", value: null };
|
|
35
|
+
}
|
|
36
|
+
function vaultNote(rows, vault) {
|
|
37
|
+
if (!vault || !rows.some((row) => row.source === "1password vault"))
|
|
38
|
+
return "";
|
|
39
|
+
return `\nKeys marked "1password vault" come from ${vault} when it has an item with that name; videobook doctor shows which do.\n`;
|
|
40
|
+
}
|
|
41
|
+
function printRows(rows, file, json, envPath) {
|
|
42
|
+
const vault = configuredVault(file);
|
|
43
|
+
if (json) {
|
|
44
|
+
process.stdout.write(`${JSON.stringify({ envPath, vault, keys: rows }, null, 2)}\n`);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const lines = rows.map((row) => `${row.key.padEnd(28)} ${row.source.padEnd(20)} ${row.value ?? ""}`.trimEnd());
|
|
48
|
+
process.stdout.write(`${lines.join("\n")}\n${vaultNote(rows, vault)}`);
|
|
49
|
+
}
|
|
50
|
+
function knownKey(key) {
|
|
51
|
+
if (CONFIG_KEYS.includes(key))
|
|
52
|
+
return true;
|
|
53
|
+
process.stderr.write(`${key} is not a videobook setting. Known keys: ${CONFIG_KEYS.join(", ")}\n`);
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
function invalidValue(key, value) {
|
|
57
|
+
if (!value || hasControlCharacters(value))
|
|
58
|
+
return `${key} needs a non-empty value without control characters`;
|
|
59
|
+
if (value.includes("'") && value.includes("\""))
|
|
60
|
+
return `${key} cannot contain both single and double quotes`;
|
|
61
|
+
if (process.env[key]?.trim())
|
|
62
|
+
return `${key} is set in the environment, which overrides the .env file; change it where it is exported instead`;
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
async function setKey(key, raw, envPath) {
|
|
66
|
+
const value = raw.trim();
|
|
67
|
+
const problem = invalidValue(key, value);
|
|
68
|
+
if (problem) {
|
|
69
|
+
process.stderr.write(`${problem}\n`);
|
|
70
|
+
return 1;
|
|
71
|
+
}
|
|
72
|
+
await updateEnvFile(envPath, { [key]: value });
|
|
73
|
+
process.stdout.write(`Set ${key}=${displayValue(key, value)} in ${envPath}\n`);
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
async function unsetKey(key, envPath) {
|
|
77
|
+
const present = key in readEnvValues(envPath);
|
|
78
|
+
if (present)
|
|
79
|
+
await updateEnvFile(envPath, { [key]: null });
|
|
80
|
+
process.stdout.write(present ? `Removed ${key} from ${envPath}\n` : `${key} was not set in ${envPath}\n`);
|
|
81
|
+
if (process.env[key]?.trim())
|
|
82
|
+
process.stderr.write(`Note: ${key} is still set in the environment.\n`);
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
export async function runConfigCommand(command) {
|
|
86
|
+
const envPath = getDefaultSecretStore().envPath();
|
|
87
|
+
if (command.action === "list") {
|
|
88
|
+
const file = readEnvValues(envPath);
|
|
89
|
+
printRows(CONFIG_KEYS.map((key) => configRow(key, file)), file, command.flags.json, envPath);
|
|
90
|
+
return 0;
|
|
91
|
+
}
|
|
92
|
+
const key = command.key ?? "";
|
|
93
|
+
if (!knownKey(key))
|
|
94
|
+
return 1;
|
|
95
|
+
if (command.action === "get") {
|
|
96
|
+
const file = readEnvValues(envPath);
|
|
97
|
+
printRows([configRow(key, file)], file, command.flags.json, envPath);
|
|
98
|
+
return 0;
|
|
99
|
+
}
|
|
100
|
+
if (command.action === "set")
|
|
101
|
+
return setKey(key, command.value ?? "", envPath);
|
|
102
|
+
return unsetKey(key, envPath);
|
|
103
|
+
}
|
|
@@ -7,11 +7,13 @@ import { SCHEMA_VERSION } from "videobook-engine/catalog-metadata";
|
|
|
7
7
|
import { resolvePort } from "./args.js";
|
|
8
8
|
import { dataLayout } from "../data-home.js";
|
|
9
9
|
import { bookManager } from "../engine.js";
|
|
10
|
-
import { MANAGED_ENV_KEYS, getDefaultSecretStore
|
|
10
|
+
import { MANAGED_ENV_KEYS, getDefaultSecretStore } from "../env-secrets.js";
|
|
11
11
|
import { FFMPEG } from "../ffmpeg-binary.js";
|
|
12
|
-
import { FFPROBE } from "../ffprobe-binary.js";
|
|
12
|
+
import { FFPROBE, bundledFfprobe } from "../ffprobe-binary.js";
|
|
13
13
|
import { inspectServerOwner } from "../server-lock.js";
|
|
14
|
+
import { readOnePasswordSettings } from "../one-password.js";
|
|
14
15
|
import { remotionBrowserDir, remotionCacheRoot } from "../remotion/browser.js";
|
|
16
|
+
import { resolveYtdlp } from "../ytdlp-binary.js";
|
|
15
17
|
const execFileAsync = promisify(execFile);
|
|
16
18
|
const require = createRequire(import.meta.url);
|
|
17
19
|
const UUID_V7 = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
|
|
@@ -107,21 +109,17 @@ async function binaryCheck(id, label, binary, bundled, envName) {
|
|
|
107
109
|
}
|
|
108
110
|
}
|
|
109
111
|
function ytdlpCheck() {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
return check({ id: "yt-dlp", label: "yt-dlp", status: "ok", detail: binary, required: false });
|
|
115
|
-
}
|
|
116
|
-
catch (error) {
|
|
117
|
-
return check({ id: "yt-dlp", label: "yt-dlp", status: "warn", detail: message(error), remedy: INSTALL_REMEDY, required: false });
|
|
112
|
+
const binary = resolveYtdlp();
|
|
113
|
+
if (binary) {
|
|
114
|
+
const origin = binary.source === "PATH" ? "from PATH" : binary.source;
|
|
115
|
+
return check({ id: "yt-dlp", label: "yt-dlp", status: "ok", detail: `${binary.path} (${origin})`, required: false });
|
|
118
116
|
}
|
|
119
117
|
return check({
|
|
120
118
|
id: "yt-dlp",
|
|
121
119
|
label: "yt-dlp",
|
|
122
120
|
status: "warn",
|
|
123
|
-
detail: "not
|
|
124
|
-
remedy: INSTALL_REMEDY,
|
|
121
|
+
detail: "not found; download_video and archive_clip from URLs will fail",
|
|
122
|
+
remedy: `${INSTALL_REMEDY}, install yt-dlp on PATH, or set VIDEOBOOK_YTDLP_PATH.`,
|
|
125
123
|
required: false,
|
|
126
124
|
});
|
|
127
125
|
}
|
|
@@ -190,6 +188,43 @@ async function renderBrowserCheck() {
|
|
|
190
188
|
});
|
|
191
189
|
}
|
|
192
190
|
}
|
|
191
|
+
async function opVersion() {
|
|
192
|
+
try {
|
|
193
|
+
return (await execFileAsync("op", ["--version"], { timeout: 10_000 })).stdout.trim();
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function describeResolution(resolution) {
|
|
200
|
+
const filled = resolution.fromReferences.length + resolution.fromVault.length;
|
|
201
|
+
const vault = resolution.vault ? `vault ${resolution.vault}` : "op:// references";
|
|
202
|
+
const token = resolution.tokenSource ? `token ${resolution.tokenSource}` : "op's own sign-in";
|
|
203
|
+
return `${vault}, ${token}: ${filled} key(s) from 1Password`;
|
|
204
|
+
}
|
|
205
|
+
function unresolvedDetail(resolution) {
|
|
206
|
+
const failures = resolution.unresolved.map((item) => `${item.key} from ${item.reference} (${item.reason})`);
|
|
207
|
+
return [resolution.error, ...failures].filter((part) => Boolean(part)).join("; ");
|
|
208
|
+
}
|
|
209
|
+
async function onePasswordCheck() {
|
|
210
|
+
const resolution = getDefaultSecretStore().onePasswordResolution();
|
|
211
|
+
const version = await opVersion();
|
|
212
|
+
const cli = version ? `op ${version}` : "op not on PATH";
|
|
213
|
+
if (!resolution?.attempted) {
|
|
214
|
+
const tokenSource = readOnePasswordSettings(process.env).token.source;
|
|
215
|
+
const token = tokenSource ? `token ${tokenSource}, ` : "";
|
|
216
|
+
return check({ id: "one-password", label: "1Password", status: "ok", detail: `not used (${token}${cli}); to read keys from a vault: videobook setup 1password --op-vault <vault>`, required: false });
|
|
217
|
+
}
|
|
218
|
+
const problems = unresolvedDetail(resolution);
|
|
219
|
+
return check({
|
|
220
|
+
id: "one-password",
|
|
221
|
+
label: "1Password",
|
|
222
|
+
status: problems ? "warn" : "ok",
|
|
223
|
+
detail: `${describeResolution(resolution)}${problems ? `; unresolved: ${problems}` : ""} (${cli})`,
|
|
224
|
+
...(problems ? { remedy: "Check the references and the service-account token, or run videobook setup 1password again." } : {}),
|
|
225
|
+
required: false,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
193
228
|
function providerKeysCheck() {
|
|
194
229
|
const store = getDefaultSecretStore();
|
|
195
230
|
const configured = MANAGED_ENV_KEYS.filter((key) => key.endsWith("_API_KEY") && store.getSource(key) !== "missing");
|
|
@@ -203,10 +238,7 @@ function providerKeysCheck() {
|
|
|
203
238
|
required: false,
|
|
204
239
|
});
|
|
205
240
|
}
|
|
206
|
-
const shown = configured.map((key) => {
|
|
207
|
-
const value = store.getValue(key) ?? "";
|
|
208
|
-
return `${key}=${isSecretEnvKey(key) ? maskEnvValue(value) : value} (${store.getSource(key)})`;
|
|
209
|
-
});
|
|
241
|
+
const shown = configured.map((key) => `${key} (${store.getSource(key)})`);
|
|
210
242
|
return check({ id: "provider-keys", label: "Provider keys", status: "ok", detail: shown.join(", "), required: false });
|
|
211
243
|
}
|
|
212
244
|
async function ownerCheck(port) {
|
|
@@ -225,11 +257,12 @@ async function collectDoctorChecks() {
|
|
|
225
257
|
await dataHomeCheck(),
|
|
226
258
|
await libraryCheck(),
|
|
227
259
|
await binaryCheck("ffmpeg", "ffmpeg", FFMPEG, bundledPath("ffmpeg-static"), "VIDEOBOOK_FFMPEG_PATH"),
|
|
228
|
-
await binaryCheck("ffprobe", "ffprobe", FFPROBE,
|
|
260
|
+
await binaryCheck("ffprobe", "ffprobe", FFPROBE, bundledFfprobe(), "VIDEOBOOK_FFPROBE_PATH"),
|
|
229
261
|
ytdlpCheck(),
|
|
230
262
|
nativeCoreCheck(),
|
|
231
263
|
searchRuntimeCheck(),
|
|
232
264
|
await renderBrowserCheck(),
|
|
265
|
+
await onePasswordCheck(),
|
|
233
266
|
providerKeysCheck(),
|
|
234
267
|
await ownerCheck(resolvePort(process.env)),
|
|
235
268
|
];
|
|
@@ -248,6 +281,7 @@ function doctorExitCode(checks) {
|
|
|
248
281
|
return checks.some((item) => item.required && item.status === "fail") ? 1 : 0;
|
|
249
282
|
}
|
|
250
283
|
export async function runDoctorCommand(flags) {
|
|
284
|
+
getDefaultSecretStore().resolveOnePassword();
|
|
251
285
|
const checks = await collectDoctorChecks();
|
|
252
286
|
const code = doctorExitCode(checks);
|
|
253
287
|
process.stdout.write(flags.json ? `${JSON.stringify({ ok: code === 0, checks }, null, 2)}\n` : renderText(checks));
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { getDefaultSecretStore } from "../env-secrets.js";
|
|
2
|
+
export function onePasswordSummary() {
|
|
3
|
+
const resolution = getDefaultSecretStore().onePasswordResolution();
|
|
4
|
+
if (!resolution?.attempted)
|
|
5
|
+
return null;
|
|
6
|
+
const filled = resolution.fromReferences.length + resolution.fromVault.length;
|
|
7
|
+
const vault = resolution.vault ? ` (vault ${resolution.vault})` : "";
|
|
8
|
+
return `${filled} key(s) from 1Password${vault}`;
|
|
9
|
+
}
|
|
10
|
+
export function onePasswordWarnings() {
|
|
11
|
+
const resolution = getDefaultSecretStore().onePasswordResolution();
|
|
12
|
+
if (!resolution?.attempted)
|
|
13
|
+
return [];
|
|
14
|
+
const failures = resolution.unresolved.map((item) => `could not resolve ${item.key} from ${item.reference}: ${item.reason}`);
|
|
15
|
+
return resolution.error ? [resolution.error, ...failures] : failures;
|
|
16
|
+
}
|
package/dist/cli/main.js
CHANGED
|
@@ -46,8 +46,20 @@ async function runCommand(parsed) {
|
|
|
46
46
|
const { runDoctorCommand } = await import("./doctor-command.js");
|
|
47
47
|
return runDoctorCommand(parsed.flags);
|
|
48
48
|
}
|
|
49
|
+
case "setup": {
|
|
50
|
+
const { runSetupCommand } = await import("./setup-command.js");
|
|
51
|
+
return runSetupCommand(parsed.flags);
|
|
52
|
+
}
|
|
53
|
+
case "config": {
|
|
54
|
+
const { runConfigCommand } = await import("./config-command.js");
|
|
55
|
+
return runConfigCommand(parsed);
|
|
56
|
+
}
|
|
49
57
|
}
|
|
50
58
|
}
|
|
59
|
+
async function loadDotenv() {
|
|
60
|
+
const { getDefaultSecretStore } = await import("../env-secrets.js");
|
|
61
|
+
getDefaultSecretStore().loadDotenv();
|
|
62
|
+
}
|
|
51
63
|
async function main(argv) {
|
|
52
64
|
const parsed = parseOrReport(argv);
|
|
53
65
|
if (typeof parsed === "number")
|
|
@@ -63,7 +75,8 @@ async function main(argv) {
|
|
|
63
75
|
applyFlagsToEnv(parsed.flags, process.env, process.cwd());
|
|
64
76
|
if (parsed.command !== "serve")
|
|
65
77
|
redirectConsoleToStderr();
|
|
66
|
-
|
|
78
|
+
if (parsed.command !== "setup" && parsed.command !== "config")
|
|
79
|
+
await loadDotenv();
|
|
67
80
|
return runCommand(parsed);
|
|
68
81
|
}
|
|
69
82
|
function flushed(stream) {
|
package/dist/cli/mcp-command.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
2
2
|
import { resolvePort } from "./args.js";
|
|
3
|
+
import { onePasswordSummary, onePasswordWarnings } from "./key-report.js";
|
|
3
4
|
import { connectUpstream, createRelayServer } from "./mcp-relay.js";
|
|
4
5
|
import { bookManager, scopedEngine } from "../engine.js";
|
|
5
6
|
import { logger } from "../logger.js";
|
|
@@ -51,6 +52,11 @@ async function serveInProcess(running) {
|
|
|
51
52
|
const instructions = serverInstructions(running.url);
|
|
52
53
|
const handle = serveStdio(() => createServer(scopedEngine, bookManager, { instructions }), { onerror: reportStdioError });
|
|
53
54
|
process.stderr.write(`videobook mcp: serving stdio; web UI and HTTP MCP at ${running.url}/ for library ${running.libraryDir}\n`);
|
|
55
|
+
const keys = onePasswordSummary();
|
|
56
|
+
if (keys)
|
|
57
|
+
process.stderr.write(`videobook mcp: ${keys}\n`);
|
|
58
|
+
for (const warning of onePasswordWarnings())
|
|
59
|
+
process.stderr.write(`videobook mcp: ${warning}\n`);
|
|
54
60
|
const code = await Promise.race([stdinEnded(), signalled(), hydrationFailure(running)]);
|
|
55
61
|
await handle.close();
|
|
56
62
|
await running.close();
|