Skip to content

Commit d070ca3

Browse files
committed
feat: auto-derive output path when outputPath is omitted
All file-producing tools now derive output filename from input: - convert_to_pdf: report.docx → report.pdf - convert_to_image: doc.pdf → doc.png - convert_to_office: doc.pdf → doc.docx - ocr: scan.pdf → scan-ocr.pdf - watermark: doc.pdf → doc-watermarked.pdf - redact: doc.pdf → doc-redacted.pdf - ai_redact: contract.pdf → contract-redacted.pdf - sign: contract.pdf → contract-signed.pdf Adds deriveOutputPath() helper to files.ts. 127/127 tests passing.
1 parent 38860c6 commit d070ca3

9 files changed

Lines changed: 74 additions & 41 deletions

File tree

src/files.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,22 @@ export function buildFormData(
123123
return formData;
124124
}
125125

126+
/**
127+
* Derive an output path from an input path by swapping the extension.
128+
* If the input has no extension, appends the new one.
129+
* Adds a suffix (e.g. "-converted") to avoid overwriting the input.
130+
*/
131+
export function deriveOutputPath(
132+
inputPath: string,
133+
newExtension: string,
134+
suffix = '',
135+
): string {
136+
const ext = path.extname(inputPath);
137+
const base = ext ? inputPath.slice(0, -ext.length) : inputPath;
138+
const normalizedExt = newExtension.startsWith('.') ? newExtension : `.${newExtension}`;
139+
return `${base}${suffix}${normalizedExt}`;
140+
}
141+
126142
/**
127143
* Guard: ensure the output path differs from the input path to prevent
128144
* overwriting a file that is still being read by the API.

src/tools/ai-redact.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
assertOutputDiffersFromInput,
1414
readFileReference,
1515
writeResponseToFile,
16+
deriveOutputPath,
1617
} from '../files.js';
1718
import type { ToolDefinition, ToolResponse } from '../types.js';
1819

@@ -29,15 +30,15 @@ export const nutrient_ai_redact: ToolDefinition = {
2930

3031
parameters: {
3132
type: 'object',
32-
required: ['filePath', 'outputPath'],
33+
required: ['filePath'],
3334
properties: {
3435
filePath: {
3536
type: 'string',
3637
description: 'Path to input document',
3738
},
3839
outputPath: {
3940
type: 'string',
40-
description: 'Path for redacted output',
41+
description: 'Path for redacted output. Auto-derived from input filename with -redacted suffix if omitted.',
4142
},
4243
criteria: {
4344
type: 'string',
@@ -54,14 +55,15 @@ export const nutrient_ai_redact: ToolDefinition = {
5455
try {
5556
const {
5657
filePath,
57-
outputPath,
58+
outputPath: rawOutputPath,
5859
criteria = 'All personally identifiable information',
5960
} = args as {
6061
filePath: string;
61-
outputPath: string;
62+
outputPath?: string;
6263
criteria?: string;
6364
};
6465

66+
const outputPath = rawOutputPath || deriveOutputPath(filePath, 'pdf', '-redacted');
6567
assertOutputDiffersFromInput(filePath, outputPath, ctx.sandboxDir);
6668

6769
const fileRef = readFileReference(filePath, ctx.sandboxDir);

src/tools/convert-to-image.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
writeResponseToFile,
1111
buildFormData,
1212
assertOutputDiffersFromInput,
13+
deriveOutputPath,
1314
} from '../files.js';
1415

1516
export const convertToImageTool: ToolDefinition = {
@@ -18,15 +19,15 @@ export const convertToImageTool: ToolDefinition = {
1819
'Render PDF pages as PNG, JPEG, or WebP images. Supports custom DPI, width, height, and page ranges.',
1920
parameters: {
2021
type: 'object',
21-
required: ['filePath', 'outputPath'],
22+
required: ['filePath'],
2223
properties: {
2324
filePath: {
2425
type: 'string',
2526
description: 'Path to input PDF',
2627
},
2728
outputPath: {
2829
type: 'string',
29-
description: 'Path for output image',
30+
description: 'Path for output image. Auto-derived from input filename if omitted.',
3031
},
3132
format: {
3233
type: 'string',
@@ -49,7 +50,8 @@ export const convertToImageTool: ToolDefinition = {
4950

5051
async execute(args, ctx): Promise<ToolResponse> {
5152
try {
52-
assertOutputDiffersFromInput(args.filePath, args.outputPath, ctx.sandboxDir);
53+
const outputPath = args.outputPath || deriveOutputPath(args.filePath, args.format ?? 'png');
54+
assertOutputDiffersFromInput(args.filePath, outputPath, ctx.sandboxDir);
5355

5456
const fileRef = readFileReference(args.filePath, ctx.sandboxDir);
5557
const fileRefs = new Map<string, FileReference>([[fileRef.key, fileRef]]);
@@ -75,9 +77,9 @@ export const convertToImageTool: ToolDefinition = {
7577
const body = buildFormData(instructions, fileRefs);
7678
const response = await ctx.client.post('build', body);
7779

78-
const outputPath = writeResponseToFile(
80+
const writtenPath = writeResponseToFile(
7981
response.data as ArrayBuffer,
80-
args.outputPath,
82+
outputPath,
8183
ctx.sandboxDir,
8284
);
8385

@@ -89,7 +91,7 @@ export const convertToImageTool: ToolDefinition = {
8991

9092
return {
9193
success: true,
92-
output: `Image created at ${outputPath}`,
94+
output: `Image created at ${writtenPath}`,
9395
credits_used: response.creditsUsed ?? undefined,
9496
};
9597
} catch (e) {

src/tools/convert-to-office.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
writeResponseToFile,
1111
buildFormData,
1212
assertOutputDiffersFromInput,
13+
deriveOutputPath,
1314
} from '../files.js';
1415

1516
export const convertToOfficeTool: ToolDefinition = {
@@ -18,15 +19,15 @@ export const convertToOfficeTool: ToolDefinition = {
1819
'Convert a PDF to an Office format (DOCX, XLSX, or PPTX).',
1920
parameters: {
2021
type: 'object',
21-
required: ['filePath', 'outputPath', 'format'],
22+
required: ['filePath', 'format'],
2223
properties: {
2324
filePath: {
2425
type: 'string',
2526
description: 'Path to input PDF',
2627
},
2728
outputPath: {
2829
type: 'string',
29-
description: 'Path for output Office file',
30+
description: 'Path for output Office file. Auto-derived from input filename if omitted.',
3031
},
3132
format: {
3233
type: 'string',
@@ -38,7 +39,8 @@ export const convertToOfficeTool: ToolDefinition = {
3839

3940
async execute(args, ctx): Promise<ToolResponse> {
4041
try {
41-
assertOutputDiffersFromInput(args.filePath, args.outputPath, ctx.sandboxDir);
42+
const outputPath = args.outputPath || deriveOutputPath(args.filePath, args.format);
43+
assertOutputDiffersFromInput(args.filePath, outputPath, ctx.sandboxDir);
4244

4345
const fileRef = readFileReference(args.filePath, ctx.sandboxDir);
4446
const fileRefs = new Map<string, FileReference>([[fileRef.key, fileRef]]);
@@ -51,9 +53,9 @@ export const convertToOfficeTool: ToolDefinition = {
5153
const body = buildFormData(instructions, fileRefs);
5254
const response = await ctx.client.post('build', body);
5355

54-
const outputPath = writeResponseToFile(
56+
const writtenPath = writeResponseToFile(
5557
response.data as ArrayBuffer,
56-
args.outputPath,
58+
outputPath,
5759
ctx.sandboxDir,
5860
);
5961

@@ -65,7 +67,7 @@ export const convertToOfficeTool: ToolDefinition = {
6567

6668
return {
6769
success: true,
68-
output: `${args.format.toUpperCase()} created at ${outputPath}`,
70+
output: `${args.format.toUpperCase()} created at ${writtenPath}`,
6971
credits_used: response.creditsUsed ?? undefined,
7072
};
7173
} catch (e) {

src/tools/convert-to-pdf.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
writeResponseToFile,
1111
buildFormData,
1212
assertOutputDiffersFromInput,
13+
deriveOutputPath,
1314
} from '../files.js';
1415

1516
export const convertToPdfTool: ToolDefinition = {
@@ -19,15 +20,15 @@ export const convertToPdfTool: ToolDefinition = {
1920
'Supports page ranges, password-protected files, and HTML layout options.',
2021
parameters: {
2122
type: 'object',
22-
required: ['filePath', 'outputPath'],
23+
required: ['filePath'],
2324
properties: {
2425
filePath: {
2526
type: 'string',
2627
description: 'Path to input file (DOCX, XLSX, PPTX, HTML, image, or PDF)',
2728
},
2829
outputPath: {
2930
type: 'string',
30-
description: 'Path for output PDF',
31+
description: 'Path for output PDF. If omitted, derives from input filename (e.g. report.docx → report.pdf)',
3132
},
3233
password: {
3334
type: 'string',
@@ -70,7 +71,8 @@ export const convertToPdfTool: ToolDefinition = {
7071

7172
async execute(args, ctx): Promise<ToolResponse> {
7273
try {
73-
assertOutputDiffersFromInput(args.filePath, args.outputPath, ctx.sandboxDir);
74+
const outputPath = args.outputPath || deriveOutputPath(args.filePath, 'pdf');
75+
assertOutputDiffersFromInput(args.filePath, outputPath, ctx.sandboxDir);
7476

7577
const fileRef = readFileReference(args.filePath, ctx.sandboxDir);
7678
const fileRefs = new Map<string, FileReference>([[fileRef.key, fileRef]]);
@@ -91,9 +93,9 @@ export const convertToPdfTool: ToolDefinition = {
9193
const body = buildFormData(instructions, fileRefs);
9294
const response = await ctx.client.post('build', body);
9395

94-
const outputPath = writeResponseToFile(
96+
const writtenPath = writeResponseToFile(
9597
response.data as ArrayBuffer,
96-
args.outputPath,
98+
outputPath,
9799
ctx.sandboxDir,
98100
);
99101

@@ -105,7 +107,7 @@ export const convertToPdfTool: ToolDefinition = {
105107

106108
return {
107109
success: true,
108-
output: `PDF created at ${outputPath}`,
110+
output: `PDF created at ${writtenPath}`,
109111
credits_used: response.creditsUsed ?? undefined,
110112
};
111113
} catch (e) {

src/tools/ocr.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
writeResponseToFile,
1111
buildFormData,
1212
assertOutputDiffersFromInput,
13+
deriveOutputPath,
1314
} from '../files.js';
1415

1516
export const ocrTool: ToolDefinition = {
@@ -18,15 +19,15 @@ export const ocrTool: ToolDefinition = {
1819
'Apply OCR to a scanned PDF or image, producing a searchable PDF with selectable text.',
1920
parameters: {
2021
type: 'object',
21-
required: ['filePath', 'outputPath', 'language'],
22+
required: ['filePath', 'language'],
2223
properties: {
2324
filePath: {
2425
type: 'string',
2526
description: 'Path to input PDF or image',
2627
},
2728
outputPath: {
2829
type: 'string',
29-
description: 'Path for output PDF',
30+
description: 'Path for output PDF. Auto-derived from input filename with -ocr suffix if omitted.',
3031
},
3132
language: {
3233
type: 'string',
@@ -37,7 +38,8 @@ export const ocrTool: ToolDefinition = {
3738

3839
async execute(args, ctx): Promise<ToolResponse> {
3940
try {
40-
assertOutputDiffersFromInput(args.filePath, args.outputPath, ctx.sandboxDir);
41+
const outputPath = args.outputPath || deriveOutputPath(args.filePath, 'pdf', '-ocr');
42+
assertOutputDiffersFromInput(args.filePath, outputPath, ctx.sandboxDir);
4143

4244
const fileRef = readFileReference(args.filePath, ctx.sandboxDir);
4345
const fileRefs = new Map<string, FileReference>([[fileRef.key, fileRef]]);
@@ -51,9 +53,9 @@ export const ocrTool: ToolDefinition = {
5153
const body = buildFormData(instructions, fileRefs);
5254
const response = await ctx.client.post('build', body);
5355

54-
const outputPath = writeResponseToFile(
56+
const writtenPath = writeResponseToFile(
5557
response.data as ArrayBuffer,
56-
args.outputPath,
58+
outputPath,
5759
ctx.sandboxDir,
5860
);
5961

@@ -65,7 +67,7 @@ export const ocrTool: ToolDefinition = {
6567

6668
return {
6769
success: true,
68-
output: `OCR PDF created at ${outputPath} (language: ${args.language})`,
70+
output: `OCR PDF created at ${writtenPath} (language: ${args.language})`,
6971
credits_used: response.creditsUsed ?? undefined,
7072
};
7173
} catch (e) {

src/tools/redact.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
buildFormData,
1212
readFileReference,
1313
writeResponseToFile,
14+
deriveOutputPath,
1415
} from '../files.js';
1516
import type { ToolDefinition, ToolResponse } from '../types.js';
1617

@@ -40,10 +41,10 @@ export const nutrient_redact: ToolDefinition = {
4041

4142
parameters: {
4243
type: 'object',
43-
required: ['filePath', 'outputPath', 'strategy'],
44+
required: ['filePath', 'strategy'],
4445
properties: {
4546
filePath: { type: 'string', description: 'Path to input PDF' },
46-
outputPath: { type: 'string', description: 'Path for redacted output PDF' },
47+
outputPath: { type: 'string', description: 'Path for redacted output PDF. Auto-derived from input filename with -redacted suffix if omitted.' },
4748
strategy: {
4849
type: 'string',
4950
enum: ['preset', 'regex', 'text'],
@@ -86,7 +87,7 @@ export const nutrient_redact: ToolDefinition = {
8687
try {
8788
const {
8889
filePath,
89-
outputPath,
90+
outputPath: rawOutputPath,
9091
strategy,
9192
preset,
9293
regex,
@@ -97,7 +98,7 @@ export const nutrient_redact: ToolDefinition = {
9798
pageLimit,
9899
} = args as {
99100
filePath: string;
100-
outputPath: string;
101+
outputPath?: string;
101102
strategy: 'preset' | 'regex' | 'text';
102103
preset?: string;
103104
regex?: string;
@@ -108,6 +109,8 @@ export const nutrient_redact: ToolDefinition = {
108109
pageLimit?: number;
109110
};
110111

112+
const outputPath = rawOutputPath || deriveOutputPath(filePath, 'pdf', '-redacted');
113+
111114
// Validate strategy-specific required fields
112115
if (strategy === 'preset' && !preset) {
113116
throw new FileError('preset is required when strategy is "preset"');

src/tools/sign.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
readFileReference,
1313
writeResponseToFile,
1414
assertOutputDiffersFromInput,
15+
deriveOutputPath,
1516
} from '../files.js';
1617

1718
/** Build the signature options JSON sent as the `data` FormData field. */
@@ -65,15 +66,15 @@ export const signTool: ToolDefinition = {
6566
'visible or invisible signatures, and optional watermark/graphic images.',
6667
parameters: {
6768
type: 'object',
68-
required: ['filePath', 'outputPath'],
69+
required: ['filePath'],
6970
properties: {
7071
filePath: {
7172
type: 'string',
7273
description: 'Path to the PDF to sign',
7374
},
7475
outputPath: {
7576
type: 'string',
76-
description: 'Path for the signed output PDF',
77+
description: 'Path for the signed output PDF. Auto-derived from input filename with -signed suffix if omitted.',
7778
},
7879
signatureType: {
7980
type: 'string',
@@ -131,7 +132,7 @@ export const signTool: ToolDefinition = {
131132
try {
132133
const {
133134
filePath,
134-
outputPath,
135+
outputPath: rawOutputPath,
135136
signatureType = 'cms',
136137
signerName,
137138
reason,
@@ -144,6 +145,7 @@ export const signTool: ToolDefinition = {
144145
graphicImagePath,
145146
} = args;
146147

148+
const outputPath = rawOutputPath || deriveOutputPath(filePath, 'pdf', '-signed');
147149
assertOutputDiffersFromInput(filePath, outputPath, ctx.sandboxDir);
148150

149151
// Read the PDF — signing requires a local file, not a URL

0 commit comments

Comments
 (0)