getagenthook 0.2.1 → 1.0.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/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # agenthook
2
2
 
3
+ [![npm](https://img.shields.io/npm/v/getagenthook?color=c6fe1e&label=getagenthook&style=flat-square)](https://www.npmjs.com/package/getagenthook)
4
+
3
5
  CLI for the AgentHook hosted media-generation API. Built for agents:
4
6
  progress goes to stderr, output URLs go to stdout, exit codes are meaningful.
5
7
 
@@ -10,7 +12,7 @@ agenthook login --key <your-api-key>
10
12
  agenthook tools
11
13
  agenthook run make_video --prompt "A barista holds a latte and says: try our new oat flat white" --quality pro --aspect-ratio 9:16 --captions
12
14
  agenthook run make_image --prompt "studio shot of a ceramic mug" --count 2
13
- agenthook run caption_video --video-url https://…/video.mp4 --style tiktok
15
+ agenthook run caption_video --video-url https://…/video.mp4 --style chunk
14
16
  agenthook list --search "flat white"
15
17
  agenthook balance
16
18
  agenthook history
@@ -18,7 +20,7 @@ agenthook history
18
20
 
19
21
  - Credentials live at `~/.agenthook/credentials.json` (chmod 600).
20
22
  - API base: `--api-url` flag > `AGENTHOOK_API_URL` env > stored value >
21
- `https://localhost:3000`; all requests hit `<base>/api/v1/…`.
23
+ `https://getagenthook.com`; all requests hit `<base>/api/v1/…`.
22
24
  - Reference images (`--ref`, repeatable) require `--owns-references`: you attest
23
25
  you own, or have the rights to use, the likeness of every person appearing in
24
26
  the referenced images.
@@ -27,4 +29,28 @@ agenthook history
27
29
  submitted: consent, prompt length caps, enum values, ranges, and
28
30
  `nano-banana-2` (edit-only) without references.
29
31
 
30
- Develop: `npm run build` (emits `dist/`), `npx vitest run`, `npx tsc --noEmit`.
32
+ ## MCP server
33
+
34
+ If your agent speaks MCP, it can call AgentHook directly instead of going through
35
+ the CLI. The server ships as `@getagenthook/mcp` and runs over `npx`. Add this to
36
+ your MCP client config (Claude Code, Cursor, and similar):
37
+
38
+ ```json
39
+ {
40
+ "mcpServers": {
41
+ "agenthook": {
42
+ "command": "npx",
43
+ "args": ["-y", "@getagenthook/mcp"],
44
+ "env": { "AGENTHOOK_API_KEY": "ah_your_key" }
45
+ }
46
+ }
47
+ }
48
+ ```
49
+
50
+ It exposes `make_video`, `make_image`, `caption_video`, and `create_influencer`,
51
+ plus `get_run`. The generation tools return a `run_id`; pass it to `get_run` to
52
+ fetch the finished URL once the job is done.
53
+
54
+ ## Develop
55
+
56
+ `npm run build` (emits `dist/`), `npx vitest run`, `npx tsc --noEmit`.
package/dist/cli.js CHANGED
@@ -33,8 +33,10 @@ Commands:
33
33
  Run flags:
34
34
  --prompt <text> --ref <url> (repeatable) --owns-references --model <m>
35
35
  --quality <standard|pro> --duration <s> --aspect-ratio <r> --no-audio
36
- --captions --caption-style <movie|tiktok> --enhance-prompt
37
- --video-url <url> --style <movie|tiktok> --count <n> --resolution <1k|2k|4k>
36
+ --captions --caption-style <chunk|highlight|subtitle> --caption-size <small|medium|large>
37
+ --caption-placement <top|center|bottom> --enhance-prompt
38
+ --video-url <url> --style <chunk|highlight|subtitle> --size <small|medium|large>
39
+ --placement <top|center|bottom> --count <n> --resolution <1k|2k|4k>
38
40
  --dry-run price + validate against the server without spending credits
39
41
 
40
42
  Auth: pass --key, set AGENTHOOK_API_KEY, or run auth:login (precedence: flag > env > file).
@@ -38,36 +38,12 @@ const crypto = __importStar(require("node:crypto"));
38
38
  const args_1 = require("../args");
39
39
  const config_1 = require("../config");
40
40
  const exit_1 = require("../exit");
41
+ const flags_1 = require("../flags");
41
42
  const http_1 = require("../http");
42
43
  const jobs_1 = require("../jobs");
43
44
  const schemas_1 = require("../schemas");
44
45
  const types_1 = require("../types");
45
46
  const validate_1 = require("../validate");
46
- const RUN_FLAGS = {
47
- "api-url": "string",
48
- key: "string",
49
- json: "boolean",
50
- "dry-run": "boolean",
51
- prompt: "string",
52
- ref: "array",
53
- "owns-references": "boolean",
54
- model: "string",
55
- quality: "string",
56
- duration: "number",
57
- "aspect-ratio": "string",
58
- "no-audio": "boolean",
59
- captions: "boolean",
60
- "caption-style": "string",
61
- "enhance-prompt": "boolean",
62
- "video-url": "string",
63
- style: "string",
64
- count: "number",
65
- resolution: "string",
66
- language: "string",
67
- name: "string",
68
- slug: "string",
69
- influencer: "string",
70
- };
71
47
  // Poll cadence: 5s (spec); overridable for tests. Every watcher gets a
72
48
  // give-up (donor rule): a wall-clock deadline + bounded consecutive misses.
73
49
  const pollIntervalMs = () => Number(process.env.AGENTHOOK_POLL_MS || 5000);
@@ -77,7 +53,14 @@ const MAX_POLL_MISSES = 5;
77
53
  // submit must never double-charge; the server dedupes on the key).
78
54
  const SUBMIT_RETRIES = 1;
79
55
  async function run(argv) {
80
- const { positionals, flags, errors } = (0, args_1.parseArgs)(argv, RUN_FLAGS);
56
+ // Flags are DERIVED from the live tool schema (so a new API param is a usable
57
+ // flag without a CLI upgrade), and the schema source is per-api-url — so the
58
+ // api-url is pulled from argv BEFORE the full parse, the schema is resolved,
59
+ // then the flag spec is built to parse against.
60
+ const apiUrl = (0, config_1.resolveApiUrl)(preScanApiUrl(argv));
61
+ const schemas = await (0, schemas_1.resolveRunSchemas)(apiUrl);
62
+ const { spec, paramForFlag, flagFor } = (0, flags_1.deriveRunFlags)(schemas);
63
+ const { positionals, flags, errors } = (0, args_1.parseArgs)(argv, spec);
81
64
  if (errors.length) {
82
65
  errors.forEach((e) => console.error(e));
83
66
  return exit_1.EXIT.GENERIC;
@@ -88,19 +71,16 @@ async function run(argv) {
88
71
  console.error("Usage: agenthook run <tool> [flags] — see `agenthook tools`");
89
72
  return exit_1.EXIT.GENERIC;
90
73
  }
91
- const apiUrl = (0, config_1.resolveApiUrl)(flags["api-url"]);
92
74
  const key = (0, config_1.resolveApiKey)(flags["key"]);
93
75
  if (!key) {
94
76
  emitError(asJson, "Not logged in — run `agenthook auth:login` first.", exit_1.EXIT.AUTH);
95
77
  return exit_1.EXIT.AUTH;
96
78
  }
97
- // Deterministic pre-validation BEFORE any network call (spec §3) — schemas
98
- // resolve locally in every case: cached /v1/tools fetch if one exists,
99
- // otherwise the bundled snapshot (fresh install, offline). The server
100
- // re-validates authoritatively on submit.
101
- const schemas = (0, schemas_1.getLocalToolSchemas)(apiUrl);
102
- const input = (0, validate_1.buildToolInput)(flags);
103
- const problems = (0, validate_1.preValidate)(tool, input, schemas);
79
+ // Deterministic pre-validation BEFORE the run is submitted (spec §3): every
80
+ // locally-checkable rule the server would 400 on. The server re-validates
81
+ // authoritatively on submit.
82
+ const input = (0, validate_1.buildToolInput)(flags, paramForFlag);
83
+ const problems = (0, validate_1.preValidate)(tool, input, schemas, flagFor);
104
84
  if (problems.length) {
105
85
  if (asJson)
106
86
  emitError(true, problems.join("; "), exit_1.EXIT.VALIDATION);
@@ -231,6 +211,19 @@ async function submitWithRetry(apiUrl, tool, input, key, idempotencyKey) {
231
211
  }
232
212
  }
233
213
  }
214
+ /** Pull --api-url out of raw argv before the flag spec exists (spec derivation
215
+ * needs the schema, fetched per api-url). Mirrors parseArgs' two forms; full
216
+ * precedence (flag > env > file > default) is applied by resolveApiUrl. */
217
+ function preScanApiUrl(argv) {
218
+ for (let i = 0; i < argv.length; i++) {
219
+ const a = argv[i];
220
+ if (a === "--api-url")
221
+ return argv[i + 1];
222
+ if (a.startsWith("--api-url="))
223
+ return a.slice("--api-url=".length);
224
+ }
225
+ return undefined;
226
+ }
234
227
  const topUpUrl = (apiUrl) => `${apiUrl}/credits`;
235
228
  /** Machine JSON to stdout (one object per line, ndjson). */
236
229
  function emit(obj) {
@@ -4,9 +4,9 @@ exports.tools = tools;
4
4
  const args_1 = require("../args");
5
5
  const config_1 = require("../config");
6
6
  const exit_1 = require("../exit");
7
+ const flags_1 = require("../flags");
7
8
  const json_error_1 = require("../json-error");
8
9
  const schemas_1 = require("../schemas");
9
- const validate_1 = require("../validate");
10
10
  async function tools(argv) {
11
11
  const { flags, errors } = (0, args_1.parseArgs)(argv, { "api-url": "string", key: "string", json: "boolean" });
12
12
  if (errors.length) {
@@ -22,10 +22,13 @@ async function tools(argv) {
22
22
  console.log(JSON.stringify({ tools: schemas }));
23
23
  return exit_1.EXIT.OK;
24
24
  }
25
+ // Same derivation `run` parses against — the flag spelling shown here is
26
+ // exactly the one that will work.
27
+ const { flagFor } = (0, flags_1.deriveRunFlags)(schemas);
25
28
  for (const tool of schemas) {
26
29
  console.log(`${tool.name} — ${tool.description}`);
27
30
  for (const [name, spec] of Object.entries(tool.params)) {
28
- console.log(` ${renderParam(name, spec)}`);
31
+ console.log(` ${renderParam(name, spec, flagFor)}`);
29
32
  }
30
33
  console.log("");
31
34
  }
@@ -33,8 +36,8 @@ async function tools(argv) {
33
36
  return exit_1.EXIT.OK;
34
37
  });
35
38
  }
36
- function renderParam(name, spec) {
37
- const flagName = validate_1.FLAG_FOR[name] ?? `--${name.replace(/_/g, "-")}`;
39
+ function renderParam(name, spec, flagFor) {
40
+ const flagName = flagFor[name] ?? `--${name.replace(/_/g, "-")}`;
38
41
  const hint = spec.type === "boolean"
39
42
  ? ""
40
43
  : spec.enum
package/dist/flags.js ADDED
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FLAG_ALIAS = exports.CLI_FLAGS = void 0;
4
+ exports.deriveRunFlags = deriveRunFlags;
5
+ /** Flags that are NOT tool params and must always parse (any command shape). */
6
+ exports.CLI_FLAGS = {
7
+ "api-url": "string",
8
+ key: "string",
9
+ json: "boolean",
10
+ "dry-run": "boolean",
11
+ };
12
+ /** The two places the CLI flag intentionally differs from a mechanical
13
+ * snake→kebab of the param name (everything else is derived). */
14
+ exports.FLAG_ALIAS = {
15
+ reference_images: { flag: "ref" },
16
+ audio: { flag: "no-audio", invertBoolean: true }, // --no-audio sets audio:false
17
+ };
18
+ const FLAG_TYPES = ["string", "number", "boolean", "array"];
19
+ /** Union every tool's params into the run flag spec + flag⇄param maps. Params
20
+ * repeat across tools (same flag name each time) — the union is idempotent. */
21
+ function deriveRunFlags(schemas) {
22
+ const spec = { ...exports.CLI_FLAGS };
23
+ const paramForFlag = {};
24
+ const flagFor = {};
25
+ for (const schema of schemas) {
26
+ for (const [param, p] of Object.entries(schema.params)) {
27
+ const alias = exports.FLAG_ALIAS[param];
28
+ const flag = alias?.flag ?? param.replace(/_/g, "-");
29
+ let type = FLAG_TYPES.includes(p.type) ? p.type : "string";
30
+ if (alias?.invertBoolean)
31
+ type = "boolean";
32
+ spec[flag] = type;
33
+ paramForFlag[flag] = { param, invert: alias?.invertBoolean };
34
+ flagFor[param] = "--" + flag;
35
+ }
36
+ }
37
+ return { spec, paramForFlag, flagFor };
38
+ }
package/dist/http.js CHANGED
@@ -5,6 +5,7 @@ exports.timeoutSignal = timeoutSignal;
5
5
  exports.api = api;
6
6
  exports.describeApiError = describeApiError;
7
7
  const config_1 = require("./config");
8
+ const version_1 = require("./version");
8
9
  // Timeout policy mirrors the frozen packages/core/net.ts (DEFAULT_TIMEOUT_MS +
9
10
  // AbortSignal.timeout). Duplicated here because the published CLI cannot
10
11
  // runtime-import workspace TypeScript; test/parity.test.ts pins the value.
@@ -36,7 +37,7 @@ async function api(apiUrl, pathname, opts = {}) {
36
37
  if (v !== undefined && v !== "")
37
38
  url.searchParams.set(k, v);
38
39
  }
39
- const headers = { ...opts.headers };
40
+ const headers = { "user-agent": `agenthook-cli/${version_1.VERSION}`, ...opts.headers };
40
41
  if (opts.key)
41
42
  headers.authorization = `Bearer ${opts.key}`;
42
43
  if (opts.body !== undefined)
package/dist/main.js CHANGED
File without changes
@@ -26,8 +26,23 @@ exports.TOOLS_SNAPSHOT = [
26
26
  default: "9:16",
27
27
  },
28
28
  audio: { type: "boolean", default: true },
29
- captions: { type: "boolean", default: false },
30
- caption_style: { type: "string", enum: ["movie", "tiktok"], default: "tiktok" },
29
+ captions: {
30
+ type: "boolean",
31
+ default: false,
32
+ description: "Burn spoken-word captions into the video. When true the run returns two files: output[0] is captioned, output[1] is the original uncaptioned video.",
33
+ },
34
+ caption_style: {
35
+ type: "string",
36
+ enum: ["chunk", "highlight", "subtitle"],
37
+ default: "chunk",
38
+ description: "Caption look. chunk = 2–4 words per line, centered, bold with a heavy outline (the standard TikTok/Reels style). highlight = the same block with the active word lit in colour (the Hormozi/retention style). subtitle = one clean line at the bottom (cinematic / YouTube).",
39
+ },
40
+ caption_size: { type: "string", enum: ["small", "medium", "large"], default: "medium", description: "Caption text size." },
41
+ caption_placement: {
42
+ type: "string",
43
+ enum: ["top", "center", "bottom"],
44
+ description: "Where captions sit. Defaults per style: chunk/highlight → center, subtitle → bottom.",
45
+ },
31
46
  enhance_prompt: { type: "boolean", default: false },
32
47
  influencer: {
33
48
  type: "string",
@@ -62,10 +77,21 @@ exports.TOOLS_SNAPSHOT = [
62
77
  },
63
78
  {
64
79
  name: "caption_video",
65
- description: "Burn styled subtitles into an existing video.",
80
+ description: "Burn styled captions into an existing video. Returns two files: output[0] is the captioned video, output[1] is the original uncaptioned video — so you can re-caption with a different style without regenerating. (A speechless video returns just the original.)",
66
81
  params: {
67
82
  video_url: { type: "string", required: true },
68
- style: { type: "string", enum: ["movie", "tiktok"], default: "movie" },
83
+ style: {
84
+ type: "string",
85
+ enum: ["chunk", "highlight", "subtitle"],
86
+ default: "chunk",
87
+ description: "Caption look. chunk = bold centered word blocks (standard TikTok/Reels). highlight = active word lit in colour (Hormozi/retention). subtitle = clean bottom line (cinematic / YouTube).",
88
+ },
89
+ size: { type: "string", enum: ["small", "medium", "large"], default: "medium", description: "Caption text size." },
90
+ placement: {
91
+ type: "string",
92
+ enum: ["top", "center", "bottom"],
93
+ description: "Where captions sit. Defaults per style: chunk/highlight → center, subtitle → bottom.",
94
+ },
69
95
  language: { type: "string", default: "auto" },
70
96
  },
71
97
  },
package/dist/schemas.js CHANGED
@@ -36,12 +36,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.TOOLS_CACHE_TTL_MS = void 0;
37
37
  exports.toolsCachePath = toolsCachePath;
38
38
  exports.getLocalToolSchemas = getLocalToolSchemas;
39
+ exports.resolveRunSchemas = resolveRunSchemas;
39
40
  exports.getToolSchemas = getToolSchemas;
40
- // Tool-schema source for deterministic pre-validation: GET /v1/tools is the
41
- // single live schema source (the `tools` command fetches + caches it), and
42
- // `run` validates against a purely LOCAL resolution fetched cache if
43
- // present, else the bundled snapshot so pre-validation never makes a
44
- // network call, including on a fresh install (spec §3).
41
+ // Tool-schema source. GET /v1/tools is the single live schema source, and
42
+ // `run` DERIVES its flags from it (a new API param is a usable flag with no CLI
43
+ // upgrade) via resolveRunSchemas: the cached fetch when fresh, a live keyless
44
+ // fetch on miss (public endpoint), and the bundled snapshot as the offline
45
+ // fallback so it stays current online and still works offline. (This relaxes
46
+ // the original spec §3 "no network before submit" guarantee to a cached,
47
+ // at-most-hourly, keyless fetch — the cost of never-stale flags.)
45
48
  const fs = __importStar(require("node:fs"));
46
49
  const path = __importStar(require("node:path"));
47
50
  const config_1 = require("./config");
@@ -72,6 +75,19 @@ function writeCache(cache) {
72
75
  function getLocalToolSchemas(apiUrl) {
73
76
  return readCache()[apiUrl]?.tools ?? schema_snapshot_1.TOOLS_SNAPSHOT;
74
77
  }
78
+ /** Schema source for `run`'s flag derivation + pre-validation. Live-and-cached
79
+ * (GET /v1/tools is PUBLIC — no key) so a new tool param becomes a usable flag
80
+ * without a CLI upgrade; falls back to the last cache, then the bundled
81
+ * snapshot, so it never throws (offline / fresh install still work). The server
82
+ * re-validates authoritatively on submit. */
83
+ async function resolveRunSchemas(apiUrl) {
84
+ try {
85
+ return await getToolSchemas(apiUrl);
86
+ }
87
+ catch {
88
+ return getLocalToolSchemas(apiUrl);
89
+ }
90
+ }
75
91
  /** Cached-first schema load. `fresh: true` (the `tools` command) always
76
92
  * refetches; otherwise a fresh cache entry short-circuits the network, and a
77
93
  * stale one is the fallback when the API is unreachable (the server re-checks
package/dist/validate.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.FLAG_FOR = exports.OWNS_REFERENCES_CONSENT = exports.PROMPT_CAPS = void 0;
3
+ exports.OWNS_REFERENCES_CONSENT = exports.PROMPT_CAPS = void 0;
4
4
  exports.buildToolInput = buildToolInput;
5
5
  exports.preValidate = preValidate;
6
6
  // Per-model prompt caps, mirroring packages/core/models.ts promptMax (frozen).
@@ -13,69 +13,25 @@ exports.PROMPT_CAPS = {
13
13
  };
14
14
  exports.OWNS_REFERENCES_CONSENT = "By passing --owns-references you attest that you own, or have the rights to use, " +
15
15
  "the likeness of every person appearing in the referenced images.";
16
- /** CLI flag spelling for each API param for readable error messages.
17
- * test/parity.test.ts asserts every param served by /v1/tools has an entry. */
18
- exports.FLAG_FOR = {
19
- prompt: "--prompt",
20
- reference_images: "--ref",
21
- owns_references: "--owns-references",
22
- model: "--model",
23
- quality: "--quality",
24
- duration: "--duration",
25
- aspect_ratio: "--aspect-ratio",
26
- audio: "--no-audio",
27
- captions: "--captions",
28
- caption_style: "--caption-style",
29
- enhance_prompt: "--enhance-prompt",
30
- video_url: "--video-url",
31
- style: "--style",
32
- count: "--count",
33
- resolution: "--resolution",
34
- language: "--language",
35
- name: "--name",
36
- slug: "--slug",
37
- influencer: "--influencer",
38
- };
39
- /** Map parsed CLI flags onto the tool-input body the API expects. Only flags
40
- * the user actually passed are included, so server defaults stay in charge. */
41
- function buildToolInput(flags) {
16
+ /** Map parsed CLI flags onto the tool-input body the API expects, using the
17
+ * flag→param map derived from the tool schema (see flags.ts). Only flags the
18
+ * user actually passed are present in `flags`, so server defaults stay in
19
+ * charge; a global CLI flag (api-url/key/json/dry-run) has no param and is
20
+ * skipped. The one non-direct case is `--no-audio` inverting to audio:false. */
21
+ function buildToolInput(flags, paramForFlag) {
42
22
  const input = {};
43
- const direct = [
44
- ["prompt", "prompt"],
45
- ["model", "model"],
46
- ["quality", "quality"],
47
- ["duration", "duration"],
48
- ["aspect-ratio", "aspect_ratio"],
49
- ["caption-style", "caption_style"],
50
- ["video-url", "video_url"],
51
- ["style", "style"],
52
- ["count", "count"],
53
- ["resolution", "resolution"],
54
- ["language", "language"],
55
- ["name", "name"],
56
- ["slug", "slug"],
57
- ["influencer", "influencer"],
58
- ];
59
- for (const [flag, param] of direct) {
60
- if (flags[flag] !== undefined)
61
- input[param] = flags[flag];
23
+ for (const [flag, value] of Object.entries(flags)) {
24
+ const mapping = paramForFlag[flag];
25
+ if (!mapping)
26
+ continue; // a global CLI flag, not a tool param
27
+ input[mapping.param] = mapping.invert ? !value : value;
62
28
  }
63
- const refs = flags["ref"];
64
- if (refs?.length)
65
- input.reference_images = refs;
66
- if (flags["owns-references"])
67
- input.owns_references = true;
68
- if (flags["no-audio"])
69
- input.audio = false;
70
- if (flags["captions"])
71
- input.captions = true;
72
- if (flags["enhance-prompt"])
73
- input.enhance_prompt = true;
74
29
  return input;
75
30
  }
76
- const flag = (param) => exports.FLAG_FOR[param] ?? param;
77
- /** Returns human-readable problems (empty array = locally valid). */
78
- function preValidate(tool, input, schemas) {
31
+ /** Returns human-readable problems (empty array = locally valid). `flagFor`
32
+ * (derived from the schema) supplies the CLI flag spelling for messages. */
33
+ function preValidate(tool, input, schemas, flagFor) {
34
+ const flag = (param) => flagFor[param] ?? "--" + param.replace(/_/g, "-");
79
35
  const schema = schemas.find((s) => s.name === tool);
80
36
  if (!schema) {
81
37
  return [`Unknown tool "${tool}". Available tools: ${schemas.map((s) => s.name).join(", ")}`];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "getagenthook",
3
- "version": "0.2.1",
3
+ "version": "1.0.1",
4
4
  "description": "CLI for the AgentHook hosted media-generation API — generate video, images, and captions from your terminal or agent.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://getagenthook.com",