@contenthero/cli 0.1.0

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 (94) hide show
  1. package/README.md +108 -0
  2. package/dist/args.d.ts +13 -0
  3. package/dist/args.d.ts.map +1 -0
  4. package/dist/args.js +35 -0
  5. package/dist/args.js.map +1 -0
  6. package/dist/commands/account.d.ts +7 -0
  7. package/dist/commands/account.d.ts.map +1 -0
  8. package/dist/commands/account.js +22 -0
  9. package/dist/commands/account.js.map +1 -0
  10. package/dist/commands/auth.d.ts +35 -0
  11. package/dist/commands/auth.d.ts.map +1 -0
  12. package/dist/commands/auth.js +210 -0
  13. package/dist/commands/auth.js.map +1 -0
  14. package/dist/commands/brandkit.d.ts +12 -0
  15. package/dist/commands/brandkit.d.ts.map +1 -0
  16. package/dist/commands/brandkit.js +159 -0
  17. package/dist/commands/brandkit.js.map +1 -0
  18. package/dist/commands/config.d.ts +11 -0
  19. package/dist/commands/config.d.ts.map +1 -0
  20. package/dist/commands/config.js +69 -0
  21. package/dist/commands/config.js.map +1 -0
  22. package/dist/commands/connected.d.ts +13 -0
  23. package/dist/commands/connected.d.ts.map +1 -0
  24. package/dist/commands/connected.js +86 -0
  25. package/dist/commands/connected.js.map +1 -0
  26. package/dist/commands/generate.d.ts +15 -0
  27. package/dist/commands/generate.d.ts.map +1 -0
  28. package/dist/commands/generate.js +215 -0
  29. package/dist/commands/generate.js.map +1 -0
  30. package/dist/commands/generation.d.ts +12 -0
  31. package/dist/commands/generation.d.ts.map +1 -0
  32. package/dist/commands/generation.js +61 -0
  33. package/dist/commands/generation.js.map +1 -0
  34. package/dist/commands/identity.d.ts +10 -0
  35. package/dist/commands/identity.d.ts.map +1 -0
  36. package/dist/commands/identity.js +83 -0
  37. package/dist/commands/identity.js.map +1 -0
  38. package/dist/commands/inspiration.d.ts +17 -0
  39. package/dist/commands/inspiration.d.ts.map +1 -0
  40. package/dist/commands/inspiration.js +124 -0
  41. package/dist/commands/inspiration.js.map +1 -0
  42. package/dist/commands/media.d.ts +11 -0
  43. package/dist/commands/media.d.ts.map +1 -0
  44. package/dist/commands/media.js +76 -0
  45. package/dist/commands/media.js.map +1 -0
  46. package/dist/commands/model.d.ts +10 -0
  47. package/dist/commands/model.d.ts.map +1 -0
  48. package/dist/commands/model.js +27 -0
  49. package/dist/commands/model.js.map +1 -0
  50. package/dist/commands/pipeline.d.ts +10 -0
  51. package/dist/commands/pipeline.d.ts.map +1 -0
  52. package/dist/commands/pipeline.js +21 -0
  53. package/dist/commands/pipeline.js.map +1 -0
  54. package/dist/commands/post.d.ts +14 -0
  55. package/dist/commands/post.d.ts.map +1 -0
  56. package/dist/commands/post.js +267 -0
  57. package/dist/commands/post.js.map +1 -0
  58. package/dist/commands/schema.d.ts +11 -0
  59. package/dist/commands/schema.d.ts.map +1 -0
  60. package/dist/commands/schema.js +82 -0
  61. package/dist/commands/schema.js.map +1 -0
  62. package/dist/commands/upscale.d.ts +11 -0
  63. package/dist/commands/upscale.d.ts.map +1 -0
  64. package/dist/commands/upscale.js +56 -0
  65. package/dist/commands/upscale.js.map +1 -0
  66. package/dist/config.d.ts +35 -0
  67. package/dist/config.d.ts.map +1 -0
  68. package/dist/config.js +68 -0
  69. package/dist/config.js.map +1 -0
  70. package/dist/context.d.ts +45 -0
  71. package/dist/context.d.ts.map +1 -0
  72. package/dist/context.js +63 -0
  73. package/dist/context.js.map +1 -0
  74. package/dist/errors.d.ts +32 -0
  75. package/dist/errors.d.ts.map +1 -0
  76. package/dist/errors.js +51 -0
  77. package/dist/errors.js.map +1 -0
  78. package/dist/generation.d.ts +34 -0
  79. package/dist/generation.d.ts.map +1 -0
  80. package/dist/generation.js +111 -0
  81. package/dist/generation.js.map +1 -0
  82. package/dist/index.d.ts +11 -0
  83. package/dist/index.d.ts.map +1 -0
  84. package/dist/index.js +42 -0
  85. package/dist/index.js.map +1 -0
  86. package/dist/output.d.ts +14 -0
  87. package/dist/output.d.ts.map +1 -0
  88. package/dist/output.js +33 -0
  89. package/dist/output.js.map +1 -0
  90. package/dist/program.d.ts +11 -0
  91. package/dist/program.d.ts.map +1 -0
  92. package/dist/program.js +69 -0
  93. package/dist/program.js.map +1 -0
  94. package/package.json +52 -0
package/README.md ADDED
@@ -0,0 +1,108 @@
1
+ # @contenthero/cli
2
+
3
+ The official ContentHero command-line interface. Generate media, run the content
4
+ pipeline, and read your brand and research context from the terminal or any agent
5
+ shell. It rides the [`@contenthero/sdk`](https://www.npmjs.com/package/@contenthero/sdk)
6
+ kernel, so it talks to the same `/api/v1` surface as the SDK and the
7
+ [MCP server](https://www.npmjs.com/package/@contenthero/mcp).
8
+
9
+ Built agent-first: JSON by default, predictable exit codes, and a `schema` command
10
+ so an agent can discover every command's inputs without docs.
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ npm install -g @contenthero/cli
16
+ ```
17
+
18
+ Requires Node 20+. The binary is `contenthero`.
19
+
20
+ ## Authenticate
21
+
22
+ The CLI resolves your API key in this order: the `--api-key` flag, then the
23
+ `CONTENTHERO_API_KEY` environment variable, then a stored credential.
24
+
25
+ ```bash
26
+ # Browser-assisted (recommended): opens your browser, mints a key for this machine
27
+ contenthero login
28
+
29
+ # Bring your own key (CI / headless): create one in the app under API Keys
30
+ export CONTENTHERO_API_KEY=ch_live_...
31
+ # ...or store it:
32
+ contenthero login --with-key ch_live_... # or: echo "$KEY" | contenthero login --with-key
33
+
34
+ contenthero auth status # verify the active key and show the account
35
+ contenthero logout # remove the stored credential
36
+ ```
37
+
38
+ The stored credential lives at `~/.contenthero/credentials` (mode 0600). The env
39
+ var always wins over the stored file, so CI can override a local login.
40
+
41
+ ## Output
42
+
43
+ JSON is the default (built for agents and scripts). Add `--human` for readable
44
+ tables and key/value output.
45
+
46
+ ```bash
47
+ contenthero account balance
48
+ contenthero model list --type image --human
49
+ ```
50
+
51
+ ## Generate
52
+
53
+ `generate` covers image, video, audio, board, and lip-sync. The waitable kinds
54
+ share `--cost` (preflight, charges nothing), `--wait` / `--no-wait` (default
55
+ waits), and `--timeout <seconds>`.
56
+
57
+ ```bash
58
+ # Preflight the cost, then generate and wait for the URLs
59
+ contenthero generate image "a red ceramic cube on white" --model nano-banana-2 --cost
60
+ contenthero generate image "a red ceramic cube on white" --model nano-banana-2
61
+
62
+ # Submit without blocking, then poll
63
+ ID=$(contenthero generate video "drone shot over a canyon" --model veo-3.1-fast --no-wait | jq -r .outputId)
64
+ contenthero generation status "$ID"
65
+ contenthero generation wait "$ID" --timeout 300
66
+
67
+ # Chain: feed a previous output id straight in as a reference (URL or output id)
68
+ contenthero generate video "slow zoom in" --model veo-3.1-fast --start-frame "$ID"
69
+
70
+ # Audio (synchronous) and upscaling
71
+ contenthero generate audio --model elevenlabs-tts --text "Hello there" --voice <voiceId>
72
+ contenthero upscale "$ID" --model topaz-image-upscale --factor 2x
73
+ ```
74
+
75
+ Exit code 4 means a render was accepted but did not finish before the timeout. The
76
+ `outputId` is still emitted, so you can keep polling.
77
+
78
+ ## The rest of the surface
79
+
80
+ ```
81
+ contenthero media list | get <id>
82
+ contenthero post list | get | create | update | archive | schedule | publish
83
+ | destination add|update | asset add
84
+ contenthero pipeline stages
85
+ contenthero brand-kit list | get | update | archive | section add|update|archive
86
+ contenthero avatar list | get <id>
87
+ contenthero voice list | get <id>
88
+ contenthero inspiration accounts | account <id> | outliers | content <id>
89
+ contenthero brand-account list | performance <id>
90
+ contenthero connected-account list | get <id>
91
+ contenthero account balance
92
+ contenthero model list
93
+ ```
94
+
95
+ ## For agents
96
+
97
+ `schema` dumps every command's arguments and options as JSON, so an agent can wire
98
+ up calls without reading these docs:
99
+
100
+ ```bash
101
+ contenthero schema # the whole surface
102
+ contenthero schema generate image # just one command
103
+ ```
104
+
105
+ ## Exit codes
106
+
107
+ `0` success, `1` general error, `2` usage error, `3` authentication error,
108
+ `4` timeout (the work was accepted but did not finish in time).
package/dist/args.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Small commander option coercers. They throw a usage-coded CliError on bad
3
+ * input so the central error boundary reports exit 2 with a clear message.
4
+ */
5
+ /** Parse an integer option value. */
6
+ export declare function toInt(value: string): number;
7
+ /** Parse a numeric option value (integer or decimal). */
8
+ export declare function toFloat(value: string): number;
9
+ /** Collect a repeatable option (e.g. --ref a --ref b) into an array. */
10
+ export declare function collect(value: string, previous?: string[]): string[];
11
+ /** Parse a JSON option value into an unknown, with a usage error on bad JSON. */
12
+ export declare function toJson(value: string): unknown;
13
+ //# sourceMappingURL=args.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"args.d.ts","sourceRoot":"","sources":["../src/args.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,qCAAqC;AACrC,wBAAgB,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAM3C;AAED,yDAAyD;AACzD,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAM7C;AAED,wEAAwE;AACxE,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAM,EAAO,GAAG,MAAM,EAAE,CAExE;AAED,iFAAiF;AACjF,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAM7C"}
package/dist/args.js ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Small commander option coercers. They throw a usage-coded CliError on bad
3
+ * input so the central error boundary reports exit 2 with a clear message.
4
+ */
5
+ import { CliError, EXIT } from './errors.js';
6
+ /** Parse an integer option value. */
7
+ export function toInt(value) {
8
+ const n = Number(value);
9
+ if (!Number.isInteger(n)) {
10
+ throw new CliError(`Expected an integer, got "${value}".`, EXIT.USAGE);
11
+ }
12
+ return n;
13
+ }
14
+ /** Parse a numeric option value (integer or decimal). */
15
+ export function toFloat(value) {
16
+ const n = Number(value);
17
+ if (Number.isNaN(n)) {
18
+ throw new CliError(`Expected a number, got "${value}".`, EXIT.USAGE);
19
+ }
20
+ return n;
21
+ }
22
+ /** Collect a repeatable option (e.g. --ref a --ref b) into an array. */
23
+ export function collect(value, previous = []) {
24
+ return [...previous, value];
25
+ }
26
+ /** Parse a JSON option value into an unknown, with a usage error on bad JSON. */
27
+ export function toJson(value) {
28
+ try {
29
+ return JSON.parse(value);
30
+ }
31
+ catch {
32
+ throw new CliError(`Expected valid JSON, got "${value}".`, EXIT.USAGE);
33
+ }
34
+ }
35
+ //# sourceMappingURL=args.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"args.js","sourceRoot":"","sources":["../src/args.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,aAAa,CAAA;AAE5C,qCAAqC;AACrC,MAAM,UAAU,KAAK,CAAC,KAAa;IACjC,MAAM,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;IACvB,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,QAAQ,CAAC,6BAA6B,KAAK,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,CAAA;IACxE,CAAC;IACD,OAAO,CAAC,CAAA;AACV,CAAC;AAED,yDAAyD;AACzD,MAAM,UAAU,OAAO,CAAC,KAAa;IACnC,MAAM,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;IACvB,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;QACpB,MAAM,IAAI,QAAQ,CAAC,2BAA2B,KAAK,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,CAAA;IACtE,CAAC;IACD,OAAO,CAAC,CAAA;AACV,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,OAAO,CAAC,KAAa,EAAE,WAAqB,EAAE;IAC5D,OAAO,CAAC,GAAG,QAAQ,EAAE,KAAK,CAAC,CAAA;AAC7B,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,MAAM,CAAC,KAAa;IAClC,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;IAC1B,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,QAAQ,CAAC,6BAA6B,KAAK,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,CAAA;IACxE,CAAC;AACH,CAAC"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * `contenthero account` - account-level reads.
3
+ * account balance credit balance, tier, and auto-top-up state
4
+ */
5
+ import type { Command } from 'commander';
6
+ export declare function registerAccount(program: Command): void;
7
+ //# sourceMappingURL=account.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"account.d.ts","sourceRoot":"","sources":["../../src/commands/account.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAKxC,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAiBtD"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * `contenthero account` - account-level reads.
3
+ * account balance credit balance, tier, and auto-top-up state
4
+ */
5
+ import { makeClient } from '../context.js';
6
+ import { emit, keyValues } from '../output.js';
7
+ export function registerAccount(program) {
8
+ const account = program.command('account').description('Account-level reads (balance, tier)');
9
+ account
10
+ .command('balance')
11
+ .description('Show the credit balance, subscription tier, and auto-top-up state')
12
+ .action(async (_opts, command) => {
13
+ const { client, ctx } = makeClient(command);
14
+ const balance = await client.getBalance();
15
+ emit(balance, ctx, (b) => keyValues([
16
+ ['Balance', `${b.balance} credits`],
17
+ ['Tier', b.tier],
18
+ ['Auto top-up', b.autoTopupEnabled ? 'on' : 'off'],
19
+ ]));
20
+ });
21
+ }
22
+ //# sourceMappingURL=account.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"account.js","sourceRoot":"","sources":["../../src/commands/account.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAC1C,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAG9C,MAAM,UAAU,eAAe,CAAC,OAAgB;IAC9C,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,WAAW,CAAC,qCAAqC,CAAC,CAAA;IAE7F,OAAO;SACJ,OAAO,CAAC,SAAS,CAAC;SAClB,WAAW,CAAC,mEAAmE,CAAC;SAChF,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,OAAgB,EAAE,EAAE;QACxC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,UAAU,CAAC,OAAO,CAAC,CAAA;QAC3C,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,UAAU,EAAE,CAAA;QACzC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,CAAU,EAAE,EAAE,CAChC,SAAS,CAAC;YACR,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC,OAAO,UAAU,CAAC;YACnC,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC;YAChB,CAAC,aAAa,EAAE,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC;SACnD,CAAC,CACH,CAAA;IACH,CAAC,CAAC,CAAA;AACN,CAAC"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * `contenthero auth` - manage how the CLI authenticates.
3
+ * auth login browser-assisted key provisioning (the primary path)
4
+ * auth login --with-key store a key you already have (flag value or piped stdin)
5
+ * auth status verify the active key and show the account
6
+ * auth logout delete the stored credential
7
+ *
8
+ * `login` and `logout` are also exposed at the top level (`contenthero login`)
9
+ * since that is the headline command. Both forms share these handlers.
10
+ *
11
+ * Browser login: we start a localhost listener, open the app's /cli/auth page
12
+ * (passing the loopback port + a state nonce), the user approves, the page mints a
13
+ * named `CLI (<host>)` key and navigates back to the listener with it. We verify
14
+ * the key, then store it. `--with-key` and CONTENTHERO_API_KEY cover headless /
15
+ * bring-your-own-key (CI, or a key from the app's API Keys tab).
16
+ */
17
+ import type { Command } from 'commander';
18
+ export interface LoopbackOptions {
19
+ /** The state nonce the browser callback must echo back. */
20
+ state: string;
21
+ /** Called with the bound port once the listener is up (to build + open the URL). */
22
+ onListening: (port: number) => void;
23
+ /** Give up after this many ms (default 3 minutes). */
24
+ timeoutMs?: number;
25
+ }
26
+ /**
27
+ * Start a localhost listener and resolve with the API key the browser hands back
28
+ * to `/callback?state=<state>&key=<key>`. Rejects on `error=...`, a listener
29
+ * failure, or timeout. A callback whose state does not match is rejected with 400
30
+ * and ignored (the listener keeps waiting), so a stray or forged hit cannot end
31
+ * the flow. Exported for testing the loopback contract in-process.
32
+ */
33
+ export declare function awaitLoopbackKey(opts: LoopbackOptions): Promise<string>;
34
+ export declare function registerAuth(program: Command): void;
35
+ //# sourceMappingURL=auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/commands/auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AA4DxC,MAAM,WAAW,eAAe;IAC9B,2DAA2D;IAC3D,KAAK,EAAE,MAAM,CAAA;IACb,oFAAoF;IACpF,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IACnC,sDAAsD;IACtD,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CAqDvE;AAiGD,wBAAgB,YAAY,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAanD"}
@@ -0,0 +1,210 @@
1
+ /**
2
+ * `contenthero auth` - manage how the CLI authenticates.
3
+ * auth login browser-assisted key provisioning (the primary path)
4
+ * auth login --with-key store a key you already have (flag value or piped stdin)
5
+ * auth status verify the active key and show the account
6
+ * auth logout delete the stored credential
7
+ *
8
+ * `login` and `logout` are also exposed at the top level (`contenthero login`)
9
+ * since that is the headline command. Both forms share these handlers.
10
+ *
11
+ * Browser login: we start a localhost listener, open the app's /cli/auth page
12
+ * (passing the loopback port + a state nonce), the user approves, the page mints a
13
+ * named `CLI (<host>)` key and navigates back to the listener with it. We verify
14
+ * the key, then store it. `--with-key` and CONTENTHERO_API_KEY cover headless /
15
+ * bring-your-own-key (CI, or a key from the app's API Keys tab).
16
+ */
17
+ import http from 'node:http';
18
+ import { randomBytes } from 'node:crypto';
19
+ import { hostname } from 'node:os';
20
+ import { spawn } from 'node:child_process';
21
+ import { ContentHero } from '@contenthero/sdk';
22
+ import { clearCredential, credentialsPath, writeCredential } from '../config.js';
23
+ import { resolveContext } from '../context.js';
24
+ import { emit, keyValues } from '../output.js';
25
+ import { CliError, EXIT } from '../errors.js';
26
+ const DEFAULT_BASE_URL = 'https://app.contenthero.ai';
27
+ const LOGIN_TIMEOUT_MS = 180_000;
28
+ /** Read piped stdin (returns '' when attached to a TTY or nothing is piped). */
29
+ async function readStdin() {
30
+ if (process.stdin.isTTY)
31
+ return '';
32
+ const chunks = [];
33
+ for await (const chunk of process.stdin)
34
+ chunks.push(chunk);
35
+ return Buffer.concat(chunks).toString('utf8').trim();
36
+ }
37
+ /** Open a URL in the default browser. Best-effort; the URL is always printed too.
38
+ * Skipped when CONTENTHERO_NO_BROWSER is set (headless / CI). */
39
+ function openBrowser(url) {
40
+ if (process.env.CONTENTHERO_NO_BROWSER)
41
+ return;
42
+ const platform = process.platform;
43
+ const cmd = platform === 'darwin' ? 'open' : platform === 'win32' ? 'cmd' : 'xdg-open';
44
+ const args = platform === 'win32' ? ['/c', 'start', '', url] : [url];
45
+ try {
46
+ const child = spawn(cmd, args, { stdio: 'ignore', detached: true });
47
+ child.on('error', () => { });
48
+ child.unref();
49
+ }
50
+ catch {
51
+ // Ignore: the URL was printed for manual navigation.
52
+ }
53
+ }
54
+ /** Minimal branded HTML for the loopback success / cancel page. */
55
+ function resultPage(title, sub) {
56
+ return `<!doctype html><html><head><meta charset="utf-8"><title>${title}</title><style>
57
+ body{margin:0;height:100vh;display:flex;align-items:center;justify-content:center;background:#f8f9fa;
58
+ font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;color:#121212}
59
+ .card{background:#fff;border-radius:24px;padding:40px 48px;box-shadow:0 8px 32px rgba(0,0,0,.08);text-align:center}
60
+ h1{font-size:20px;margin:0 0 8px} p{color:#6b7280;margin:0;font-size:14px}
61
+ .dot{color:#d4af37}</style></head>
62
+ <body><div class="card"><h1><span class="dot">ContentHero</span> ${title}</h1><p>${sub}</p></div></body></html>`;
63
+ }
64
+ /** Verify a key against the API, then persist it and report. */
65
+ async function verifyAndStore(key, ctx, label) {
66
+ const client = new ContentHero({ apiKey: key, baseUrl: ctx.baseUrl });
67
+ const balance = await client.getBalance();
68
+ writeCredential({ apiKey: key, label, createdAt: new Date().toISOString() });
69
+ const data = { ok: true, stored: credentialsPath(), tier: balance.tier, balance: balance.balance };
70
+ emit(data, ctx, (d) => keyValues([
71
+ ['Logged in', 'stored ' + d.stored],
72
+ ['Tier', d.tier],
73
+ ['Balance', `${d.balance} credits`],
74
+ ]));
75
+ }
76
+ /**
77
+ * Start a localhost listener and resolve with the API key the browser hands back
78
+ * to `/callback?state=<state>&key=<key>`. Rejects on `error=...`, a listener
79
+ * failure, or timeout. A callback whose state does not match is rejected with 400
80
+ * and ignored (the listener keeps waiting), so a stray or forged hit cannot end
81
+ * the flow. Exported for testing the loopback contract in-process.
82
+ */
83
+ export function awaitLoopbackKey(opts) {
84
+ const { state, onListening, timeoutMs = LOGIN_TIMEOUT_MS } = opts;
85
+ return new Promise((resolve, reject) => {
86
+ let settled = false;
87
+ const finish = (fn) => {
88
+ if (settled)
89
+ return;
90
+ settled = true;
91
+ clearTimeout(timer);
92
+ server.close();
93
+ fn();
94
+ };
95
+ const server = http.createServer((req, res) => {
96
+ const reqUrl = new URL(req.url || '/', 'http://127.0.0.1');
97
+ if (reqUrl.pathname !== '/callback') {
98
+ res.writeHead(404, { 'content-type': 'text/plain' });
99
+ res.end('Not found');
100
+ return;
101
+ }
102
+ // Bind the callback to this invocation; ignore anything with a wrong state.
103
+ if (reqUrl.searchParams.get('state') !== state) {
104
+ res.writeHead(400, { 'content-type': 'text/html' });
105
+ res.end(resultPage('login error', 'Invalid request. You can close this tab.'));
106
+ return;
107
+ }
108
+ const error = reqUrl.searchParams.get('error');
109
+ const got = reqUrl.searchParams.get('key');
110
+ if (error || !got) {
111
+ res.writeHead(200, { 'content-type': 'text/html' });
112
+ res.end(resultPage('login cancelled', 'You can close this tab and return to your terminal.'));
113
+ finish(() => reject(new CliError('Login was cancelled in the browser.', EXIT.AUTH)));
114
+ return;
115
+ }
116
+ res.writeHead(200, { 'content-type': 'text/html' });
117
+ res.end(resultPage('CLI connected', 'You can close this tab and return to your terminal.'));
118
+ finish(() => resolve(got));
119
+ });
120
+ server.on('error', (e) => finish(() => reject(new CliError(`Could not start the local login listener: ${e.message}`, EXIT.GENERAL))));
121
+ const timer = setTimeout(() => finish(() => reject(new CliError('Login timed out after 3 minutes.', EXIT.AUTH))), timeoutMs);
122
+ server.listen(0, '127.0.0.1', () => {
123
+ const addr = server.address();
124
+ const port = addr && typeof addr === 'object' ? addr.port : 0;
125
+ onListening(port);
126
+ });
127
+ });
128
+ }
129
+ /** Run the browser-assisted login: loopback listener + the app's /cli/auth page. */
130
+ async function browserLogin(ctx) {
131
+ const state = randomBytes(16).toString('hex');
132
+ const label = hostname();
133
+ const base = (ctx.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, '');
134
+ const key = await awaitLoopbackKey({
135
+ state,
136
+ onListening: (port) => {
137
+ const authUrl = `${base}/cli/auth?port=${port}&state=${state}&label=${encodeURIComponent(label)}`;
138
+ process.stderr.write(`Opening your browser to authorize the CLI.\nIf it does not open, visit:\n ${authUrl}\n\nWaiting for authorization...\n`);
139
+ openBrowser(authUrl);
140
+ },
141
+ });
142
+ await verifyAndStore(key, ctx, label);
143
+ }
144
+ async function loginAction(opts, command) {
145
+ const ctx = resolveContext(command);
146
+ // No --with-key: run the browser-assisted flow (the primary path).
147
+ if (opts.withKey === undefined) {
148
+ await browserLogin(ctx);
149
+ return;
150
+ }
151
+ const key = typeof opts.withKey === 'string' ? opts.withKey.trim() : await readStdin();
152
+ if (!key) {
153
+ throw new CliError('No key provided. Pass it as `--with-key <key>` or pipe it via stdin.', EXIT.USAGE);
154
+ }
155
+ await verifyAndStore(key, ctx);
156
+ }
157
+ async function statusAction(_opts, command) {
158
+ const ctx = resolveContext(command);
159
+ if (!ctx.apiKey) {
160
+ throw new CliError('Not authenticated. Run `contenthero login`, set CONTENTHERO_API_KEY, or pass --api-key.', EXIT.AUTH);
161
+ }
162
+ // getBalance round-trips the key, so a bad key surfaces as an auth error (exit 3).
163
+ const client = new ContentHero({ apiKey: ctx.apiKey, baseUrl: ctx.baseUrl });
164
+ const balance = await client.getBalance();
165
+ const data = {
166
+ authenticated: true,
167
+ keySource: ctx.keySource,
168
+ baseUrl: ctx.baseUrl ?? DEFAULT_BASE_URL,
169
+ tier: balance.tier,
170
+ balance: balance.balance,
171
+ };
172
+ emit(data, ctx, (d) => keyValues([
173
+ ['Authenticated', 'yes'],
174
+ ['Key source', d.keySource],
175
+ ['Base URL', d.baseUrl],
176
+ ['Tier', d.tier],
177
+ ['Balance', `${d.balance} credits`],
178
+ ]));
179
+ }
180
+ function logoutAction(_opts, command) {
181
+ const ctx = resolveContext(command);
182
+ const removed = clearCredential();
183
+ emit({ ok: true, removed }, ctx, () => removed ? 'Logged out (stored credential removed).' : 'No stored credential to remove.');
184
+ }
185
+ /** Apply the login description/option/action to a command (shared by both mounts). */
186
+ function configureLogin(cmd) {
187
+ return cmd
188
+ .description('Authenticate the CLI. Browser-assisted by default; --with-key stores an existing key.')
189
+ .option('--with-key [key]', 'store an existing API key (pass the key, or pipe it via stdin). Omit to use the browser flow.')
190
+ .action(loginAction);
191
+ }
192
+ /** Apply the logout description/action to a command (shared by both mounts). */
193
+ function configureLogout(cmd) {
194
+ return cmd
195
+ .description('Delete the stored credential (env / --api-key are unaffected)')
196
+ .action(logoutAction);
197
+ }
198
+ export function registerAuth(program) {
199
+ const auth = program.command('auth').description('Manage CLI authentication');
200
+ configureLogin(auth.command('login'));
201
+ auth
202
+ .command('status')
203
+ .description('Verify the active API key and show the account it resolves to')
204
+ .action(statusAction);
205
+ configureLogout(auth.command('logout'));
206
+ // Top-level aliases for the two headline commands.
207
+ configureLogin(program.command('login'));
208
+ configureLogout(program.command('logout'));
209
+ }
210
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/commands/auth.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * `contenthero brand-kit` - brand kits (the brand identity documents).
3
+ * brand-kit list | get | update | archive
4
+ * brand-kit section add | update | archive
5
+ *
6
+ * Reads are open; writes need brandkit:write. Free-form identity objects
7
+ * (positioning, audience, voice profile, content strategy) and section fields
8
+ * are passed as JSON.
9
+ */
10
+ import type { Command } from 'commander';
11
+ export declare function registerBrandKit(program: Command): void;
12
+ //# sourceMappingURL=brandkit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"brandkit.d.ts","sourceRoot":"","sources":["../../src/commands/brandkit.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAuBxC,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAyJvD"}
@@ -0,0 +1,159 @@
1
+ /**
2
+ * `contenthero brand-kit` - brand kits (the brand identity documents).
3
+ * brand-kit list | get | update | archive
4
+ * brand-kit section add | update | archive
5
+ *
6
+ * Reads are open; writes need brandkit:write. Free-form identity objects
7
+ * (positioning, audience, voice profile, content strategy) and section fields
8
+ * are passed as JSON.
9
+ */
10
+ import { makeClient } from '../context.js';
11
+ import { emit, keyValues, table } from '../output.js';
12
+ import { CliError, EXIT } from '../errors.js';
13
+ import { compact } from '../generation.js';
14
+ import { collect, toInt, toJson } from '../args.js';
15
+ function recordHuman(s, action) {
16
+ return keyValues([
17
+ [action, s.sectionName],
18
+ ['Section id', s.id],
19
+ ['Tab', s.tab],
20
+ ['Sort order', s.sortOrder],
21
+ ['Fields', s.fields.length],
22
+ ]);
23
+ }
24
+ export function registerBrandKit(program) {
25
+ const brandKit = program.command('brand-kit').description('Brand kits (brand identity documents)');
26
+ brandKit
27
+ .command('list')
28
+ .description('List the account\'s brand kits (default first)')
29
+ .action(async (_opts, command) => {
30
+ const { client, ctx } = makeClient(command);
31
+ const kits = await client.listBrandKits();
32
+ emit(kits, ctx, (rows) => table(['ID', 'NAME', 'BUSINESS', 'DEFAULT', 'ACTIVE'], rows.map((k) => [
33
+ k.id.slice(0, 8),
34
+ k.name,
35
+ k.businessName ?? '',
36
+ k.isDefault ? 'yes' : '',
37
+ k.isActive ? 'yes' : '',
38
+ ])));
39
+ });
40
+ brandKit
41
+ .command('get')
42
+ .description('Get one brand kit in full (the whole brand document)')
43
+ .argument('<id>', 'the brand kit id')
44
+ .action(async (id, _opts, command) => {
45
+ const { client, ctx } = makeClient(command);
46
+ const kit = await client.getBrandKit(id);
47
+ emit(kit, ctx, (k) => keyValues([
48
+ ['Name', k.name],
49
+ ['Id', k.id],
50
+ ['Business', k.businessName ?? ''],
51
+ ['Niche', k.nicheDefinition ?? ''],
52
+ ['Primary offer', k.primaryOffer ?? ''],
53
+ ['Visual style', k.visualStyle ?? ''],
54
+ ['Sections', k.sections.length],
55
+ ['Brand accounts', k.brandAccounts.length],
56
+ ['Inspiration accounts', k.inspirationAccounts.length],
57
+ ['Knowledge items', k.knowledge.length],
58
+ ]));
59
+ });
60
+ brandKit
61
+ .command('update')
62
+ .description('Update a brand kit\'s identity fields (requires brandkit:write)')
63
+ .argument('<id>', 'the brand kit id')
64
+ .option('--name <text>')
65
+ .option('--business-name <text>')
66
+ .option('--website-url <url>')
67
+ .option('--primary-offer <text>')
68
+ .option('--niche <text>', 'niche definition')
69
+ .option('--visual-style <text>')
70
+ .option('--positioning <json>', 'positioning object (JSON)', toJson)
71
+ .option('--audience <json>', 'audience object (JSON)', toJson)
72
+ .option('--voice-profile <json>', 'voice profile object (JSON)', toJson)
73
+ .option('--content-strategy <json>', 'content strategy object (JSON)', toJson)
74
+ .option('--design-principle <text>', 'a design principle; repeatable', collect)
75
+ .action(async (id, opts, command) => {
76
+ const input = compact({
77
+ name: opts.name,
78
+ businessName: opts.businessName,
79
+ websiteUrl: opts.websiteUrl,
80
+ primaryOffer: opts.primaryOffer,
81
+ nicheDefinition: opts.niche,
82
+ visualStyle: opts.visualStyle,
83
+ positioning: opts.positioning,
84
+ audience: opts.audience,
85
+ voiceProfile: opts.voiceProfile,
86
+ contentStrategy: opts.contentStrategy,
87
+ designPrinciples: opts.designPrinciple,
88
+ });
89
+ if (Object.keys(input).length === 0) {
90
+ throw new CliError('Nothing to update. Pass at least one field to change.', EXIT.USAGE);
91
+ }
92
+ const { client, ctx } = makeClient(command);
93
+ const kit = await client.updateBrandKit(id, input);
94
+ emit(kit, ctx, (k) => keyValues([
95
+ ['Updated', k.name],
96
+ ['Id', k.id],
97
+ ]));
98
+ });
99
+ brandKit
100
+ .command('archive')
101
+ .description('Archive a brand kit (reversible; requires brandkit:write)')
102
+ .argument('<id>', 'the brand kit id')
103
+ .action(async (id, _opts, command) => {
104
+ const { client, ctx } = makeClient(command);
105
+ const k = await client.archiveBrandKit(id);
106
+ emit(k, ctx, (kit) => keyValues([
107
+ ['Archived', kit.name],
108
+ ['Id', kit.id],
109
+ ]));
110
+ });
111
+ // -- brand-kit section ----------------------------------------------------
112
+ const section = brandKit.command('section').description('Manage a brand kit\'s curated sections');
113
+ section
114
+ .command('add')
115
+ .description('Add a curated section to a brand kit (requires brandkit:write)')
116
+ .argument('<brandKitId>', 'the brand kit id')
117
+ .requiredOption('--tab <tab>', 'the tab the section belongs to (e.g. voice, overview)')
118
+ .requiredOption('--name <name>', 'the section title')
119
+ .option('--sort-order <n>', 'order within the tab (default end)', toInt)
120
+ .option('--fields <json>', 'field objects as a JSON array: [{ key, label, type, value }]', toJson)
121
+ .action(async (brandKitId, opts, command) => {
122
+ const { client, ctx } = makeClient(command);
123
+ const s = await client.addBrandKitSection(brandKitId, {
124
+ tab: opts.tab,
125
+ sectionName: opts.name,
126
+ sortOrder: opts.sortOrder,
127
+ fields: opts.fields,
128
+ });
129
+ emit(s, ctx, (rec) => recordHuman(rec, 'Added section'));
130
+ });
131
+ section
132
+ .command('update')
133
+ .description('Update a brand-kit section (requires brandkit:write)')
134
+ .argument('<brandKitId>', 'the brand kit id')
135
+ .argument('<sectionId>', 'the section id (from `brand-kit get`)')
136
+ .option('--name <name>', 'new section title')
137
+ .option('--sort-order <n>', 'new order within the tab', toInt)
138
+ .option('--fields <json>', 'replacement field objects as a JSON array', toJson)
139
+ .action(async (brandKitId, sectionId, opts, command) => {
140
+ const { client, ctx } = makeClient(command);
141
+ const s = await client.updateBrandKitSection(brandKitId, sectionId, {
142
+ sectionName: opts.name,
143
+ sortOrder: opts.sortOrder,
144
+ fields: opts.fields,
145
+ });
146
+ emit(s, ctx, (rec) => recordHuman(rec, 'Updated section'));
147
+ });
148
+ section
149
+ .command('archive')
150
+ .description('Archive a brand-kit section (soft delete, reversible; requires brandkit:write)')
151
+ .argument('<brandKitId>', 'the brand kit id')
152
+ .argument('<sectionId>', 'the section id to archive')
153
+ .action(async (brandKitId, sectionId, _opts, command) => {
154
+ const { client, ctx } = makeClient(command);
155
+ const s = await client.archiveBrandKitSection(brandKitId, sectionId);
156
+ emit(s, ctx, (rec) => recordHuman(rec, 'Archived section'));
157
+ });
158
+ }
159
+ //# sourceMappingURL=brandkit.js.map