clawbooks 0.1.0 → 0.1.2

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
@@ -2,22 +2,47 @@
2
2
 
3
3
  Accounting by inference, not by engine.
4
4
 
5
- An append-only ledger + plain english policy + CLI.
6
- Your LLM agent reads the data, reads the policy, does the accounting.
5
+ Financial memory for agents.
6
+
7
+ Clawbooks is an append-only ledger, a plain-English accounting policy, and a CLI.
8
+ Your agent reads the data, reads the policy, and does the accounting.
9
+
7
10
  No rules engine. No SDK. No framework.
8
11
 
9
12
  **Two source files. Zero runtime dependencies.**
10
13
 
11
- ## Setup
14
+ ## Why
12
15
 
13
- ```bash
14
- git clone https://github.com/rev1ck/clawbooks.git
15
- cd clawbooks
16
- npm install
17
- npm run build
18
- cp policy.md.example policy.md # edit with your own accounting rules
16
+ Most accounting software assumes the product should contain the accounting logic.
17
+ Clawbooks takes the opposite view:
18
+
19
+ - the ledger stores facts
20
+ - the policy states the rules in plain English
21
+ - the agent does the reasoning
22
+
23
+ That makes clawbooks useful anywhere an agent can read files and run shell commands.
24
+
25
+ ## What you get
26
+
27
+ - Append-only JSONL ledger with hash chaining
28
+ - Plain-English policy file instead of embedded bookkeeping logic
29
+ - CLI commands for recording, reviewing, reconciling, compacting, and packaging records
30
+ - Structured `context` output designed for agent reasoning
31
+ - Zero runtime dependencies
32
+
33
+ ## Example
34
+
35
+ ```text
36
+ You: "What's my P&L for March?"
37
+
38
+ Agent runs: clawbooks context 2026-03
39
+ Agent reads: policy + snapshot + events
40
+ Agent reasons: applies the policy to the records
41
+ Agent replies: "Revenue: $1,700. Expenses: $475. Net: $1,225."
19
42
  ```
20
43
 
44
+ There is no accounting engine. In clawbooks, the agent is the engine.
45
+
21
46
  ## Install
22
47
 
23
48
  ```bash
@@ -26,20 +51,20 @@ clawbooks --help
26
51
  cp policy.md.example policy.md
27
52
  ```
28
53
 
29
- ## How it works
30
-
31
- Clawbooks stores financial events and outputs context. The LLM you're already talking to does the accounting.
54
+ ## Local setup
32
55
 
56
+ ```bash
57
+ git clone https://github.com/rev1ck/clawbooks.git
58
+ cd clawbooks
59
+ npm install
60
+ npm run build
61
+ cp policy.md.example policy.md # edit with your own accounting rules
33
62
  ```
34
- You: "What's my P&L for March?"
35
63
 
36
- Agent runs: clawbooks context 2026-03
37
- Agent reads: policy + events
38
- Agent thinks: *applies policy to events*
39
- Agent responds: "Revenue: $1,700. Expenses: $475. Net: $1,225."
40
- ```
64
+ ## How it works
41
65
 
42
- There is no accounting engine. The LLM *is* the engine.
66
+ Clawbooks stores financial events and outputs accounting context.
67
+ The important command is `clawbooks context`: it prints your policy, the latest snapshot, and the relevant events in XML-style blocks so an agent can read and reason over them.
43
68
 
44
69
  ## Commands
45
70
 
@@ -58,13 +83,17 @@ clawbooks context 2026-03
58
83
  clawbooks context --after 2026-01-01
59
84
 
60
85
  # Analysis
61
- clawbooks verify 2026-03 # integrity + chain + duplicates
62
- clawbooks verify --balance 50000 --currency USD # cross-check closing balance
86
+ clawbooks verify 2026-03 # integrity + chain + duplicates
87
+ clawbooks verify --balance 50000 --currency USD # cross-check closing balance
63
88
  clawbooks reconcile 2026-03 --source bank --count 50 --debits -12000 --gaps
64
- clawbooks review --source bank # items needing classification
65
- clawbooks summary 2026-03 # aggregates for reports
66
- clawbooks snapshot 2026-03 --save # persist period snapshot
67
- clawbooks assets --as-of 2026-03-31 # asset register + depreciation
89
+ clawbooks review --source bank # items needing classification
90
+ clawbooks summary 2026-03 # aggregates for reports
91
+ clawbooks snapshot 2026-03 --save # persist period snapshot
92
+ clawbooks assets --as-of 2026-03-31 # asset register + depreciation
93
+
94
+ # Maintenance
95
+ clawbooks compact 2025-12 # archive old events, shrink ledger
96
+ clawbooks pack 2026-03 --out ./march-pack # generate audit pack (CSVs + JSON)
68
97
 
69
98
  # Print the policy
70
99
  clawbooks policy
@@ -72,7 +101,7 @@ clawbooks policy
72
101
 
73
102
  ## The context command
74
103
 
75
- This is the important one. It outputs your accounting policy + the latest snapshot + all events in a period, wrapped in XML tags. The agent reads this output and reasons over it.
104
+ This is the core command. It prints your accounting policy, the latest snapshot, and the events for a period, wrapped in XML tags so the agent can read and reason over them.
76
105
 
77
106
  ```bash
78
107
  $ clawbooks context 2026-03
@@ -95,14 +124,15 @@ Cash basis. Crypto trades are revenue income...
95
124
 
96
125
  ## Importing data
97
126
 
98
- There is no import command. Your agent IS the importer.
127
+ There is no import command. The agent is the importer.
99
128
 
100
- ```
129
+ ```text
101
130
  You: [paste CSV] "Import this bank statement"
102
131
 
103
- Agent: *reads CSV, reads policy via `clawbooks policy`*
104
- *classifies each row per the policy*
105
- *outputs JSONL, pipes to `clawbooks batch`*
132
+ Agent: reads the CSV
133
+ reads policy via `clawbooks policy`
134
+ classifies each row per the policy
135
+ outputs JSONL and pipes it to `clawbooks batch`
106
136
 
107
137
  Agent: "Recorded 47 events from Chase March statement."
108
138
  ```
@@ -122,22 +152,57 @@ clawbooks assets --as-of 2026-03-31
122
152
  clawbooks record '{"source":"manual","type":"disposal","data":{"asset_id":"<id>","proceeds":5000,"currency":"USD"}}'
123
153
  ```
124
154
 
155
+ ## Scaling
156
+
157
+ When the ledger grows large, compact old periods into an archive:
158
+
159
+ ```bash
160
+ clawbooks compact 2025-12
161
+ # -> archives old events to ledger-archive-2025-12-31.jsonl
162
+ # -> rewrites the main ledger as: 1 snapshot + newer events
163
+ ```
164
+
165
+ The archive remains a complete hash-chained ledger for audits. The main ledger stays small enough for agent context windows.
166
+
167
+ ## Audit packs
168
+
169
+ Generate a folder of standard-format files for accountants or auditors:
170
+
171
+ ```bash
172
+ clawbooks pack 2026-01/2026-12-31 --out ./annual-pack
173
+ ```
174
+
175
+ This produces `general_ledger.csv`, `summary.json`, `asset_register.csv`, `reclassifications.csv`, `verify.json`, and a copy of `policy.md`.
176
+ The output is assistive. It gives an accountant structured working material, not a pretend finished report.
177
+
125
178
  ## Agent setup
126
179
 
127
- Point your agent at `program.md` for instructions on how to use clawbooks. For example:
180
+ Point your agent at `program.md` for instructions on how to use clawbooks.
128
181
 
129
- - **Claude Code** add to your `CLAUDE.md`: `Read program.md in the clawbooks directory for financial record-keeping instructions.`
130
- - **Codex** add to your `AGENTS.md` or system prompt with the same pointer
131
- - **Any agent** — any agent that can shell out can use clawbooks. The CLI outputs structured text. The agent reads it and reasons.
182
+ - **Claude Code**: add `Read program.md in the clawbooks directory for financial record-keeping instructions.`
183
+ - **Codex**: add the same pointer in `AGENTS.md` or your system prompt
184
+ - **Any shell-capable agent**: clawbooks prints structured text for the agent to read and reason over
132
185
 
133
- The npm package includes `program.md` plus all policy examples, so this workflow also works from a global install.
186
+ The npm package includes `program.md` and the policy examples, so this workflow also works from a global install.
134
187
 
135
- ## Files
188
+ ## Packaging
136
189
 
190
+ The primary package should stay `clawbooks` for the clean install path.
191
+ If you later want a brand-owned scoped companion package, the repo can stage `@clawbooks/cli` without renaming the live package:
192
+
193
+ ```bash
194
+ npm run scoped:prepare
195
+ npm run scoped:pack:dry-run
137
196
  ```
197
+
198
+ This writes a temporary scoped package into `.dist/scoped-cli` for inspection or future publish work.
199
+
200
+ ## Files
201
+
202
+ ```text
138
203
  cli.ts CLI commands
139
204
  ledger.ts JSONL read/write/filter
140
- program.md Agent instructions (how to use clawbooks)
205
+ program.md Agent instructions
141
206
  policy.md Your accounting rules (you write this, gitignored)
142
207
  policy.md.example Example policy to start from
143
208
  ledger.jsonl Your financial events (append-only, gitignored)
@@ -150,7 +215,7 @@ ledger.jsonl Your financial events (append-only, gitignored)
150
215
  | `CLAWBOOKS_LEDGER` | `./ledger.jsonl` | Path to ledger |
151
216
  | `CLAWBOOKS_POLICY` | `./policy.md` | Path to policy |
152
217
 
153
- No API key needed. The agent brings its own LLM.
218
+ No API key needed. Bring your own agent.
154
219
 
155
220
  ## License
156
221
 
package/build/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { createHash } from "node:crypto";
3
- import { readFileSync, existsSync } from "node:fs";
4
- import { computeId, readAll, filter, append, hashLine, latestSnapshot, } from "./ledger.js";
3
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
4
+ import { computeId, readAll, filter, append, hashLine, rewrite, latestSnapshot, } from "./ledger.js";
5
5
  const LEDGER = process.env.CLAWBOOKS_LEDGER ?? "./ledger.jsonl";
6
6
  const POLICY = process.env.CLAWBOOKS_POLICY ?? "./policy.md";
7
7
  const OUTFLOW_TYPES = new Set([
@@ -762,6 +762,279 @@ function cmdAssets(args) {
762
762
  },
763
763
  }, null, 2));
764
764
  }
765
+ function cmdCompact(args) {
766
+ const f = flags(args);
767
+ const { before } = periodFromArgs(args);
768
+ if (!before) {
769
+ console.error("Usage: clawbooks compact <period> or --before <date>");
770
+ console.error(" Moves events before the cutoff to an archive file and saves a snapshot.");
771
+ console.error(" Example: clawbooks compact 2025-12");
772
+ process.exit(1);
773
+ }
774
+ const all = readAll(LEDGER);
775
+ const keep = all.filter((e) => e.ts > before);
776
+ const archive = all.filter((e) => e.ts <= before);
777
+ if (archive.length === 0) {
778
+ console.log(JSON.stringify({ compacted: false, reason: "no events before cutoff" }));
779
+ return;
780
+ }
781
+ // Build snapshot of archived events
782
+ const balances = {};
783
+ const byCategory = {};
784
+ const pnl = {};
785
+ let eventCount = 0;
786
+ for (const e of archive) {
787
+ if (META_TYPES.has(e.type))
788
+ continue;
789
+ const amount = Number(e.data.amount);
790
+ if (isNaN(amount))
791
+ continue;
792
+ eventCount++;
793
+ const currency = String(e.data.currency ?? "UNKNOWN");
794
+ const category = String(e.data.category ?? e.type);
795
+ balances[currency] = round2((balances[currency] ?? 0) + amount);
796
+ byCategory[category] = round2((byCategory[category] ?? 0) + amount);
797
+ if (!pnl[currency])
798
+ pnl[currency] = { income: 0, expenses: 0, tax: 0, net: 0 };
799
+ if (e.type === "income")
800
+ pnl[currency].income = round2(pnl[currency].income + amount);
801
+ else if (e.type === "tax_payment")
802
+ pnl[currency].tax = round2(pnl[currency].tax + amount);
803
+ else if (OUTFLOW_TYPES.has(e.type))
804
+ pnl[currency].expenses = round2(pnl[currency].expenses + amount);
805
+ pnl[currency].net = round2(pnl[currency].net + amount);
806
+ }
807
+ const snapshotData = {
808
+ period: { after: "all", before },
809
+ event_count: eventCount,
810
+ balances,
811
+ by_category: byCategory,
812
+ pnl,
813
+ compacted_from: archive.length,
814
+ };
815
+ const ts = before;
816
+ const snapshotEvent = {
817
+ ts,
818
+ source: "clawbooks:compact",
819
+ type: "snapshot",
820
+ data: snapshotData,
821
+ id: computeId(snapshotData, {
822
+ source: "clawbooks:compact", type: "snapshot", ts,
823
+ }),
824
+ prev: "",
825
+ };
826
+ // Write archive
827
+ const archivePath = f.archive ?? LEDGER.replace(".jsonl", `-archive-${before.slice(0, 10)}.jsonl`);
828
+ rewrite(archivePath, archive);
829
+ // Rewrite main ledger: snapshot + remaining events
830
+ rewrite(LEDGER, [snapshotEvent, ...keep]);
831
+ console.log(JSON.stringify({
832
+ compacted: true,
833
+ archived: archive.length,
834
+ archive_path: archivePath,
835
+ snapshot_id: snapshotEvent.id,
836
+ remaining: keep.length + 1,
837
+ }, null, 2));
838
+ }
839
+ function csvEscape(val) {
840
+ if (val.includes(",") || val.includes('"') || val.includes("\n")) {
841
+ return '"' + val.replace(/"/g, '""') + '"';
842
+ }
843
+ return val;
844
+ }
845
+ function cmdPack(args) {
846
+ const f = flags(args);
847
+ const { after, before } = periodFromArgs(args);
848
+ const outDir = f.out ?? `./audit-pack-${(before ?? new Date().toISOString()).slice(0, 10)}`;
849
+ const all = readAll(LEDGER);
850
+ const events = filter(all, { after, before, source: f.source });
851
+ mkdirSync(outDir, { recursive: true });
852
+ // --- general_ledger.csv ---
853
+ const glHeader = "date,source,type,category,description,amount,currency,confidence,id";
854
+ const glRows = events
855
+ .filter((e) => !META_TYPES.has(e.type))
856
+ .map((e) => [
857
+ e.ts.slice(0, 10),
858
+ csvEscape(e.source),
859
+ e.type,
860
+ csvEscape(String(e.data.category ?? "")),
861
+ csvEscape(String(e.data.description ?? "")),
862
+ String(e.data.amount ?? ""),
863
+ String(e.data.currency ?? ""),
864
+ String(e.data.confidence ?? ""),
865
+ e.id,
866
+ ].join(","));
867
+ writeFileSync(`${outDir}/general_ledger.csv`, [glHeader, ...glRows].join("\n") + "\n", "utf-8");
868
+ // --- reclassifications.csv ---
869
+ const reclassEvents = all.filter((e) => e.type === "reclassify");
870
+ if (reclassEvents.length > 0) {
871
+ const rcHeader = "date,original_id,new_category,new_type,reason";
872
+ const rcRows = reclassEvents.map((e) => [
873
+ e.ts.slice(0, 10),
874
+ String(e.data.original_id ?? ""),
875
+ csvEscape(String(e.data.new_category ?? "")),
876
+ csvEscape(String(e.data.new_type ?? "")),
877
+ csvEscape(String(e.data.reason ?? "")),
878
+ ].join(","));
879
+ writeFileSync(`${outDir}/reclassifications.csv`, [rcHeader, ...rcRows].join("\n") + "\n", "utf-8");
880
+ }
881
+ // --- summary.json ---
882
+ // Build reclassification map
883
+ const reclassifyMap = {};
884
+ for (const e of all) {
885
+ if (e.type === "reclassify" && e.data.original_id && e.data.new_category) {
886
+ reclassifyMap[String(e.data.original_id)] = String(e.data.new_category);
887
+ }
888
+ }
889
+ const byType = {};
890
+ const byCategory = {};
891
+ const byCurrency = {};
892
+ let inflows = 0, outflows = 0;
893
+ for (const e of events) {
894
+ if (META_TYPES.has(e.type))
895
+ continue;
896
+ const amount = Number(e.data.amount);
897
+ if (isNaN(amount))
898
+ continue;
899
+ const type = e.type;
900
+ const category = reclassifyMap[e.id] ?? String(e.data.category ?? e.type);
901
+ const currency = String(e.data.currency ?? "UNKNOWN");
902
+ if (!byType[type])
903
+ byType[type] = { count: 0, total: 0 };
904
+ byType[type].count++;
905
+ byType[type].total = round2(byType[type].total + amount);
906
+ if (!byCategory[category])
907
+ byCategory[category] = { count: 0, total: 0 };
908
+ byCategory[category].count++;
909
+ byCategory[category].total = round2(byCategory[category].total + amount);
910
+ if (!byCurrency[currency])
911
+ byCurrency[currency] = { count: 0, total: 0 };
912
+ byCurrency[currency].count++;
913
+ byCurrency[currency].total = round2(byCurrency[currency].total + amount);
914
+ if (amount > 0)
915
+ inflows = round2(inflows + amount);
916
+ else
917
+ outflows = round2(outflows + amount);
918
+ }
919
+ writeFileSync(`${outDir}/summary.json`, JSON.stringify({
920
+ period: { after: after ?? "all", before: before ?? "now" },
921
+ by_type: byType,
922
+ by_category: byCategory,
923
+ by_currency: byCurrency,
924
+ cash_flow: { inflows, outflows, net: round2(inflows + outflows) },
925
+ }, null, 2) + "\n", "utf-8");
926
+ // --- asset_register.csv ---
927
+ const capitalizedEvents = all.filter((e) => e.data.capitalize === true);
928
+ if (capitalizedEvents.length > 0) {
929
+ const disposals = {};
930
+ const writeOffsMap = {};
931
+ const impairmentsMap = {};
932
+ for (const e of all) {
933
+ const aid = String(e.data.asset_id ?? "");
934
+ if (!aid)
935
+ continue;
936
+ if (e.type === "disposal")
937
+ disposals[aid] = e;
938
+ else if (e.type === "write_off")
939
+ writeOffsMap[aid] = e;
940
+ else if (e.type === "impairment") {
941
+ if (!impairmentsMap[aid])
942
+ impairmentsMap[aid] = [];
943
+ impairmentsMap[aid].push(e);
944
+ }
945
+ }
946
+ const asOf = before ?? new Date().toISOString();
947
+ const defaultLife = 36;
948
+ const arHeader = "date,description,category,cost,currency,useful_life,monthly_dep,months_elapsed,acc_dep,impairment,nbv,status,proceeds,gain_loss,id";
949
+ const arRows = capitalizedEvents.map((e) => {
950
+ const amount = Math.abs(Number(e.data.amount));
951
+ const lifeMonths = Number(e.data.useful_life_months) || defaultLife;
952
+ const purchaseDate = new Date(e.ts);
953
+ const reportDate = new Date(asOf);
954
+ const monthsElapsed = Math.max(0, (reportDate.getFullYear() - purchaseDate.getFullYear()) * 12 +
955
+ (reportDate.getMonth() - purchaseDate.getMonth()));
956
+ const monthlyDep = round2(amount / lifeMonths);
957
+ const accDep = round2(Math.min(amount, monthlyDep * monthsElapsed));
958
+ let impTotal = 0;
959
+ if (impairmentsMap[e.id]) {
960
+ for (const imp of impairmentsMap[e.id]) {
961
+ impTotal = round2(impTotal + Math.abs(Number(imp.data.impairment_amount) || 0));
962
+ }
963
+ }
964
+ const nbv = round2(Math.max(0, amount - accDep - impTotal));
965
+ let status = "active";
966
+ let proceeds = "";
967
+ let gainLoss = "";
968
+ if (disposals[e.id]) {
969
+ status = "disposed";
970
+ const p = Number(disposals[e.id].data.proceeds) || 0;
971
+ proceeds = String(p);
972
+ gainLoss = String(round2(p - nbv));
973
+ }
974
+ else if (writeOffsMap[e.id]) {
975
+ status = "written_off";
976
+ gainLoss = String(round2(-nbv));
977
+ }
978
+ return [
979
+ e.ts.slice(0, 10),
980
+ csvEscape(String(e.data.description ?? "")),
981
+ csvEscape(String(e.data.category ?? "")),
982
+ String(amount),
983
+ String(e.data.currency ?? ""),
984
+ String(lifeMonths),
985
+ String(monthlyDep),
986
+ String(Math.min(monthsElapsed, lifeMonths)),
987
+ String(accDep),
988
+ String(impTotal),
989
+ status === "active" ? String(nbv) : "0",
990
+ status,
991
+ proceeds,
992
+ gainLoss,
993
+ e.id,
994
+ ].join(",");
995
+ });
996
+ writeFileSync(`${outDir}/asset_register.csv`, [arHeader, ...arRows].join("\n") + "\n", "utf-8");
997
+ }
998
+ // --- verify.json ---
999
+ const hash = createHash("sha256").update(events.map((e) => e.id).join(",")).digest("hex");
1000
+ let debits = 0, credits = 0;
1001
+ const issues = [];
1002
+ for (const e of events) {
1003
+ const amount = Number(e.data.amount);
1004
+ if (e.data.amount !== undefined && !isNaN(amount)) {
1005
+ if (amount < 0)
1006
+ debits = round2(debits + amount);
1007
+ else
1008
+ credits = round2(credits + amount);
1009
+ if (OUTFLOW_TYPES.has(e.type) && amount > 0)
1010
+ issues.push(`${e.id}: outflow "${e.type}" positive ${amount}`);
1011
+ if (INFLOW_TYPES.has(e.type) && amount < 0)
1012
+ issues.push(`${e.id}: inflow "${e.type}" negative ${amount}`);
1013
+ }
1014
+ }
1015
+ writeFileSync(`${outDir}/verify.json`, JSON.stringify({
1016
+ event_count: events.length, debits, credits, hash, issues,
1017
+ generated: new Date().toISOString(),
1018
+ }, null, 2) + "\n", "utf-8");
1019
+ // --- policy.md ---
1020
+ if (existsSync(POLICY)) {
1021
+ writeFileSync(`${outDir}/policy.md`, readFileSync(POLICY, "utf-8"), "utf-8");
1022
+ }
1023
+ // Summary output
1024
+ const files = ["general_ledger.csv", "summary.json", "verify.json"];
1025
+ if (reclassEvents.length > 0)
1026
+ files.push("reclassifications.csv");
1027
+ if (capitalizedEvents.length > 0)
1028
+ files.push("asset_register.csv");
1029
+ if (existsSync(POLICY))
1030
+ files.push("policy.md");
1031
+ console.log(JSON.stringify({
1032
+ pack: outDir,
1033
+ period: { after: after ?? "all", before: before ?? "now" },
1034
+ events: events.length,
1035
+ files,
1036
+ }, null, 2));
1037
+ }
765
1038
  // --- Help ---
766
1039
  const HELP = `clawbooks — accounting by inference, not by engine.
767
1040
 
@@ -782,6 +1055,9 @@ Analysis commands:
782
1055
  snapshot [period] [--save] Compute period snapshot (balances, P&L)
783
1056
  assets [--category C] [--life N] [--as-of DATE]
784
1057
  Asset register (capitalize-flag based) with depreciation
1058
+ compact <period> [--archive PATH] Archive old events, save snapshot, shrink ledger
1059
+ pack [period] [--source S] [--out DIR]
1060
+ Generate audit pack (CSVs + JSON + policy)
785
1061
 
786
1062
  Common flags:
787
1063
  --after <ISO date> Events after this date
@@ -828,6 +1104,8 @@ Examples:
828
1104
  clawbooks summary 2026-03
829
1105
  clawbooks snapshot 2026-03 --save
830
1106
  clawbooks assets --as-of 2026-03-31
1107
+ clawbooks compact 2025-12
1108
+ clawbooks pack 2026-03 --out ./march-pack
831
1109
 
832
1110
  Agent workflow:
833
1111
  1. Agent runs: clawbooks context 2026-03
@@ -877,5 +1155,11 @@ switch (cmd) {
877
1155
  case "assets":
878
1156
  cmdAssets(args);
879
1157
  break;
1158
+ case "compact":
1159
+ cmdCompact(args);
1160
+ break;
1161
+ case "pack":
1162
+ cmdPack(args);
1163
+ break;
880
1164
  default: console.log(HELP);
881
1165
  }
package/build/ledger.js CHANGED
@@ -52,6 +52,17 @@ export function append(path, event) {
52
52
  appendFileSync(path, JSON.stringify(event) + "\n", "utf-8");
53
53
  return true;
54
54
  }
55
+ export function rewrite(path, events) {
56
+ let prev = "genesis";
57
+ const lines = [];
58
+ for (const e of events) {
59
+ e.prev = prev;
60
+ const line = JSON.stringify(e);
61
+ prev = hashLine(line);
62
+ lines.push(line);
63
+ }
64
+ writeFileSync(path, lines.join("\n") + (lines.length ? "\n" : ""), "utf-8");
65
+ }
55
66
  export function latestSnapshot(events, before) {
56
67
  let snapshots = events.filter((e) => e.type === "snapshot");
57
68
  if (before)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawbooks",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Accounting by inference, not by engine. Zero dependencies.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -20,12 +20,14 @@
20
20
  "llm"
21
21
  ],
22
22
  "bin": {
23
- "clawbooks": "./build/cli.js"
23
+ "clawbooks": "build/cli.js"
24
24
  },
25
25
  "scripts": {
26
26
  "build": "tsc && chmod 755 build/cli.js",
27
27
  "prepare": "npm run build",
28
- "prepack": "npm run build"
28
+ "prepack": "npm run build",
29
+ "scoped:prepare": "npm run build && node scripts/prepare-scoped-package.mjs",
30
+ "scoped:pack:dry-run": "npm run scoped:prepare && node scripts/prepare-scoped-package.mjs --pack --dry-run"
29
31
  },
30
32
  "files": [
31
33
  "build",
package/program.md CHANGED
@@ -113,12 +113,13 @@ Reduces carrying value by the impairment amount. Multiple impairments can accumu
113
113
 
114
114
  When asked for a P&L, tax summary, balance, etc.:
115
115
 
116
- 1. Run `clawbooks summary <period>` for pre-computed aggregates
116
+ 1. **Start with `summary`**, not `context`. `clawbooks summary <period>` gives you pre-computed aggregates without loading every event into context.
117
117
  2. Map the output to the requested report:
118
118
  - **P&L**: `by_type` + `by_category` → Revenue - OpEx = Gross Profit - Tax = Net
119
119
  - **Balance Sheet**: `cash_flow.net` + opening balance (from snapshot or opening_balance events) → Assets. Capitalized assets from `clawbooks assets`. Equity = Assets - Liabilities
120
120
  - **Cash Flow Statement**: Map categories to Operating/Investing/Financing per policy
121
- 3. For details or edge cases, also run `clawbooks context <period>` and reason over raw events
121
+ 3. Only use `clawbooks context <period>` when you need to drill into individual events — e.g., investigating a specific transaction, answering "what was that $500 charge?", or debugging a reconciliation mismatch.
122
+ 4. For large ledgers, use `clawbooks pack <period>` to generate a full audit pack (CSVs + JSON) that you or an accountant can review outside the agent.
122
123
 
123
124
  ## Reconciliation workflow
124
125
 
@@ -159,6 +160,42 @@ clawbooks snapshot 2026-03 # compute and print (no save)
159
160
 
160
161
  The snapshot includes balances by currency, totals by category, and P&L by currency.
161
162
 
163
+ ## Compacting the ledger
164
+
165
+ When the ledger grows large (thousands of events), compact old periods into an archive:
166
+
167
+ ```bash
168
+ clawbooks compact 2025-12
169
+ ```
170
+
171
+ This:
172
+ 1. Saves a snapshot summarizing all events up to the cutoff
173
+ 2. Moves those events to `ledger-archive-2025-12-31.jsonl`
174
+ 3. Rewrites the main ledger with just the snapshot + newer events
175
+
176
+ The archive file is a complete, hash-chained ledger — it can be re-read for audits. The main ledger stays small for fast context loading.
177
+
178
+ Compact aggressively for busy ledgers. Monthly or quarterly compaction keeps context manageable.
179
+
180
+ ## Audit packs
181
+
182
+ Generate a folder of CSVs and JSON for accountants, auditors, or your own review:
183
+
184
+ ```bash
185
+ clawbooks pack 2026-03 # pack a single month
186
+ clawbooks pack 2026-01/2026-12-31 --out ./annual-pack # pack a full year
187
+ ```
188
+
189
+ The pack includes:
190
+ - `general_ledger.csv` — every transaction with date, source, type, category, description, amount, currency, confidence, id
191
+ - `summary.json` — aggregates by type, category, currency, and cash flow
192
+ - `asset_register.csv` — capitalized assets with depreciation, disposal, write-off status (if any)
193
+ - `reclassifications.csv` — all reclassification events (if any)
194
+ - `verify.json` — integrity hash, debit/credit totals, issues
195
+ - `policy.md` — copy of the accounting policy applied
196
+
197
+ These files are assistive — they give the accountant standard-format data to work with. The agent can also read them back to answer questions.
198
+
162
199
  ## Quick reference
163
200
 
164
201
  ```
@@ -177,8 +214,23 @@ clawbooks summary [period] # pre-computed aggregates for reports
177
214
  clawbooks snapshot [period] [--save] # compute period snapshot (balances, P&L)
178
215
  clawbooks assets [--category C] [--life N] [--as-of DATE]
179
216
  # asset register (capitalize-flag based)
217
+ clawbooks compact <period> # archive old events, shrink ledger
218
+ clawbooks pack [period] [--out DIR] # generate audit pack (CSVs + JSON)
180
219
  ```
181
220
 
221
+ ## Improving the policy
222
+
223
+ The accounting policy (`policy.md`) should improve over time as you process more data. After classification review cycles:
224
+
225
+ 1. Run `clawbooks review` to see reclassifications and patterns
226
+ 2. If you notice repeated corrections (e.g., "GITHUB" always gets reclassified from `office_supplies` to `software`), update `policy.md` with the new rule
227
+ 3. Add the rule to the appropriate section (expense classification, source-specific rules, etc.)
228
+ 4. Be specific — "GitHub charges are software subscriptions" is better than "tech charges are software"
229
+
230
+ The goal is that each import gets more accurate as the policy captures learned patterns. The agent should proactively suggest policy updates when it sees recurring reclassifications, but should not update the policy without the user's awareness.
231
+
232
+ When updating the policy, keep it plain english. The policy is read by the agent on every `context` call — it should be clear, concise, and actionable.
233
+
182
234
  ## Idempotent imports
183
235
 
184
236
  When importing from a source (CSV, statement), include a stable `data.ref` field derived