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.
Files changed (39) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +80 -18
  3. package/dist/cli/args.js +53 -3
  4. package/dist/cli/config-command.js +103 -0
  5. package/dist/cli/doctor-command.js +51 -17
  6. package/dist/cli/key-report.js +16 -0
  7. package/dist/cli/main.js +14 -1
  8. package/dist/cli/mcp-command.js +6 -0
  9. package/dist/cli/run-command.js +60 -16
  10. package/dist/cli/serve-command.js +5 -0
  11. package/dist/cli/setup-command.js +60 -0
  12. package/dist/dancing-workflow.js +2 -1
  13. package/dist/env-file.js +91 -0
  14. package/dist/env-secrets.js +38 -74
  15. package/dist/ffprobe-binary.js +29 -7
  16. package/dist/hydration.js +56 -0
  17. package/dist/jobs/archive-clip.js +2 -1
  18. package/dist/jobs/download-video.js +2 -1
  19. package/dist/load-env.js +5 -2
  20. package/dist/one-password.js +200 -0
  21. package/dist/queue.js +4 -0
  22. package/dist/serve.js +5 -30
  23. package/dist/tools/videobook-domain.js +24 -9
  24. package/dist/ytdlp-binary.js +65 -0
  25. package/package.json +4 -12
  26. package/ui/assets/{alive-captions-DPEr1nKM.js → alive-captions-e-n3xmCd.js} +1 -1
  27. package/ui/assets/{confirm-bodies-D8ouHXKy.js → confirm-bodies-DS6jh0Kb.js} +1 -1
  28. package/ui/assets/{fullscreen-B7nw4YbE.js → fullscreen-VY7f4mJ-.js} +1 -1
  29. package/ui/assets/{goal-ui-DK-xTCK0.js → goal-ui-B__NgZYz.js} +1 -1
  30. package/ui/assets/{index-kS1905s5.js → index-BPco0HoT.js} +2 -2
  31. package/ui/assets/{inspector-MlzQbTVo.js → inspector-BBuZIaqg.js} +1 -1
  32. package/ui/assets/{jobs-ui-CoinOWx0.js → jobs-ui-vmEH4zBT.js} +1 -1
  33. package/ui/assets/{music-video-Cbe80kmQ.js → music-video-CklLKD1R.js} +1 -1
  34. package/ui/assets/{ui-xo_Y1yQg.js → ui-CtYXY_po.js} +1 -1
  35. package/ui/assets/{vendor-DfTCWgKm.js → vendor-OClqq_Da.js} +2 -2
  36. package/ui/assets/{vendor-flow-BXbeWxW7.js → vendor-flow-DY81i6dC.js} +1 -1
  37. package/ui/assets/{vendor-mcp-BTlEJcHV.js → vendor-mcp-BsGNAD4I.js} +1 -1
  38. package/ui/assets/{videobook-Qlp9HQFu.js → videobook-lhLK6e6Q.js} +1 -1
  39. 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 x64 has
16
- every native binary but is untested.
17
- - About 1.2 GB of disk. Installing downloads the ffmpeg and yt-dlp binaries
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
- - `ffmpeg` and `ffprobe` on `PATH` are recommended. Apple Silicon and Linux
21
- arm64 always use the system `ffprobe`, and every platform falls back to the
22
- system binaries when the bundled ones are missing.
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. Exit
66
- codes are 0 for success, 1 for a failure and 2 for a usage error.
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
- Provider keys: `OPENAI_API_KEY`, `KIE_API_KEY`, `XAI_API_KEY`, `FAL_API_KEY`,
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
- Caches live in `~/.cache/videobook`. Other settings: `PORT`,
111
- `VIDEOBOOK_FFMPEG_PATH`, `VIDEOBOOK_FFPROBE_PATH`,
112
- `VIDEOBOOK_REMOTION_CACHE_DIR`, `VIDEOBOOK_MODEL_CACHE_DIR`,
113
- `VIDEOBOOK_DOWNLOADS_DIR` and `VIDEOBOOK_LOG_LEVEL`.
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. The bundled ffprobe is an
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 is supported. Linux arm64 needs `ffprobe` from the system package
122
- manager.
123
- - Windows x64 is untested.
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 === "run",
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, isSecretEnvKey, maskEnvValue } from "../env-secrets.js";
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
- try {
111
- const { helpers } = require("ytdlp-nodejs");
112
- const binary = helpers.findYtdlpBinary();
113
- if (binary)
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 downloaded; download_video and archive_clip from URLs will fail",
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, bundledPath("ffprobe-static"), "VIDEOBOOK_FFPROBE_PATH"),
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
- await import("../load-env.js");
78
+ if (parsed.command !== "setup" && parsed.command !== "config")
79
+ await loadDotenv();
67
80
  return runCommand(parsed);
68
81
  }
69
82
  function flushed(stream) {
@@ -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();