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 +104 -39
- package/build/cli.js +286 -2
- package/build/ledger.js +11 -0
- package/package.json +5 -3
- package/program.md +54 -2
package/README.md
CHANGED
|
@@ -2,22 +2,47 @@
|
|
|
2
2
|
|
|
3
3
|
Accounting by inference, not by engine.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
##
|
|
14
|
+
## Why
|
|
12
15
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
62
|
-
clawbooks verify --balance 50000 --currency USD
|
|
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
|
|
65
|
-
clawbooks summary 2026-03
|
|
66
|
-
clawbooks snapshot 2026-03 --save
|
|
67
|
-
clawbooks assets --as-of 2026-03-31
|
|
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
|
|
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.
|
|
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:
|
|
104
|
-
|
|
105
|
-
|
|
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.
|
|
180
|
+
Point your agent at `program.md` for instructions on how to use clawbooks.
|
|
128
181
|
|
|
129
|
-
- **Claude Code
|
|
130
|
-
- **Codex
|
|
131
|
-
- **Any
|
|
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`
|
|
186
|
+
The npm package includes `program.md` and the policy examples, so this workflow also works from a global install.
|
|
134
187
|
|
|
135
|
-
##
|
|
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
|
|
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.
|
|
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.
|
|
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": "
|
|
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.
|
|
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.
|
|
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
|