@docwhispr/mcp-server 0.1.0 → 0.1.3

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,25 +1,48 @@
1
1
  # @docwhispr/mcp-server
2
2
 
3
- Thin MCP client for **docwhispr** cloud curriculum compiler (HiliFlow).
3
+ Thin client for **docwhispr** cloud curriculum compiler (HiliFlow).
4
4
 
5
- - Free: `hiliflow_plan_curriculum` (topic scope + multi-episode blueprint)
6
- - Metered: `hiliflow_compile_episode` / `hiliflow_compile_series` (charges from the same monthly video pool as the web app)
7
- - Upsell: `hiliflow_open_studio` → hosted interactive player URL
5
+ ## One-time setup (paste from Settings)
8
6
 
9
- ## Setup
7
+ ```bash
8
+ npx -y @docwhispr/mcp-server configure --key "hl_live_…"
9
+ # optional, Cursor only:
10
+ # npx -y @docwhispr/mcp-server configure --key "hl_live_…" --cursor-mcp
11
+ ```
12
+
13
+ Saves `~/.docwhispr/credentials.json`. CLI and any MCP host that runs this package will read it automatically. `--cursor-mcp` only updates `~/.cursor/mcp.json` (still **without** embedding the key).
10
14
 
11
- 1. Sign in at [docwhispr.com](https://docwhispr.com), create an API key:
15
+ ## CLI
12
16
 
13
17
  ```bash
14
- curl -X POST "$HILIFLOW_API_BASE/api/mcp/keys" \
15
- -H "Authorization: Bearer <session_jwt>" \
16
- -H "Content-Type: application/json" \
17
- -d '{"name":"Cursor MCP","plan":"free"}'
18
+ npx -y @docwhispr/mcp-server plan \
19
+ --pdf "/path/to/book.pdf" \
20
+ --pages 602-604 \
21
+ --topics EXTEST,INTEST
22
+
23
+ npx -y @docwhispr/mcp-server compile \
24
+ --pdf "/path/to/book.pdf" \
25
+ --pages 602-604 \
26
+ --topics EXTEST,INTEST \
27
+ --lang zh \
28
+ --character chr_pixel_voxel \
29
+ --wait
18
30
  ```
19
31
 
20
- Save the returned `apiKey` (`hl_live_…`) once.
32
+ | Command | Credits |
33
+ | --- | ---: |
34
+ | `configure` | 0 |
35
+ | `plan` | 0 |
36
+ | `compile` | 1 |
37
+ | `credits` / `status` | 0 |
38
+
39
+ Auth: `HILIFLOW_API_KEY` env **or** credentials file from `configure`.
21
40
 
22
- 2. Cursor `mcp.json` (or Claude Desktop MCP config):
41
+ `402` = out of credits · `409` = another job already generating.
42
+
43
+ ## MCP stdio
44
+
45
+ After `configure --cursor-mcp`, or manually:
23
46
 
24
47
  ```json
25
48
  {
@@ -28,7 +51,6 @@ Save the returned `apiKey` (`hl_live_…`) once.
28
51
  "command": "npx",
29
52
  "args": ["-y", "@docwhispr/mcp-server"],
30
53
  "env": {
31
- "HILIFLOW_API_KEY": "hl_live_…",
32
54
  "HILIFLOW_API_BASE": "https://docwhispr.fly.dev",
33
55
  "HILIFLOW_WEB_BASE": "https://docwhispr.com"
34
56
  }
@@ -37,36 +59,16 @@ Save the returned `apiKey` (`hl_live_…`) once.
37
59
  }
38
60
  ```
39
61
 
40
- Local monorepo:
41
-
42
- ```json
43
- {
44
- "mcpServers": {
45
- "docwhispr": {
46
- "command": "npx",
47
- "args": ["tsx", "packages/mcp-server/src/index.ts"],
48
- "cwd": "<repo-root>",
49
- "env": {
50
- "HILIFLOW_API_KEY": "hl_live_…",
51
- "HILIFLOW_API_BASE": "http://127.0.0.1:8080"
52
- }
53
- }
54
- }
55
- }
56
- ```
57
-
58
- ## Tools
59
-
60
- | Tool | Credits | Purpose |
61
- | --- | ---: | --- |
62
- | `hiliflow_credits` | 0 | Balance / monthly allowance |
63
- | `hiliflow_plan_curriculum` | 0 | Book↔PDF resolve, keep/drop topics, episode split |
64
- | `hiliflow_compile_episode` | 1 | Single project job |
65
- | `hiliflow_compile_series` | N | Multi-episode split & enqueue |
66
- | `hiliflow_job_status` | 0 | Poll job |
67
- | `hiliflow_open_studio` | 0 | Hosted interactive studio URL |
62
+ | Tool | Credits |
63
+ | --- | ---: |
64
+ | `hiliflow_credits` | 0 |
65
+ | `hiliflow_plan_curriculum` | 0 |
66
+ | `hiliflow_compile_episode` | 1 |
67
+ | `hiliflow_compile_series` | N |
68
+ | `hiliflow_job_status` | 0 |
69
+ | `hiliflow_open_studio` | 0 |
68
70
 
69
- ## Monthly credits (same pool as web Generate)
71
+ ## Monthly credits
70
72
 
71
73
  | Plan | Episodes / month |
72
74
  | --- | ---: |
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function run(argv: string[]): Promise<void>;
package/dist/cli.js ADDED
@@ -0,0 +1,139 @@
1
+ /**
2
+ * CLI handlers for configure / plan / compile / credits / status.
3
+ * Invoked from index.ts when argv includes a subcommand.
4
+ */
5
+ import { api, compileEpisode, ensureCursorMcpConfig, getWebBase, jobStatus, parsePages, parseTopics, planCurriculum, saveCredentials } from "./lib.js";
6
+ function usage(exitCode = 1) {
7
+ console.error(`Usage:
8
+ npx @docwhispr/mcp-server configure --key hl_live_… [--cursor-mcp]
9
+ npx @docwhispr/mcp-server plan --pdf <path> --pages <min>-<max> [--topics a,b] [--title ...]
10
+ npx @docwhispr/mcp-server compile --pdf <path> --pages <min>-<max> [--topics a,b] [--lang zh]
11
+ [--character chr_pixel_voxel] [--title ...] [--plan free|pro|pro_plus]
12
+ [--wait] [--poll-ms 20000]
13
+ npx @docwhispr/mcp-server credits [--plan free|pro|pro_plus]
14
+ npx @docwhispr/mcp-server status --job <id>
15
+
16
+ Auth: HILIFLOW_API_KEY env, or ~/.docwhispr/credentials.json from \`configure\`
17
+ `);
18
+ process.exit(exitCode);
19
+ }
20
+ function flag(args, name) {
21
+ const i = args.indexOf(name);
22
+ if (i < 0)
23
+ return undefined;
24
+ return args[i + 1];
25
+ }
26
+ function has(args, name) {
27
+ return args.includes(name);
28
+ }
29
+ async function sleep(ms) {
30
+ await new Promise((r) => setTimeout(r, ms));
31
+ }
32
+ async function waitForJob(jobId, pollMs) {
33
+ for (;;) {
34
+ const job = await jobStatus(jobId, "cli");
35
+ const status = String(job.status || "");
36
+ const progress = job.progressMessage ? ` · ${job.progressMessage}` : "";
37
+ console.error(`[status] ${status}${progress}`);
38
+ if (status === "succeeded" || status === "failed" || status === "error") {
39
+ return job;
40
+ }
41
+ await sleep(pollMs);
42
+ }
43
+ }
44
+ export async function run(argv) {
45
+ const [cmd, ...rest] = argv;
46
+ if (!cmd || cmd === "-h" || cmd === "--help" || cmd === "help")
47
+ usage(0);
48
+ try {
49
+ if (cmd === "configure") {
50
+ const key = flag(rest, "--key");
51
+ if (!key)
52
+ usage();
53
+ const path = saveCredentials({ apiKey: key });
54
+ const out = {
55
+ credentialsPath: path,
56
+ note: "API key saved. plan/compile/MCP will read it automatically."
57
+ };
58
+ if (has(rest, "--cursor-mcp")) {
59
+ out.cursorMcpPath = ensureCursorMcpConfig();
60
+ out.cursorNote =
61
+ "Cursor mcp.json updated (no key embedded). Restart MCP docwhispr in Cursor.";
62
+ }
63
+ console.log(JSON.stringify(out, null, 2));
64
+ return;
65
+ }
66
+ if (cmd === "credits") {
67
+ const plan = flag(rest, "--plan");
68
+ const q = plan ? `?plan=${encodeURIComponent(plan)}` : "";
69
+ const data = await api(`/api/mcp/credits${q}`, {}, "cli");
70
+ console.log(JSON.stringify(data, null, 2));
71
+ return;
72
+ }
73
+ if (cmd === "status") {
74
+ const jobId = flag(rest, "--job");
75
+ if (!jobId)
76
+ usage();
77
+ console.log(JSON.stringify(await jobStatus(jobId, "cli"), null, 2));
78
+ return;
79
+ }
80
+ if (cmd === "plan" || cmd === "compile") {
81
+ const pdf = flag(rest, "--pdf");
82
+ const pages = flag(rest, "--pages");
83
+ if (!pdf || !pages)
84
+ usage();
85
+ const { pageMin, pageMax } = parsePages(pages);
86
+ const includeTopics = parseTopics(flag(rest, "--topics"));
87
+ const title = flag(rest, "--title");
88
+ const common = {
89
+ pdfPath: pdf,
90
+ pageMin,
91
+ pageMax,
92
+ includeTopics,
93
+ title,
94
+ client: "cli"
95
+ };
96
+ if (cmd === "plan") {
97
+ const plan = await planCurriculum(common);
98
+ console.log(JSON.stringify({
99
+ note: "Free curriculum preview. Confirm, then run compile.",
100
+ plan
101
+ }, null, 2));
102
+ return;
103
+ }
104
+ const result = await compileEpisode({
105
+ ...common,
106
+ contentLanguage: flag(rest, "--lang") || "en",
107
+ characterId: flag(rest, "--character") || undefined,
108
+ plan: flag(rest, "--plan") ||
109
+ "free"
110
+ });
111
+ const jobId = String(result.job?.id || "");
112
+ console.log(JSON.stringify({ ...result, note: "1 episode credit charged." }, null, 2));
113
+ if (has(rest, "--wait") && jobId) {
114
+ const pollMs = Number(flag(rest, "--poll-ms") || 20000);
115
+ const finalJob = await waitForJob(jobId, Number.isFinite(pollMs) ? pollMs : 20000);
116
+ console.log(JSON.stringify({
117
+ job: finalJob,
118
+ studioUrl: `${getWebBase()}/app/projects/${jobId}`
119
+ }, null, 2));
120
+ if (String(finalJob.status) !== "succeeded")
121
+ process.exit(2);
122
+ }
123
+ return;
124
+ }
125
+ usage();
126
+ }
127
+ catch (err) {
128
+ const status = err.status;
129
+ const message = err instanceof Error ? err.message : String(err);
130
+ if (status === 402) {
131
+ console.error("Insufficient credits (402). Upgrade plan or wait for monthly refresh.");
132
+ }
133
+ else if (status === 409) {
134
+ console.error("Another video is already generating (409). Wait or stop the running job.");
135
+ }
136
+ console.error(message);
137
+ process.exit(1);
138
+ }
139
+ }
package/dist/index.js CHANGED
@@ -1,61 +1,33 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * HiliFlow MCP server (stdio).
3
+ * HiliFlow / docwhispr entry:
4
+ * - CLI: npx @docwhispr/mcp-server plan|compile|credits|status …
5
+ * - MCP: npx @docwhispr/mcp-server (stdio, via Cursor mcp.json)
4
6
  *
5
- * Env:
6
- * HILIFLOW_API_KEY — required (hl_live_…)
7
- * HILIFLOW_API_BASE — optional, default https://docwhispr.fly.dev
8
- * HILIFLOW_WEB_BASE — optional studio host, default https://docwhispr.com
7
+ * Auth (first match wins):
8
+ * HILIFLOW_API_KEY env, or ~/.docwhispr/credentials.json from `configure`
9
+ * Optional:
10
+ * HILIFLOW_API_BASE / HILIFLOW_WEB_BASE
9
11
  */
10
12
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
11
13
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
12
14
  import { z } from "zod";
13
- import { readFileSync } from "node:fs";
14
- import { resolve } from "node:path";
15
- const API_BASE = (process.env.HILIFLOW_API_BASE ||
16
- process.env.DOCWHISPR_API_BASE ||
17
- process.env.NEXT_PUBLIC_VIDEO_WORKER_BASE_URL ||
18
- "https://docwhispr.fly.dev").replace(/\/$/, "");
19
- const WEB_BASE = (process.env.HILIFLOW_WEB_BASE ||
20
- process.env.DOCWHISPR_WEB_BASE ||
21
- process.env.NEXT_PUBLIC_APP_URL ||
22
- "https://docwhispr.com").replace(/\/$/, "");
23
- function requireApiKey() {
24
- const key = (process.env.HILIFLOW_API_KEY || "").trim();
25
- if (!key) {
26
- throw new Error("HILIFLOW_API_KEY is required. Create one via POST /api/mcp/keys (signed-in browser session), then set it in MCP env.");
27
- }
28
- return key;
29
- }
30
- async function api(path, init = {}) {
31
- const key = requireApiKey();
32
- const headers = {
33
- Authorization: `Bearer ${key}`,
34
- "X-HiliFlow-Client": "mcp",
35
- Accept: "application/json",
36
- ...init.headers
37
- };
38
- let body = init.body;
39
- if (init.json !== undefined) {
40
- headers["Content-Type"] = "application/json";
41
- body = JSON.stringify(init.json);
42
- }
43
- const res = await fetch(`${API_BASE}${path}`, { ...init, headers, body });
44
- const text = await res.text();
45
- let data = null;
46
- try {
47
- data = text ? JSON.parse(text) : null;
48
- }
49
- catch {
50
- data = { raw: text };
51
- }
52
- if (!res.ok) {
53
- const detail = typeof data === "object" && data && "detail" in data
54
- ? String(data.detail)
55
- : text || res.statusText;
56
- throw new Error(`HiliFlow API ${res.status}: ${detail}`);
57
- }
58
- return data;
15
+ import { api, buildTeachingScope, compileEpisode, fileNameOf, fileToBase64, getWebBase, jobStatus, planCurriculum, sha256HexOfFile } from "./lib.js";
16
+ const CLI_COMMANDS = new Set([
17
+ "configure",
18
+ "plan",
19
+ "compile",
20
+ "credits",
21
+ "status",
22
+ "help",
23
+ "-h",
24
+ "--help"
25
+ ]);
26
+ const maybeCmd = process.argv[2];
27
+ if (maybeCmd && CLI_COMMANDS.has(maybeCmd)) {
28
+ const { run } = await import("./cli.js");
29
+ await run(process.argv.slice(2));
30
+ process.exit(0);
59
31
  }
60
32
  function textResult(payload) {
61
33
  return {
@@ -67,29 +39,9 @@ function textResult(payload) {
67
39
  ]
68
40
  };
69
41
  }
70
- function fileToBase64(filePath) {
71
- const abs = resolve(filePath);
72
- return readFileSync(abs).toString("base64");
73
- }
74
- async function sha256HexOfFile(filePath) {
75
- const { createHash } = await import("node:crypto");
76
- const buf = readFileSync(resolve(filePath));
77
- return createHash("sha256").update(buf).digest("hex");
78
- }
79
- const teachingScopeSchema = z
80
- .object({
81
- include_topics: z.array(z.string()).optional(),
82
- exclude_topics: z.array(z.string()).optional(),
83
- background_policy: z
84
- .enum(["none", "necessary_only", "all_in_section"])
85
- .optional(),
86
- page_numbering: z.enum(["auto", "pdf", "book"]).optional(),
87
- user_prompt: z.string().optional()
88
- })
89
- .optional();
90
42
  const server = new McpServer({
91
43
  name: "hiliflow",
92
- version: "0.1.0"
44
+ version: "0.1.1"
93
45
  });
94
46
  server.tool("hiliflow_credits", {
95
47
  plan: z.enum(["free", "pro", "pro_plus"]).optional()
@@ -117,31 +69,16 @@ server.tool("hiliflow_plan_curriculum", {
117
69
  maxPagesPerJob: z.number().int().min(1).max(20).optional().default(10),
118
70
  title: z.string().optional()
119
71
  }, async (args) => {
120
- const contentHash = await sha256HexOfFile(args.pdfPath);
121
- const contentBase64 = fileToBase64(args.pdfPath);
122
- const teachingScope = {
123
- include_topics: args.includeTopics ?? [],
124
- background_policy: args.backgroundPolicy ?? "necessary_only",
125
- page_numbering: args.pageNumbering ?? "auto",
126
- user_prompt: args.userPrompt ||
127
- (args.includeTopics?.length
128
- ? `Focus on: ${args.includeTopics.join(", ")}`
129
- : "")
130
- };
131
- const plan = await api("/api/series/plan", {
132
- method: "POST",
133
- json: {
134
- fileName: resolve(args.pdfPath).split(/[/\\]/).pop(),
135
- pageMin: args.pageMin,
136
- pageMax: args.pageMax,
137
- contentHash,
138
- contentBase64,
139
- sourceType: "pdf",
140
- title: args.title,
141
- teachingScope,
142
- maxPagesPerJob: args.maxPagesPerJob ?? 10,
143
- extractDocName: true
144
- }
72
+ void args.pageNumbering; // always auto in teachingScope
73
+ const plan = await planCurriculum({
74
+ pdfPath: args.pdfPath,
75
+ pageMin: args.pageMin,
76
+ pageMax: args.pageMax,
77
+ includeTopics: args.includeTopics,
78
+ backgroundPolicy: args.backgroundPolicy,
79
+ userPrompt: args.userPrompt,
80
+ maxPagesPerJob: args.maxPagesPerJob,
81
+ title: args.title
145
82
  });
146
83
  return textResult({
147
84
  note: "Free curriculum preview. Confirm with the user, then call hiliflow_compile_series (or hiliflow_compile_episode for a single job).",
@@ -161,37 +98,27 @@ server.tool("hiliflow_compile_episode", {
161
98
  userPrompt: z.string().optional(),
162
99
  title: z.string().optional(),
163
100
  contentLanguage: z.string().optional().default("en"),
101
+ characterId: z
102
+ .string()
103
+ .optional()
104
+ .describe('e.g. "chr_pixel_voxel" for Voxel teacher'),
164
105
  plan: z.enum(["free", "pro", "pro_plus"]).optional().default("free")
165
106
  }, async (args) => {
166
- const contentHash = await sha256HexOfFile(args.pdfPath);
167
- const contentBase64 = fileToBase64(args.pdfPath);
168
- const teachingScope = {
169
- include_topics: args.includeTopics ?? [],
170
- background_policy: args.backgroundPolicy ?? "necessary_only",
171
- page_numbering: args.pageNumbering ?? "auto",
172
- user_prompt: args.userPrompt || ""
173
- };
174
- const job = await api("/api/jobs", {
175
- method: "POST",
176
- json: {
177
- fileName: resolve(args.pdfPath).split(/[/\\]/).pop(),
178
- pageMin: args.pageMin,
179
- pageMax: args.pageMax,
180
- contentHash,
181
- contentBase64,
182
- sourceType: "pdf",
183
- title: args.title,
184
- teachingScope,
185
- contentLanguage: args.contentLanguage ?? "en",
186
- plan: args.plan ?? "free",
187
- extractDocName: true,
188
- highlight: true
189
- }
107
+ void args.pageNumbering;
108
+ const result = await compileEpisode({
109
+ pdfPath: args.pdfPath,
110
+ pageMin: args.pageMin,
111
+ pageMax: args.pageMax,
112
+ includeTopics: args.includeTopics,
113
+ backgroundPolicy: args.backgroundPolicy,
114
+ userPrompt: args.userPrompt,
115
+ title: args.title,
116
+ contentLanguage: args.contentLanguage,
117
+ characterId: args.characterId,
118
+ plan: args.plan
190
119
  });
191
- const jobId = String(job.id || "");
192
120
  return textResult({
193
- job,
194
- studioUrl: jobId ? `${WEB_BASE}/app/projects/${jobId}` : null,
121
+ ...result,
195
122
  note: "1 episode credit charged. Poll hiliflow_job_status until status=succeeded."
196
123
  });
197
124
  });
@@ -209,20 +136,20 @@ server.tool("hiliflow_compile_series", {
209
136
  maxPagesPerJob: z.number().int().min(1).max(20).optional().default(10),
210
137
  title: z.string().optional(),
211
138
  contentLanguage: z.string().optional().default("en"),
139
+ characterId: z
140
+ .string()
141
+ .optional()
142
+ .describe('e.g. "chr_pixel_voxel" for Voxel teacher'),
212
143
  plan: z.enum(["free", "pro", "pro_plus"]).optional().default("free")
213
144
  }, async (args) => {
145
+ void args.pageNumbering;
214
146
  const contentHash = await sha256HexOfFile(args.pdfPath);
215
147
  const contentBase64 = fileToBase64(args.pdfPath);
216
- const teachingScope = {
217
- include_topics: args.includeTopics ?? [],
218
- background_policy: args.backgroundPolicy ?? "necessary_only",
219
- page_numbering: args.pageNumbering ?? "auto",
220
- user_prompt: args.userPrompt || ""
221
- };
148
+ const teachingScope = buildTeachingScope(args);
222
149
  const result = await api("/api/series/create", {
223
150
  method: "POST",
224
151
  json: {
225
- fileName: resolve(args.pdfPath).split(/[/\\]/).pop(),
152
+ fileName: fileNameOf(args.pdfPath),
226
153
  pageMin: args.pageMin,
227
154
  pageMax: args.pageMax,
228
155
  contentHash,
@@ -232,6 +159,7 @@ server.tool("hiliflow_compile_series", {
232
159
  teachingScope,
233
160
  maxPagesPerJob: args.maxPagesPerJob ?? 10,
234
161
  contentLanguage: args.contentLanguage ?? "en",
162
+ ...(args.characterId ? { characterId: args.characterId } : {}),
235
163
  plan: args.plan ?? "free",
236
164
  extractDocName: true,
237
165
  highlight: true
@@ -242,27 +170,19 @@ server.tool("hiliflow_compile_series", {
242
170
  studioUrls: (result.jobs || []).map((j) => ({
243
171
  jobId: j.id,
244
172
  title: j.title,
245
- studioUrl: `${WEB_BASE}/app/projects/${j.id}`
173
+ studioUrl: `${getWebBase()}/app/projects/${j.id}`
246
174
  })),
247
175
  note: `${result.totalEpisodes} episode credit(s) charged.`
248
176
  });
249
177
  });
250
178
  server.tool("hiliflow_job_status", {
251
179
  jobId: z.string()
252
- }, async ({ jobId }) => {
253
- const job = await api(`/api/jobs/${jobId}`);
254
- return textResult({
255
- ...job,
256
- studioUrl: `${WEB_BASE}/app/projects/${jobId}`
257
- });
258
- });
180
+ }, async ({ jobId }) => textResult(await jobStatus(jobId)));
259
181
  server.tool("hiliflow_open_studio", {
260
182
  jobId: z.string()
261
183
  }, async ({ jobId }) => {
262
184
  const data = await api(`/api/mcp/studio/${jobId}`);
263
185
  return textResult(data);
264
186
  });
265
- // silence unused schema warning in some bundlers
266
- void teachingScopeSchema;
267
187
  const transport = new StdioServerTransport();
268
188
  await server.connect(transport);
package/dist/lib.d.ts ADDED
@@ -0,0 +1,70 @@
1
+ export type StoredCredentials = {
2
+ apiKey?: string;
3
+ apiBase?: string;
4
+ webBase?: string;
5
+ updatedAt?: string;
6
+ };
7
+ export declare function credentialsDir(): string;
8
+ export declare function credentialsPath(): string;
9
+ export declare function readCredentials(): StoredCredentials;
10
+ /** Persist API key for CLI/MCP (env still wins when set). */
11
+ export declare function saveCredentials(input: {
12
+ apiKey: string;
13
+ apiBase?: string;
14
+ webBase?: string;
15
+ }): string;
16
+ /** Ensure Cursor user MCP config points at docwhispr without embedding the key. */
17
+ export declare function ensureCursorMcpConfig(): string;
18
+ export declare function getApiBase(): string;
19
+ export declare function getWebBase(): string;
20
+ /** @deprecated use getApiBase() — kept for call sites that import API_BASE */
21
+ export declare const API_BASE: string;
22
+ /** @deprecated use getWebBase() — kept for call sites that import WEB_BASE */
23
+ export declare const WEB_BASE: string;
24
+ export declare function requireApiKey(): string;
25
+ export declare function api<T>(path: string, init?: RequestInit & {
26
+ json?: unknown;
27
+ }, client?: string): Promise<T>;
28
+ export declare function fileToBase64(filePath: string): string;
29
+ export declare function sha256HexOfFile(filePath: string): Promise<string>;
30
+ export declare function fileNameOf(filePath: string): string;
31
+ export type TeachingScopeInput = {
32
+ includeTopics?: string[];
33
+ backgroundPolicy?: "none" | "necessary_only" | "all_in_section";
34
+ userPrompt?: string;
35
+ };
36
+ export declare function buildTeachingScope(input: TeachingScopeInput): {
37
+ include_topics: string[];
38
+ background_policy: "none" | "necessary_only" | "all_in_section";
39
+ page_numbering: "auto";
40
+ user_prompt: string;
41
+ };
42
+ export type PlanArgs = {
43
+ pdfPath: string;
44
+ pageMin: number;
45
+ pageMax: number;
46
+ includeTopics?: string[];
47
+ backgroundPolicy?: "none" | "necessary_only" | "all_in_section";
48
+ userPrompt?: string;
49
+ maxPagesPerJob?: number;
50
+ title?: string;
51
+ client?: string;
52
+ };
53
+ export declare function planCurriculum(args: PlanArgs): Promise<unknown>;
54
+ export type CompileArgs = PlanArgs & {
55
+ contentLanguage?: string;
56
+ characterId?: string;
57
+ plan?: "free" | "pro" | "pro_plus";
58
+ };
59
+ export declare function compileEpisode(args: CompileArgs): Promise<{
60
+ job: Record<string, unknown>;
61
+ studioUrl: string | null;
62
+ }>;
63
+ export declare function jobStatus(jobId: string, client?: string): Promise<Record<string, unknown> & {
64
+ studioUrl: string;
65
+ }>;
66
+ export declare function parsePages(raw: string): {
67
+ pageMin: number;
68
+ pageMax: number;
69
+ };
70
+ export declare function parseTopics(raw: string | undefined): string[] | undefined;
package/dist/lib.js ADDED
@@ -0,0 +1,237 @@
1
+ /**
2
+ * Shared HiliFlow / docwhispr API helpers for MCP tools and CLI.
3
+ */
4
+ import { createHash } from "node:crypto";
5
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
6
+ import { homedir } from "node:os";
7
+ import { join, resolve } from "node:path";
8
+ export function credentialsDir() {
9
+ return join(homedir(), ".docwhispr");
10
+ }
11
+ export function credentialsPath() {
12
+ return join(credentialsDir(), "credentials.json");
13
+ }
14
+ export function readCredentials() {
15
+ try {
16
+ const raw = readFileSync(credentialsPath(), "utf8");
17
+ const data = JSON.parse(raw);
18
+ return data && typeof data === "object" ? data : {};
19
+ }
20
+ catch {
21
+ return {};
22
+ }
23
+ }
24
+ /** Persist API key for CLI/MCP (env still wins when set). */
25
+ export function saveCredentials(input) {
26
+ const key = input.apiKey.trim();
27
+ if (!key.startsWith("hl_live_")) {
28
+ throw new Error('API key must start with "hl_live_"');
29
+ }
30
+ const prev = readCredentials();
31
+ const dir = credentialsDir();
32
+ mkdirSync(dir, { recursive: true });
33
+ const path = credentialsPath();
34
+ const payload = {
35
+ apiKey: key,
36
+ apiBase: (input.apiBase ||
37
+ prev.apiBase ||
38
+ "https://docwhispr.fly.dev").replace(/\/$/, ""),
39
+ webBase: (input.webBase ||
40
+ prev.webBase ||
41
+ "https://docwhispr.com").replace(/\/$/, ""),
42
+ updatedAt: new Date().toISOString()
43
+ };
44
+ writeFileSync(path, `${JSON.stringify(payload, null, 2)}\n`, {
45
+ encoding: "utf8",
46
+ mode: 0o600
47
+ });
48
+ return path;
49
+ }
50
+ /** Ensure Cursor user MCP config points at docwhispr without embedding the key. */
51
+ export function ensureCursorMcpConfig() {
52
+ const cursorDir = join(homedir(), ".cursor");
53
+ mkdirSync(cursorDir, { recursive: true });
54
+ const path = join(cursorDir, "mcp.json");
55
+ let root = { mcpServers: {} };
56
+ if (existsSync(path)) {
57
+ try {
58
+ root = JSON.parse(readFileSync(path, "utf8"));
59
+ }
60
+ catch {
61
+ root = { mcpServers: {} };
62
+ }
63
+ }
64
+ if (!root.mcpServers || typeof root.mcpServers !== "object") {
65
+ root.mcpServers = {};
66
+ }
67
+ root.mcpServers.docwhispr = {
68
+ command: "npx",
69
+ args: ["-y", "@docwhispr/mcp-server"],
70
+ env: {
71
+ HILIFLOW_API_BASE: getApiBase(),
72
+ HILIFLOW_WEB_BASE: getWebBase()
73
+ }
74
+ };
75
+ writeFileSync(path, `${JSON.stringify(root, null, 2)}\n`, "utf8");
76
+ return path;
77
+ }
78
+ export function getApiBase() {
79
+ return (process.env.HILIFLOW_API_BASE ||
80
+ process.env.DOCWHISPR_API_BASE ||
81
+ process.env.NEXT_PUBLIC_VIDEO_WORKER_BASE_URL ||
82
+ readCredentials().apiBase ||
83
+ "https://docwhispr.fly.dev").replace(/\/$/, "");
84
+ }
85
+ export function getWebBase() {
86
+ return (process.env.HILIFLOW_WEB_BASE ||
87
+ process.env.DOCWHISPR_WEB_BASE ||
88
+ process.env.NEXT_PUBLIC_APP_URL ||
89
+ readCredentials().webBase ||
90
+ "https://docwhispr.com").replace(/\/$/, "");
91
+ }
92
+ /** @deprecated use getApiBase() — kept for call sites that import API_BASE */
93
+ export const API_BASE = getApiBase();
94
+ /** @deprecated use getWebBase() — kept for call sites that import WEB_BASE */
95
+ export const WEB_BASE = getWebBase();
96
+ export function requireApiKey() {
97
+ const fromEnv = (process.env.HILIFLOW_API_KEY ||
98
+ process.env.DOCWHISPR_API_KEY ||
99
+ "").trim();
100
+ if (fromEnv)
101
+ return fromEnv;
102
+ const fromFile = (readCredentials().apiKey || "").trim();
103
+ if (fromFile)
104
+ return fromFile;
105
+ throw new Error(`No API key found. Run: npx @docwhispr/mcp-server configure --key hl_live_…\n` +
106
+ `Or set HILIFLOW_API_KEY / write ${credentialsPath()}`);
107
+ }
108
+ export async function api(path, init = {}, client = "mcp") {
109
+ const key = requireApiKey();
110
+ const headers = {
111
+ Authorization: `Bearer ${key}`,
112
+ "X-HiliFlow-Client": client,
113
+ Accept: "application/json",
114
+ ...init.headers
115
+ };
116
+ let body = init.body;
117
+ if (init.json !== undefined) {
118
+ headers["Content-Type"] = "application/json";
119
+ body = JSON.stringify(init.json);
120
+ }
121
+ const res = await fetch(`${getApiBase()}${path}`, { ...init, headers, body });
122
+ const text = await res.text();
123
+ let data = null;
124
+ try {
125
+ data = text ? JSON.parse(text) : null;
126
+ }
127
+ catch {
128
+ data = { raw: text };
129
+ }
130
+ if (!res.ok) {
131
+ const detail = typeof data === "object" && data && "detail" in data
132
+ ? String(data.detail)
133
+ : text || res.statusText;
134
+ const err = new Error(`HiliFlow API ${res.status}: ${detail}`);
135
+ err.status = res.status;
136
+ throw err;
137
+ }
138
+ return data;
139
+ }
140
+ export function fileToBase64(filePath) {
141
+ return readFileSync(resolve(filePath)).toString("base64");
142
+ }
143
+ export async function sha256HexOfFile(filePath) {
144
+ const buf = readFileSync(resolve(filePath));
145
+ return createHash("sha256").update(buf).digest("hex");
146
+ }
147
+ export function fileNameOf(filePath) {
148
+ return resolve(filePath).split(/[/\\]/).pop() || "document.pdf";
149
+ }
150
+ export function buildTeachingScope(input) {
151
+ const topics = input.includeTopics ?? [];
152
+ return {
153
+ include_topics: topics,
154
+ background_policy: input.backgroundPolicy ?? "necessary_only",
155
+ page_numbering: "auto",
156
+ user_prompt: input.userPrompt ||
157
+ (topics.length ? `Focus on: ${topics.join(", ")}` : "")
158
+ };
159
+ }
160
+ export async function planCurriculum(args) {
161
+ const contentHash = await sha256HexOfFile(args.pdfPath);
162
+ const contentBase64 = fileToBase64(args.pdfPath);
163
+ const teachingScope = buildTeachingScope(args);
164
+ return api("/api/series/plan", {
165
+ method: "POST",
166
+ json: {
167
+ fileName: fileNameOf(args.pdfPath),
168
+ pageMin: args.pageMin,
169
+ pageMax: args.pageMax,
170
+ contentHash,
171
+ contentBase64,
172
+ sourceType: "pdf",
173
+ title: args.title,
174
+ teachingScope,
175
+ maxPagesPerJob: args.maxPagesPerJob ?? 10,
176
+ extractDocName: true
177
+ }
178
+ }, args.client ?? "mcp");
179
+ }
180
+ export async function compileEpisode(args) {
181
+ const contentHash = await sha256HexOfFile(args.pdfPath);
182
+ const contentBase64 = fileToBase64(args.pdfPath);
183
+ const teachingScope = buildTeachingScope(args);
184
+ const job = await api("/api/jobs", {
185
+ method: "POST",
186
+ json: {
187
+ fileName: fileNameOf(args.pdfPath),
188
+ pageMin: args.pageMin,
189
+ pageMax: args.pageMax,
190
+ contentHash,
191
+ contentBase64,
192
+ sourceType: "pdf",
193
+ title: args.title,
194
+ teachingScope,
195
+ contentLanguage: args.contentLanguage ?? "en",
196
+ ...(args.characterId ? { characterId: args.characterId } : {}),
197
+ plan: args.plan ?? "free",
198
+ extractDocName: true,
199
+ highlight: true
200
+ }
201
+ }, args.client ?? "mcp");
202
+ const jobId = String(job.id || "");
203
+ return {
204
+ job,
205
+ studioUrl: jobId ? `${getWebBase()}/app/projects/${jobId}` : null
206
+ };
207
+ }
208
+ export async function jobStatus(jobId, client = "mcp") {
209
+ const job = await api(`/api/jobs/${jobId}`, {}, client);
210
+ return {
211
+ ...job,
212
+ studioUrl: `${getWebBase()}/app/projects/${jobId}`
213
+ };
214
+ }
215
+ export function parsePages(raw) {
216
+ const m = String(raw || "")
217
+ .trim()
218
+ .match(/^(\d+)\s*[-–—:]\s*(\d+)$/);
219
+ if (!m) {
220
+ throw new Error(`Invalid --pages value "${raw}". Use e.g. 602-604`);
221
+ }
222
+ const pageMin = Number(m[1]);
223
+ const pageMax = Number(m[2]);
224
+ if (!(pageMin >= 1 && pageMax >= pageMin)) {
225
+ throw new Error(`Invalid page range ${pageMin}-${pageMax}`);
226
+ }
227
+ return { pageMin, pageMax };
228
+ }
229
+ export function parseTopics(raw) {
230
+ if (!raw?.trim())
231
+ return undefined;
232
+ const parts = raw
233
+ .split(/[,;,;\n]+/)
234
+ .map((s) => s.trim())
235
+ .filter(Boolean);
236
+ return parts.length ? parts : undefined;
237
+ }
package/package.json CHANGED
@@ -1,9 +1,11 @@
1
1
  {
2
2
  "name": "@docwhispr/mcp-server",
3
- "version": "0.1.0",
4
- "description": "docwhispr MCP server — plan curriculum, compile whiteboard lesson videos, open interactive studio",
3
+ "version": "0.1.3",
4
+ "description": "docwhispr MCP server + CLI — plan curriculum, compile whiteboard lesson videos, open interactive studio",
5
5
  "type": "module",
6
6
  "bin": {
7
+ "mcp-server": "dist/index.js",
8
+ "docwhispr": "dist/index.js",
7
9
  "docwhispr-mcp": "dist/index.js",
8
10
  "hiliflow-mcp": "dist/index.js"
9
11
  },