jaz-clio 4.34.5 → 4.35.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 +3 -2
- package/assets/skills/cli/SKILL.md +1 -1
- package/assets/skills/conversion/SKILL.md +1 -1
- package/assets/skills/jobs/SKILL.md +1 -1
- package/assets/skills/transaction-recipes/SKILL.md +1 -1
- package/dist/commands/api-action.js +22 -22
- package/dist/commands/auth.js +10 -8
- package/dist/commands/format-helpers.js +23 -14
- package/dist/commands/init.js +18 -20
- package/dist/commands/jobs.js +47 -45
- package/dist/commands/magic.js +18 -6
- package/dist/commands/output.js +2 -1
- package/dist/commands/pagination.js +8 -16
- package/dist/commands/picker.js +35 -56
- package/dist/commands/table-formatter.js +3 -82
- package/dist/commands/ui/banner.js +21 -0
- package/dist/commands/ui/error.js +96 -0
- package/dist/commands/ui/index.js +7 -0
- package/dist/commands/ui/progress.js +32 -0
- package/dist/commands/ui/record.js +72 -0
- package/dist/commands/ui/table.js +110 -0
- package/dist/commands/ui/theme.js +54 -0
- package/dist/core/api/magic.js +2 -6
- package/dist/index.js +19 -20
- package/dist/utils/logger.js +17 -7
- package/package.json +4 -3
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: jaz-api
|
|
3
|
-
version: 4.
|
|
3
|
+
version: 4.35.0
|
|
4
4
|
description: >-
|
|
5
5
|
Use this skill whenever you call, debug, or review code that touches the Jaz
|
|
6
6
|
REST API. Covers field names, response shapes, 117 production gotchas, error
|
|
@@ -150,7 +150,8 @@ You are working with the **Jaz REST API** — the accounting platform backend. A
|
|
|
150
150
|
### Jaz Magic — Extraction & Autofill
|
|
151
151
|
57. **When the user starts from an attachment, always use Jaz Magic** — if the input is a PDF, JPG, or any document image (invoice, bill, receipt), the correct path is `POST /magic/createBusinessTransactionFromAttachment`. Do NOT manually construct a `POST /invoices` or `POST /bills` payload from an attachment — Jaz Magic handles the entire extraction-and-autofill pipeline server-side: OCR, line item detection, contact matching, CoA auto-mapping via ML learning, and draft creation with all fields pre-filled. Only use `POST /invoices` or `POST /bills` when building transactions from structured data (JSON, CSV, database rows) where the fields are already known.
|
|
152
152
|
58. **Two upload modes with different content types** — `sourceType: "FILE"` requires **multipart/form-data** with `sourceFile` blob (JSON body fails with 400 "sourceFile is a required field"). `sourceType: "URL"` accepts **application/json** with `sourceURL` string. The OAS only documents URL mode — FILE mode (the common case) is undocumented.
|
|
153
|
-
59. **Three required fields**: `sourceFile` (multipart blob — NOT `file`), `businessTransactionType` (`"INVOICE"`, `"BILL"`, `"CUSTOMER_CREDIT_NOTE"`, or `"SUPPLIER_CREDIT_NOTE"` — `EXPENSE` rejected), `sourceType` (`"FILE"` or `"URL"`). All
|
|
153
|
+
59. **Three required fields + one optional**: `sourceFile` (multipart blob — NOT `file`), `businessTransactionType` (`"INVOICE"`, `"BILL"`, `"CUSTOMER_CREDIT_NOTE"`, or `"SUPPLIER_CREDIT_NOTE"` — `EXPENSE` rejected), `sourceType` (`"FILE"` or `"URL"`). Optional: `uploadMode` (`"SEPARATE"` default, or `"MERGED"` for a single PDF containing multiple documents — the backend splits it via boundary detection before extraction). All required fields are validated server-side. **CRITICAL: multipart form field names are camelCase** — `businessTransactionType`, `sourceType`, `sourceFile`, `uploadMode`, NOT snake_case. Using `business_transaction_type` returns 422 "businessTransactionType is a required field". The File blob must include a filename and correct MIME type (e.g. `application/pdf`, `image/jpeg`) — bare `application/octet-stream` blobs are rejected with 400 "Invalid file type".
|
|
154
|
+
59a. **MERGED upload workflow tracking** — When `uploadMode: "MERGED"`, the upload response `workflowResourceId` is a **parent** tracking ID. The backend splits the PDF, then creates **child** workflows for each split page — these child IDs appear in `POST /magic/workflows/search` (by fileName or createdAt), NOT the parent ID. To track MERGED progress, search by `fileName` rather than the parent `workflowResourceId`.
|
|
154
155
|
60. **Response maps transaction types**: Request `INVOICE` → response `SALE`. Request `BILL` → response `PURCHASE`. Request `CUSTOMER_CREDIT_NOTE` → response `SALE_CREDIT_NOTE`. Request `SUPPLIER_CREDIT_NOTE` → response `PURCHASE_CREDIT_NOTE`. S3 paths follow the response type. The response `validFiles[]` array contains `workflowResourceId` for tracking extraction progress via `POST /magic/workflows/search`.
|
|
155
156
|
61. **Extraction is asynchronous** — the API response is immediate (file upload confirmation only). The actual Magic pipeline — OCR, line item extraction, contact matching, CoA learning, and autofill — runs asynchronously. Use `POST /magic/workflows/search` with `filter.resourceId.eq: "<workflowResourceId>"` to check status (SUBMITTED → PROCESSING → COMPLETED/FAILED). When COMPLETED, `businessTransactionDetails.businessTransactionResourceId` contains the created draft BT ID. The `subscriptionFBPath` in the response is a Firebase Realtime Database path for real-time status updates (alternative to polling).
|
|
156
157
|
62. **Accepts PDF and JPG/JPEG** — both file types confirmed working. Handwritten documents are accepted at upload stage (extraction quality varies). `fileType` in response reflects actual format: `"PDF"`, `"JPEG"`.
|
|
@@ -1,15 +1,23 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
1
|
import { JazClient, JazApiError } from '../core/api/client.js';
|
|
3
2
|
import { requireAuth, AuthError, resolvedProfileLabel, resolvedAuthSource, getProfile, listProfiles } from '../core/auth/index.js';
|
|
4
3
|
import { isMachineFormat } from './output.js';
|
|
4
|
+
import { renderOrgBanner } from './ui/banner.js';
|
|
5
|
+
import { formatApiError, formatAuthError, formatGenericError } from './ui/error.js';
|
|
5
6
|
/**
|
|
6
7
|
* Shared action wrapper for all online CLI commands.
|
|
7
|
-
* Handles auth resolution, client creation, org banner, and error formatting.
|
|
8
|
+
* Handles auth resolution, client creation, org banner, spinner, and error formatting.
|
|
8
9
|
*
|
|
9
10
|
* Exit codes: 1 = validation, 2 = API error, 3 = auth error.
|
|
10
11
|
*/
|
|
11
12
|
export function apiAction(fn) {
|
|
12
13
|
return async (opts) => {
|
|
14
|
+
let spinner;
|
|
15
|
+
// Hoist machine flag — computed once, safe to use in catch even if resolveFormat would throw
|
|
16
|
+
let machine = false;
|
|
17
|
+
try {
|
|
18
|
+
machine = isMachineFormat(opts);
|
|
19
|
+
}
|
|
20
|
+
catch { /* --json + --format conflict — treat as human mode for error display */ }
|
|
13
21
|
try {
|
|
14
22
|
// Conflict check: --api-key and --org are mutually exclusive
|
|
15
23
|
if (opts.apiKey && opts.org) {
|
|
@@ -17,9 +25,8 @@ export function apiAction(fn) {
|
|
|
17
25
|
}
|
|
18
26
|
const auth = requireAuth(opts.apiKey);
|
|
19
27
|
const client = new JazClient(auth);
|
|
20
|
-
// Org banner —
|
|
21
|
-
|
|
22
|
-
if (!isMachineFormat(opts)) {
|
|
28
|
+
// Org banner — stderr, suppressed in machine formats
|
|
29
|
+
if (!machine) {
|
|
23
30
|
const label = resolvedProfileLabel();
|
|
24
31
|
if (label) {
|
|
25
32
|
const entry = getProfile(label);
|
|
@@ -31,43 +38,36 @@ export function apiAction(fn) {
|
|
|
31
38
|
orgCount = Object.keys(listProfiles() ?? {}).length;
|
|
32
39
|
}
|
|
33
40
|
catch { /* best-effort */ }
|
|
34
|
-
|
|
35
|
-
// UNPINNED multi-org: prominent yellow warning
|
|
36
|
-
process.stderr.write(chalk.yellow(` \u26A0 ${label} \u00B7 ${entry.orgName} (${entry.currency})`) +
|
|
37
|
-
chalk.dim(` \u2014 not pinned to this terminal\n`) +
|
|
38
|
-
chalk.dim(` Pin: export JAZ_ORG=${label} or --org ${label}\n`));
|
|
39
|
-
}
|
|
40
|
-
else {
|
|
41
|
-
// Pinned or single-org: normal dim banner
|
|
42
|
-
process.stderr.write(chalk.dim(` \u25B8 ${label} \u00B7 ${entry.orgName} (${entry.currency})\n`));
|
|
43
|
-
}
|
|
41
|
+
renderOrgBanner({ label, orgName: entry.orgName, currency: entry.currency }, isPinned, orgCount > 1);
|
|
44
42
|
}
|
|
45
43
|
}
|
|
46
44
|
}
|
|
47
45
|
await fn(client, opts, auth);
|
|
48
46
|
}
|
|
49
47
|
catch (err) {
|
|
50
|
-
const machine = isMachineFormat(opts);
|
|
51
48
|
if (err instanceof AuthError) {
|
|
52
49
|
if (machine)
|
|
53
|
-
console.
|
|
50
|
+
console.error(JSON.stringify({ error: { code: 'AUTH_ERROR', message: err.message } }));
|
|
54
51
|
else
|
|
55
|
-
console.error(
|
|
52
|
+
console.error(formatAuthError(err.message));
|
|
56
53
|
process.exit(3);
|
|
57
54
|
}
|
|
58
55
|
if (err instanceof JazApiError) {
|
|
59
56
|
if (machine)
|
|
60
|
-
console.
|
|
57
|
+
console.error(JSON.stringify({ error: { code: 'API_ERROR', status: err.status, message: err.message } }));
|
|
61
58
|
else
|
|
62
|
-
console.error(
|
|
59
|
+
console.error(formatApiError(err.status, err.message, err.endpoint));
|
|
63
60
|
process.exit(2);
|
|
64
61
|
}
|
|
65
62
|
const message = err.message;
|
|
66
63
|
if (machine)
|
|
67
|
-
console.
|
|
64
|
+
console.error(JSON.stringify({ error: { code: 'UNKNOWN_ERROR', message } }));
|
|
68
65
|
else
|
|
69
|
-
console.error(
|
|
66
|
+
console.error(formatGenericError(message));
|
|
70
67
|
process.exit(2);
|
|
71
68
|
}
|
|
69
|
+
finally {
|
|
70
|
+
spinner?.stop();
|
|
71
|
+
}
|
|
72
72
|
};
|
|
73
73
|
}
|
package/dist/commands/auth.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import
|
|
2
|
+
import * as p from '@clack/prompts';
|
|
3
3
|
import { clearStoredCredentials, requireAuth, getProfile, setProfile, removeProfile, getActiveLabel, setActiveLabel, listProfiles, findLabelByApiKey, resolvedAuthSource, } from '../core/auth/index.js';
|
|
4
4
|
import { JazClient } from '../core/api/client.js';
|
|
5
5
|
import { getOrganization } from '../core/api/organization.js';
|
|
@@ -118,19 +118,21 @@ export function registerAuthCommand(program) {
|
|
|
118
118
|
console.error(chalk.red('Error: --json and --export require a label argument.'));
|
|
119
119
|
process.exit(1);
|
|
120
120
|
}
|
|
121
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
122
|
+
console.error(chalk.red('Error: interactive picker requires a TTY. Provide a label argument.'));
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
121
125
|
const active = getActiveLabel();
|
|
122
|
-
const
|
|
123
|
-
type: 'select',
|
|
124
|
-
name: 'label',
|
|
126
|
+
const selected = await p.select({
|
|
125
127
|
message: 'Switch to:',
|
|
126
|
-
|
|
127
|
-
|
|
128
|
+
options: labels.map(l => ({
|
|
129
|
+
label: `${l === active ? '\u2605 ' : ' '}${l} \u2014 ${orgs[l].orgName} (${orgs[l].currency})`,
|
|
128
130
|
value: l,
|
|
129
131
|
})),
|
|
130
132
|
});
|
|
131
|
-
if (
|
|
133
|
+
if (p.isCancel(selected))
|
|
132
134
|
return; // User cancelled
|
|
133
|
-
target =
|
|
135
|
+
target = selected;
|
|
134
136
|
}
|
|
135
137
|
try {
|
|
136
138
|
setActiveLabel(target);
|
|
@@ -1,35 +1,44 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { success, danger, warning, muted, accent } from './ui/theme.js';
|
|
2
2
|
const STATUS_COLORS = {
|
|
3
|
-
DRAFT:
|
|
4
|
-
VOID:
|
|
5
|
-
VOIDED:
|
|
6
|
-
FAILED:
|
|
7
|
-
DELETED:
|
|
3
|
+
DRAFT: warning,
|
|
4
|
+
VOID: danger,
|
|
5
|
+
VOIDED: danger,
|
|
6
|
+
FAILED: danger,
|
|
7
|
+
DELETED: danger,
|
|
8
8
|
};
|
|
9
9
|
export function formatStatus(status) {
|
|
10
10
|
const s = status || 'UNKNOWN';
|
|
11
|
-
return (STATUS_COLORS[s] ??
|
|
11
|
+
return (STATUS_COLORS[s] ?? success)(s);
|
|
12
12
|
}
|
|
13
13
|
// ── Shared column formatters (DRY across all command files) ─────
|
|
14
|
-
/** Format a resource ID in
|
|
14
|
+
/** Format a resource ID in accent color. */
|
|
15
15
|
export function formatId(v) {
|
|
16
|
-
return
|
|
16
|
+
return accent(String(v));
|
|
17
17
|
}
|
|
18
18
|
/** Format a reference field, showing '(no ref)' for empty values. */
|
|
19
19
|
export function formatReference(v) {
|
|
20
|
-
return String(v || '(no ref)');
|
|
20
|
+
return String(v || muted('(no ref)'));
|
|
21
21
|
}
|
|
22
|
-
/** Format a currency amount to 2 decimal places. */
|
|
22
|
+
/** Format a currency amount to 2 decimal places. Preserves decimal string precision. */
|
|
23
23
|
export function formatCurrency(v) {
|
|
24
|
-
|
|
24
|
+
if (v == null || v === '')
|
|
25
|
+
return muted('-');
|
|
26
|
+
const s = String(v);
|
|
27
|
+
// If already a valid decimal string, format without binary float conversion
|
|
28
|
+
if (/^-?\d+(\.\d+)?$/.test(s)) {
|
|
29
|
+
const [int, dec] = s.split('.');
|
|
30
|
+
return `$${int}.${(dec ?? '').padEnd(2, '0').slice(0, 2)}`;
|
|
31
|
+
}
|
|
32
|
+
const n = Number(v);
|
|
33
|
+
return Number.isNaN(n) ? muted('-') : `$${n.toFixed(2)}`;
|
|
25
34
|
}
|
|
26
35
|
/** Format a payment direction as colored IN/OUT. */
|
|
27
36
|
export function formatDirection(v) {
|
|
28
|
-
return String(v) === 'PAYIN' ?
|
|
37
|
+
return String(v) === 'PAYIN' ? success('IN') : danger('OUT');
|
|
29
38
|
}
|
|
30
39
|
/** Format an epoch-ms timestamp as YYYY-MM-DD. */
|
|
31
40
|
export function formatEpochDate(v) {
|
|
32
41
|
if (typeof v !== 'number' || v === 0 || Number.isNaN(v))
|
|
33
|
-
return '-';
|
|
42
|
+
return muted('-');
|
|
34
43
|
return new Date(v).toISOString().slice(0, 10);
|
|
35
44
|
}
|
package/dist/commands/init.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import ora from 'ora';
|
|
3
|
-
import
|
|
3
|
+
import * as p from '@clack/prompts';
|
|
4
4
|
import { SKILL_DESCRIPTIONS } from '../types/index.js';
|
|
5
5
|
import { installSkills, detectPlatform } from '../utils/template.js';
|
|
6
6
|
import { logger } from '../utils/logger.js';
|
|
@@ -9,49 +9,47 @@ export async function initCommand(options) {
|
|
|
9
9
|
let skillType = options.skill ?? 'all';
|
|
10
10
|
// Prompt for skill selection if not specified
|
|
11
11
|
if (!options.skill) {
|
|
12
|
-
const
|
|
13
|
-
type: 'select',
|
|
14
|
-
name: 'skill',
|
|
12
|
+
const skill = await p.select({
|
|
15
13
|
message: 'Which skills do you want to install?',
|
|
16
|
-
|
|
14
|
+
options: [
|
|
17
15
|
{
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
label: 'All (Recommended)',
|
|
17
|
+
hint: 'API reference + CLI + data conversion + transaction recipes + accounting jobs',
|
|
20
18
|
value: 'all',
|
|
21
19
|
},
|
|
22
20
|
{
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
label: 'API only',
|
|
22
|
+
hint: SKILL_DESCRIPTIONS['jaz-api'],
|
|
25
23
|
value: 'jaz-api',
|
|
26
24
|
},
|
|
27
25
|
{
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
label: 'CLI only',
|
|
27
|
+
hint: SKILL_DESCRIPTIONS['jaz-cli'],
|
|
30
28
|
value: 'jaz-cli',
|
|
31
29
|
},
|
|
32
30
|
{
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
label: 'Conversion only',
|
|
32
|
+
hint: SKILL_DESCRIPTIONS['jaz-conversion'],
|
|
35
33
|
value: 'jaz-conversion',
|
|
36
34
|
},
|
|
37
35
|
{
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
label: 'Transaction Recipes only',
|
|
37
|
+
hint: SKILL_DESCRIPTIONS['jaz-recipes'],
|
|
40
38
|
value: 'jaz-recipes',
|
|
41
39
|
},
|
|
42
40
|
{
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
label: 'Jobs only',
|
|
42
|
+
hint: SKILL_DESCRIPTIONS['jaz-jobs'],
|
|
45
43
|
value: 'jaz-jobs',
|
|
46
44
|
},
|
|
47
45
|
],
|
|
48
|
-
|
|
46
|
+
initialValue: 'all',
|
|
49
47
|
});
|
|
50
|
-
if (
|
|
48
|
+
if (p.isCancel(skill)) {
|
|
51
49
|
logger.warn('Installation cancelled');
|
|
52
50
|
return;
|
|
53
51
|
}
|
|
54
|
-
skillType =
|
|
52
|
+
skillType = skill;
|
|
55
53
|
}
|
|
56
54
|
const skillLabel = skillType === 'all'
|
|
57
55
|
? 'jaz-api + jaz-cli + jaz-conversion + jaz-recipes + jaz-jobs'
|
package/dist/commands/jobs.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import
|
|
2
|
+
import * as p from '@clack/prompts';
|
|
3
3
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { resolve } from 'node:path';
|
|
5
5
|
import { generateMonthEndBlueprint } from '../core/jobs/month-end/blueprint.js';
|
|
@@ -93,16 +93,16 @@ async function resolveBankFormat(opts) {
|
|
|
93
93
|
if (selected.length === 0) {
|
|
94
94
|
// Interactive: prompt for format in TTY
|
|
95
95
|
if (process.stdin.isTTY && !opts.json) {
|
|
96
|
-
const
|
|
97
|
-
type: 'select',
|
|
98
|
-
name: 'format',
|
|
96
|
+
const format = await p.select({
|
|
99
97
|
message: 'Bank format',
|
|
100
|
-
|
|
101
|
-
{
|
|
102
|
-
{
|
|
103
|
-
{
|
|
98
|
+
options: [
|
|
99
|
+
{ label: 'DBS GIRO (IDEAL UFF — CSV)', value: 'dbs-giro' },
|
|
100
|
+
{ label: 'OCBC GIRO/FAST (fixed-width 1000 chars)', value: 'ocbc-giro' },
|
|
101
|
+
{ label: 'UOB GIRO (tilde-delimited UFF)', value: 'uob-giro' },
|
|
104
102
|
],
|
|
105
|
-
}
|
|
103
|
+
});
|
|
104
|
+
if (p.isCancel(format))
|
|
105
|
+
process.exit(0);
|
|
106
106
|
return format;
|
|
107
107
|
}
|
|
108
108
|
throw new BankFileValidationError('No bank format specified. Pick one:\n\n' +
|
|
@@ -126,12 +126,12 @@ async function resolveBankFormat(opts) {
|
|
|
126
126
|
* a pre-filled bank-file JSON input.
|
|
127
127
|
*/
|
|
128
128
|
async function fetchOutstandingForBankFile(opts, format) {
|
|
129
|
-
const
|
|
130
|
-
type: 'confirm',
|
|
131
|
-
name: 'fetch',
|
|
129
|
+
const doFetch = await p.confirm({
|
|
132
130
|
message: 'No input file. Fetch outstanding bills from API?',
|
|
133
|
-
|
|
134
|
-
}
|
|
131
|
+
initialValue: true,
|
|
132
|
+
});
|
|
133
|
+
if (p.isCancel(doFetch))
|
|
134
|
+
process.exit(0);
|
|
135
135
|
if (!doFetch) {
|
|
136
136
|
throw new BankFileValidationError('No input provided. Use --input <file> or pipe JSON via stdin.\n\n' +
|
|
137
137
|
`Sample JSON for ${format}:\n` +
|
|
@@ -190,40 +190,44 @@ async function fetchOutstandingForBankFile(opts, format) {
|
|
|
190
190
|
value: s.contactResourceId,
|
|
191
191
|
selected: true,
|
|
192
192
|
}));
|
|
193
|
-
const
|
|
194
|
-
type: 'multiselect',
|
|
195
|
-
name: 'selected',
|
|
193
|
+
const selected = await p.multiselect({
|
|
196
194
|
message: 'Select suppliers to pay',
|
|
197
|
-
choices
|
|
198
|
-
|
|
199
|
-
|
|
195
|
+
options: choices.map(c => ({
|
|
196
|
+
label: c.title,
|
|
197
|
+
value: c.value,
|
|
198
|
+
})),
|
|
199
|
+
initialValues: choices.map(c => c.value),
|
|
200
|
+
required: true,
|
|
201
|
+
});
|
|
202
|
+
if (p.isCancel(selected))
|
|
203
|
+
process.exit(0);
|
|
200
204
|
if (!selected || selected.length === 0) {
|
|
201
205
|
throw new BankFileValidationError('No suppliers selected.');
|
|
202
206
|
}
|
|
203
207
|
const selectedSet = new Set(selected);
|
|
204
208
|
const selectedSuppliers = outstanding.suppliers.filter(s => selectedSet.has(s.contactResourceId));
|
|
205
209
|
// Prompt for originator details
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
210
|
+
const accountNumber = await p.text({
|
|
211
|
+
message: 'Originator account number',
|
|
212
|
+
validate: (v) => (!v || v.trim().length === 0) ? 'Required' : undefined,
|
|
213
|
+
});
|
|
214
|
+
if (p.isCancel(accountNumber))
|
|
215
|
+
process.exit(0);
|
|
216
|
+
const accountName = await p.text({
|
|
217
|
+
message: 'Originator company name',
|
|
218
|
+
validate: (v) => (!v || v.trim().length === 0) ? 'Required' : undefined,
|
|
219
|
+
});
|
|
220
|
+
if (p.isCancel(accountName))
|
|
221
|
+
process.exit(0);
|
|
222
|
+
const valueDate = await p.text({
|
|
223
|
+
message: 'Value date (YYYY-MM-DD)',
|
|
224
|
+
placeholder: new Date().toISOString().slice(0, 10),
|
|
225
|
+
defaultValue: new Date().toISOString().slice(0, 10),
|
|
226
|
+
validate: (v) => !/^\d{4}-\d{2}-\d{2}$/.test(v ?? '') ? 'Format: YYYY-MM-DD' : undefined,
|
|
227
|
+
});
|
|
228
|
+
if (p.isCancel(valueDate))
|
|
229
|
+
process.exit(0);
|
|
230
|
+
const originatorAnswers = { accountNumber, accountName, valueDate };
|
|
227
231
|
// Build the bank-file input structure
|
|
228
232
|
// NOTE: payee.accountNumber and payee.bankCode are NOT available from the API.
|
|
229
233
|
// We use placeholders — user must fill them in or the generator will validate.
|
|
@@ -725,12 +729,10 @@ export function registerJobsCommand(program) {
|
|
|
725
729
|
// Interactive mode — prompt user for each encrypted file
|
|
726
730
|
console.error(chalk.yellow(`\n${needPassword.length} encrypted PDF(s) need a password:\n`));
|
|
727
731
|
for (const f of needPassword) {
|
|
728
|
-
const
|
|
729
|
-
type: 'text',
|
|
730
|
-
name: 'password',
|
|
732
|
+
const password = await p.text({
|
|
731
733
|
message: `PDF password for ${f.folder}/${f.filename}`,
|
|
732
734
|
});
|
|
733
|
-
if (!password) {
|
|
735
|
+
if (p.isCancel(password) || !password) {
|
|
734
736
|
console.error(chalk.red('Aborted — no password provided.'));
|
|
735
737
|
console.error(chalk.dim('Tip: embed password in filename to skip prompts: filename__pw__password.pdf'));
|
|
736
738
|
process.exit(1);
|
package/dist/commands/magic.js
CHANGED
|
@@ -3,7 +3,7 @@ import { mkdtempSync, readFileSync, readdirSync, rmSync, statSync } from 'node:f
|
|
|
3
3
|
import { formatStatus } from './format-helpers.js';
|
|
4
4
|
import { basename, extname, join, resolve } from 'node:path';
|
|
5
5
|
import { tmpdir } from 'node:os';
|
|
6
|
-
import
|
|
6
|
+
import * as p from '@clack/prompts';
|
|
7
7
|
import { createFromAttachment, searchMagicWorkflows, waitForWorkflows, } from '../core/api/magic.js';
|
|
8
8
|
import { extractFilePassword, isPdfEncrypted, isQpdfAvailable, decryptPdf, cleanupDecryptedFile, } from '../core/jobs/document-collection/tools/ingest/decrypt.js';
|
|
9
9
|
import { extractZipToDir, flattenSingleRoot } from '../core/jobs/document-collection/tools/ingest/cloud/zip.js';
|
|
@@ -51,6 +51,7 @@ export function registerMagicCommand(program) {
|
|
|
51
51
|
.option('--file <path>', 'Local file path (PDF, JPG, PNG, HEIC, XLS, XLSX, EML, ZIP). ZIP: extracts and uploads each file. Encrypted PDFs: name__pw__password.pdf')
|
|
52
52
|
.option('--url <url>', 'Remote file URL (alternative to --file)')
|
|
53
53
|
.option('--type <type>', `Document type: ${VALID_TYPES}`)
|
|
54
|
+
.option('--merged', 'Treat file as a merged PDF containing multiple documents (split before extraction)')
|
|
54
55
|
.option('--api-key <key>', 'API key (overrides stored/env)')
|
|
55
56
|
.option('--json', 'Output as JSON')
|
|
56
57
|
.action(apiAction(async (client, opts) => {
|
|
@@ -72,6 +73,18 @@ export function registerMagicCommand(program) {
|
|
|
72
73
|
console.error(chalk.red(`Error: invalid type "${opts.type}". Valid: ${VALID_TYPES}`));
|
|
73
74
|
process.exit(1);
|
|
74
75
|
}
|
|
76
|
+
// Validate --merged constraints
|
|
77
|
+
if (opts.merged) {
|
|
78
|
+
if (opts.url) {
|
|
79
|
+
console.error(chalk.red('Error: --merged is only supported with --file, not --url'));
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
const ext = extname(opts.file).toLowerCase();
|
|
83
|
+
if (ext !== '.pdf') {
|
|
84
|
+
console.error(chalk.red('Error: --merged requires a PDF file (got ' + ext + ')'));
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
75
88
|
let sourceFile;
|
|
76
89
|
let sourceFileName;
|
|
77
90
|
let decryptedPath;
|
|
@@ -105,6 +118,7 @@ export function registerMagicCommand(program) {
|
|
|
105
118
|
sourceFile,
|
|
106
119
|
sourceFileName,
|
|
107
120
|
sourceUrl: opts.url,
|
|
121
|
+
uploadMode: opts.merged ? 'MERGED' : undefined,
|
|
108
122
|
});
|
|
109
123
|
const data = res.data;
|
|
110
124
|
const validFile = data.validFiles?.[0];
|
|
@@ -284,17 +298,15 @@ async function resolveInputPdf(filePath, ext, opts) {
|
|
|
284
298
|
}));
|
|
285
299
|
process.exit(1);
|
|
286
300
|
}
|
|
287
|
-
const
|
|
288
|
-
type: 'text',
|
|
289
|
-
name: 'password',
|
|
301
|
+
const pwInput = await p.text({
|
|
290
302
|
message: `PDF password for ${rawName}`,
|
|
291
303
|
});
|
|
292
|
-
if (
|
|
304
|
+
if (p.isCancel(pwInput) || !pwInput) {
|
|
293
305
|
console.error(chalk.red('Aborted — no password provided.'));
|
|
294
306
|
console.error(chalk.dim('Tip: embed password in filename: name__pw__password.pdf'));
|
|
295
307
|
process.exit(1);
|
|
296
308
|
}
|
|
297
|
-
password =
|
|
309
|
+
password = pwInput;
|
|
298
310
|
}
|
|
299
311
|
const decryptedPath = decryptPdf(filePath, password);
|
|
300
312
|
return { effectivePath: decryptedPath, cleanName, decryptedPath };
|
package/dist/commands/output.js
CHANGED
|
@@ -2,6 +2,7 @@ import chalk from 'chalk';
|
|
|
2
2
|
import { stringify as yamlStringify } from 'yaml';
|
|
3
3
|
import { formatTable } from './table-formatter.js';
|
|
4
4
|
import { displaySlice, paginatedJson } from './pagination.js';
|
|
5
|
+
import { formatRecord } from './ui/record.js';
|
|
5
6
|
/** Resolve the output format from CLI flags. --json is shorthand for --format json. */
|
|
6
7
|
export function resolveFormat(opts) {
|
|
7
8
|
if (opts.json && opts.format) {
|
|
@@ -114,7 +115,7 @@ export function outputRecord(record, opts) {
|
|
|
114
115
|
console.log(yamlStringify(record).trimEnd());
|
|
115
116
|
break;
|
|
116
117
|
case 'table':
|
|
117
|
-
console.log(
|
|
118
|
+
console.log(formatRecord(record));
|
|
118
119
|
break;
|
|
119
120
|
}
|
|
120
121
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { fetchAllPages } from '../core/api/pagination.js';
|
|
3
|
+
import { createProgress } from './ui/progress.js';
|
|
4
|
+
import { isMachineFormat } from './output.js';
|
|
3
5
|
/** Default row cap for --all mode. Prevents runaway fetches and agent token waste. */
|
|
4
6
|
const DEFAULT_MAX_ROWS = 10_000;
|
|
5
7
|
/** Display cap for human (non-JSON) output. */
|
|
@@ -14,13 +16,6 @@ const DISPLAY_CAP = 500;
|
|
|
14
16
|
* - Progress display on stderr (TTY-aware, suppressed for --json)
|
|
15
17
|
* - Truncation metadata (truncated field) for agent consumption
|
|
16
18
|
* - Single-page fetch when --all is not set
|
|
17
|
-
*
|
|
18
|
-
* Usage:
|
|
19
|
-
* const { data, totalElements, truncated } = await paginatedFetch(
|
|
20
|
-
* opts,
|
|
21
|
-
* (p) => listInvoices(client, p),
|
|
22
|
-
* { label: 'Fetching invoices' },
|
|
23
|
-
* );
|
|
24
19
|
*/
|
|
25
20
|
export async function paginatedFetch(opts, fetcher, options) {
|
|
26
21
|
const defaultLimit = options.defaultLimit ?? 100;
|
|
@@ -31,18 +26,15 @@ export async function paginatedFetch(opts, fetcher, options) {
|
|
|
31
26
|
// ── Auto-paginate mode ──
|
|
32
27
|
if (opts.all) {
|
|
33
28
|
const resolvedFormat = opts.format?.toLowerCase() ?? (opts.json ? 'json' : 'table');
|
|
34
|
-
const showProgress = resolvedFormat === 'table'
|
|
29
|
+
const showProgress = resolvedFormat === 'table';
|
|
30
|
+
const progress = showProgress ? createProgress(options.label) : undefined;
|
|
35
31
|
const result = await fetchAllPages((offset, limit) => fetcher({ limit, offset }), {
|
|
36
32
|
pageSize: opts.limit ?? 200,
|
|
37
|
-
onProgress:
|
|
38
|
-
? (fetched, total) =>
|
|
39
|
-
process.stderr.write(`\r${chalk.dim(`${options.label}... ${fetched.toLocaleString()}/${total.toLocaleString()}`)}`);
|
|
40
|
-
}
|
|
33
|
+
onProgress: progress
|
|
34
|
+
? (fetched, total) => progress.update(fetched, total)
|
|
41
35
|
: undefined,
|
|
42
36
|
});
|
|
43
|
-
|
|
44
|
-
process.stderr.write('\r\x1b[K'); // clear progress line
|
|
45
|
-
}
|
|
37
|
+
progress?.clear();
|
|
46
38
|
// Apply max-rows soft cap
|
|
47
39
|
const maxRows = opts.maxRows ?? DEFAULT_MAX_ROWS;
|
|
48
40
|
let { truncated } = result;
|
|
@@ -51,7 +43,7 @@ export async function paginatedFetch(opts, fetcher, options) {
|
|
|
51
43
|
data = data.slice(0, maxRows);
|
|
52
44
|
truncated = true;
|
|
53
45
|
}
|
|
54
|
-
if (truncated && !opts
|
|
46
|
+
if (truncated && !isMachineFormat(opts)) {
|
|
55
47
|
console.error(chalk.yellow(`Warning: dataset has ${result.totalElements.toLocaleString()} items — showing ${data.length.toLocaleString()} (max-rows: ${maxRows.toLocaleString()})`));
|
|
56
48
|
}
|
|
57
49
|
const pageSize = opts.limit ?? defaultLimit;
|