jaz-clio 3.3.0 → 3.4.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.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: api
3
- version: 3.3.0
3
+ version: 3.4.0
4
4
  description: Complete reference for the Jaz/Juan 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/Juan API key (x-jk-api-key header). Works with Claude Code, Claude Cowork, Claude.ai, and any agent that reads markdown.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: conversion
3
- version: 3.3.0
3
+ version: 3.4.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: jobs
3
- version: 3.3.0
3
+ version: 3.4.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 transaction-recipes skill.
@@ -41,7 +41,7 @@ Period-close jobs build on each other. Quarter = month + extras. Year = quarter
41
41
  | Job | CLI Command | Description |
42
42
  |-----|-------------|-------------|
43
43
  | **Bank Recon** | `clio jobs bank-recon` | Clear unreconciled items: match, categorize, resolve. Paired tool: `clio jobs bank-recon match`. |
44
- | **Document Collection** | `clio jobs document-collection` | Scan, classify, and upload client documents (invoices, bills, bank statements). Paired tool: `clio jobs document-collection ingest`. |
44
+ | **Document Collection** | `clio jobs document-collection` | Scan and classify client documents from local directories and cloud links (Dropbox, Drive, OneDrive). Outputs file paths for agent upload. Paired tool: `clio jobs document-collection ingest`. |
45
45
  | **GST/VAT Filing** | `clio jobs gst-vat --period YYYY-QN` | Tax ledger review, discrepancy check, filing summary. |
46
46
  | **Payment Run** | `clio jobs payment-run` | Select outstanding bills by due date, process payments. |
47
47
  | **Credit Control** | `clio jobs credit-control` | AR aging review, overdue chase list, bad debt assessment. |
@@ -99,6 +99,8 @@ clio jobs fa-review [--json]
99
99
  - **[references/quarter-end-close.md](./references/quarter-end-close.md)** — Quarter-end close: monthly + quarterly extras
100
100
  - **[references/year-end-close.md](./references/year-end-close.md)** — Year-end close: quarterly + annual extras
101
101
  - **[references/bank-recon.md](./references/bank-recon.md)** — Bank reconciliation catch-up
102
+ - **[references/bank-match.md](./references/bank-match.md)** — Bank reconciliation matcher: 5-phase cascade algorithm (1:1, N:1, 1:N, N:M matches)
103
+ - **[references/document-collection.md](./references/document-collection.md)** — Document collection: scan, classify, upload — local + cloud (Dropbox, Drive, OneDrive)
102
104
  - **[references/gst-vat-filing.md](./references/gst-vat-filing.md)** — GST/VAT filing preparation
103
105
  - **[references/payment-run.md](./references/payment-run.md)** — Payment run (bulk bill payments)
104
106
  - **[references/credit-control.md](./references/credit-control.md)** — Credit control / AR chase
@@ -79,7 +79,7 @@ Work through each unreconciled item. There are four resolution paths.
79
79
 
80
80
  The bank record matches an invoice payment, bill payment, or journal already in the books. This is the ideal case — the transaction exists, it just hasn't been linked to the bank record.
81
81
 
82
- **For large volumes:** Use `clio jobs bank-recon match --input data.json --json` to auto-match bank records to transactions before manual review. The calculator finds 1:1, N:1, 1:N, and N:M matches with confidence scores. See the [bank-match reference](../../transaction-recipes/references/bank-match.md) for input format and algorithm details.
82
+ **For large volumes:** Use `clio jobs bank-recon match --input data.json --json` to auto-match bank records to transactions before manual review. The calculator finds 1:1, N:1, 1:N, and N:M matches with confidence scores. See the [bank-match reference](./bank-match.md) for input format and algorithm details.
83
83
 
84
84
  **How to find the match manually:** Search cashflow transactions for the same amount and approximate date:
85
85
 
@@ -1,42 +1,45 @@
1
1
  # Document Collection
2
2
 
3
- Scan, classify, and upload client documents (invoices, bills, bank statements) to Jaz via the Magic API endpoints.
3
+ Scan and classify client documents (invoices, bills, bank statements) from local directories or cloud share links (Dropbox, Google Drive, OneDrive). Outputs classified file paths with metadata — the AI agent handles uploads via the api skill.
4
4
 
5
5
  ## When to Use
6
6
 
7
- - Client sends a folder of PDFs (invoices, bills, receipts) for bulk upload
7
+ - Client sends a folder of PDFs (invoices, bills, receipts) for bulk processing
8
8
  - Processing bank statement CSVs/OFX files for import
9
9
  - Migrating documents from file dumps (Dropbox, shared folders, email attachments)
10
- - Batch-uploading scanned documents during onboarding
10
+ - Processing documents from a shared Dropbox, Google Drive, or OneDrive link
11
+ - Batch-processing scanned documents during onboarding
11
12
 
12
13
  ## How It Works
13
14
 
14
15
  ```
15
- Local folder Jaz Magic API
16
- ┌───────────────┐ ┌──────────────────────────────────────────┐
17
- │ invoices/ │──── PDF/JPG ──► POST /magic/createBusinessTransaction
18
- │ inv-001.pdf │ FromAttachment
19
- │ inv-002.jpg │ → OCR + extraction + contact match
20
- │ │ → Draft invoice created
21
- bills/ │──── PDF/JPG ──► POST /magic/createBusinessTransaction
22
- acme-jan.pdf │ FromAttachment │
23
- │ │ Draft bill created
24
- │ bank/ │──── CSV/OFX ──► POST /magic/importBankStatement │
25
- dbs-jan.csv │ │ FromAttachment │
26
- │ │ │ → Bank records imported │
27
- └───────────────┘ └──────────────────────────────────────────┘
16
+ Source (local or cloud) CLI Output (IngestPlan)
17
+ ┌───────────────┐ ┌──────────────────────────────────────────┐
18
+ │ invoices/ │── scan + classify ─► absolutePath, documentType: INVOICE
19
+ │ inv-001.pdf │ sizeBytes: 45230
20
+ │ inv-002.jpg │
21
+ bills/ │── scan + classify ─► absolutePath, documentType: BILL
22
+ acme-jan.pdf │ │
23
+ bank/ │── scan + classify ─► absolutePath, documentType: BANK_STATEMENT
24
+ dbs-jan.csv │ │
25
+ └───────────────┘ └──────────────────────────────────────────┘
26
+
27
+ AI Agent reads plan,
28
+ uploads via api skill (curl)
28
29
  ```
29
30
 
31
+ Cloud links are downloaded to a temp directory first, then scanned through the same pipeline.
32
+
30
33
  ## Folder Classification
31
34
 
32
35
  The tool auto-classifies documents by **folder name** (case-insensitive prefix match):
33
36
 
34
- | Folder name pattern | Classification | API Endpoint |
35
- |---|---|---|
36
- | `invoice*`, `sales*`, `ar*`, `receivable*`, `revenue*` | INVOICE | `createBusinessTransactionFromAttachment` |
37
- | `bill*`, `purchase*`, `expense*`, `ap*`, `payable*`, `supplier*`, `vendor*`, `cost*` | BILL | `createBusinessTransactionFromAttachment` |
38
- | `bank*`, `statement*`, `recon*` | BANK_STATEMENT | `importBankStatementFromAttachment` |
39
- | (unknown) | UNKNOWN — skipped unless `--type` forced | — |
37
+ | Folder name pattern | Classification |
38
+ |---|---|
39
+ | `invoice*`, `sales*`, `ar*`, `receivable*`, `revenue*` | INVOICE |
40
+ | `bill*`, `purchase*`, `expense*`, `ap*`, `payable*`, `supplier*`, `vendor*`, `cost*` | BILL |
41
+ | `bank*`, `statement*`, `recon*` | BANK_STATEMENT |
42
+ | (unknown) | UNKNOWN — skipped unless `--type` forced |
40
43
 
41
44
  ### File Extension Filters
42
45
 
@@ -51,87 +54,101 @@ The tool auto-classifies documents by **folder name** (case-insensitive prefix m
51
54
  3. **Max depth** — 10 levels (prevents runaway recursion)
52
55
  4. **Hidden files/dirs** — skipped (anything starting with `.`)
53
56
 
57
+ ## Cloud Provider Support
58
+
59
+ The `--source` flag accepts public share links from Dropbox, Google Drive, and OneDrive. Files are downloaded to a temp directory, then processed through the same scan/classify pipeline as local directories.
60
+
61
+ | Provider | File Links | Folder Links | Auth Required |
62
+ |----------|-----------|--------------|---------------|
63
+ | **Dropbox** | Direct download (dl=1 trick) | ZIP download + extract | No |
64
+ | **Google Drive** | Direct download (large file confirmation) | Not supported (requires API key) | No |
65
+ | **OneDrive/SharePoint** | MS Graph sharing API | MS Graph sharing API (first page only) | No (best-effort) |
66
+
67
+ ### Cloud Limitations
68
+
69
+ - **Google Drive folders** require authentication — download manually and use a local path
70
+ - **OneDrive** is best-effort: Microsoft has restricted public link access since Feb 2025
71
+ - **Dropbox folders** download as ZIP — extracted automatically, macOS metadata stripped
72
+ - **Max file size**: 100MB per file, 500MB total for folder downloads
73
+ - **Timeout**: Default 30s (files) / 120s (folders). Override with `--timeout <ms>`
74
+
54
75
  ## CLI Usage
55
76
 
56
77
  ```bash
57
- # Dry-run (default) — scan + classify, no uploads
78
+ # Scan + classify local directory
58
79
  clio jobs document-collection ingest --source ./client-docs/ [--json]
59
80
 
60
- # Executescan + classify + upload to Jaz
61
- clio jobs document-collection ingest --source ./client-docs/ --execute \
62
- --api-key <key> --api-url https://api.jaz.ai [--json]
81
+ # Cloud sources Dropbox, Google Drive, OneDrive
82
+ clio jobs document-collection ingest --source "https://www.dropbox.com/scl/fo/.../folder?rlkey=..." [--json]
83
+ clio jobs document-collection ingest --source "https://drive.google.com/file/d/FILE_ID/view" [--json]
84
+ clio jobs document-collection ingest --source "https://1drv.ms/f/s!..." [--json]
63
85
 
64
- # Force classification (skip auto-detect)
65
- clio jobs document-collection ingest --source ./scans/ --type invoice --execute \
66
- --api-key <key> --api-url https://api.jaz.ai
86
+ # With timeout for large cloud downloads
87
+ clio jobs document-collection ingest --source "https://www.dropbox.com/..." --timeout 120000 [--json]
67
88
 
68
- # Bank statements need --bank-account (CoA resourceId)
69
- clio jobs document-collection ingest --source ./bank-csvs/ --execute \
70
- --bank-account <resourceId> --api-key <key> --api-url https://api.jaz.ai
89
+ # Force classification (skip auto-detect)
90
+ clio jobs document-collection ingest --source ./scans/ --type invoice [--json]
71
91
  ```
72
92
 
73
93
  ### Options
74
94
 
75
95
  | Flag | Description |
76
96
  |------|-------------|
77
- | `--source <path>` | Local directory path (required) |
97
+ | `--source <path\|url>` | Local directory path or public cloud share link — Dropbox, Google Drive, OneDrive (required) |
78
98
  | `--type <type>` | Force all files to: `invoice`, `bill`, or `bank-statement` |
79
- | `--execute` | Upload to Jaz API (without this flag, only scans/classifies) |
80
- | `--api-key <key>` | Jaz Magic API key (required for `--execute`) |
81
- | `--api-url <url>` | Jaz API base URL (required for `--execute`) |
82
- | `--bank-account <id>` | Bank account CoA resourceId (required for bank statement uploads) |
83
- | `--json` | Structured JSON output |
84
-
85
- ## Magic API Details
86
-
87
- ### Invoices & Bills
88
-
89
- ```
90
- POST /api/v1/magic/createBusinessTransactionFromAttachment
91
- Content-Type: multipart/form-data
92
- Header: x-magic-api-key: <key>
93
-
94
- Fields:
95
- - sourceFile: PDF/JPG file blob (NOT "file")
96
- - businessTransactionType: "INVOICE" or "BILL"
97
- - sourceType: "FILE"
99
+ | `--timeout <ms>` | Download timeout in milliseconds (default: 30000 for files, 120000 for folders) |
100
+ | `--currency <code>` | Functional/reporting currency label |
101
+ | `--json` | Structured JSON output with absolute file paths |
102
+
103
+ ### JSON Output
104
+
105
+ 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.
106
+
107
+ ```json
108
+ {
109
+ "source": "./client-docs/",
110
+ "sourceType": "local",
111
+ "localPath": "/tmp/client-docs",
112
+ "folders": [{
113
+ "folder": "invoices",
114
+ "documentType": "INVOICE",
115
+ "files": [{
116
+ "path": "invoices/inv-001.pdf",
117
+ "filename": "inv-001.pdf",
118
+ "extension": ".pdf",
119
+ "documentType": "INVOICE",
120
+ "absolutePath": "/tmp/client-docs/invoices/inv-001.pdf",
121
+ "sizeBytes": 45230,
122
+ "confidence": "auto",
123
+ "reason": "Folder \"invoices\" → INVOICE"
124
+ }],
125
+ "count": 1
126
+ }],
127
+ "summary": {
128
+ "total": 1,
129
+ "uploadable": 1,
130
+ "needClassification": 0,
131
+ "skipped": 0,
132
+ "byType": { "INVOICE": 1 }
133
+ }
134
+ }
98
135
  ```
99
136
 
100
- **Key points:**
101
- - Extraction is **asynchronous** — response confirms upload, extraction runs server-side
102
- - `subscriptionFBPath` in response tracks extraction progress via Firebase
103
- - Response maps: `INVOICE` → `SALE`, `BILL` → `PURCHASE`
104
- - Jaz Magic extracts: line items, contact, CoA mapping, tax, dates, currency
105
-
106
- ### Bank Statements
107
-
108
- ```
109
- POST /api/v1/magic/importBankStatementFromAttachment
110
- Content-Type: multipart/form-data
111
- Header: x-magic-api-key: <key>
112
-
113
- Fields:
114
- - sourceFile: CSV/OFX file (NOT "file")
115
- - accountResourceId: bank account CoA resourceId
116
- - businessTransactionType: "BANK_STATEMENT"
117
- - sourceType: "FILE"
118
- ```
119
-
120
- **CSV format:** `Date,Description,Debit,Credit`
137
+ For cloud sources, `localPath` points to the temp directory where files were downloaded.
121
138
 
122
139
  ## Phases (Blueprint)
123
140
 
124
- When run without `ingest` subcommand, produces a 6-phase blueprint:
141
+ When run without `ingest` subcommand, produces a 4-phase blueprint:
125
142
 
126
143
  1. **Intake** — Identify source, validate access
127
144
  2. **Scan** — Traverse directory tree, list all files
128
145
  3. **Classify** — Auto-classify by folder name
129
- 4. **Review** — Present plan for user approval (dry-run)
130
- 5. **Upload** — Upload each file to correct Magic API endpoint
131
- 6. **Verify** Check extraction results, flag failures
146
+ 4. **Review** — Present plan for user/agent action
147
+
148
+ The AI agent then uses the classified file paths to upload via the Jaz Magic API (see api skill for endpoint details).
132
149
 
133
150
  ## Relationship to Other Skills
134
151
 
135
- - **api skill** — Field names, auth headers, error codes for Magic endpoints
152
+ - **api skill** — Field names, auth headers, error codes for Magic endpoints. Agent uses this to upload classified files.
136
153
  - **bank-recon job** — After bank statement import, use bank-recon to match and reconcile
137
154
  - **transaction-recipes** — After Magic creates draft transactions, use recipes for complex accounting patterns
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: transaction-recipes
3
- version: 3.3.0
3
+ version: 3.4.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.
@@ -117,10 +117,6 @@ Each recipe includes: scenario description, accounts involved, journal entries,
117
117
 
118
118
  16. **[Capital WIP to Fixed Asset](references/capital-wip.md)** — Cost accumulation in CIP account during construction/development, transfer to FA on completion, auto-depreciation via Jaz FA module.
119
119
 
120
- ### Utility — Reconciliation
121
-
122
- 17. **[Bank Reconciliation Matcher](references/bank-match.md)** — 5-phase cascade algorithm matching bank records to cashflow transactions. Finds 1:1, N:1, 1:N, and N:M matches with confidence scores. *Paired job tool: `clio jobs bank-recon match`*
123
-
124
120
  ## How to Use These Recipes
125
121
 
126
122
  1. **Read the recipe** for your scenario — understand the accounts, journal entries, and capsule structure.
@@ -12,8 +12,8 @@ import { generateAuditPrepBlueprint } from '../jobs/audit-prep/blueprint.js';
12
12
  import { generateFaReviewBlueprint } from '../jobs/fa-review/blueprint.js';
13
13
  import { generateDocumentCollectionBlueprint } from '../jobs/document-collection/blueprint.js';
14
14
  import { generateStatutoryFilingBlueprint } from '../jobs/statutory-filing/blueprint.js';
15
- import { ingestDryRun, ingestExecute, validateExecuteOptions } from '../jobs/document-collection/tools/ingest/ingest.js';
16
- import { printIngestPlan, printIngestPlanJson, printIngestResult, printIngestResultJson } from '../jobs/document-collection/tools/ingest/format.js';
15
+ import { ingest } from '../jobs/document-collection/tools/ingest/ingest.js';
16
+ import { printIngestPlan, printIngestPlanJson } from '../jobs/document-collection/tools/ingest/format.js';
17
17
  import { matchBankRecords } from '../jobs/bank-recon/tools/match/match.js';
18
18
  import { computeFormCs } from '../jobs/statutory-filing/tools/sg-tax/form-cs.js';
19
19
  import { computeCapitalAllowances } from '../jobs/statutory-filing/tools/sg-tax/capital-allowances.js';
@@ -41,6 +41,7 @@ function jobAction(fn) {
41
41
  export function registerJobsCommand(program) {
42
42
  const jobs = program
43
43
  .command('jobs')
44
+ .enablePositionalOptions()
44
45
  .description('Accounting job blueprints — month-end, quarter-end, year-end, bank-recon, gst-vat, payment-run, credit-control, supplier-recon, audit-prep, fa-review, document-collection, statutory-filing');
45
46
  // ── clio jobs month-end ──────────────────────────────────────────
46
47
  jobs
@@ -92,6 +93,8 @@ export function registerJobsCommand(program) {
92
93
  const bankRecon = jobs
93
94
  .command('bank-recon')
94
95
  .description('Bank reconciliation — blueprint + match tool')
96
+ .enablePositionalOptions()
97
+ .passThroughOptions()
95
98
  .option('--account <name>', 'Specific bank account name')
96
99
  .option('--period <YYYY-MM>', 'Month period to reconcile')
97
100
  .option('--currency <code>', 'Currency code (e.g. SGD, USD)')
@@ -249,12 +252,12 @@ export function registerJobsCommand(program) {
249
252
  const docCollection = jobs
250
253
  .command('document-collection')
251
254
  .description('Document collection — scan, classify, and upload client documents')
252
- .option('--source <path>', 'Source path or URL hint for blueprint')
255
+ .enablePositionalOptions()
256
+ .passThroughOptions()
253
257
  .option('--currency <code>', 'Currency code (e.g. SGD, USD)')
254
258
  .option('--json', 'Output as JSON')
255
259
  .action(jobAction((opts) => {
256
260
  const bp = generateDocumentCollectionBlueprint({
257
- source: opts.source,
258
261
  currency: opts.currency,
259
262
  });
260
263
  opts.json ? printBlueprintJson(bp) : printBlueprint(bp);
@@ -262,13 +265,9 @@ export function registerJobsCommand(program) {
262
265
  // ── clio jobs document-collection ingest ────────────────────────
263
266
  docCollection
264
267
  .command('ingest')
265
- .description('Scan, classify, and upload client documents to Jaz')
268
+ .description('Scan and classify client documents outputs file paths for agent upload')
266
269
  .requiredOption('--source <path>', 'Local directory path or public share URL (Dropbox, Google Drive, OneDrive)')
267
270
  .option('--type <type>', 'Force document type: invoice, bill, or bank-statement')
268
- .option('--execute', 'Upload files to Jaz (without this flag, shows a preview)')
269
- .option('--api-key <key>', 'Jaz Magic API key (required for --execute)')
270
- .option('--api-url <url>', 'Jaz API base URL (required for --execute)')
271
- .option('--bank-account <id>', 'Bank account CoA resourceId (required for bank statement uploads)')
272
271
  .option('--timeout <ms>', 'Download timeout in milliseconds for cloud sources (default: 30000)', parseInt)
273
272
  .option('--currency <code>', 'Functional/reporting currency (e.g. SGD)')
274
273
  .option('--json', 'Output as JSON')
@@ -284,31 +283,12 @@ export function registerJobsCommand(program) {
284
283
  if (opts.type && !forceType) {
285
284
  throw new JobValidationError(`Invalid --type "${opts.type}". Use: invoice, bill, or bank-statement`);
286
285
  }
287
- const ingestOpts = {
286
+ ingest({
288
287
  source: opts.source,
289
288
  type: forceType,
290
- execute: opts.execute,
291
- apiKey: opts.apiKey,
292
- apiUrl: opts.apiUrl,
293
- bankAccount: opts.bankAccount,
294
289
  currency: opts.currency,
295
290
  timeout: opts.timeout,
296
- };
297
- if (ingestOpts.execute) {
298
- validateExecuteOptions(ingestOpts);
299
- // Execute mode — scan + classify + upload to Jaz Magic API
300
- ingestExecute(ingestOpts).then((result) => {
301
- opts.json ? printIngestResultJson(result) : printIngestResult(result);
302
- if (result.summary.failed > 0)
303
- process.exit(1);
304
- }).catch((err) => {
305
- console.error(chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
306
- process.exit(1);
307
- });
308
- return;
309
- }
310
- // Dry-run mode — scan + classify (async for cloud sources)
311
- ingestDryRun(ingestOpts).then((plan) => {
291
+ }).then((plan) => {
312
292
  opts.json ? printIngestPlanJson(plan) : printIngestPlan(plan);
313
293
  }).catch((err) => {
314
294
  console.error(chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
@@ -319,6 +299,8 @@ export function registerJobsCommand(program) {
319
299
  const statFiling = jobs
320
300
  .command('statutory-filing')
321
301
  .description('Statutory filing — corporate income tax computation and filing')
302
+ .enablePositionalOptions()
303
+ .passThroughOptions()
322
304
  .option('--ya <year>', 'Year of Assessment', parseInt)
323
305
  .option('--jurisdiction <code>', 'Jurisdiction: sg (default)', 'sg')
324
306
  .option('--currency <code>', 'Currency code (e.g. SGD)')
package/dist/index.js CHANGED
@@ -16,7 +16,8 @@ const program = new Command();
16
16
  program
17
17
  .name('clio')
18
18
  .description('Clio — Command Line Interface Orchestrator for Jaz AI')
19
- .version(pkg.version);
19
+ .version(pkg.version)
20
+ .enablePositionalOptions();
20
21
  program
21
22
  .command('init')
22
23
  .description('Install Jaz AI skills into the current project')
@@ -6,7 +6,6 @@
6
6
  import { buildSummary } from '../types.js';
7
7
  export function generateDocumentCollectionBlueprint(opts = {}) {
8
8
  const currency = opts.currency ?? 'SGD';
9
- const source = opts.source ?? '(local folder or shared link)';
10
9
  // Phase 1: Intake
11
10
  const intake = {
12
11
  name: 'Intake',
@@ -16,7 +15,7 @@ export function generateDocumentCollectionBlueprint(opts = {}) {
16
15
  order: 1,
17
16
  description: 'Identify document source',
18
17
  category: 'verify',
19
- notes: `Source: ${source}. Supported: local folders, Dropbox/GDrive/OneDrive shared links.`,
18
+ notes: 'Supported sources: local folders, Dropbox/GDrive/OneDrive shared links.',
20
19
  },
21
20
  {
22
21
  order: 2,
@@ -81,14 +80,14 @@ export function generateDocumentCollectionBlueprint(opts = {}) {
81
80
  order: 8,
82
81
  description: 'Confirm plan with user',
83
82
  category: 'review',
84
- notes: 'User reviews classification, approves or adjusts. Run with --execute to proceed.',
83
+ notes: 'User reviews classification, approves or adjusts. Agent then uploads each file using the api skill.',
85
84
  },
86
85
  ],
87
86
  };
88
- // Phase 5: Upload
87
+ // Phase 5: Upload (agent-executed — uses file paths from ingest plan)
89
88
  const upload = {
90
89
  name: 'Upload',
91
- description: 'Upload each classified file to the appropriate Jaz Magic API endpoint.',
90
+ description: 'Agent uploads each classified file to the appropriate Jaz Magic API endpoint using absolute paths from the ingest plan.',
92
91
  steps: [
93
92
  {
94
93
  order: 9,
@@ -99,7 +98,7 @@ export function generateDocumentCollectionBlueprint(opts = {}) {
99
98
  sourceType: 'FILE',
100
99
  businessTransactionType: '(INVOICE or BILL per classification)',
101
100
  },
102
- notes: 'Multipart upload with sourceFile field. Extraction is async — returns subscriptionFBPath for tracking.',
101
+ notes: 'Agent uses curl -F "sourceFile=@<absolutePath>" for each file. Extraction is async — returns subscriptionFBPath for tracking.',
103
102
  },
104
103
  {
105
104
  order: 10,
@@ -110,14 +109,14 @@ export function generateDocumentCollectionBlueprint(opts = {}) {
110
109
  sourceType: 'FILE',
111
110
  businessTransactionType: 'BANK_STATEMENT',
112
111
  },
113
- notes: 'Requires accountResourceId for the target bank account. Supports CSV and OFX.',
112
+ notes: 'Agent uses curl -F "sourceFile=@<absolutePath>". Requires accountResourceId for the target bank account. Supports CSV and OFX.',
114
113
  },
115
114
  ],
116
115
  };
117
- // Phase 6: Verify
116
+ // Phase 6: Verify (agent-executed)
118
117
  const verify = {
119
118
  name: 'Verify',
120
- description: 'Check extraction results and flag failures.',
119
+ description: 'Agent checks extraction results and flags failures.',
121
120
  steps: [
122
121
  {
123
122
  order: 11,
@@ -2,7 +2,7 @@
2
2
  * Pretty-print and JSON formatters for document-collection ingest output.
3
3
  */
4
4
  import chalk from 'chalk';
5
- /** Print a dry-run ingestion plan in human-readable format. */
5
+ /** Print an ingestion plan in human-readable format. */
6
6
  export function printIngestPlan(plan) {
7
7
  console.log();
8
8
  console.log(chalk.bold('Document Collection — Ingestion Plan'));
@@ -10,6 +10,7 @@ export function printIngestPlan(plan) {
10
10
  if (plan.sourceType === 'url' && plan.cloudProvider) {
11
11
  const providerName = { dropbox: 'Dropbox', gdrive: 'Google Drive', onedrive: 'OneDrive' };
12
12
  console.log(chalk.gray(`Provider: ${providerName[plan.cloudProvider] ?? plan.cloudProvider}`));
13
+ console.log(chalk.gray(`Local path: ${plan.localPath}`));
13
14
  }
14
15
  console.log();
15
16
  for (const folder of plan.folders) {
@@ -42,45 +43,8 @@ export function printIngestPlan(plan) {
42
43
  }
43
44
  }
44
45
  console.log();
45
- console.log(chalk.gray('Add --execute --api-key <key> --api-url <url> to upload.'));
46
- console.log();
47
46
  }
48
47
  /** Print ingestion plan as JSON. */
49
48
  export function printIngestPlanJson(plan) {
50
49
  console.log(JSON.stringify(plan, null, 2));
51
50
  }
52
- /** Print an execution result in human-readable format. */
53
- export function printIngestResult(result) {
54
- console.log();
55
- console.log(chalk.bold('Document Collection — Ingestion Result'));
56
- console.log(chalk.gray(`Source: ${result.source}`));
57
- console.log();
58
- for (const r of result.results) {
59
- const status = r.status === 'success'
60
- ? chalk.green('✓')
61
- : r.status === 'skipped'
62
- ? chalk.gray('–')
63
- : chalk.red('✗');
64
- const extra = r.recordsCreated ? ` (${r.recordsCreated} records)` : '';
65
- const errMsg = r.error ? chalk.red(` — ${r.error}`) : '';
66
- console.log(` ${status} ${r.file} [${r.classification}]${extra}${errMsg}`);
67
- }
68
- console.log();
69
- console.log(chalk.bold('Summary'));
70
- console.log(` Total: ${result.summary.total}`);
71
- console.log(` Uploaded: ${chalk.green(String(result.summary.uploaded))}`);
72
- if (result.summary.skipped > 0) {
73
- console.log(` Skipped: ${chalk.gray(String(result.summary.skipped))}`);
74
- }
75
- if (result.summary.failed > 0) {
76
- console.log(` Failed: ${chalk.red(String(result.summary.failed))}`);
77
- for (const err of result.summary.errors) {
78
- console.log(chalk.red(` ${err.file}: ${err.error}`));
79
- }
80
- }
81
- console.log();
82
- }
83
- /** Print execution result as JSON. */
84
- export function printIngestResultJson(result) {
85
- console.log(JSON.stringify(result, null, 2));
86
- }
@@ -1,17 +1,15 @@
1
1
  /**
2
2
  * Document Collection ingest tool.
3
3
  *
4
- * Two modes:
5
- * 1. Dry-run (default): Scan + classify IngestPlan
6
- * 2. Execute (--execute): Scan + classify + upload to Jaz Magic API → IngestResult
4
+ * Scans a local directory or cloud share link, classifies documents by folder name,
5
+ * and produces an IngestPlan with absolute file paths for the AI agent to upload.
7
6
  *
8
- * Supports local directories and public share links (Dropbox, Google Drive, OneDrive).
7
+ * The CLI does NOT make API calls the agent uses the api skill for that.
9
8
  */
10
9
  import { existsSync, statSync } from 'node:fs';
11
- import { resolve, join } from 'node:path';
10
+ import { resolve } from 'node:path';
12
11
  import { JobValidationError } from '../../../validate.js';
13
12
  import { scanLocalDirectory } from './scanner.js';
14
- import { uploadFile } from './magic-upload.js';
15
13
  import { downloadCloudSource } from './cloud/index.js';
16
14
  /**
17
15
  * Detect if a source string is a URL (public share link) or local path.
@@ -40,7 +38,8 @@ function validateLocalSource(source) {
40
38
  }
41
39
  /**
42
40
  * Resolve a source (local path or URL) to a local directory path.
43
- * For URLs, downloads to a temp dir. Returns the path and optional cleanup.
41
+ * For URLs, downloads to a temp dir. The temp dir is NOT cleaned up —
42
+ * the agent needs the files to upload via the API.
44
43
  */
45
44
  async function resolveSource(opts) {
46
45
  if (isUrl(opts.source)) {
@@ -52,118 +51,20 @@ async function resolveSource(opts) {
52
51
  return { localPath: validateLocalSource(opts.source), originalSource: opts.source, cloud: null };
53
52
  }
54
53
  /**
55
- * Run the ingest tool in dry-run mode.
56
- * Returns an IngestPlan showing what would be uploaded.
57
- */
58
- export async function ingestDryRun(opts) {
59
- const { localPath, originalSource, cloud } = await resolveSource(opts);
60
- try {
61
- const plan = scanLocalDirectory(localPath, { forceType: opts.type });
62
- // Override source display for URL sources
63
- if (cloud) {
64
- plan.source = originalSource;
65
- plan.sourceType = 'url';
66
- plan.cloudProvider = cloud.provider;
67
- }
68
- return plan;
69
- }
70
- finally {
71
- cloud?.cleanup();
72
- }
73
- }
74
- /**
75
- * Run the ingest tool in execute mode.
76
- * Scans, classifies, then uploads each file to the Jaz Magic API.
54
+ * Scan, classify, and return an IngestPlan with absolute file paths.
55
+ *
56
+ * For cloud sources, files are downloaded to a temp directory first.
57
+ * The temp dir is preserved so the agent can use the file paths for uploads.
77
58
  */
78
- export async function ingestExecute(opts) {
59
+ export async function ingest(opts) {
79
60
  const { localPath, originalSource, cloud } = await resolveSource(opts);
80
- try {
81
- const plan = scanLocalDirectory(localPath, { forceType: opts.type });
82
- const results = [];
83
- let uploaded = 0;
84
- let skipped = 0;
85
- let failed = 0;
86
- const errors = [];
87
- // Flatten all files from all folders
88
- const allFiles = plan.folders.flatMap(f => f.files);
89
- for (const file of allFiles) {
90
- // Skip UNKNOWN and SKIPPED files
91
- if (file.documentType === 'UNKNOWN' || file.documentType === 'SKIPPED') {
92
- skipped++;
93
- results.push({
94
- file: file.path,
95
- classification: 'SKIPPED',
96
- status: 'skipped',
97
- reason: file.reason,
98
- });
99
- continue;
100
- }
101
- const absolutePath = join(localPath, file.path);
102
- const result = await uploadFile({
103
- filePath: absolutePath,
104
- relativePath: file.path,
105
- documentType: file.documentType,
106
- apiKey: opts.apiKey,
107
- apiUrl: opts.apiUrl,
108
- bankAccountId: opts.bankAccount,
109
- });
110
- results.push(result);
111
- if (result.status === 'success') {
112
- uploaded++;
113
- }
114
- else {
115
- failed++;
116
- if (result.error) {
117
- errors.push({ file: result.file, error: result.error });
118
- }
119
- }
120
- }
121
- return {
122
- type: 'document-collection-ingest',
123
- source: originalSource,
124
- results,
125
- summary: {
126
- total: allFiles.length,
127
- uploaded,
128
- skipped,
129
- failed,
130
- errors,
131
- },
132
- };
133
- }
134
- finally {
135
- cloud?.cleanup();
136
- }
137
- }
138
- /** Allowed API hosts for execute mode. */
139
- const ALLOWED_API_HOSTS = new Set(['api.jaz.ai', 'api.juan.ac', 'staging-api.jaz.ai', 'localhost']);
140
- /**
141
- * Validate options for execute mode.
142
- * Throws JobValidationError if required options are missing or unsafe.
143
- */
144
- export function validateExecuteOptions(opts) {
145
- if (!opts.apiKey) {
146
- throw new JobValidationError('--api-key is required for --execute mode');
147
- }
148
- if (!opts.apiUrl) {
149
- throw new JobValidationError('--api-url is required for --execute mode');
150
- }
151
- // Validate URL to prevent credential exfiltration (SSRF)
152
- let parsed;
153
- try {
154
- parsed = new URL(opts.apiUrl);
155
- }
156
- catch {
157
- throw new JobValidationError('--api-url must be a valid URL');
158
- }
159
- if (parsed.protocol !== 'https:' && parsed.hostname !== 'localhost') {
160
- throw new JobValidationError('--api-url must use HTTPS');
161
- }
162
- if (parsed.username || parsed.password) {
163
- throw new JobValidationError('--api-url must not contain credentials');
164
- }
165
- if (!ALLOWED_API_HOSTS.has(parsed.hostname)) {
166
- throw new JobValidationError(`--api-url host "${parsed.hostname}" is not allowed. ` +
167
- `Allowed: ${[...ALLOWED_API_HOSTS].join(', ')}`);
168
- }
61
+ const plan = scanLocalDirectory(localPath, { forceType: opts.type });
62
+ // Override source display and localPath for URL sources
63
+ if (cloud) {
64
+ plan.source = originalSource;
65
+ plan.sourceType = 'url';
66
+ plan.cloudProvider = cloud.provider;
67
+ plan.localPath = localPath;
68
+ }
69
+ return plan;
169
70
  }
@@ -5,7 +5,7 @@
5
5
  * and produces an IngestPlan (dry-run output).
6
6
  */
7
7
  import { readdirSync, statSync } from 'node:fs';
8
- import { join, basename, extname, relative } from 'node:path';
8
+ import { join, basename, extname, relative, resolve } from 'node:path';
9
9
  import { classifyFolder, checkExtension } from './classify.js';
10
10
  /** Max recursion depth to prevent runaway traversal. */
11
11
  const MAX_DEPTH = 10;
@@ -19,9 +19,10 @@ const MAX_DEPTH = 10;
19
19
  * 4. Nested subfolders (depth > 1) inherit from nearest classified ancestor.
20
20
  */
21
21
  export function scanLocalDirectory(sourcePath, opts = {}) {
22
+ const base = resolve(sourcePath);
22
23
  const maxDepth = opts.maxDepth ?? MAX_DEPTH;
23
24
  const files = [];
24
- scanDir(sourcePath, sourcePath, null, opts.forceType ?? null, 0, maxDepth, files);
25
+ scanDir(base, base, null, opts.forceType ?? null, 0, maxDepth, files);
25
26
  // Group files by folder
26
27
  const folderMap = new Map();
27
28
  for (const f of files) {
@@ -75,8 +76,9 @@ export function scanLocalDirectory(sourcePath, opts = {}) {
75
76
  }
76
77
  }
77
78
  return {
78
- source: sourcePath,
79
+ source: base,
79
80
  sourceType: 'local',
81
+ localPath: base,
80
82
  folders,
81
83
  summary: { total, uploadable, needClassification, skipped, byType },
82
84
  };
@@ -151,6 +153,8 @@ function scanDir(rootPath, dirPath, inheritedType, forceType, depth, maxDepth, o
151
153
  folder: relDir,
152
154
  confidence,
153
155
  reason,
156
+ absolutePath: fullPath,
157
+ sizeBytes: stat.size,
154
158
  });
155
159
  }
156
160
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jaz-clio",
3
- "version": "3.3.0",
3
+ "version": "3.4.0",
4
4
  "description": "Clio — Command Line Interface Orchestrator for Jaz AI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,142 +0,0 @@
1
- /**
2
- * Jaz Magic API upload logic for document collection ingest.
3
- *
4
- * Two endpoints:
5
- * 1. POST /magic/createBusinessTransactionFromAttachment — invoices + bills (PDF/JPG/PNG)
6
- * 2. POST /magic/importBankStatementFromAttachment — bank statements (CSV/OFX)
7
- *
8
- * Both use multipart/form-data with `sourceFile` field (NOT "file").
9
- * Magic endpoints use `x-magic-api-key` header (NOT `x-jk-api-key`).
10
- * Extraction is asynchronous — response confirms upload, not extraction.
11
- */
12
- import { readFileSync } from 'node:fs';
13
- import { basename } from 'node:path';
14
- /**
15
- * Upload a single file to the appropriate Jaz Magic API endpoint.
16
- *
17
- * Returns an IngestFileResult (never throws — errors are captured in the result).
18
- */
19
- export async function uploadFile(opts) {
20
- const { filePath, relativePath, documentType, apiKey, apiUrl, bankAccountId } = opts;
21
- try {
22
- const fileBuffer = readFileSync(filePath);
23
- const fileName = basename(filePath);
24
- const blob = new Blob([fileBuffer]);
25
- if (documentType === 'BANK_STATEMENT') {
26
- return await uploadBankStatement(blob, fileName, relativePath, apiKey, apiUrl, bankAccountId);
27
- }
28
- else {
29
- const txType = documentType === 'INVOICE' ? 'INVOICE' : 'BILL';
30
- return await uploadTransaction(blob, fileName, relativePath, txType, apiKey, apiUrl);
31
- }
32
- }
33
- catch (err) {
34
- return {
35
- file: relativePath,
36
- classification: documentType,
37
- status: 'failed',
38
- error: err instanceof Error ? err.message : String(err),
39
- };
40
- }
41
- }
42
- /**
43
- * Upload invoice/bill via POST /magic/createBusinessTransactionFromAttachment.
44
- */
45
- async function uploadTransaction(blob, fileName, relativePath, txType, apiKey, apiUrl) {
46
- const formData = new FormData();
47
- formData.append('sourceFile', blob, fileName);
48
- formData.append('businessTransactionType', txType);
49
- formData.append('sourceType', 'FILE');
50
- const url = `${apiUrl.replace(/\/+$/, '')}/api/v1/magic/createBusinessTransactionFromAttachment`;
51
- const response = await fetch(url, {
52
- method: 'POST',
53
- headers: { 'x-magic-api-key': apiKey },
54
- body: formData,
55
- });
56
- if (!response.ok) {
57
- const errorBody = await safeReadBody(response);
58
- return {
59
- file: relativePath,
60
- classification: txType === 'INVOICE' ? 'INVOICE' : 'BILL',
61
- status: 'failed',
62
- error: `${response.status}: ${errorBody}`,
63
- };
64
- }
65
- const json = (await response.json());
66
- const valid = json.data?.validFiles?.[0];
67
- const invalid = json.data?.invalidFiles?.[0];
68
- if (invalid) {
69
- return {
70
- file: relativePath,
71
- classification: txType === 'INVOICE' ? 'INVOICE' : 'BILL',
72
- status: 'failed',
73
- error: `${invalid.errorCode}: ${invalid.errorMessage}`,
74
- };
75
- }
76
- return {
77
- file: relativePath,
78
- classification: txType === 'INVOICE' ? 'INVOICE' : 'BILL',
79
- status: 'success',
80
- subscriptionFBPath: valid?.subscriptionFBPath,
81
- fileId: valid?.fileDetails?.fileId,
82
- };
83
- }
84
- /**
85
- * Upload bank statement via POST /magic/importBankStatementFromAttachment.
86
- */
87
- async function uploadBankStatement(blob, fileName, relativePath, apiKey, apiUrl, bankAccountId) {
88
- if (!bankAccountId) {
89
- return {
90
- file: relativePath,
91
- classification: 'BANK_STATEMENT',
92
- status: 'failed',
93
- error: 'Missing --bank-account (accountResourceId required for bank statement uploads)',
94
- };
95
- }
96
- const formData = new FormData();
97
- formData.append('sourceFile', blob, fileName);
98
- formData.append('accountResourceId', bankAccountId);
99
- formData.append('businessTransactionType', 'BANK_STATEMENT');
100
- formData.append('sourceType', 'FILE');
101
- const url = `${apiUrl.replace(/\/+$/, '')}/api/v1/magic/importBankStatementFromAttachment`;
102
- const response = await fetch(url, {
103
- method: 'POST',
104
- headers: { 'x-magic-api-key': apiKey },
105
- body: formData,
106
- });
107
- if (!response.ok) {
108
- const errorBody = await safeReadBody(response);
109
- return {
110
- file: relativePath,
111
- classification: 'BANK_STATEMENT',
112
- status: 'failed',
113
- error: `${response.status}: ${errorBody}`,
114
- };
115
- }
116
- // Bank statement response shape varies — extract record count if available
117
- const json = (await response.json());
118
- const data = json.data;
119
- return {
120
- file: relativePath,
121
- classification: 'BANK_STATEMENT',
122
- status: 'success',
123
- recordsCreated: typeof data?.recordsCreated === 'number' ? data.recordsCreated : undefined,
124
- };
125
- }
126
- /** Safely read response body as text, truncated to 200 chars. */
127
- async function safeReadBody(response) {
128
- try {
129
- const text = await response.text();
130
- // Try to extract message from JSON error body
131
- try {
132
- const parsed = JSON.parse(text);
133
- return parsed.message ?? parsed.error ?? text.slice(0, 200);
134
- }
135
- catch {
136
- return text.slice(0, 200);
137
- }
138
- }
139
- catch {
140
- return response.statusText;
141
- }
142
- }