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 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
+ }