@robin7331/papyrus-cli 0.1.6 → 0.1.7

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
@@ -27,20 +27,20 @@ papyrus --help
27
27
  # Show installed CLI version
28
28
  papyrus --version
29
29
 
30
- # Single file (auto mode; if no API key is found, Papyrus prompts you to paste one)
30
+ # Single file (default behavior; if no API key is found, Papyrus prompts you to paste one)
31
31
  papyrus ./path/to/input.pdf
32
32
 
33
33
  # Single file with explicit format/output/model
34
34
  papyrus ./path/to/input.pdf --format md --output ./out/result.md --model gpt-4o-mini
35
35
 
36
- # Auto mode with extra instructions
36
+ # Default conversion with extra instructions
37
37
  papyrus ./path/to/input.pdf --instructions "Prioritize table accuracy." --format txt
38
38
 
39
- # Prompt mode (inline prompt)
40
- papyrus ./path/to/input.pdf --mode prompt --prompt "Extract all invoice line items as bullet points." --format md
39
+ # Prompt conversion (inline prompt)
40
+ papyrus ./path/to/input.pdf --prompt "Extract all invoice line items as bullet points." --format md
41
41
 
42
- # Prompt mode (prompt file)
43
- papyrus ./path/to/input.pdf --mode prompt --prompt-file ./my-prompt.txt --format txt
42
+ # Prompt conversion (prompt file)
43
+ papyrus ./path/to/input.pdf --prompt-file ./my-prompt.txt --format txt
44
44
 
45
45
  # Folder mode (recursive scan, asks for confirmation)
46
46
  papyrus ./path/to/folder
@@ -132,46 +132,34 @@ Example:
132
132
  papyrus ./docs --output ./converted
133
133
  ```
134
134
 
135
- ### `--mode <mode>`
136
-
137
- Conversion mode:
138
- - `auto` (default): built-in conversion behavior.
139
- - `prompt`: use your own prompt via `--prompt` or `--prompt-file`.
140
-
141
- Example:
142
-
143
- ```bash
144
- papyrus ./docs/invoice.pdf --mode prompt --prompt "Extract all line items."
145
- ```
146
-
147
135
  ### `--instructions <text>`
148
136
 
149
- Additional conversion instructions in `auto` mode only.
137
+ Additional conversion instructions for default conversion behavior. Cannot be combined with `--prompt` or `--prompt-file`.
150
138
 
151
139
  Example:
152
140
 
153
141
  ```bash
154
- papyrus ./docs/invoice.pdf --mode auto --instructions "Keep table columns aligned."
142
+ papyrus ./docs/invoice.pdf --instructions "Keep table columns aligned."
155
143
  ```
156
144
 
157
145
  ### `--prompt <text>`
158
146
 
159
- Inline prompt text for `prompt` mode. Must be non-empty. In `prompt` mode, use exactly one of `--prompt` or `--prompt-file`.
147
+ Inline prompt text for prompt-based conversion. Must be non-empty. Use exactly one of `--prompt` or `--prompt-file`.
160
148
 
161
149
  Example:
162
150
 
163
151
  ```bash
164
- papyrus ./docs/invoice.pdf --mode prompt --prompt "Summarize payment terms."
152
+ papyrus ./docs/invoice.pdf --prompt "Summarize payment terms."
165
153
  ```
166
154
 
167
155
  ### `--prompt-file <path>`
168
156
 
169
- Path to a text file containing the prompt for `prompt` mode. File must contain non-empty text. In `prompt` mode, use exactly one of `--prompt` or `--prompt-file`.
157
+ Path to a text file containing the prompt for prompt-based conversion. File must contain non-empty text. Use exactly one of `--prompt` or `--prompt-file`.
170
158
 
171
159
  Example:
172
160
 
173
161
  ```bash
174
- papyrus ./docs/invoice.pdf --mode prompt --prompt-file ./my-prompt.txt
162
+ papyrus ./docs/invoice.pdf --prompt-file ./my-prompt.txt
175
163
  ```
176
164
 
177
165
  ### `-m, --model <model>`
@@ -206,7 +194,7 @@ papyrus ./docs --yes
206
194
 
207
195
  ## Notes
208
196
 
209
- - In `auto` mode without `--format`, the model returns structured JSON with `format` + `content`.
197
+ - In default conversion (without `--prompt`/`--prompt-file`) and without `--format`, the model returns structured JSON with `format` + `content`.
210
198
  - Single-file input now also shows a live worker lane (spinner in TTY) while conversion is running.
211
199
  - Folder input is scanned recursively for `.pdf` files and processed in parallel.
212
200
  - In folder mode, `--output` must be a directory path and mirrored subfolders are preserved.
package/dist/cli.js CHANGED
@@ -6,7 +6,7 @@ import { dirname, join, relative, resolve } from "node:path";
6
6
  import { Command } from "commander";
7
7
  import { clearStoredApiKey, getConfigFilePath, getStoredApiKey, maskApiKey, setStoredApiKey } from "./config.js";
8
8
  import { convertPdf } from "./openaiPdfToMarkdown.js";
9
- import { defaultOutputPath, formatDurationMs, isPdfPath, looksLikeFileOutput, parseConcurrency, parseFormat, parseMode, resolveFolderOutputPath, truncate, validateOptionCombination } from "./cliHelpers.js";
9
+ import { defaultOutputPath, formatDurationMs, isPdfPath, looksLikeFileOutput, parseConcurrency, parseFormat, resolveFolderOutputPath, truncate, validateOptionCombination } from "./cliHelpers.js";
10
10
  const program = new Command();
11
11
  const configFilePath = getConfigFilePath();
12
12
  const OPENAI_API_KEYS_URL = "https://platform.openai.com/settings/organization/api-keys";
@@ -20,24 +20,24 @@ program
20
20
  .option("-m, --model <model>", "OpenAI model to use", "gpt-4o-mini")
21
21
  .option("--concurrency <n>", "Max parallel workers for folder input (default: 10)", parseConcurrency)
22
22
  .option("-y, --yes", "Skip confirmation prompt in folder mode")
23
- .option("--mode <mode>", "Conversion mode: auto or prompt", parseMode, "auto")
24
23
  .option("--format <format>", "Output format override: md or txt", parseFormat)
25
- .option("--instructions <text>", "Additional conversion instructions for auto mode")
26
- .option("--prompt <text>", "Custom prompt text for prompt mode")
27
- .option("--prompt-file <path>", "Path to file containing prompt text for prompt mode")
24
+ .option("--instructions <text>", "Additional conversion instructions (only when not using --prompt/--prompt-file)")
25
+ .option("--prompt <text>", "Custom prompt text (enables prompt mode)")
26
+ .option("--prompt-file <path>", "Path to file containing prompt text (enables prompt mode)")
28
27
  .action(async (input, options) => {
29
28
  const inputPath = resolve(input);
30
29
  const startedAt = Date.now();
31
30
  try {
32
31
  validateOptionCombination(options);
33
32
  const promptText = await resolvePromptText(options);
33
+ const conversionMode = resolveConversionMode(promptText);
34
34
  const inputKind = await detectInputKind(inputPath);
35
35
  let usageTotals = emptyUsage();
36
36
  if (inputKind === "file") {
37
- usageTotals = await processSingleFile(inputPath, options, promptText);
37
+ usageTotals = await processSingleFile(inputPath, options, conversionMode, promptText);
38
38
  }
39
39
  else {
40
- const summary = await processFolder(inputPath, options, promptText);
40
+ const summary = await processFolder(inputPath, options, conversionMode, promptText);
41
41
  usageTotals = summary.usage;
42
42
  if (!summary.cancelled && summary.failed > 0) {
43
43
  process.exitCode = 1;
@@ -112,7 +112,7 @@ program.parseAsync(process.argv).catch((error) => {
112
112
  console.error(`Command failed: ${message}`);
113
113
  process.exitCode = 1;
114
114
  });
115
- async function processSingleFile(inputPath, options, promptText) {
115
+ async function processSingleFile(inputPath, options, mode, promptText) {
116
116
  if (!isPdfPath(inputPath)) {
117
117
  throw new Error("Input file must have a .pdf extension.");
118
118
  }
@@ -131,7 +131,7 @@ async function processSingleFile(inputPath, options, promptText) {
131
131
  const result = await convertPdf({
132
132
  inputPath,
133
133
  model: options.model,
134
- mode: options.mode,
134
+ mode,
135
135
  format: options.format,
136
136
  instructions: options.instructions,
137
137
  promptText
@@ -164,7 +164,7 @@ async function processSingleFile(inputPath, options, promptText) {
164
164
  workerDashboard?.stop();
165
165
  }
166
166
  }
167
- async function processFolder(inputDir, options, promptText) {
167
+ async function processFolder(inputDir, options, mode, promptText) {
168
168
  if (options.output && looksLikeFileOutput(options.output)) {
169
169
  throw new Error("In folder mode, --output must be a directory path (not a .md/.txt file path).");
170
170
  }
@@ -200,7 +200,7 @@ async function processFolder(inputDir, options, promptText) {
200
200
  const result = await convertPdf({
201
201
  inputPath: filePath,
202
202
  model: options.model,
203
- mode: options.mode,
203
+ mode,
204
204
  format: options.format,
205
205
  instructions: options.instructions,
206
206
  promptText
@@ -250,9 +250,6 @@ async function processFolder(inputDir, options, promptText) {
250
250
  return { total: files.length, succeeded, failed, cancelled: false, usage };
251
251
  }
252
252
  async function resolvePromptText(options) {
253
- if (options.mode !== "prompt") {
254
- return undefined;
255
- }
256
253
  if (options.prompt) {
257
254
  const prompt = options.prompt.trim();
258
255
  if (!prompt) {
@@ -270,6 +267,9 @@ async function resolvePromptText(options) {
270
267
  }
271
268
  return promptFromFile;
272
269
  }
270
+ function resolveConversionMode(promptText) {
271
+ return promptText ? "prompt" : "auto";
272
+ }
273
273
  async function handleConfigInit(options) {
274
274
  const existingKey = await getStoredApiKey();
275
275
  if (existingKey && !options.force) {
@@ -1,16 +1,14 @@
1
- import { type ConversionMode, type OutputFormat } from "./openaiPdfToMarkdown.js";
1
+ import { type OutputFormat } from "./openaiPdfToMarkdown.js";
2
2
  export type CliOptions = {
3
3
  output?: string;
4
4
  model: string;
5
5
  concurrency?: number;
6
6
  yes?: boolean;
7
- mode: ConversionMode;
8
7
  format?: OutputFormat;
9
8
  instructions?: string;
10
9
  prompt?: string;
11
10
  promptFile?: string;
12
11
  };
13
- export declare function parseMode(value: string): ConversionMode;
14
12
  export declare function parseFormat(value: string): OutputFormat;
15
13
  export declare function parseConcurrency(value: string): number;
16
14
  export declare function validateOptionCombination(options: CliOptions): void;
@@ -1,11 +1,5 @@
1
1
  import { InvalidArgumentError } from "commander";
2
2
  import { basename, dirname, extname, join, relative } from "node:path";
3
- export function parseMode(value) {
4
- if (value === "auto" || value === "prompt") {
5
- return value;
6
- }
7
- throw new InvalidArgumentError("Mode must be either 'auto' or 'prompt'.");
8
- }
9
3
  export function parseFormat(value) {
10
4
  if (value === "md" || value === "txt") {
11
5
  return value;
@@ -20,18 +14,12 @@ export function parseConcurrency(value) {
20
14
  return parsed;
21
15
  }
22
16
  export function validateOptionCombination(options) {
23
- if (options.mode === "prompt") {
24
- const promptSourceCount = Number(Boolean(options.prompt)) + Number(Boolean(options.promptFile));
25
- if (promptSourceCount !== 1) {
26
- throw new Error("Prompt mode requires exactly one of --prompt or --prompt-file.");
27
- }
28
- if (options.instructions) {
29
- throw new Error("--instructions is only supported in auto mode.");
30
- }
31
- return;
17
+ const promptSourceCount = Number(Boolean(options.prompt)) + Number(Boolean(options.promptFile));
18
+ if (promptSourceCount > 1) {
19
+ throw new Error("Use exactly one of --prompt or --prompt-file.");
32
20
  }
33
- if (options.prompt || options.promptFile) {
34
- throw new Error("--prompt and --prompt-file are only supported in prompt mode.");
21
+ if (promptSourceCount === 1 && options.instructions) {
22
+ throw new Error("--instructions cannot be combined with --prompt or --prompt-file.");
35
23
  }
36
24
  }
37
25
  export function defaultOutputPath(inputPath, format) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@robin7331/papyrus-cli",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "private": false,
5
5
  "description": "Convert PDF to markdown or text with the OpenAI Agents SDK",
6
6
  "repository": {
package/src/cli.ts CHANGED
@@ -14,6 +14,7 @@ import {
14
14
  } from "./config.js";
15
15
  import {
16
16
  convertPdf,
17
+ type ConversionMode,
17
18
  type ConvertUsage
18
19
  } from "./openaiPdfToMarkdown.js";
19
20
  import {
@@ -23,7 +24,6 @@ import {
23
24
  looksLikeFileOutput,
24
25
  parseConcurrency,
25
26
  parseFormat,
26
- parseMode,
27
27
  resolveFolderOutputPath,
28
28
  truncate,
29
29
  type CliOptions,
@@ -52,14 +52,13 @@ program
52
52
  parseConcurrency
53
53
  )
54
54
  .option("-y, --yes", "Skip confirmation prompt in folder mode")
55
- .option("--mode <mode>", "Conversion mode: auto or prompt", parseMode, "auto")
56
55
  .option("--format <format>", "Output format override: md or txt", parseFormat)
57
56
  .option(
58
57
  "--instructions <text>",
59
- "Additional conversion instructions for auto mode"
58
+ "Additional conversion instructions (only when not using --prompt/--prompt-file)"
60
59
  )
61
- .option("--prompt <text>", "Custom prompt text for prompt mode")
62
- .option("--prompt-file <path>", "Path to file containing prompt text for prompt mode")
60
+ .option("--prompt <text>", "Custom prompt text (enables prompt mode)")
61
+ .option("--prompt-file <path>", "Path to file containing prompt text (enables prompt mode)")
63
62
  .action(async (input: string, options: CliOptions) => {
64
63
  const inputPath = resolve(input);
65
64
  const startedAt = Date.now();
@@ -68,13 +67,14 @@ program
68
67
  validateOptionCombination(options);
69
68
 
70
69
  const promptText = await resolvePromptText(options);
70
+ const conversionMode = resolveConversionMode(promptText);
71
71
  const inputKind = await detectInputKind(inputPath);
72
72
  let usageTotals: ConvertUsage = emptyUsage();
73
73
 
74
74
  if (inputKind === "file") {
75
- usageTotals = await processSingleFile(inputPath, options, promptText);
75
+ usageTotals = await processSingleFile(inputPath, options, conversionMode, promptText);
76
76
  } else {
77
- const summary = await processFolder(inputPath, options, promptText);
77
+ const summary = await processFolder(inputPath, options, conversionMode, promptText);
78
78
  usageTotals = summary.usage;
79
79
  if (!summary.cancelled && summary.failed > 0) {
80
80
  process.exitCode = 1;
@@ -157,6 +157,7 @@ program.parseAsync(process.argv).catch((error: unknown) => {
157
157
  async function processSingleFile(
158
158
  inputPath: string,
159
159
  options: CliOptions,
160
+ mode: ConversionMode,
160
161
  promptText?: string
161
162
  ): Promise<ConvertUsage> {
162
163
  if (!isPdfPath(inputPath)) {
@@ -180,7 +181,7 @@ async function processSingleFile(
180
181
  const result = await convertPdf({
181
182
  inputPath,
182
183
  model: options.model,
183
- mode: options.mode,
184
+ mode,
184
185
  format: options.format,
185
186
  instructions: options.instructions,
186
187
  promptText
@@ -237,6 +238,7 @@ type FolderSummary = {
237
238
  async function processFolder(
238
239
  inputDir: string,
239
240
  options: CliOptions,
241
+ mode: ConversionMode,
240
242
  promptText?: string
241
243
  ): Promise<FolderSummary> {
242
244
  if (options.output && looksLikeFileOutput(options.output)) {
@@ -282,7 +284,7 @@ async function processFolder(
282
284
  const result = await convertPdf({
283
285
  inputPath: filePath,
284
286
  model: options.model,
285
- mode: options.mode,
287
+ mode,
286
288
  format: options.format,
287
289
  instructions: options.instructions,
288
290
  promptText
@@ -347,10 +349,6 @@ async function processFolder(
347
349
  }
348
350
 
349
351
  async function resolvePromptText(options: CliOptions): Promise<string | undefined> {
350
- if (options.mode !== "prompt") {
351
- return undefined;
352
- }
353
-
354
352
  if (options.prompt) {
355
353
  const prompt = options.prompt.trim();
356
354
  if (!prompt) {
@@ -373,6 +371,10 @@ async function resolvePromptText(options: CliOptions): Promise<string | undefine
373
371
  return promptFromFile;
374
372
  }
375
373
 
374
+ function resolveConversionMode(promptText: string | undefined): ConversionMode {
375
+ return promptText ? "prompt" : "auto";
376
+ }
377
+
376
378
  async function handleConfigInit(options: ConfigInitOptions): Promise<void> {
377
379
  const existingKey = await getStoredApiKey();
378
380
  if (existingKey && !options.force) {
package/src/cliHelpers.ts CHANGED
@@ -1,27 +1,18 @@
1
1
  import { InvalidArgumentError } from "commander";
2
2
  import { basename, dirname, extname, join, relative } from "node:path";
3
- import { type ConversionMode, type OutputFormat } from "./openaiPdfToMarkdown.js";
3
+ import { type OutputFormat } from "./openaiPdfToMarkdown.js";
4
4
 
5
5
  export type CliOptions = {
6
6
  output?: string;
7
7
  model: string;
8
8
  concurrency?: number;
9
9
  yes?: boolean;
10
- mode: ConversionMode;
11
10
  format?: OutputFormat;
12
11
  instructions?: string;
13
12
  prompt?: string;
14
13
  promptFile?: string;
15
14
  };
16
15
 
17
- export function parseMode(value: string): ConversionMode {
18
- if (value === "auto" || value === "prompt") {
19
- return value;
20
- }
21
-
22
- throw new InvalidArgumentError("Mode must be either 'auto' or 'prompt'.");
23
- }
24
-
25
16
  export function parseFormat(value: string): OutputFormat {
26
17
  if (value === "md" || value === "txt") {
27
18
  return value;
@@ -40,21 +31,13 @@ export function parseConcurrency(value: string): number {
40
31
  }
41
32
 
42
33
  export function validateOptionCombination(options: CliOptions): void {
43
- if (options.mode === "prompt") {
44
- const promptSourceCount = Number(Boolean(options.prompt)) + Number(Boolean(options.promptFile));
45
- if (promptSourceCount !== 1) {
46
- throw new Error("Prompt mode requires exactly one of --prompt or --prompt-file.");
47
- }
48
-
49
- if (options.instructions) {
50
- throw new Error("--instructions is only supported in auto mode.");
51
- }
52
-
53
- return;
34
+ const promptSourceCount = Number(Boolean(options.prompt)) + Number(Boolean(options.promptFile));
35
+ if (promptSourceCount > 1) {
36
+ throw new Error("Use exactly one of --prompt or --prompt-file.");
54
37
  }
55
38
 
56
- if (options.prompt || options.promptFile) {
57
- throw new Error("--prompt and --prompt-file are only supported in prompt mode.");
39
+ if (promptSourceCount === 1 && options.instructions) {
40
+ throw new Error("--instructions cannot be combined with --prompt or --prompt-file.");
58
41
  }
59
42
  }
60
43
 
@@ -8,22 +8,12 @@ import {
8
8
  looksLikeFileOutput,
9
9
  parseConcurrency,
10
10
  parseFormat,
11
- parseMode,
12
11
  resolveFolderOutputPath,
13
12
  truncate,
14
13
  validateOptionCombination,
15
14
  type CliOptions
16
15
  } from "../src/cliHelpers.js";
17
16
 
18
- test("parseMode accepts valid values", () => {
19
- assert.equal(parseMode("auto"), "auto");
20
- assert.equal(parseMode("prompt"), "prompt");
21
- });
22
-
23
- test("parseMode rejects invalid values", () => {
24
- assert.throws(() => parseMode("invalid"), InvalidArgumentError);
25
- });
26
-
27
17
  test("parseFormat accepts valid values", () => {
28
18
  assert.equal(parseFormat("md"), "md");
29
19
  assert.equal(parseFormat("txt"), "txt");
@@ -45,42 +35,40 @@ test("parseConcurrency rejects invalid values", () => {
45
35
  assert.throws(() => parseConcurrency("abc"), InvalidArgumentError);
46
36
  });
47
37
 
48
- test("validateOptionCombination enforces prompt mode requirements", () => {
38
+ test("validateOptionCombination allows default auto behavior without prompt flags", () => {
49
39
  const base: CliOptions = {
50
- model: "gpt-4o-mini",
51
- mode: "prompt"
40
+ model: "gpt-4o-mini"
52
41
  };
53
42
 
54
- assert.throws(
55
- () => validateOptionCombination(base),
56
- /Prompt mode requires exactly one of --prompt or --prompt-file\./
57
- );
58
- assert.doesNotThrow(() => validateOptionCombination({ ...base, prompt: "Convert this" }));
43
+ assert.doesNotThrow(() => validateOptionCombination(base));
44
+ assert.doesNotThrow(() => validateOptionCombination({ ...base, instructions: "Extra formatting rules" }));
45
+ });
46
+
47
+ test("validateOptionCombination treats --prompt and --prompt-file as mutually exclusive", () => {
48
+ const base: CliOptions = {
49
+ model: "gpt-4o-mini"
50
+ };
51
+
52
+ assert.doesNotThrow(() => validateOptionCombination({ ...base, prompt: "Convert" }));
59
53
  assert.doesNotThrow(() => validateOptionCombination({ ...base, promptFile: "./prompt.txt" }));
60
54
  assert.throws(
61
55
  () => validateOptionCombination({ ...base, prompt: "x", promptFile: "./prompt.txt" }),
62
- /Prompt mode requires exactly one of --prompt or --prompt-file\./
63
- );
64
- assert.throws(
65
- () => validateOptionCombination({ ...base, prompt: "x", instructions: "Extra" }),
66
- /--instructions is only supported in auto mode\./
56
+ /Use exactly one of --prompt or --prompt-file\./
67
57
  );
68
58
  });
69
59
 
70
- test("validateOptionCombination rejects prompt flags in auto mode", () => {
60
+ test("validateOptionCombination rejects --instructions with prompt flags", () => {
71
61
  const base: CliOptions = {
72
- model: "gpt-4o-mini",
73
- mode: "auto"
62
+ model: "gpt-4o-mini"
74
63
  };
75
64
 
76
- assert.doesNotThrow(() => validateOptionCombination(base));
77
65
  assert.throws(
78
- () => validateOptionCombination({ ...base, prompt: "Convert" }),
79
- /--prompt and --prompt-file are only supported in prompt mode\./
66
+ () => validateOptionCombination({ ...base, prompt: "x", instructions: "Extra" }),
67
+ /--instructions cannot be combined with --prompt or --prompt-file\./
80
68
  );
81
69
  assert.throws(
82
- () => validateOptionCombination({ ...base, promptFile: "./prompt.txt" }),
83
- /--prompt and --prompt-file are only supported in prompt mode\./
70
+ () => validateOptionCombination({ ...base, promptFile: "./prompt.txt", instructions: "Extra" }),
71
+ /--instructions cannot be combined with --prompt or --prompt-file\./
84
72
  );
85
73
  });
86
74