plasalid 0.3.0 → 0.3.3

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 CHANGED
@@ -11,13 +11,13 @@
11
11
 
12
12
  <br />
13
13
 
14
- Plasalid lets you actually *talk* to your money. Drop the bank and credit-card statements you already receive into a folder; Plasalid scans them into a queryable, double-entry database on your own machine. Then chat with it — an AI partner that has read every line and tells you, sharply and proactively, what's going on with your money. Local, private, yours.
14
+ Plasalid lets you actually *talk* to your money. Drop your bank and credit-card statement PDFs into a folder. Plasalid scans every transaction, every balance, every late fee into a double-entry database on your own machine. Then you chat with it — an AI that's read every line and tells you, sharply and proactively, what's going on. Local, private, yours.
15
15
 
16
- Plasalid exists because in markets like Thailand there's no Plaid: bank data is locked behind statement PDFs, and most people can't afford a real financial advisor either. Plasalid closes both gaps. Your statements become a database, and an AI that knows that database becomes the money coach you'd otherwise have to hire. Your machine, your encryption key, your data.
16
+ Plasalid exists because in markets like Thailand there's no Plaid. In the US, a single API gives apps a live view of every balance and transaction across all your accounts one complete picture of your money. In Thailand, knowing where you stand means logging into five different bank apps in sequence and opening password-protected statement PDFs one by one. Most people just don't bother. And most people can't afford a financial advisor to do it for them either.
17
17
 
18
- In Thailand, personal finance isn't taught in school. Fee-based advisors are out of reach for most households. The loudest "advice" channels are bank salespeople pitching their employer's products. Over 5 million people are already flagged as non-performing borrowers, and a generation that wants to manage money better has nowhere accessible to learn how. Plasalid's bet is that capable AI changes this the same intelligence that can read a statement can also explain what the numbers mean, surface what's about to go wrong, and coach someone through real decisions about debt, budget, and savings. At zero marginal cost, in your language, on your own machine.
18
+ Personal finance isn't taught well in Thai schools. Fee-based advisors are out of reach for most households. The loudest "advice" channels are bank salespeople pitching their employer's products. The result: over **5 million Thais** are already flagged as non-performing borrowers, and a generation that wants to manage money better has nowhere accessible to learn how. Plasalid's bet is that capable AI changes that. The same intelligence that reads your statements can explain what the numbers mean. It flags what's about to go wrong. It coaches you through real decisions debt, budget, savings.
19
19
 
20
- Plasalid can also scales upward in case financial survival isn't your question help you setup a goal for vacation trip, building an emergency fund, choosing investments, planning a down payment or retirement, working toward the freedom to walk away from a bad job. From getting out of debt to financial freedom, Plasalid grows with you.
20
+ And when survival isn't the question anymore, the same Plasalid can scales up with you. Saving for a trip. Building an emergency fund. Choosing investments. Planning a down payment or retirement. Working toward the freedom to walk away from a bad job. From getting out of debt to financial freedom, Plasalid grows with you.
21
21
 
22
22
  ## Features
23
23
 
package/dist/cli/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from "commander";
3
3
  import { createRequire } from "module";
4
+ import chalk from "chalk";
4
5
  import { config, isConfigured } from "../config.js";
5
6
  import { helpScreen } from "./format.js";
6
7
  const require = createRequire(import.meta.url);
@@ -17,6 +18,7 @@ program
17
18
  .description("The local-first data layer for personal finance")
18
19
  .version(version)
19
20
  .addHelpCommand(false)
21
+ .showHelpAfterError(`Run ${chalk.cyan("plasalid --help")} for the list of commands.`)
20
22
  .action(async () => {
21
23
  if (!isConfigured()) {
22
24
  console.log("Plasalid is not configured yet. Running setup...\n");
@@ -36,6 +38,7 @@ program
36
38
  });
37
39
  program
38
40
  .command("data")
41
+ .alias("open")
39
42
  .description("Open the Plasalid data folder in your OS file explorer")
40
43
  .action(async () => {
41
44
  const { runDataCommand } = await import("./commands/data.js");
@@ -77,13 +80,24 @@ program
77
80
  });
78
81
  });
79
82
  program
80
- .command("scan [regex]")
83
+ .command("scan [regex...]")
81
84
  .description("Scan every new PDF under ~/.plasalid/data (optionally filtered by regex)")
82
85
  .option("-f, --force", "Re-scan matching files (cascade-deletes prior records)")
83
- .action(async (regex, opts) => {
86
+ .action(async (regexes, opts) => {
84
87
  ensureConfigured();
88
+ if (regexes.length > 1) {
89
+ console.error(chalk.red(`scan takes a single regex (or none). got ${regexes.length} arguments — your shell likely expanded a glob like '*' to filenames.`));
90
+ console.error("");
91
+ console.error("To scan everything in the data dir:");
92
+ console.error(` ${chalk.cyan("plasalid scan")}`);
93
+ console.error("");
94
+ console.error("To filter with a regex, quote it:");
95
+ console.error(` ${chalk.cyan("plasalid scan '.*'")}`);
96
+ console.error(` ${chalk.cyan("plasalid scan 'KBank|SCB'")}`);
97
+ process.exit(1);
98
+ }
85
99
  const { runScanCommand } = await import("./commands/scan.js");
86
- await runScanCommand({ regex, force: !!opts.force });
100
+ await runScanCommand({ regex: regexes[0], force: !!opts.force });
87
101
  });
88
102
  program
89
103
  .command("reconcile")
@@ -112,14 +126,32 @@ program
112
126
  });
113
127
  program.configureHelp({
114
128
  formatHelp: () => helpScreen([
115
- { name: "setup", desc: "Configure Plasalid (API key, encryption, data dir)" },
116
- { name: "data", desc: "Open the data folder in your OS file explorer" },
129
+ {
130
+ name: "setup",
131
+ desc: "Configure Plasalid (API key, encryption, data dir)",
132
+ },
133
+ {
134
+ name: "data",
135
+ desc: "Open the data folder in your OS file explorer (alias: open)",
136
+ },
117
137
  { name: "accounts", desc: "Show the chart of accounts with balances" },
118
138
  { name: "status", desc: "Show net worth and this-month totals" },
119
- { name: "transactions", desc: "List journal lines (filter by account/date/text)" },
120
- { name: "scan", desc: "Scan new PDFs (optionally by regex; --force to re-scan)" },
121
- { name: "reconcile", desc: "Review and fix existing journal entries / accounts" },
122
- { name: "revert", desc: "Delete scanned files matching <regex> and their journal entries" },
139
+ {
140
+ name: "transactions",
141
+ desc: "List journal lines (filter by account/date/text)",
142
+ },
143
+ {
144
+ name: "scan",
145
+ desc: "Scan new PDFs (optionally by regex; --force to re-scan)",
146
+ },
147
+ {
148
+ name: "reconcile",
149
+ desc: "Review and fix existing journal entries / accounts",
150
+ },
151
+ {
152
+ name: "revert",
153
+ desc: "Delete scanned files matching <regex> and their journal entries",
154
+ },
123
155
  ]),
124
156
  });
125
157
  void config; // keep config import live so dotenv loads
@@ -4,7 +4,7 @@ import type { StoredPassword } from "./password-store.js";
4
4
  * (mupdf calls, prompts, DB reads) live in the orchestrator; this module only
5
5
  * encodes the transition logic so it can be exhaustively unit-tested.
6
6
  */
7
- export declare const MAX_PASSWORD_ATTEMPTS = 3;
7
+ export declare const MAX_PASSWORD_ATTEMPTS = 10;
8
8
  export type UnlockOutcome = {
9
9
  kind: "plaintext";
10
10
  } | {
@@ -3,7 +3,7 @@
3
3
  * (mupdf calls, prompts, DB reads) live in the orchestrator; this module only
4
4
  * encodes the transition logic so it can be exhaustively unit-tested.
5
5
  */
6
- export const MAX_PASSWORD_ATTEMPTS = 3;
6
+ export const MAX_PASSWORD_ATTEMPTS = 10;
7
7
  export function isTerminal(state) {
8
8
  return state.kind === "done" || state.kind === "failed";
9
9
  }
@@ -3,7 +3,7 @@ import { basename } from "path";
3
3
  import { config } from "../config.js";
4
4
  import { statusSpinner } from "../cli/ux.js";
5
5
  import { findCandidates, savePassword, recordUse, suggestPattern, } from "./password-store.js";
6
- import { transition, isTerminal, } from "./state-machine.js";
6
+ import { transition, isTerminal, MAX_PASSWORD_ATTEMPTS, } from "./state-machine.js";
7
7
  import { isEncrypted, unlock } from "./pdf-unlock.js";
8
8
  /**
9
9
  * Drive the pure unlock state machine to a terminal state, returning the
@@ -59,7 +59,7 @@ async function stepUnlock(state, ctx) {
59
59
  spinner.succeed("Decrypted.");
60
60
  return { kind: "UNLOCK_OK", decrypted: result.decrypted, password };
61
61
  }
62
- spinner.fail(`Incorrect password (attempt ${state.attempt}/3).`);
62
+ spinner.fail(`Incorrect password (attempt ${state.attempt}/${MAX_PASSWORD_ATTEMPTS}).`);
63
63
  return { kind: "UNLOCK_FAIL" };
64
64
  }
65
65
  default:
@@ -90,7 +90,7 @@ async function tryStoredPasswords(bytes, candidates) {
90
90
  async function promptForPassword(fileName, attempt) {
91
91
  const message = attempt === 1
92
92
  ? `This PDF is encrypted. Password for ${fileName}:`
93
- : `Password for ${fileName} (attempt ${attempt}/3):`;
93
+ : `Password for ${fileName} (attempt ${attempt}/${MAX_PASSWORD_ATTEMPTS}):`;
94
94
  const { password } = await inquirer.prompt([
95
95
  { type: "password", name: "password", mask: "*", message },
96
96
  ]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plasalid",
3
- "version": "0.3.0",
3
+ "version": "0.3.3",
4
4
  "description": "A local-first AI that reads every line of your transactions and coaches you the best move.",
5
5
  "keywords": [
6
6
  "finance",