cata-centavo 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.
Files changed (127) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +159 -0
  3. package/dist/bin/cata-centavo.js +226 -0
  4. package/dist/bin/cata-centavo.js.map +1 -0
  5. package/dist/cli/dispatch.js +67 -0
  6. package/dist/cli/dispatch.js.map +1 -0
  7. package/dist/cli/doctor.js +178 -0
  8. package/dist/cli/doctor.js.map +1 -0
  9. package/dist/cli/init.js +125 -0
  10. package/dist/cli/init.js.map +1 -0
  11. package/dist/cli/progress.js +85 -0
  12. package/dist/cli/progress.js.map +1 -0
  13. package/dist/cli/sync.js +26 -0
  14. package/dist/cli/sync.js.map +1 -0
  15. package/dist/config.js +119 -0
  16. package/dist/config.js.map +1 -0
  17. package/dist/core/account.js +12 -0
  18. package/dist/core/account.js.map +1 -0
  19. package/dist/core/accounts.js +91 -0
  20. package/dist/core/accounts.js.map +1 -0
  21. package/dist/core/aggregate.js +83 -0
  22. package/dist/core/aggregate.js.map +1 -0
  23. package/dist/core/balance.js +68 -0
  24. package/dist/core/balance.js.map +1 -0
  25. package/dist/core/bill-rows.js +132 -0
  26. package/dist/core/bill-rows.js.map +1 -0
  27. package/dist/core/bill.js +103 -0
  28. package/dist/core/bill.js.map +1 -0
  29. package/dist/core/category-source.js +26 -0
  30. package/dist/core/category-source.js.map +1 -0
  31. package/dist/core/category.js +72 -0
  32. package/dist/core/category.js.map +1 -0
  33. package/dist/core/consent.js +19 -0
  34. package/dist/core/consent.js.map +1 -0
  35. package/dist/core/contracts.js +9 -0
  36. package/dist/core/contracts.js.map +1 -0
  37. package/dist/core/counterparty.js +69 -0
  38. package/dist/core/counterparty.js.map +1 -0
  39. package/dist/core/date.js +25 -0
  40. package/dist/core/date.js.map +1 -0
  41. package/dist/core/description.js +58 -0
  42. package/dist/core/description.js.map +1 -0
  43. package/dist/core/diagnose.js +32 -0
  44. package/dist/core/diagnose.js.map +1 -0
  45. package/dist/core/mcc.js +124 -0
  46. package/dist/core/mcc.js.map +1 -0
  47. package/dist/core/refresh.js +142 -0
  48. package/dist/core/refresh.js.map +1 -0
  49. package/dist/core/self-transfer.js +22 -0
  50. package/dist/core/self-transfer.js.map +1 -0
  51. package/dist/core/taxonomy-tree.js +148 -0
  52. package/dist/core/taxonomy-tree.js.map +1 -0
  53. package/dist/core/taxonomy.js +70 -0
  54. package/dist/core/taxonomy.js.map +1 -0
  55. package/dist/core/transaction.js +2 -0
  56. package/dist/core/transaction.js.map +1 -0
  57. package/dist/core/transactions.js +46 -0
  58. package/dist/core/transactions.js.map +1 -0
  59. package/dist/logging.js +93 -0
  60. package/dist/logging.js.map +1 -0
  61. package/dist/mcp/cursor.js +64 -0
  62. package/dist/mcp/cursor.js.map +1 -0
  63. package/dist/mcp/format.js +38 -0
  64. package/dist/mcp/format.js.map +1 -0
  65. package/dist/mcp/server.js +50 -0
  66. package/dist/mcp/server.js.map +1 -0
  67. package/dist/mcp/source.js +2 -0
  68. package/dist/mcp/source.js.map +1 -0
  69. package/dist/mcp/tools/accounts.js +144 -0
  70. package/dist/mcp/tools/accounts.js.map +1 -0
  71. package/dist/mcp/tools/balance.js +96 -0
  72. package/dist/mcp/tools/balance.js.map +1 -0
  73. package/dist/mcp/tools/bill-summary-format.js +161 -0
  74. package/dist/mcp/tools/bill-summary-format.js.map +1 -0
  75. package/dist/mcp/tools/bill-summary.js +105 -0
  76. package/dist/mcp/tools/bill-summary.js.map +1 -0
  77. package/dist/mcp/tools/bills.js +144 -0
  78. package/dist/mcp/tools/bills.js.map +1 -0
  79. package/dist/mcp/tools/closing-days.js +90 -0
  80. package/dist/mcp/tools/closing-days.js.map +1 -0
  81. package/dist/mcp/tools/list-transactions.js +122 -0
  82. package/dist/mcp/tools/list-transactions.js.map +1 -0
  83. package/dist/mcp/tools/result.js +12 -0
  84. package/dist/mcp/tools/result.js.map +1 -0
  85. package/dist/mcp/tools/set-category.js +74 -0
  86. package/dist/mcp/tools/set-category.js.map +1 -0
  87. package/dist/mcp/tools/sources.js +73 -0
  88. package/dist/mcp/tools/sources.js.map +1 -0
  89. package/dist/mcp/tools/transaction-details.js +146 -0
  90. package/dist/mcp/tools/transaction-details.js.map +1 -0
  91. package/dist/mcp/tools/transaction-input.js +51 -0
  92. package/dist/mcp/tools/transaction-input.js.map +1 -0
  93. package/dist/mcp/tools/transactions.js +186 -0
  94. package/dist/mcp/tools/transactions.js.map +1 -0
  95. package/dist/pluggy/client.js +123 -0
  96. package/dist/pluggy/client.js.map +1 -0
  97. package/dist/pluggy/errors.js +152 -0
  98. package/dist/pluggy/errors.js.map +1 -0
  99. package/dist/pluggy/mapper.js +155 -0
  100. package/dist/pluggy/mapper.js.map +1 -0
  101. package/dist/pluggy/money.js +53 -0
  102. package/dist/pluggy/money.js.map +1 -0
  103. package/dist/pluggy/transaction-mapper.js +124 -0
  104. package/dist/pluggy/transaction-mapper.js.map +1 -0
  105. package/dist/pluggy/transport.js +170 -0
  106. package/dist/pluggy/transport.js.map +1 -0
  107. package/dist/pluggy/wire.js +207 -0
  108. package/dist/pluggy/wire.js.map +1 -0
  109. package/dist/storage/categories.js +101 -0
  110. package/dist/storage/categories.js.map +1 -0
  111. package/dist/storage/category-sql.js +37 -0
  112. package/dist/storage/category-sql.js.map +1 -0
  113. package/dist/storage/closing-days.js +27 -0
  114. package/dist/storage/closing-days.js.map +1 -0
  115. package/dist/storage/db.js +162 -0
  116. package/dist/storage/db.js.map +1 -0
  117. package/dist/storage/diagnostics.js +57 -0
  118. package/dist/storage/diagnostics.js.map +1 -0
  119. package/dist/storage/harvest.js +87 -0
  120. package/dist/storage/harvest.js.map +1 -0
  121. package/dist/storage/migrations.js +125 -0
  122. package/dist/storage/migrations.js.map +1 -0
  123. package/dist/storage/transaction-row.js +74 -0
  124. package/dist/storage/transaction-row.js.map +1 -0
  125. package/dist/storage/transactions.js +213 -0
  126. package/dist/storage/transactions.js.map +1 -0
  127. package/package.json +70 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Marcus Xavier
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,159 @@
1
+ <div align="center">
2
+
3
+ <img src="https://github.com/user-attachments/assets/e990ea13-b266-4def-917f-afc15c1b4275" alt="cata-centavo" width="200" height="200">
4
+
5
+ # Cata-centavo
6
+ **Ask an agent about your own money.** Brazilian Open Finance data over [MCP](https://modelcontextprotocol.io), via [Pluggy](https://pluggy.ai).
7
+
8
+ <p>
9
+ <a href="https://www.npmjs.com/package/cata-centavo"><img src="https://img.shields.io/npm/v/cata-centavo?logo=npm&logoColor=white&color=cb3837" alt="npm version"></a>
10
+ <a href="https://www.npmjs.com/package/cata-centavo"><img src="https://img.shields.io/npm/dm/cata-centavo?logo=npm&logoColor=white" alt="npm downloads"></a>
11
+ <a href="https://github.com/MarcusXavierr/cata-centavo/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/MarcusXavierr/cata-centavo/ci.yml?branch=main&logo=githubactions&logoColor=white&label=CI" alt="CI"></a>
12
+ <a href="https://github.com/MarcusXavierr/cata-centavo/blob/main/LICENSE"><img src="https://img.shields.io/github/license/MarcusXavierr/cata-centavo?color=blue" alt="MIT license"></a>
13
+ <img src="https://img.shields.io/badge/node-%3E%3D22.13-5FA04E?logo=nodedotjs&logoColor=white" alt="node >= 22.13">
14
+ <img src="https://img.shields.io/badge/MCP-server-0A7CFF" alt="MCP server">
15
+ </p>
16
+
17
+ <a href="#install">Install</a> ·
18
+ <a href="#commands">Commands</a> ·
19
+ <a href="#categories">Categories</a> ·
20
+ <a href="#what-it-cannot-see">Limits</a> ·
21
+ <a href="#tools">Tools</a> ·
22
+ <a href="#license">License</a>
23
+
24
+ </div>
25
+
26
+ <p align="center">
27
+ <img width="855" height="352" alt="image" src="https://github.com/user-attachments/assets/ee489a5c-1024-4a93-b476-32dec8f6cad6" />
28
+ </p>
29
+
30
+ ---
31
+
32
+ Cata-centavo lets you ask an agent about your own money. Point it at your bank and credit card accounts and ask what you spent on food last month, how the current card statement is going, or where a strange charge came from.
33
+
34
+ You run it on your machine, against your own accounts, and the categories you correct stay there. There is no hosted version and nothing multi-user about it.
35
+
36
+ ## Install
37
+
38
+ You need Node 22.13 or newer, and a Pluggy account with your banks already connected.
39
+
40
+ ### Getting your Pluggy keys
41
+
42
+ The bank data comes from [Pluggy](https://pluggy.ai), the Open Finance provider that does the talking to the banks. Cata-centavo only reads connections that already exist, so make them first.
43
+
44
+
45
+
46
+ 1. At [MeuPluggy](https://meu.pluggy.ai/en), create your account and connect your banks
47
+ <img width="1530" height="617" alt="image" src="https://github.com/user-attachments/assets/15bde1c7-dfd9-4f9e-9f6b-e254522afd58" />
48
+
49
+ 2. Then go to the other [Pluggy portal](https://pluggy.ai/), log in, and connect your MeuPluggy accounts to the Demo app. Also copy your `PLUGGY_CLIENT_ID` and `PLUGGY_CLIENT_SECRET` from this page
50
+ <img width="1918" height="595" alt="image" src="https://github.com/user-attachments/assets/be7670e9-35fc-4ea3-98d3-115298cd488f" />
51
+
52
+ 3. Then connect your MeuPluggy accounts one by one into the Demo App
53
+ <img width="1206" height="813" alt="image" src="https://github.com/user-attachments/assets/66402550-ebeb-43ba-962b-3d091242e807" />
54
+
55
+
56
+ 4. And then copy the `PLUGGY_ITEM_IDS`, one by one
57
+ <img width="1851" height="627" alt="image" src="https://github.com/user-attachments/assets/16fc1d2a-39b2-4531-9ae6-487ade128006" />
58
+
59
+
60
+ That gives you three values, all required:
61
+
62
+ ```
63
+ PLUGGY_CLIENT_ID from your Pluggy dashboard
64
+ PLUGGY_CLIENT_SECRET from your Pluggy dashboard
65
+ PLUGGY_ITEM_IDS connection ids, separated by commas
66
+ ```
67
+
68
+ The plain way is to export them from your `.zshrc` or `.bashrc`, and in the MCP configuration file write `"PLUGGY_CLIENT_ID": "${PLUGGY_CLIENT_ID}"`. That leaves your keys in a file every shell reads. If you would rather not, `secret-tool` keeps them in your keyring and a small wrapper script can pull them out right before the server starts. Setups differ enough that it is worth pointing your agent at this page and asking it which one fits your machine.
69
+
70
+ After configuring this, run `npx cata-centavo doctor` to check that your environment variables are working
71
+
72
+ <img width="894" height="191" alt="image" src="https://github.com/user-attachments/assets/adc2bb35-a161-4241-a33a-42846aa507a1" />
73
+
74
+
75
+ ### Adding it to Claude Code
76
+
77
+ ```bash
78
+ claude mcp add cata-centavo \
79
+ -e PLUGGY_CLIENT_ID=... \
80
+ -e PLUGGY_CLIENT_SECRET=... \
81
+ -e PLUGGY_ITEM_IDS=... \
82
+ -- npx -y cata-centavo
83
+ ```
84
+
85
+ Drop the `-e` flags if the three variables are already exported in the shell you start Claude Code from, since the server inherits that environment.
86
+
87
+ Other clients take the same thing as JSON:
88
+
89
+ ```json
90
+ {
91
+ "mcpServers": {
92
+ "cata-centavo": {
93
+ "command": "npx",
94
+ "args": ["-y", "cata-centavo"],
95
+ "env": {
96
+ "PLUGGY_CLIENT_ID": "...",
97
+ "PLUGGY_CLIENT_SECRET": "...",
98
+ "PLUGGY_ITEM_IDS": "..."
99
+ }
100
+ }
101
+ }
102
+ }
103
+ ```
104
+
105
+ ## Commands
106
+
107
+ - `cata-centavo` (no argument): runs the MCP server over stdio.
108
+ - `cata-centavo init`: checks that the credentials and every configured connection are readable, and reports which ones are not.
109
+ - `cata-centavo doctor`: a fuller diagnosis, covering connection status, consent state, what is cached locally, and whether the learned categorization map has anything in it yet.
110
+
111
+ ## Categories
112
+
113
+ Categories come from your provider while your plan includes transaction enrichment. Every sync copies them into `data.db`, which is never dropped, so they survive the day the enrichment stops and the day the cache is rebuilt.
114
+
115
+ Alongside that, the server learns which categories go with which CNPJs from your own transactions. Only from a CNPJ, never from a CPF, and only when that merchant's transactions actually agree. A CNPJ has a line of business. A CPF is a person, and guessing that everything you send your sister is a "transfer" would then be applied backwards over everything you ever sent her.
116
+
117
+ That learned map is built from your data, on your machine, and is not shipped with the tool, because a CNPJ-to-category table is a line of somebody's bank statement. **So there is a real asymmetry: if you install this after your own enrichment has already stopped, the map starts empty and has nothing to learn from.** You still get merchant-code categorization on card purchases, plus whatever you correct by hand, and corrections apply retroactively. But you will be doing more of the work than someone who installed earlier.
118
+
119
+ Ask the agent to show you what is uncategorized and tell it what those merchants are; both kinds of correction stick.
120
+
121
+ ## What it cannot see
122
+
123
+ - A bank linked in MeuPluggy whose UUID never reached `PLUGGY_ITEM_IDS` is invisible to this server. No endpoint lists the items on a Pluggy account, so this cannot be fixed in software. Compare `doctor`'s list against the banks you know you linked.
124
+ - Freshness is Pluggy's schedule, not this server's. There is no "sync now": on-demand refresh is refused outright. One of the author's own three connections went three days without syncing while still reporting itself up to date, with nothing in the response explaining why.
125
+ - A credit card's `usedCredit` figure is not what the card owes this month. It mixes the current billing cycle with instalments that have not been charged yet, and will not match what a banking app shows. `getBillSummary` answers that question instead, and it answers with a range rather than one number.
126
+
127
+ ## Tools
128
+
129
+ **Accounts and balances**
130
+ - `getAccounts`: lists every account across your configured connections, with balances and credit card limits.
131
+ - `getBalance`: consolidated cash and credit-used figures across all connections, reported separately because they are not the same kind of number.
132
+ - `getBalanceByAccount`: the current figures and details for one account.
133
+
134
+ **Spending**
135
+ - `getTransactions`: totals spending and income over a date range, grouped by category.
136
+ - `listTransactions`: individual transactions over a date range, paged.
137
+ - `getTransactionDetails`: full details for a bounded set of transaction ids.
138
+
139
+ **Credit cards**
140
+ - `getBills`: statements for one card, newest first.
141
+ - `getBillSummary`: the cycle still in progress, as two independent estimates rather than one invented number.
142
+ - `setClosingDay`: records a card's closing day locally, for banks that do not report it.
143
+ - `listClosingDays`: the closing days stored so far.
144
+ - `deleteClosingDay`: drops a stored closing day.
145
+
146
+ **Categories**
147
+ - `setCategory`: corrects the category of specific transactions.
148
+ - `setCounterpartyCategory`: assigns a category to everyone a CPF or CNPJ identifies, backwards and forwards.
149
+
150
+ **Diagnostics**
151
+ - `listSources`: lists every configured connection with its sync status and consent state.
152
+
153
+ Each tool's exact parameters and return shape are published in its own MCP description, which is what a model actually reads and the one version that cannot drift out of sync with a signature.
154
+
155
+ Instalment purchases do not have a tool of their own yet. Card transactions do carry instalment metadata, so `getTransactionDetails` will show it per transaction, but nothing reconstructs a purchase across its instalments.
156
+
157
+ ## License
158
+
159
+ MIT. See [LICENSE](https://github.com/MarcusXavierr/cata-centavo/blob/main/LICENSE).
@@ -0,0 +1,226 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import { COMMANDS, resolveInvocation } from "../cli/dispatch.js";
6
+ import * as doctor from "../cli/doctor.js";
7
+ import { exitCodeFor, formatInit, runInit } from "../cli/init.js";
8
+ import { loadConfig, resolvePaths } from "../config.js";
9
+ import { createLogger } from "../logging.js";
10
+ import { createServer } from "../mcp/server.js";
11
+ import { createTransactionReader } from "../core/transactions.js";
12
+ import { createPluggyClient } from "../pluggy/client.js";
13
+ import { toFailure } from "../pluggy/errors.js";
14
+ import { createCategoryWriter } from "../storage/categories.js";
15
+ import { createClosingDayStore } from "../storage/closing-days.js";
16
+ import { createTransactionStore } from "../storage/transactions.js";
17
+ import { readLocalState } from "../storage/diagnostics.js";
18
+ import { openDatabases, schemaVersion } from "../storage/db.js";
19
+ const USAGE = `cata-centavo — Brazilian Open Finance over MCP
20
+
21
+ Usage:
22
+ cata-centavo MCP server over stdio (default mode)
23
+ cata-centavo init validates the credentials and every configured connection
24
+ cata-centavo doctor diagnostics: consent, connection status, last sync
25
+
26
+ Options:
27
+ -h, --help show this help
28
+ -v, --version show the version
29
+
30
+ Configuration comes from the environment:
31
+ PLUGGY_CLIENT_ID from your Pluggy dashboard
32
+ PLUGGY_CLIENT_SECRET from your Pluggy dashboard
33
+ PLUGGY_ITEM_IDS connection ids from MeuPluggy, separated by commas
34
+
35
+ An MCP client does not read your shell profile, so declare these in the "env"
36
+ block of its config as well.
37
+ `;
38
+ /**
39
+ * We read package.json at runtime rather than with `import ... with { type: "json" }`.
40
+ * An import would enter the tsc module graph and clash with "rootDir": "src".
41
+ * With `new URL(..., import.meta.url)` the path resolves identically whether we
42
+ * run from src/bin/ or dist/bin/ — both sit two levels below the package root.
43
+ */
44
+ function readVersion() {
45
+ const raw = readFileSync(new URL("../../package.json", import.meta.url), "utf8");
46
+ const parsed = JSON.parse(raw);
47
+ if (typeof parsed === "object" && parsed !== null && "version" in parsed) {
48
+ const { version } = parsed;
49
+ if (typeof version === "string")
50
+ return version;
51
+ }
52
+ return "unknown";
53
+ }
54
+ /**
55
+ * ADR §4: nothing but JSON-RPC may reach stdout, because in server mode stdout
56
+ * *is* the protocol channel. All human-facing output goes to stderr.
57
+ */
58
+ function say(message) {
59
+ if (message.endsWith("\n")) {
60
+ process.stderr.write(message);
61
+ }
62
+ else {
63
+ process.stderr.write(`${message}\n`);
64
+ }
65
+ }
66
+ const systemClock = { now: () => new Date() };
67
+ /** Creates both files, runs their migrations, and lets go of the handles. */
68
+ function prepareStorage(paths) {
69
+ const databases = openDatabases(paths);
70
+ try {
71
+ return {
72
+ cacheDb: paths.cacheDb,
73
+ dataDb: paths.dataDb,
74
+ cacheVersion: schemaVersion(databases.db),
75
+ dataVersion: schemaVersion(databases.db, "userdata"),
76
+ };
77
+ }
78
+ finally {
79
+ databases.close();
80
+ }
81
+ }
82
+ const sleep = (milliseconds) => new Promise((resolve) => {
83
+ setTimeout(resolve, milliseconds);
84
+ });
85
+ /** Builds the source the server runs against, or the problems that block it, from a loaded config. */
86
+ function toSource(result, log, services) {
87
+ if (!result.ok) {
88
+ return { ok: false, problems: result.problems };
89
+ }
90
+ if (services === undefined) {
91
+ const problem = "Local transaction cache is unavailable.";
92
+ return { ok: false, problems: [problem], databaseProblems: [problem] };
93
+ }
94
+ return {
95
+ ok: true,
96
+ connections: result.config.itemIds,
97
+ bank: createPluggyClient({
98
+ credentials: result.config.credentials,
99
+ clock: systemClock,
100
+ fetch: globalThis.fetch,
101
+ sleep,
102
+ log,
103
+ }),
104
+ toFailure,
105
+ reader: services.reader,
106
+ writer: services.writer,
107
+ closingDays: services.closingDays,
108
+ };
109
+ }
110
+ function describe(error) {
111
+ if (error instanceof Error) {
112
+ return error.message;
113
+ }
114
+ return String(error);
115
+ }
116
+ function closeOnShutdown(databases) {
117
+ let closed = false;
118
+ const close = () => {
119
+ if (closed) {
120
+ return;
121
+ }
122
+ closed = true;
123
+ databases.close();
124
+ };
125
+ process.once("SIGINT", close);
126
+ process.once("SIGTERM", close);
127
+ }
128
+ async function run(command) {
129
+ if (command === COMMANDS.init) {
130
+ const paths = resolvePaths(process.env, { platform: process.platform, home: homedir() });
131
+ const log = createLogger({ env: process.env, logFile: paths.logFile });
132
+ const report = await runInit({
133
+ env: process.env,
134
+ prepareStorage: () => prepareStorage(paths),
135
+ createBank: (credentials) => createPluggyClient({ credentials, clock: systemClock, fetch: globalThis.fetch, sleep, log }),
136
+ });
137
+ for (const line of formatInit(report, systemClock)) {
138
+ say(line);
139
+ }
140
+ return exitCodeFor(report);
141
+ }
142
+ if (command === COMMANDS.doctor) {
143
+ const paths = resolvePaths(process.env, { platform: process.platform, home: homedir() });
144
+ const log = createLogger({ env: process.env, logFile: paths.logFile });
145
+ const report = await doctor.runDoctor({
146
+ env: process.env,
147
+ createBank: (credentials) => createPluggyClient({ credentials, clock: systemClock, fetch: globalThis.fetch, sleep, log }),
148
+ readLocalState: () => readLocalStateOf(paths),
149
+ toFailure,
150
+ clock: systemClock,
151
+ });
152
+ for (const line of doctor.formatDoctor(report, systemClock)) {
153
+ say(line);
154
+ }
155
+ return doctor.exitCodeFor(report);
156
+ }
157
+ return runServe();
158
+ }
159
+ /** Opens both files, reads their diagnostic state, and closes them again. */
160
+ function readLocalStateOf(paths) {
161
+ const databases = openDatabases(paths);
162
+ try {
163
+ return readLocalState(databases.db);
164
+ }
165
+ finally {
166
+ databases.close();
167
+ }
168
+ }
169
+ async function runServe() {
170
+ const paths = resolvePaths(process.env, { platform: process.platform, home: homedir() });
171
+ const log = createLogger({ env: process.env, logFile: paths.logFile });
172
+ const source = createServeSource(loadConfig(process.env), paths, log);
173
+ await createServer({ source, version: readVersion(), log }).connect(new StdioServerTransport());
174
+ return 0;
175
+ }
176
+ function createServeSource(result, paths, log) {
177
+ if (!result.ok) {
178
+ return toSource(result, log);
179
+ }
180
+ try {
181
+ const databases = openDatabases(paths);
182
+ closeOnShutdown(databases);
183
+ return createReadySource(result, databases, log);
184
+ }
185
+ catch (error) {
186
+ const problem = `Local storage is unavailable: ${describe(error)}`;
187
+ return { ok: false, problems: [problem], databaseProblems: [problem] };
188
+ }
189
+ }
190
+ function createReadySource(result, databases, log) {
191
+ const bank = createPluggyClient({
192
+ credentials: result.config.credentials,
193
+ clock: systemClock,
194
+ fetch: globalThis.fetch,
195
+ sleep,
196
+ log,
197
+ });
198
+ const reader = createTransactionReader({
199
+ bank,
200
+ store: createTransactionStore(databases.db, log),
201
+ toFailure,
202
+ log,
203
+ clock: systemClock,
204
+ });
205
+ const writer = createCategoryWriter(databases.db, systemClock);
206
+ const closingDays = createClosingDayStore(databases.db, systemClock);
207
+ return toSource(result, log, { reader, writer, closingDays });
208
+ }
209
+ const invocation = resolveInvocation(process.argv.slice(2));
210
+ switch (invocation.kind) {
211
+ case "help":
212
+ say(USAGE);
213
+ break;
214
+ case "version":
215
+ say(readVersion());
216
+ break;
217
+ case "error":
218
+ say(`error: ${invocation.message}\n`);
219
+ say(USAGE);
220
+ process.exitCode = 2;
221
+ break;
222
+ case "command":
223
+ process.exitCode = await run(invocation.command);
224
+ break;
225
+ }
226
+ //# sourceMappingURL=cata-centavo.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cata-centavo.js","sourceRoot":"","sources":["../../src/bin/cata-centavo.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,67 @@
1
+ import { parseArgs } from "node:util";
2
+ /**
3
+ * A `const` object plus a derived union, instead of `enum`.
4
+ *
5
+ * ADR §13 recommends this: `enum` is not erasable syntax, so `erasableSyntaxOnly`
6
+ * rejects it — and a TS string enum is *nominal*, meaning a string arriving from
7
+ * JSON cannot be assigned to it without a cast, precisely where we want real
8
+ * validation instead.
9
+ */
10
+ export const COMMANDS = {
11
+ init: "init",
12
+ doctor: "doctor",
13
+ serve: "serve",
14
+ };
15
+ function isCommand(value) {
16
+ return Object.hasOwn(COMMANDS, value);
17
+ }
18
+ /**
19
+ * Translates argv into an intent. Pure: reads no environment, writes nowhere,
20
+ * never exits the process. That is what makes it testable.
21
+ */
22
+ export function resolveInvocation(argv) {
23
+ let parsed;
24
+ try {
25
+ parsed = parseArgs({
26
+ args: [...argv],
27
+ options: {
28
+ help: { type: "boolean", short: "h" },
29
+ version: { type: "boolean", short: "v" },
30
+ },
31
+ allowPositionals: true,
32
+ strict: true,
33
+ });
34
+ }
35
+ catch (error) {
36
+ // parseArgs throws on an unknown flag. Returning it as a value keeps this
37
+ // function pure and leaves the choice of how to fail with the caller.
38
+ let message;
39
+ if (error instanceof Error) {
40
+ message = error.message;
41
+ }
42
+ else {
43
+ message = String(error);
44
+ }
45
+ return { kind: "error", message };
46
+ }
47
+ if (parsed.values.help === true)
48
+ return { kind: "help" };
49
+ if (parsed.values.version === true)
50
+ return { kind: "version" };
51
+ return commandFrom(parsed.positionals);
52
+ }
53
+ function commandFrom(positionals) {
54
+ const [first, ...rest] = positionals;
55
+ if (rest.length > 0) {
56
+ return { kind: "error", message: `unexpected argument: ${rest[0]}` };
57
+ }
58
+ // No argument = MCP server over stdio (ADR §4).
59
+ if (first === undefined) {
60
+ return { kind: "command", command: COMMANDS.serve };
61
+ }
62
+ if (!isCommand(first)) {
63
+ return { kind: "error", message: `unknown command: ${first}` };
64
+ }
65
+ return { kind: "command", command: first };
66
+ }
67
+ //# sourceMappingURL=dispatch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dispatch.js","sourceRoot":"","sources":["../../src/cli/dispatch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEtC;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG;IACtB,IAAI,EAAE,MAAM;IACZ,MAAM,EAAE,QAAQ;IAChB,KAAK,EAAE,OAAO;CACN,CAAC;AAWX,SAAS,SAAS,CAAC,KAAa;IAC9B,OAAO,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;AACxC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAuB;IACvD,IAAI,MAAM,CAAC;IACX,IAAI,CAAC;QACH,MAAM,GAAG,SAAS,CAAC;YACjB,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC;YACf,OAAO,EAAE;gBACP,IAAI,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,EAAE;gBACrC,OAAO,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,EAAE;aACzC;YACD,gBAAgB,EAAE,IAAI;YACtB,MAAM,EAAE,IAAI;SACb,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,0EAA0E;QAC1E,sEAAsE;QACtE,IAAI,OAAe,CAAC;QACpB,IAAI,KAAK,YAAY,KAAK,EAAE,CAAC;YAC3B,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC;QAC1B,CAAC;aAAM,CAAC;YACN,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;QAC1B,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;IACpC,CAAC;IAED,IAAI,MAAM,CAAC,MAAM,CAAC,IAAI,KAAK,IAAI;QAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IACzD,IAAI,MAAM,CAAC,MAAM,CAAC,OAAO,KAAK,IAAI;QAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;IAE/D,OAAO,WAAW,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;AACzC,CAAC;AAED,SAAS,WAAW,CAAC,WAA8B;IACjD,MAAM,CAAC,KAAK,EAAE,GAAG,IAAI,CAAC,GAAG,WAAW,CAAC;IAErC,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACpB,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,wBAAwB,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;IACvE,CAAC;IAED,gDAAgD;IAChD,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,QAAQ,CAAC,KAAK,EAAE,CAAC;IACtD,CAAC;IAED,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC;QACtB,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,oBAAoB,KAAK,EAAE,EAAE,CAAC;IACjE,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;AAC7C,CAAC"}
@@ -0,0 +1,178 @@
1
+ import { loadConfig } from "../config.js";
2
+ import { diagnose } from "../core/diagnose.js";
3
+ import { describeSync, relativeAgo } from "./sync.js";
4
+ export async function runDoctor(deps) {
5
+ const config = loadConfig(deps.env);
6
+ if (!config.ok) {
7
+ return { kind: "config", problems: config.problems };
8
+ }
9
+ let localState;
10
+ try {
11
+ localState = deps.readLocalState();
12
+ }
13
+ catch (error) {
14
+ return { kind: "storage", reason: describe(error) };
15
+ }
16
+ const bank = deps.createBank(config.config.credentials);
17
+ try {
18
+ await bank.verifyCredentials();
19
+ }
20
+ catch (error) {
21
+ return { kind: "credentials", localState, reason: describe(error) };
22
+ }
23
+ const diagnoses = await diagnose(bank, config.config.itemIds, deps.toFailure, deps.clock);
24
+ return { kind: "checked", localState, diagnoses };
25
+ }
26
+ function isUsable(diagnosis) {
27
+ return diagnosis.connection !== null && diagnosis.state !== "revoked" && diagnosis.state !== "expired";
28
+ }
29
+ export function exitCodeFor(report) {
30
+ switch (report.kind) {
31
+ case "config":
32
+ return 2;
33
+ case "storage":
34
+ case "credentials":
35
+ return 1;
36
+ case "checked":
37
+ if (report.diagnoses.every(isUsable)) {
38
+ return 0;
39
+ }
40
+ return 1;
41
+ }
42
+ }
43
+ /** Lines for stderr. Nothing here may reach stdout (ADR §4). */
44
+ export function formatDoctor(report, clock) {
45
+ switch (report.kind) {
46
+ case "config":
47
+ return ["✗ configuration is incomplete", ...report.problems.map((problem) => ` ${problem}`)];
48
+ case "storage":
49
+ return [`✗ local storage is unreadable: ${report.reason}`];
50
+ case "credentials":
51
+ return [
52
+ ...formatStorage(report.localState),
53
+ "",
54
+ ...formatCache(report.localState, clock),
55
+ "",
56
+ ...formatCategorization(report.localState),
57
+ "",
58
+ `✗ ${report.reason}`,
59
+ ];
60
+ case "checked":
61
+ return formatChecked(report, clock);
62
+ }
63
+ }
64
+ function formatChecked(report, clock) {
65
+ const usable = report.diagnoses.filter(isUsable).length;
66
+ return [
67
+ "connections",
68
+ ...report.diagnoses.flatMap((diagnosis) => formatConnection(diagnosis, clock)),
69
+ "",
70
+ ...formatStorage(report.localState),
71
+ "",
72
+ ...formatCache(report.localState, clock),
73
+ "",
74
+ ...formatCategorization(report.localState),
75
+ "",
76
+ `${usable} of ${report.diagnoses.length} connections are usable`,
77
+ ];
78
+ }
79
+ function formatConnection(diagnosis, clock) {
80
+ if (diagnosis.connection === null) {
81
+ return [` ✗ ${diagnosis.id} — ${diagnosis.failure?.message ?? "unknown failure"}`];
82
+ }
83
+ if (diagnosis.state === "revoked") {
84
+ return [` ✗ ${diagnosis.id} — ${revokedReason(diagnosis.consent, diagnosis.id)}`];
85
+ }
86
+ if (diagnosis.state === "expired") {
87
+ return [` ✗ ${diagnosis.id} — ${expiredReason(diagnosis.consent, diagnosis.id)}`];
88
+ }
89
+ return formatUsableConnection(diagnosis, diagnosis.connection, clock);
90
+ }
91
+ function revokedReason(consent, id) {
92
+ if (consent?.revokedAt === undefined || consent.revokedAt === null) {
93
+ return `${id} has an unreadable consent`;
94
+ }
95
+ return `consent revoked on ${dateOnly(consent.revokedAt)}; re-link this connection to restore access.`;
96
+ }
97
+ function expiredReason(consent, id) {
98
+ if (consent?.expiresAt === undefined || consent.expiresAt === null) {
99
+ return `${id} has an unreadable consent`;
100
+ }
101
+ return `consent expired on ${dateOnly(consent.expiresAt)}.`;
102
+ }
103
+ function connectionMarker(connection) {
104
+ if (connection.warnings.length > 0) {
105
+ return "!";
106
+ }
107
+ return "✓";
108
+ }
109
+ function formatUsableConnection(diagnosis, connection, clock) {
110
+ const { id, state, consent } = diagnosis;
111
+ const sync = describeSync(connection.lastUpdatedAt, clock.now());
112
+ return [
113
+ ` ${connectionMarker(connection)} ${id} ${connection.institution} ${connection.status} ${sync}`,
114
+ ` ${consentLine(state, consent)}`,
115
+ ...connection.warnings.map((warning) => ` ${warning}`),
116
+ ];
117
+ }
118
+ function consentLine(state, consent) {
119
+ if (consent === null) {
120
+ return `consent: ${state}`;
121
+ }
122
+ return `consent: ${state}, ${consent.products.length} products`;
123
+ }
124
+ function dateOnly(date) {
125
+ return date.toISOString().slice(0, 10);
126
+ }
127
+ function formatStorage(state) {
128
+ return [
129
+ "storage",
130
+ ` ✓ cache v${state.cacheVersion} ${state.cacheDb}`,
131
+ ` ✓ data v${state.dataVersion} ${state.dataDb}`,
132
+ ];
133
+ }
134
+ function formatCache(state, clock) {
135
+ return [
136
+ "cache",
137
+ ` ${cacheSummaryLine(state)}`,
138
+ ...[...state.perConnection.entries()].map(([connectionId, entry]) => formatCacheConnection(connectionId, entry, clock)),
139
+ ];
140
+ }
141
+ function cacheSummaryLine(state) {
142
+ if (state.newestLocalDate === null) {
143
+ return "no transactions cached yet";
144
+ }
145
+ return `${state.accountsWalked} accounts walked, newest transaction ${state.newestLocalDate}`;
146
+ }
147
+ function formatCacheConnection(connectionId, entry, clock) {
148
+ const oldestWalkDate = oldestWalkDateOf(entry.oldestWalk);
149
+ return ` ${connectionId} ${entry.accounts} accounts, oldest walk ${relativeAgo(oldestWalkDate, clock.now())}`;
150
+ }
151
+ function oldestWalkDateOf(oldestWalk) {
152
+ if (oldestWalk === null) {
153
+ return null;
154
+ }
155
+ return new Date(oldestWalk);
156
+ }
157
+ function merchantCodesLine(mccRows) {
158
+ if (mccRows > 0) {
159
+ return "merchant codes present";
160
+ }
161
+ return "merchant codes missing";
162
+ }
163
+ function formatCategorization(state) {
164
+ const merchantLine = merchantCodesLine(state.mccRows);
165
+ return [
166
+ "categorization",
167
+ ` snapshot ${state.snapshotRows} transactions`,
168
+ ` counterparty ${state.counterpartyDocuments} documents`,
169
+ ` ${merchantLine}`,
170
+ ];
171
+ }
172
+ function describe(error) {
173
+ if (error instanceof Error) {
174
+ return error.message;
175
+ }
176
+ return String(error);
177
+ }
178
+ //# sourceMappingURL=doctor.js.map