jaz-clio 4.4.0 → 4.6.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.
- package/assets/skills/api/SKILL.md +1 -1
- package/assets/skills/api/references/endpoints.md +41 -0
- package/assets/skills/conversion/SKILL.md +1 -1
- package/assets/skills/jobs/SKILL.md +1 -1
- package/assets/skills/jobs/references/document-collection.md +47 -7
- package/assets/skills/transaction-recipes/SKILL.md +1 -1
- package/dist/commands/draft-helpers.js +26 -6
- package/dist/commands/jobs.js +48 -40
- package/dist/commands/magic.js +468 -8
- package/dist/core/jobs/document-collection/tools/ingest/classify.js +1 -1
- package/dist/core/jobs/document-collection/tools/ingest/cloud/dropbox.js +28 -3
- package/dist/core/jobs/document-collection/tools/ingest/decrypt.js +19 -0
- package/dist/core/jobs/document-collection/tools/ingest/format.js +15 -2
- package/dist/core/jobs/document-collection/tools/ingest/ingest.js +45 -14
- package/dist/core/jobs/document-collection/tools/ingest/scanner.js +46 -7
- package/dist/core/jobs/document-collection/tools/ingest/upload.js +4 -4
- package/dist/core/pdf/detect.js +344 -0
- package/dist/core/pdf/index.js +8 -0
- package/dist/core/pdf/split.js +81 -0
- package/dist/core/pdf/types.js +4 -0
- package/package.json +2 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: jaz-api
|
|
3
|
-
version: 4.
|
|
3
|
+
version: 4.6.0
|
|
4
4
|
description: Complete reference for the Jaz REST API — the accounting platform backend. Use this skill whenever building, modifying, debugging, or extending any code that calls the API — including API clients, integrations, data seeding, test data, or new endpoint work. Contains every field name, response shape, error, gotcha, and edge case discovered through live production testing.
|
|
5
5
|
license: MIT
|
|
6
6
|
compatibility: Requires Jaz API key (x-jk-api-key header). Works with Claude Code, Google Antigravity, OpenAI Codex, GitHub Copilot, Cursor, and any agent that reads markdown.
|
|
@@ -985,6 +985,12 @@ Content-Type: application/json
|
|
|
985
985
|
- Tax amounts and profiles
|
|
986
986
|
- Document reference numbers, dates, currency
|
|
987
987
|
|
|
988
|
+
**Encrypted PDFs:** Magic cannot process password-protected PDFs. The CLI auto-detects and decrypts before upload:
|
|
989
|
+
- Embed password in filename: `receipt__pw__s3cRetP@ss.pdf` → decrypts with password `s3cRetP@ss`, uploads as `receipt.pdf`
|
|
990
|
+
- `__pw__` delimiter is case-insensitive; password is case-sensitive
|
|
991
|
+
- Requires `qpdf` installed (`brew install qpdf`)
|
|
992
|
+
- If no password in filename, CLI prompts interactively (or errors in `--json` mode with actionable rename instructions)
|
|
993
|
+
|
|
988
994
|
**Key gotchas:**
|
|
989
995
|
- `sourceFile` is the field name (NOT `file`) — same pattern as bank statement endpoint
|
|
990
996
|
- `EXPENSE` returns 422 — use one of the 4 valid types above
|
|
@@ -1056,6 +1062,41 @@ Content-Type: application/json
|
|
|
1056
1062
|
3. When `COMPLETED` → read `businessTransactionDetails.businessTransactionResourceId`
|
|
1057
1063
|
4. Use the BT resource ID with `GET /invoices/:id`, `GET /bills/:id`, `GET /customer-credit-notes/:id`, or `GET /supplier-credit-notes/:id`
|
|
1058
1064
|
|
|
1065
|
+
### CLI: clio magic split — Merged PDF Splitting
|
|
1066
|
+
|
|
1067
|
+
Splits a merged PDF containing multiple documents (invoices, bills, credit notes) into individual files and uploads each to Magic. Uses structural PDF signals (bookmarks, page labels) + text heuristics (keywords, "Page 1 of N" patterns) for boundary detection. **No AI tokens used.**
|
|
1068
|
+
|
|
1069
|
+
```bash
|
|
1070
|
+
# Auto-detect boundaries + upload
|
|
1071
|
+
clio magic split --file merged.pdf --type bill
|
|
1072
|
+
|
|
1073
|
+
# Manual page ranges (for scanned PDFs or override)
|
|
1074
|
+
clio magic split --file merged.pdf --type bill --pages "1-3,4-6,7-9"
|
|
1075
|
+
|
|
1076
|
+
# Dry-run: detect boundaries only (no qpdf needed)
|
|
1077
|
+
clio magic split --file merged.pdf --type bill --dry-run
|
|
1078
|
+
|
|
1079
|
+
# JSON output (for agents)
|
|
1080
|
+
clio magic split --file merged.pdf --type bill --dry-run --json
|
|
1081
|
+
```
|
|
1082
|
+
|
|
1083
|
+
**Detection signals (score-based, threshold >= 50):**
|
|
1084
|
+
- outline-bookmark (+80): PDF bookmark points to this page
|
|
1085
|
+
- page-label-reset (+70): PDF page label restarts at "1"
|
|
1086
|
+
- keyword in header (+40): Document keyword (INVOICE, BILL, etc.) in upper 40%
|
|
1087
|
+
- page-one-of (+35): "Page 1 of N" pattern
|
|
1088
|
+
- keyword-large (+25): Large font (>18pt) keyword bonus
|
|
1089
|
+
- doc-ref (+20): Document reference (INV-001, SO-2024-100, etc.)
|
|
1090
|
+
- continuation (-60): "Page N>1 of M" anti-signal
|
|
1091
|
+
- continuation-text (-40): "Continued" anti-signal
|
|
1092
|
+
|
|
1093
|
+
**Edge cases:**
|
|
1094
|
+
- Scanned PDFs (no extractable text): warns and requires `--pages` manual override
|
|
1095
|
+
- Mixed scanned+digital: low confidence on scanned portions triggers confirmation prompt
|
|
1096
|
+
- Single document detected: suggests `clio magic create` instead
|
|
1097
|
+
- Encrypted PDFs: same `__pw__` pattern as `magic create`
|
|
1098
|
+
- Requires `qpdf` for splitting (not needed for `--dry-run` auto-detect mode)
|
|
1099
|
+
|
|
1059
1100
|
---
|
|
1060
1101
|
|
|
1061
1102
|
## 15. Bank Records
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: jaz-conversion
|
|
3
|
-
version: 4.
|
|
3
|
+
version: 4.6.0
|
|
4
4
|
description: Accounting data conversion skill — migrates customer data from Xero, QuickBooks, Sage, MYOB, and Excel exports to Jaz. Covers config, quick, and full conversion workflows, Excel parsing, CoA/contact/tax/items mapping, clearing accounts, TTB, and TB verification.
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: jaz-jobs
|
|
3
|
-
version: 4.
|
|
3
|
+
version: 4.6.0
|
|
4
4
|
description: 12 accounting jobs for SMB bookkeepers and accountants — month-end, quarter-end, and year-end close playbooks plus 9 ad-hoc operational jobs (bank recon, document collection, GST/VAT filing, payment runs, credit control, supplier recon, audit prep, fixed asset review, statutory filing). Jobs can have paired tools as nested subcommands (e.g., `clio jobs bank-recon match`, `clio jobs document-collection ingest`, `clio jobs statutory-filing sg-cs`). Paired with an interactive CLI blueprint generator (clio jobs).
|
|
5
5
|
license: MIT
|
|
6
6
|
compatibility: Works with Claude Code, Claude Cowork, Claude.ai, and any agent that reads markdown. For API payloads, load the jaz-api skill. For individual transaction patterns, load the jaz-recipes skill.
|
|
@@ -52,7 +52,8 @@ Multilingual support: Filipino/Tagalog, Bahasa Indonesia/Malay, Vietnamese, and
|
|
|
52
52
|
|
|
53
53
|
- **Invoices/Bills/Credit Notes**: `.pdf`, `.jpg`, `.jpeg`, `.png`
|
|
54
54
|
- **Bank Statements**: `.csv`, `.ofx`
|
|
55
|
-
- **
|
|
55
|
+
- **Containers** (auto-extracted): `.zip` — contents extracted and processed individually
|
|
56
|
+
- **Skipped** (with warning): `.xlsx`, `.xls`, `.doc`, `.docx`, `.txt`, `.rar`, `.7z`
|
|
56
57
|
|
|
57
58
|
### Traversal Rules
|
|
58
59
|
|
|
@@ -60,6 +61,7 @@ Multilingual support: Filipino/Tagalog, Bahasa Indonesia/Malay, Vietnamese, and
|
|
|
60
61
|
2. **Root-level files** — classified as UNKNOWN (no folder context)
|
|
61
62
|
3. **Max depth** — 10 levels (prevents runaway recursion)
|
|
62
63
|
4. **Hidden files/dirs** — skipped (anything starting with `.`)
|
|
64
|
+
5. **ZIP files** — extracted to temp dir, contents scanned through same pipeline (max 1 level, no nested ZIPs)
|
|
63
65
|
|
|
64
66
|
## Cloud Provider Support
|
|
65
67
|
|
|
@@ -85,11 +87,17 @@ The `--source` flag accepts public share links from Dropbox, Google Drive, and O
|
|
|
85
87
|
# Scan + classify local directory
|
|
86
88
|
clio jobs document-collection ingest --source ./client-docs/ [--json]
|
|
87
89
|
|
|
90
|
+
# Scan + classify a ZIP file (auto-extracted)
|
|
91
|
+
clio jobs document-collection ingest --source ./client-docs.zip [--json]
|
|
92
|
+
|
|
88
93
|
# Cloud sources — Dropbox, Google Drive, OneDrive
|
|
89
94
|
clio jobs document-collection ingest --source "https://www.dropbox.com/scl/fo/.../folder?rlkey=..." [--json]
|
|
90
95
|
clio jobs document-collection ingest --source "https://drive.google.com/file/d/FILE_ID/view" [--json]
|
|
91
96
|
clio jobs document-collection ingest --source "https://1drv.ms/f/s!..." [--json]
|
|
92
97
|
|
|
98
|
+
# Dropbox file link to a ZIP (auto-extracted)
|
|
99
|
+
clio jobs document-collection ingest --source "https://www.dropbox.com/scl/fi/.../docs.zip?rlkey=...&dl=0" [--json]
|
|
100
|
+
|
|
93
101
|
# With timeout for large cloud downloads
|
|
94
102
|
clio jobs document-collection ingest --source "https://www.dropbox.com/..." --timeout 120000 [--json]
|
|
95
103
|
|
|
@@ -97,24 +105,38 @@ clio jobs document-collection ingest --source "https://www.dropbox.com/..." --ti
|
|
|
97
105
|
clio jobs document-collection ingest --source ./scans/ --type invoice [--json]
|
|
98
106
|
clio jobs document-collection ingest --source ./scans/ --type credit-note-customer [--json]
|
|
99
107
|
|
|
100
|
-
#
|
|
101
|
-
clio
|
|
108
|
+
# Upload a ZIP of invoices via Magic (all files treated as same type)
|
|
109
|
+
clio magic create --file ./invoices.zip --type invoice --json
|
|
110
|
+
|
|
111
|
+
# Scan + upload (encrypted PDFs with password embedded in filename)
|
|
112
|
+
clio jobs document-collection ingest --source ./bank-docs/ --upload --bank-account "DBS Checking" --json
|
|
113
|
+
# To decrypt: rename encrypted PDF to: receipt__pw__actualPassword.pdf
|
|
114
|
+
# The __pw__ delimiter is case-insensitive; the password itself is case-sensitive.
|
|
102
115
|
```
|
|
103
116
|
|
|
104
117
|
### Options
|
|
105
118
|
|
|
106
119
|
| Flag | Description |
|
|
107
120
|
|------|-------------|
|
|
108
|
-
| `--source <path\|url>` | Local directory
|
|
121
|
+
| `--source <path\|url>` | Local directory, .zip file, or public cloud share link — Dropbox, Google Drive, OneDrive (required). ZIPs are auto-extracted. |
|
|
109
122
|
| `--type <type>` | Force all files to: `invoice`, `bill`, `credit-note-customer`, `credit-note-supplier`, or `bank-statement` |
|
|
110
123
|
| `--upload` | Upload classified files to Jaz after scanning (requires auth) |
|
|
111
124
|
| `--bank-account <name-or-id>` | Bank account name or resourceId (required for bank statements) |
|
|
112
|
-
| `--pdf-password <password>` | Password for encrypted PDFs — same password applied to all. Requires `qpdf` installed (`brew install qpdf`) |
|
|
113
125
|
| `--api-key <key>` | API key for upload (or use `JAZ_API_KEY` env var) |
|
|
114
126
|
| `--timeout <ms>` | Download timeout in milliseconds (default: 30000 for files, 120000 for folders) |
|
|
115
127
|
| `--currency <code>` | Functional/reporting currency label |
|
|
116
128
|
| `--json` | Structured JSON output with absolute file paths |
|
|
117
129
|
|
|
130
|
+
### Encrypted PDF Passwords
|
|
131
|
+
|
|
132
|
+
Embed the password in the filename using the `__pw__` pattern:
|
|
133
|
+
```
|
|
134
|
+
receipt__pw__s3cRetP@ss.pdf → password: "s3cRetP@ss", display name: "receipt.pdf"
|
|
135
|
+
```
|
|
136
|
+
- `__pw__` is case-insensitive (`__PW__`, `__Pw__`, etc.)
|
|
137
|
+
- The password after `__pw__` is case-sensitive
|
|
138
|
+
- Requires `qpdf` installed (`brew install qpdf`)
|
|
139
|
+
|
|
118
140
|
### JSON Output
|
|
119
141
|
|
|
120
142
|
The `--json` output includes absolute file paths, classification, and size for each file. The AI agent uses these paths to upload via the api skill.
|
|
@@ -171,7 +193,7 @@ sudo apt install qpdf # Ubuntu/Debian
|
|
|
171
193
|
choco install qpdf # Windows
|
|
172
194
|
```
|
|
173
195
|
|
|
174
|
-
|
|
196
|
+
Passwords are embedded in the filename using the `__pw__` pattern: `receipt__pw__myPass.pdf`. During upload, encrypted PDFs with a filename password are decrypted to a temp file, uploaded, then cleaned up.
|
|
175
197
|
|
|
176
198
|
### Error Handling (JSON mode)
|
|
177
199
|
|
|
@@ -180,7 +202,7 @@ If encrypted PDFs are found during `--upload` without the required dependencies,
|
|
|
180
202
|
| Error Code | Condition | Action |
|
|
181
203
|
|------------|-----------|--------|
|
|
182
204
|
| `ENCRYPTED_PDF_NO_QPDF` | qpdf not installed | Install qpdf, then retry |
|
|
183
|
-
| `ENCRYPTED_PDF_NO_PASSWORD` |
|
|
205
|
+
| `ENCRYPTED_PDF_NO_PASSWORD` | Encrypted PDF without `__pw__` in filename | Rename file to embed password: `name__pw__password.pdf` |
|
|
184
206
|
|
|
185
207
|
## Phases (Blueprint)
|
|
186
208
|
|
|
@@ -193,6 +215,24 @@ When run without `ingest` subcommand, produces a 4-phase blueprint:
|
|
|
193
215
|
|
|
194
216
|
The AI agent then uses the classified file paths to upload via the Jaz Magic API (see api skill for endpoint details).
|
|
195
217
|
|
|
218
|
+
## ZIP File Support
|
|
219
|
+
|
|
220
|
+
ZIP files are treated as containers — their contents are extracted and each file is processed individually through the same scan/classify pipeline.
|
|
221
|
+
|
|
222
|
+
### Supported Scenarios
|
|
223
|
+
|
|
224
|
+
1. **ZIP in a scanned directory**: Extracted to a temp dir, contents scanned recursively
|
|
225
|
+
2. **ZIP as --source**: `clio jobs document-collection ingest --source ./archive.zip` extracts and scans
|
|
226
|
+
3. **ZIP from Dropbox file link**: Auto-extracted after download
|
|
227
|
+
4. **ZIP via clio magic create**: `clio magic create --file archive.zip --type invoice` extracts and uploads each file
|
|
228
|
+
|
|
229
|
+
### Limitations
|
|
230
|
+
|
|
231
|
+
- Nested ZIPs (ZIP inside ZIP) are not extracted — only one level
|
|
232
|
+
- Password-protected ZIPs are not supported (adm-zip limitation)
|
|
233
|
+
- Max ZIP size: 500MB
|
|
234
|
+
- `.rar` and `.7z` are still skipped (no built-in support)
|
|
235
|
+
|
|
196
236
|
## Relationship to Other Skills
|
|
197
237
|
|
|
198
238
|
- **api skill** — Field names, auth headers, error codes for Magic endpoints. Agent uses this to upload classified files.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: jaz-recipes
|
|
3
|
-
version: 4.
|
|
3
|
+
version: 4.6.0
|
|
4
4
|
description: 16 IFRS-compliant recipes for complex multi-step accounting in Jaz — prepaid amortization, deferred revenue, loan schedules, IFRS 16 leases, hire purchase, fixed deposits, asset disposal, FX revaluation, ECL provisioning, IAS 37 provisions, dividends, intercompany, and capital WIP. Each recipe includes journal entries, capsule structure, and verification steps. Paired with 10 financial calculators that produce execution-ready blueprints with workings.
|
|
5
5
|
license: MIT
|
|
6
6
|
compatibility: Works with Claude Code, Claude Cowork, Claude.ai, and any agent that reads markdown. For API payloads, load the jaz-api skill alongside this one.
|
|
@@ -27,7 +27,9 @@ export const JOURNAL_REQUIRED_FIELDS = [
|
|
|
27
27
|
{ field: 'valueDate', label: 'Date', hint: '--date <YYYY-MM-DD>', check: (j) => !!j.valueDate },
|
|
28
28
|
{ field: 'journalEntries', label: 'Journal entries', hint: '--entries <json>', check: (j) => j.journalEntries?.length > 0 },
|
|
29
29
|
{ field: 'accountResourceId', label: 'Account', hint: '--account <name or UUID>', check: (e) => !!(e.accountResourceId || e.organizationAccountResourceId), perLineItem: true },
|
|
30
|
-
{ field: 'amount', label: 'Amount', hint: 'via --entries', check: (e) => e.amount != null && e.amount > 0
|
|
30
|
+
{ field: 'amount', label: 'Amount', hint: 'via --entries', check: (e) => (e.amount != null && e.amount > 0) ||
|
|
31
|
+
(e.debitAmount != null && e.debitAmount > 0) ||
|
|
32
|
+
(e.creditAmount != null && e.creditAmount > 0), perLineItem: true },
|
|
31
33
|
];
|
|
32
34
|
// ── Core Validation ─────────────────────────────────────────────
|
|
33
35
|
/**
|
|
@@ -91,14 +93,17 @@ specs, lineItemsKey = 'lineItems') {
|
|
|
91
93
|
? { status: 'ok', resourceId: acctId }
|
|
92
94
|
: { status: 'missing', hint: '--account <name or UUID>' };
|
|
93
95
|
// Journal entries: show amount/type instead of name/unitPrice
|
|
96
|
+
// GET returns debitAmount/creditAmount (not amount/type)
|
|
94
97
|
if (lineItemsKey === 'journalEntries') {
|
|
95
98
|
const amountSpec = liSpecs.find((s) => s.field === 'amount');
|
|
96
99
|
const amountOk = amountSpec ? amountSpec.check(li) : true;
|
|
100
|
+
const entryAmount = li.amount ?? li.debitAmount ?? li.creditAmount ?? null;
|
|
101
|
+
const entryType = li.type ?? (li.debitAmount > 0 ? 'DEBIT' : li.creditAmount > 0 ? 'CREDIT' : null);
|
|
97
102
|
return {
|
|
98
103
|
index: i,
|
|
99
|
-
name: li.description ||
|
|
104
|
+
name: li.description || entryType || null,
|
|
100
105
|
nameStatus: 'ok',
|
|
101
|
-
unitPrice:
|
|
106
|
+
unitPrice: entryAmount,
|
|
102
107
|
unitPriceStatus: amountOk ? 'ok' : 'missing',
|
|
103
108
|
account,
|
|
104
109
|
};
|
|
@@ -358,10 +363,25 @@ function sanitizeJournalEntry(e) {
|
|
|
358
363
|
const acctId = e.accountResourceId || e.organizationAccountResourceId;
|
|
359
364
|
if (acctId)
|
|
360
365
|
clean.accountResourceId = acctId;
|
|
361
|
-
|
|
366
|
+
// Amount/type: GET uses debitAmount/creditAmount, PUT uses amount+type
|
|
367
|
+
if (e.amount != null) {
|
|
362
368
|
clean.amount = e.amount;
|
|
363
|
-
|
|
364
|
-
|
|
369
|
+
}
|
|
370
|
+
else if (e.debitAmount != null && e.debitAmount > 0) {
|
|
371
|
+
clean.amount = e.debitAmount;
|
|
372
|
+
}
|
|
373
|
+
else if (e.creditAmount != null && e.creditAmount > 0) {
|
|
374
|
+
clean.amount = e.creditAmount;
|
|
375
|
+
}
|
|
376
|
+
if (e.type) {
|
|
377
|
+
clean.type = e.type; // Already in PUT form (DEBIT or CREDIT)
|
|
378
|
+
}
|
|
379
|
+
else if (e.debitAmount != null && e.debitAmount > 0) {
|
|
380
|
+
clean.type = 'DEBIT';
|
|
381
|
+
}
|
|
382
|
+
else if (e.creditAmount != null && e.creditAmount > 0) {
|
|
383
|
+
clean.type = 'CREDIT';
|
|
384
|
+
}
|
|
365
385
|
if (e.description)
|
|
366
386
|
clean.description = e.description;
|
|
367
387
|
if (e.contactResourceId)
|
package/dist/commands/jobs.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import prompts from 'prompts';
|
|
2
3
|
import { readFileSync } from 'node:fs';
|
|
3
4
|
import { generateMonthEndBlueprint } from '../core/jobs/month-end/blueprint.js';
|
|
4
5
|
import { generateQuarterEndBlueprint } from '../core/jobs/quarter-end/blueprint.js';
|
|
@@ -272,11 +273,10 @@ export function registerJobsCommand(program) {
|
|
|
272
273
|
const ingestCmd = docCollection
|
|
273
274
|
.command('ingest')
|
|
274
275
|
.description('Scan and classify client documents — optionally upload via Magic API')
|
|
275
|
-
.requiredOption('--source <path>', 'Local directory
|
|
276
|
+
.requiredOption('--source <path>', 'Local directory, .zip file, or public share URL (Dropbox, Google Drive, OneDrive). ZIPs are auto-extracted.')
|
|
276
277
|
.option('--type <type>', 'Force document type: invoice, bill, credit-note-customer, credit-note-supplier, or bank-statement')
|
|
277
278
|
.option('--upload', 'Upload classified files to Jaz after scanning (requires auth)')
|
|
278
279
|
.option('--bank-account <name-or-id>', 'Bank account name or resourceId (required for bank statements)')
|
|
279
|
-
.option('--pdf-password <password>', 'Password for encrypted PDFs (same password applied to all)')
|
|
280
280
|
.option('--api-key <key>', 'API key (or use JAZ_API_KEY env var)')
|
|
281
281
|
.option('--timeout <ms>', 'Download timeout in milliseconds for cloud sources (default: 30000)', parseInt)
|
|
282
282
|
.option('--currency <code>', 'Functional/reporting currency (e.g. SGD)')
|
|
@@ -342,46 +342,55 @@ export function registerJobsCommand(program) {
|
|
|
342
342
|
process.exit(1);
|
|
343
343
|
}
|
|
344
344
|
// ── Encrypted PDF checks ──────────────────────────────────────
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
345
|
+
// Files with __pw__<password> in the filename auto-supply their password.
|
|
346
|
+
// Only files without a filePassword need qpdf + user action.
|
|
347
|
+
const encryptedFiles = plan.folders.flatMap(f => f.files.filter(file => file.encrypted));
|
|
348
|
+
const needPassword = encryptedFiles.filter(f => !f.filePassword);
|
|
349
|
+
if (encryptedFiles.length > 0 && !isQpdfAvailable()) {
|
|
350
|
+
const paths = encryptedFiles.map(f => `${f.folder}/${f.filename}`);
|
|
351
|
+
if (opts.json) {
|
|
352
|
+
console.log(JSON.stringify({
|
|
353
|
+
error: 'ENCRYPTED_PDF_NO_QPDF',
|
|
354
|
+
message: 'Encrypted PDFs found but qpdf is not installed',
|
|
355
|
+
action: 'Install qpdf: brew install qpdf (macOS) or sudo apt install qpdf (Linux), then retry',
|
|
356
|
+
encryptedFiles: paths,
|
|
357
|
+
}));
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
console.error(chalk.red('Error: Encrypted PDFs found but qpdf is not installed.'));
|
|
361
|
+
console.error(chalk.yellow('Install qpdf to decrypt before upload:'));
|
|
362
|
+
console.error(chalk.dim(' macOS: brew install qpdf'));
|
|
363
|
+
console.error(chalk.dim(' Ubuntu: sudo apt install qpdf'));
|
|
364
|
+
console.error(chalk.dim(' Windows: choco install qpdf'));
|
|
365
|
+
}
|
|
366
|
+
process.exit(1);
|
|
367
|
+
}
|
|
368
|
+
if (needPassword.length > 0) {
|
|
369
|
+
const paths = needPassword.map(f => `${f.folder}/${f.filename}`);
|
|
370
|
+
if (opts.json) {
|
|
371
|
+
// Agents can't type — error with actionable instructions
|
|
372
|
+
console.log(JSON.stringify({
|
|
373
|
+
error: 'ENCRYPTED_PDF_NO_PASSWORD',
|
|
374
|
+
message: `${needPassword.length} encrypted PDF(s) found without a password`,
|
|
375
|
+
action: 'Embed password in filename: rename to filename__pw__password.pdf',
|
|
376
|
+
encryptedFiles: paths,
|
|
377
|
+
}));
|
|
365
378
|
process.exit(1);
|
|
366
379
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
380
|
+
// Interactive mode — prompt user for each encrypted file
|
|
381
|
+
console.error(chalk.yellow(`\n${needPassword.length} encrypted PDF(s) need a password:\n`));
|
|
382
|
+
for (const f of needPassword) {
|
|
383
|
+
const { password } = await prompts({
|
|
384
|
+
type: 'text',
|
|
385
|
+
name: 'password',
|
|
386
|
+
message: `PDF password for ${f.folder}/${f.filename}`,
|
|
387
|
+
});
|
|
388
|
+
if (!password) {
|
|
389
|
+
console.error(chalk.red('Aborted — no password provided.'));
|
|
390
|
+
console.error(chalk.dim('Tip: embed password in filename to skip prompts: filename__pw__password.pdf'));
|
|
391
|
+
process.exit(1);
|
|
375
392
|
}
|
|
376
|
-
|
|
377
|
-
console.error(chalk.red(`Error: ${encryptedFiles.length} encrypted PDF(s) found — password required.`));
|
|
378
|
-
console.error(chalk.dim('Encrypted files:'));
|
|
379
|
-
for (const f of encryptedFiles)
|
|
380
|
-
console.error(chalk.dim(` ${f}`));
|
|
381
|
-
console.error();
|
|
382
|
-
console.error(chalk.dim('Retry with: --pdf-password <password>'));
|
|
383
|
-
}
|
|
384
|
-
process.exit(1);
|
|
393
|
+
f.filePassword = password;
|
|
385
394
|
}
|
|
386
395
|
}
|
|
387
396
|
// ── Upload with auth ─────────────────────────────────────────
|
|
@@ -402,7 +411,6 @@ export function registerJobsCommand(program) {
|
|
|
402
411
|
plan,
|
|
403
412
|
client,
|
|
404
413
|
bankAccountId,
|
|
405
|
-
pdfPassword: opts.pdfPassword,
|
|
406
414
|
onProgress: apiOpts.json ? undefined : printUploadProgress,
|
|
407
415
|
});
|
|
408
416
|
const result = { ...plan, upload };
|