up-bank-cli 0.1.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/README.md +30 -0
- package/package.json +26 -0
- package/src/commands/accounts.ts +61 -0
- package/src/commands/ping.ts +11 -0
- package/src/commands/transactions.ts +78 -0
- package/src/index.ts +48 -0
- package/src/utils.ts +98 -0
package/README.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# up-bank-cli
|
|
2
|
+
|
|
3
|
+
CLI for the [Up Banking API](https://developer.up.com.au/) — view accounts, balances, and transactions.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun install -g up-bank-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Setup
|
|
12
|
+
|
|
13
|
+
Generate a Personal Access Token at [api.up.com.au](https://api.up.com.au) and set it:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
export UP_BANK_API_TOKEN="up:yeah:your-token-here"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Commands
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
up-bank accounts # List all accounts
|
|
23
|
+
up-bank accounts --type saver # Filter by type
|
|
24
|
+
up-bank transactions # Recent transactions
|
|
25
|
+
up-bank transactions --since 2026-03-01 --until 2026-03-15
|
|
26
|
+
up-bank transactions --category groceries --limit 10
|
|
27
|
+
up-bank ping # Test token
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Add `--json` to any command for raw JSON output.
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "up-bank-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI for the Up Banking API — accounts, transactions, and balances",
|
|
5
|
+
"bin": {
|
|
6
|
+
"up-bank": "./src/index.ts"
|
|
7
|
+
},
|
|
8
|
+
"type": "module",
|
|
9
|
+
"files": [
|
|
10
|
+
"src"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "https://github.com/deanmikan/up-bank-cli"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"up-bank",
|
|
19
|
+
"banking",
|
|
20
|
+
"finance",
|
|
21
|
+
"cli"
|
|
22
|
+
],
|
|
23
|
+
"engines": {
|
|
24
|
+
"bun": ">=1.0.0"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { parseArgs, apiRequest } from "../utils.js"
|
|
2
|
+
|
|
3
|
+
function formatMoney(value: string, currencyCode: string): string {
|
|
4
|
+
const num = Number.parseFloat(value)
|
|
5
|
+
return num.toLocaleString("en-AU", { style: "currency", currency: currencyCode })
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function formatAccount(account: any): string {
|
|
9
|
+
const a = account.attributes
|
|
10
|
+
const balance = formatMoney(a.balance.value, a.balance.currencyCode)
|
|
11
|
+
return `${a.displayName} (${a.accountType})\n Balance: ${balance}\n Owner: ${a.ownershipType}`
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function formatAccountList(accounts: any[]): string {
|
|
15
|
+
if (accounts.length === 0) return "No accounts found."
|
|
16
|
+
return accounts
|
|
17
|
+
.map((acc) => {
|
|
18
|
+
const a = acc.attributes
|
|
19
|
+
const balance = formatMoney(a.balance.value, a.balance.currencyCode)
|
|
20
|
+
return ` ${a.displayName.padEnd(25)} ${a.accountType.padEnd(16)} ${balance}`
|
|
21
|
+
})
|
|
22
|
+
.join("\n")
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function accounts(argv: string[]) {
|
|
26
|
+
const { positional, flags } = parseArgs(argv)
|
|
27
|
+
|
|
28
|
+
if (flags.help === "true" || flags.h === "true") {
|
|
29
|
+
console.log(`Usage:
|
|
30
|
+
up-bank accounts [--type saver|transactional|home_loan] [--json]
|
|
31
|
+
up-bank accounts <id> [--json]`)
|
|
32
|
+
process.exit(0)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const json = flags.json === "true"
|
|
36
|
+
const accountId = positional[0]
|
|
37
|
+
|
|
38
|
+
if (accountId) {
|
|
39
|
+
// Get specific account
|
|
40
|
+
const response = await apiRequest(`/accounts/${accountId}`)
|
|
41
|
+
if (json) {
|
|
42
|
+
console.log(JSON.stringify(response, null, 2))
|
|
43
|
+
} else {
|
|
44
|
+
console.log(formatAccount(response.data))
|
|
45
|
+
}
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// List accounts
|
|
50
|
+
const params: Record<string, string> = {}
|
|
51
|
+
if (flags.type) {
|
|
52
|
+
params["filter[accountType]"] = flags.type.toUpperCase()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const response = await apiRequest("/accounts", params)
|
|
56
|
+
if (json) {
|
|
57
|
+
console.log(JSON.stringify(response, null, 2))
|
|
58
|
+
} else {
|
|
59
|
+
console.log(formatAccountList(response.data))
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { apiRequest } from "../utils.js"
|
|
2
|
+
|
|
3
|
+
export async function ping(_argv: string[]) {
|
|
4
|
+
try {
|
|
5
|
+
const response = await apiRequest("/util/ping")
|
|
6
|
+
const meta = response.meta ?? {}
|
|
7
|
+
console.log(`✓ Authenticated — ${meta.statusEmoji ?? "🙂"}`)
|
|
8
|
+
} catch {
|
|
9
|
+
// apiRequest already handles error output and process.exit(1)
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { parseArgs, apiRequest, apiRequestPaginated } from "../utils.js"
|
|
2
|
+
|
|
3
|
+
function formatMoney(value: string, currencyCode: string): string {
|
|
4
|
+
const num = Number.parseFloat(value)
|
|
5
|
+
const formatted = Math.abs(num).toLocaleString("en-AU", {
|
|
6
|
+
style: "currency",
|
|
7
|
+
currency: currencyCode,
|
|
8
|
+
})
|
|
9
|
+
return num < 0 ? `-${formatted}` : `+${formatted}`
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function formatDate(iso: string | null): string {
|
|
13
|
+
if (!iso) return "pending"
|
|
14
|
+
const d = new Date(iso)
|
|
15
|
+
return d.toLocaleDateString("en-AU", { day: "2-digit", month: "short", year: "numeric" })
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function formatTransaction(txn: any): string {
|
|
19
|
+
const a = txn.attributes
|
|
20
|
+
const amount = formatMoney(a.amount.value, a.amount.currencyCode)
|
|
21
|
+
const date = formatDate(a.settledAt ?? a.createdAt)
|
|
22
|
+
const category = txn.relationships?.category?.data?.id ?? ""
|
|
23
|
+
const status = a.status === "HELD" ? " [HELD]" : ""
|
|
24
|
+
return `${date} ${amount.padStart(12)} ${a.description}${status}${category ? ` (${category})` : ""}`
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function formatTransactionList(transactions: any[]): string {
|
|
28
|
+
if (transactions.length === 0) return "No transactions found."
|
|
29
|
+
return transactions.map(formatTransaction).join("\n")
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function transactions(argv: string[]) {
|
|
33
|
+
const { positional, flags } = parseArgs(argv)
|
|
34
|
+
|
|
35
|
+
if (flags.help === "true" || flags.h === "true") {
|
|
36
|
+
console.log(`Usage:
|
|
37
|
+
up-bank transactions [options]
|
|
38
|
+
up-bank transactions <id> [--json]
|
|
39
|
+
|
|
40
|
+
Options:
|
|
41
|
+
--since <YYYY-MM-DD> Filter from date
|
|
42
|
+
--until <YYYY-MM-DD> Filter to date
|
|
43
|
+
--category <name> Filter by category
|
|
44
|
+
--status <settled|held> Filter by status
|
|
45
|
+
--limit <n> Max results (default: 50)
|
|
46
|
+
--json Output raw JSON`)
|
|
47
|
+
process.exit(0)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const json = flags.json === "true"
|
|
51
|
+
const txnId = positional[0]
|
|
52
|
+
|
|
53
|
+
if (txnId) {
|
|
54
|
+
const response = await apiRequest(`/transactions/${txnId}`)
|
|
55
|
+
if (json) {
|
|
56
|
+
console.log(JSON.stringify(response, null, 2))
|
|
57
|
+
} else {
|
|
58
|
+
console.log(formatTransaction(response.data))
|
|
59
|
+
}
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Build filter params
|
|
64
|
+
const params: Record<string, string> = {}
|
|
65
|
+
if (flags.since) params["filter[since]"] = new Date(flags.since).toISOString()
|
|
66
|
+
if (flags.until) params["filter[until]"] = new Date(flags.until).toISOString()
|
|
67
|
+
if (flags.status) params["filter[status]"] = flags.status.toUpperCase()
|
|
68
|
+
if (flags.category) params["filter[category]"] = flags.category
|
|
69
|
+
|
|
70
|
+
const limit = flags.limit ? parseInt(flags.limit, 10) : 50
|
|
71
|
+
const data = await apiRequestPaginated("/transactions", params, limit)
|
|
72
|
+
|
|
73
|
+
if (json) {
|
|
74
|
+
console.log(JSON.stringify({ data }, null, 2))
|
|
75
|
+
} else {
|
|
76
|
+
console.log(formatTransactionList(data))
|
|
77
|
+
}
|
|
78
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { accounts } from "./commands/accounts.js"
|
|
4
|
+
import { transactions } from "./commands/transactions.js"
|
|
5
|
+
import { ping } from "./commands/ping.js"
|
|
6
|
+
|
|
7
|
+
const args = process.argv.slice(2)
|
|
8
|
+
const command = args[0]
|
|
9
|
+
const commandArgs = args.slice(1)
|
|
10
|
+
|
|
11
|
+
if (!command || command === "--help" || command === "-h") {
|
|
12
|
+
console.log(`up-bank — Up Banking API CLI
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
up-bank accounts [options] List accounts
|
|
16
|
+
up-bank accounts <id> [options] Get specific account
|
|
17
|
+
up-bank transactions [options] List transactions
|
|
18
|
+
up-bank transactions <id> [options] Get specific transaction
|
|
19
|
+
up-bank ping Test API token
|
|
20
|
+
|
|
21
|
+
Options:
|
|
22
|
+
--type <saver|transactional|home_loan> Filter accounts by type
|
|
23
|
+
--since <YYYY-MM-DD> Transactions since date
|
|
24
|
+
--until <YYYY-MM-DD> Transactions until date
|
|
25
|
+
--category <name> Filter by category
|
|
26
|
+
--status <settled|held> Filter by status
|
|
27
|
+
--limit <n> Max results
|
|
28
|
+
--json Output raw JSON
|
|
29
|
+
--help, -h Show help
|
|
30
|
+
|
|
31
|
+
Environment:
|
|
32
|
+
UP_BANK_API_TOKEN Required. Personal access token from api.up.com.au`)
|
|
33
|
+
process.exit(0)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const commands: Record<string, (argv: string[]) => Promise<void>> = {
|
|
37
|
+
accounts,
|
|
38
|
+
transactions,
|
|
39
|
+
ping,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const handler = commands[command]
|
|
43
|
+
if (!handler) {
|
|
44
|
+
console.error(`Unknown command: ${command}. Run 'up-bank --help' for usage.`)
|
|
45
|
+
process.exit(1)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
await handler(commandArgs)
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
const BASE_URL = "https://api.up.com.au/api/v1"
|
|
2
|
+
|
|
3
|
+
export function getApiToken(): string {
|
|
4
|
+
const token = process.env.UP_BANK_API_TOKEN
|
|
5
|
+
if (!token) {
|
|
6
|
+
console.error("Error: UP_BANK_API_TOKEN environment variable is required.")
|
|
7
|
+
console.error("Generate a token at https://api.up.com.au")
|
|
8
|
+
process.exit(1)
|
|
9
|
+
}
|
|
10
|
+
return token
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function parseArgs(argv: string[]): { positional: string[]; flags: Record<string, string> } {
|
|
14
|
+
const positional: string[] = []
|
|
15
|
+
const flags: Record<string, string> = {}
|
|
16
|
+
let i = 0
|
|
17
|
+
while (i < argv.length) {
|
|
18
|
+
const arg = argv[i]
|
|
19
|
+
if (arg.startsWith("--")) {
|
|
20
|
+
const key = arg.slice(2)
|
|
21
|
+
const next = argv[i + 1]
|
|
22
|
+
if (next && !next.startsWith("--")) {
|
|
23
|
+
flags[key] = next
|
|
24
|
+
i += 2
|
|
25
|
+
} else {
|
|
26
|
+
flags[key] = "true"
|
|
27
|
+
i += 1
|
|
28
|
+
}
|
|
29
|
+
} else {
|
|
30
|
+
positional.push(arg)
|
|
31
|
+
i += 1
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return { positional, flags }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function apiRequest(
|
|
38
|
+
path: string,
|
|
39
|
+
params?: Record<string, string>,
|
|
40
|
+
): Promise<any> {
|
|
41
|
+
const token = getApiToken()
|
|
42
|
+
const url = new URL(`${BASE_URL}${path}`)
|
|
43
|
+
if (params) {
|
|
44
|
+
for (const [key, value] of Object.entries(params)) {
|
|
45
|
+
url.searchParams.set(key, value)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const response = await fetch(url.toString(), {
|
|
50
|
+
headers: {
|
|
51
|
+
Authorization: `Bearer ${token}`,
|
|
52
|
+
Accept: "application/json",
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
if (response.status === 401) {
|
|
57
|
+
console.error("Error: Invalid or expired token. Regenerate at https://api.up.com.au")
|
|
58
|
+
process.exit(1)
|
|
59
|
+
}
|
|
60
|
+
if (response.status === 429) {
|
|
61
|
+
console.error("Error: Rate limited. Try again shortly.")
|
|
62
|
+
process.exit(1)
|
|
63
|
+
}
|
|
64
|
+
if (!response.ok) {
|
|
65
|
+
const body = await response.text()
|
|
66
|
+
console.error(`Error: Up API returned ${response.status}: ${body}`)
|
|
67
|
+
process.exit(1)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return response.json()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Fetch all pages up to a limit, following cursor-based pagination */
|
|
74
|
+
export async function apiRequestPaginated(
|
|
75
|
+
path: string,
|
|
76
|
+
params?: Record<string, string>,
|
|
77
|
+
limit?: number,
|
|
78
|
+
): Promise<any[]> {
|
|
79
|
+
const firstPage = await apiRequest(path, { ...params, "page[size]": "100" })
|
|
80
|
+
let allData = firstPage.data ?? []
|
|
81
|
+
let nextUrl = firstPage.links?.next
|
|
82
|
+
|
|
83
|
+
while (nextUrl && (!limit || allData.length < limit)) {
|
|
84
|
+
const response = await fetch(nextUrl, {
|
|
85
|
+
headers: {
|
|
86
|
+
Authorization: `Bearer ${getApiToken()}`,
|
|
87
|
+
Accept: "application/json",
|
|
88
|
+
},
|
|
89
|
+
})
|
|
90
|
+
if (!response.ok) break
|
|
91
|
+
const page = await response.json()
|
|
92
|
+
allData = allData.concat(page.data ?? [])
|
|
93
|
+
nextUrl = page.links?.next
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (limit) allData = allData.slice(0, limit)
|
|
97
|
+
return allData
|
|
98
|
+
}
|