arek-e-docsnap 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 (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +314 -0
  3. package/dist/bin/cli.d.ts +14 -0
  4. package/dist/bin/cli.d.ts.map +1 -0
  5. package/dist/cli/App.d.ts +15 -0
  6. package/dist/cli/App.d.ts.map +1 -0
  7. package/dist/cli/screens/ClassificationScreen.d.ts +13 -0
  8. package/dist/cli/screens/ClassificationScreen.d.ts.map +1 -0
  9. package/dist/cli/screens/CompleteScreen.d.ts +13 -0
  10. package/dist/cli/screens/CompleteScreen.d.ts.map +1 -0
  11. package/dist/cli/screens/PartnerSetupScreen.d.ts +8 -0
  12. package/dist/cli/screens/PartnerSetupScreen.d.ts.map +1 -0
  13. package/dist/cli/screens/SettlementPreviewScreen.d.ts +13 -0
  14. package/dist/cli/screens/SettlementPreviewScreen.d.ts.map +1 -0
  15. package/dist/cli/screens/TransactionReviewScreen.d.ts +13 -0
  16. package/dist/cli/screens/TransactionReviewScreen.d.ts.map +1 -0
  17. package/dist/cli/state.d.ts +79 -0
  18. package/dist/cli/state.d.ts.map +1 -0
  19. package/dist/cli.js +33723 -0
  20. package/dist/domain/errors.d.ts +70 -0
  21. package/dist/domain/errors.d.ts.map +1 -0
  22. package/dist/domain/models.d.ts +289 -0
  23. package/dist/domain/models.d.ts.map +1 -0
  24. package/dist/domain/settlement.d.ts +30 -0
  25. package/dist/domain/settlement.d.ts.map +1 -0
  26. package/dist/index.d.ts +18 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +312 -0
  29. package/dist/layers/config.d.ts +14 -0
  30. package/dist/layers/config.d.ts.map +1 -0
  31. package/dist/layers/filesystem.d.ts +16 -0
  32. package/dist/layers/filesystem.d.ts.map +1 -0
  33. package/dist/layers/logger.d.ts +16 -0
  34. package/dist/layers/logger.d.ts.map +1 -0
  35. package/dist/pdf.worker.mjs +28 -0
  36. package/dist/services/export.d.ts +56 -0
  37. package/dist/services/export.d.ts.map +1 -0
  38. package/dist/services/ocr-tesseract.d.ts +2 -0
  39. package/dist/services/ocr-tesseract.d.ts.map +1 -0
  40. package/dist/services/ocr.d.ts +32 -0
  41. package/dist/services/ocr.d.ts.map +1 -0
  42. package/dist/utils/compression.d.ts +31 -0
  43. package/dist/utils/compression.d.ts.map +1 -0
  44. package/dist/utils/console.d.ts +27 -0
  45. package/dist/utils/console.d.ts.map +1 -0
  46. package/dist/utils/progress.d.ts +30 -0
  47. package/dist/utils/progress.d.ts.map +1 -0
  48. package/dist/utils/transaction-parser.d.ts +41 -0
  49. package/dist/utils/transaction-parser.d.ts.map +1 -0
  50. package/package.json +95 -0
package/dist/index.js ADDED
@@ -0,0 +1,312 @@
1
+ // src/domain/models.ts
2
+ import * as S from "effect/Schema";
3
+
4
+ class Partner extends S.Class("Partner")({
5
+ id: S.String,
6
+ name: S.String.pipe(S.minLength(1)),
7
+ color: S.optional(S.String)
8
+ }) {
9
+ }
10
+
11
+ class RawTransaction extends S.Class("RawTransaction")({
12
+ dateStr: S.String,
13
+ description: S.String.pipe(S.minLength(1)),
14
+ amountStr: S.String,
15
+ cardholder: S.optional(S.String),
16
+ ocrConfidence: S.optional(S.Number.pipe(S.between(0, 1)))
17
+ }) {
18
+ }
19
+
20
+ class Transaction extends S.Class("Transaction")({
21
+ id: S.String,
22
+ date: S.Date,
23
+ description: S.String.pipe(S.minLength(1), S.maxLength(500)),
24
+ amount: S.Number.pipe(S.int(), S.positive()),
25
+ currency: S.Literal("SEK", "EUR", "USD", "CHF"),
26
+ category: S.String,
27
+ confidence: S.Number.pipe(S.between(0, 1))
28
+ }) {
29
+ }
30
+
31
+ class ExpenseItem extends S.Class("ExpenseItem")({
32
+ id: S.String,
33
+ date: S.Date,
34
+ description: S.String.pipe(S.minLength(1), S.maxLength(500)),
35
+ amount: S.Number.pipe(S.int(), S.positive()),
36
+ currency: S.Literal("SEK", "EUR", "USD", "CHF"),
37
+ category: S.String,
38
+ confidence: S.Number.pipe(S.between(0, 1)),
39
+ partner: S.String,
40
+ isShared: S.Boolean,
41
+ notes: S.optional(S.String)
42
+ }) {
43
+ }
44
+
45
+ class ExpenseDocument extends S.Class("ExpenseDocument")({
46
+ id: S.String,
47
+ date: S.Date,
48
+ source: S.String,
49
+ items: S.Array(ExpenseItem),
50
+ partners: S.Array(Partner)
51
+ }) {
52
+ }
53
+
54
+ class Settlement extends S.Class("Settlement")({
55
+ from: S.String,
56
+ to: S.String,
57
+ amount: S.Number.pipe(S.int(), S.positive()),
58
+ reason: S.String
59
+ }) {
60
+ }
61
+
62
+ class ItemSummary extends S.Class("ItemSummary")({
63
+ byPartner: S.Record({
64
+ key: S.String,
65
+ value: S.Struct({
66
+ count: S.Number,
67
+ total: S.Number
68
+ })
69
+ }),
70
+ byCategory: S.Record({
71
+ key: S.String,
72
+ value: S.Struct({
73
+ count: S.Number,
74
+ total: S.Number
75
+ })
76
+ })
77
+ }) {
78
+ }
79
+
80
+ class ExpenseReport extends S.Class("ExpenseReport")({
81
+ document: ExpenseDocument,
82
+ itemsSummary: ItemSummary,
83
+ settlements: S.Array(Settlement)
84
+ }) {
85
+ }
86
+
87
+ class AppConfig extends S.Class("AppConfig")({
88
+ defaultPartners: S.Array(S.String),
89
+ defaultCategories: S.Array(S.String),
90
+ language: S.Literal("en", "sv", "de", "fr"),
91
+ currency: S.Literal("SEK", "EUR", "USD", "CHF"),
92
+ outputFormat: S.Literal("xlsx", "csv", "json"),
93
+ autoOpenExcel: S.Boolean,
94
+ debug: S.optional(S.Boolean)
95
+ }) {
96
+ }
97
+
98
+ class StatementMetadata extends S.Class("StatementMetadata")({
99
+ statementDate: S.Date,
100
+ source: S.String,
101
+ totalTransactions: S.Number.pipe(S.int(), S.positive()),
102
+ downloadDate: S.Date,
103
+ currency: S.Literal("SEK", "EUR", "USD", "CHF"),
104
+ language: S.String
105
+ }) {
106
+ }
107
+ // src/domain/errors.ts
108
+ class FileError extends Error {
109
+ _tag = "FileError";
110
+ path;
111
+ message;
112
+ constructor(path, message) {
113
+ super(message);
114
+ this.name = "FileError";
115
+ this.path = path;
116
+ this.message = message;
117
+ }
118
+ }
119
+ var isParseError = (error) => error._tag === "ParseError";
120
+ var isOCRError = (error) => error._tag === "OCRError";
121
+ var isValidationError = (error) => error._tag === "ValidationError";
122
+ var isClassificationError = (error) => error._tag === "ClassificationError";
123
+ var isFileError = (error) => error._tag === "FileError";
124
+ var isUserCancelled = (error) => error._tag === "UserCancelled";
125
+ var isUnknownError = (error) => error._tag === "UnknownError";
126
+ var errorMessage = (error) => {
127
+ switch (error._tag) {
128
+ case "ParseError":
129
+ return `Parse Error${error.line ? ` (line ${error.line})` : ""}: ${error.reason}`;
130
+ case "OCRError":
131
+ return `OCR Error (page ${error.pageNumber}, confidence ${(error.confidence * 100).toFixed(0)}%): ${error.reason}`;
132
+ case "ValidationError":
133
+ return `Validation Error (${error.field}): ${error.reason}`;
134
+ case "ClassificationError":
135
+ return `Classification Error (item ${error.itemId}): ${error.reason}`;
136
+ case "FileError":
137
+ return `File Error (${error.path}): ${error.message}`;
138
+ case "UserCancelled":
139
+ return `Operation Cancelled: ${error.message}`;
140
+ case "UnknownError":
141
+ return `Unknown Error: ${error.message}`;
142
+ default:
143
+ return "Unknown error occurred";
144
+ }
145
+ };
146
+ // src/domain/settlement.ts
147
+ var calculateSettlement = (items, partners) => {
148
+ const balances = new Map;
149
+ for (const partner of partners) {
150
+ balances.set(partner.id, 0);
151
+ }
152
+ for (const item of items) {
153
+ if (item.isShared) {
154
+ const sharePerPerson = item.amount / partners.length;
155
+ for (const partner of partners) {
156
+ if (partner.id === item.partner) {
157
+ const currentBalance = balances.get(partner.id) ?? 0;
158
+ balances.set(partner.id, currentBalance + (item.amount - sharePerPerson));
159
+ } else {
160
+ const currentBalance = balances.get(partner.id) ?? 0;
161
+ balances.set(partner.id, currentBalance - sharePerPerson);
162
+ }
163
+ }
164
+ }
165
+ }
166
+ const settlements = [];
167
+ const creditors = Array.from(balances.entries()).filter(([, balance]) => balance > 0);
168
+ const debtorsArray = Array.from(balances.entries()).filter(([, balance]) => balance < 0);
169
+ for (const [creditorId, owed] of creditors) {
170
+ let remaining = owed;
171
+ for (let i = 0;i < debtorsArray.length && remaining > 0; i++) {
172
+ const debtor = debtorsArray[i];
173
+ if (!debtor)
174
+ break;
175
+ const [debtorId, debt] = debtor;
176
+ const amount = Math.min(remaining, Math.abs(debt));
177
+ const creditor = partners.find((p) => p.id === creditorId);
178
+ const debtorPartner = partners.find((p) => p.id === debtorId);
179
+ if (creditor && debtorPartner) {
180
+ settlements.push({
181
+ from: debtorId,
182
+ to: creditorId,
183
+ amount,
184
+ reason: "Shared expenses settlement"
185
+ });
186
+ }
187
+ remaining -= amount;
188
+ if (debtor) {
189
+ debtorsArray[i] = [debtorId, debt + amount];
190
+ }
191
+ }
192
+ }
193
+ return { balances, settlements };
194
+ };
195
+ var formatSettlementAmount = (amountCents, currency = "SEK") => {
196
+ const amount = (amountCents / 100).toFixed(2);
197
+ return `${amount} ${currency}`;
198
+ };
199
+ var settlementSummary = (settlements, partners, currency = "SEK") => {
200
+ if (settlements.length === 0) {
201
+ return "No settlements needed - expenses are balanced!";
202
+ }
203
+ const lines = ["Settlement Summary:", ""];
204
+ for (const settlement of settlements) {
205
+ const from = partners.get(settlement.from);
206
+ const to = partners.get(settlement.to);
207
+ if (from && to) {
208
+ const amount = formatSettlementAmount(settlement.amount, currency);
209
+ lines.push(` ✓ ${from.name} owes ${to.name}: ${amount}`);
210
+ }
211
+ }
212
+ return lines.join(`
213
+ `);
214
+ };
215
+ // src/layers/logger.ts
216
+ import { Context, Effect, Layer } from "effect";
217
+ var Logger = Context.GenericTag("Logger");
218
+ var ConsoleLogger = Layer.succeed(Logger, {
219
+ debug: (message) => Effect.sync(() => {
220
+ if (process.env.DEBUG) {
221
+ console.log(`[DEBUG] ${message}`);
222
+ }
223
+ }),
224
+ info: (message) => Effect.sync(() => {
225
+ console.log(`[INFO] ${message}`);
226
+ }),
227
+ warn: (message) => Effect.sync(() => {
228
+ console.warn(`[WARN] ${message}`);
229
+ }),
230
+ error: (message, error) => Effect.sync(() => {
231
+ console.error(`[ERROR] ${message}`);
232
+ if (error) {
233
+ console.error(error.stack);
234
+ }
235
+ })
236
+ });
237
+ var LoggerLive = ConsoleLogger;
238
+ // src/layers/filesystem.ts
239
+ import { Context as Context2, Effect as Effect2, Layer as Layer2 } from "effect";
240
+ var FileSystem = Context2.GenericTag("FileSystem");
241
+ var NativeFileSystem = Layer2.succeed(FileSystem, {
242
+ readFile: (path) => Effect2.tryPromise({
243
+ try: () => Bun.file(path).arrayBuffer().then((b) => Buffer.from(b)),
244
+ catch: (error) => new FileError(path, `Failed to read file: ${String(error)}`)
245
+ }),
246
+ writeFile: (path, content) => Effect2.tryPromise({
247
+ try: () => {
248
+ const file = Bun.file(path);
249
+ return Bun.write(file, content);
250
+ },
251
+ catch: (error) => new FileError(path, `Failed to write file: ${String(error)}`)
252
+ }).pipe(Effect2.map(() => {
253
+ return;
254
+ })),
255
+ exists: (path) => Effect2.sync(() => {
256
+ try {
257
+ return Bun.file(path).size > 0;
258
+ } catch {
259
+ return false;
260
+ }
261
+ })
262
+ });
263
+ // src/layers/config.ts
264
+ import { Context as Context3, Effect as Effect3, Layer as Layer3 } from "effect";
265
+ var ConfigService = Context3.GenericTag("ConfigService");
266
+ var DEFAULT_CONFIG = {
267
+ defaultPartners: ["Person A", "Person B"],
268
+ defaultCategories: [
269
+ "Groceries",
270
+ "Utilities",
271
+ "Transport",
272
+ "Dining",
273
+ "Entertainment",
274
+ "Personal",
275
+ "Other"
276
+ ],
277
+ language: "en",
278
+ currency: "SEK",
279
+ outputFormat: "xlsx",
280
+ autoOpenExcel: true,
281
+ debug: false
282
+ };
283
+ var DefaultConfigService = Layer3.succeed(ConfigService, {
284
+ getConfig: () => Effect3.sync(() => DEFAULT_CONFIG)
285
+ });
286
+ var provideConfigService = Layer3.provide(DefaultConfigService);
287
+ export {
288
+ settlementSummary,
289
+ isValidationError,
290
+ isUserCancelled,
291
+ isUnknownError,
292
+ isParseError,
293
+ isOCRError,
294
+ isFileError,
295
+ isClassificationError,
296
+ formatSettlementAmount,
297
+ errorMessage,
298
+ calculateSettlement,
299
+ Transaction as TransactionClass,
300
+ StatementMetadata as StatementMetadataClass,
301
+ Settlement as SettlementClass,
302
+ RawTransaction as RawTransactionClass,
303
+ Partner as PartnerClass,
304
+ Logger as LoggerTag,
305
+ LoggerLive,
306
+ FileSystem as FileSystemTag,
307
+ ExpenseReport as ExpenseReportClass,
308
+ ExpenseItem as ExpenseItemClass,
309
+ ExpenseDocument as ExpenseDocumentClass,
310
+ ConfigService as ConfigServiceTag,
311
+ AppConfig as AppConfigClass
312
+ };
@@ -0,0 +1,14 @@
1
+ import { Context, Effect, Layer } from "effect";
2
+ import { AppConfig } from "../domain/models";
3
+ /**
4
+ * Config Service Layer
5
+ *
6
+ * Manages application configuration
7
+ * Loads from ~/.docsnap/config.json or uses defaults
8
+ */
9
+ export interface ConfigService {
10
+ readonly getConfig: () => Effect.Effect<AppConfig>;
11
+ }
12
+ export declare const ConfigService: Context.Tag<ConfigService, ConfigService>;
13
+ export declare const provideConfigService: <RIn2, E2, ROut2>(self: Layer.Layer<ROut2, E2, RIn2>) => Layer.Layer<ROut2, E2, Exclude<RIn2, ConfigService>>;
14
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/layers/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAChD,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAE7C;;;;;GAKG;AAEH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,SAAS,EAAE,MAAM,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;CACpD;AAED,eAAO,MAAM,aAAa,2CAAqD,CAAC;AAiChF,eAAO,MAAM,oBAAoB,+GAAsC,CAAC"}
@@ -0,0 +1,16 @@
1
+ import { Context, Effect, Layer } from "effect";
2
+ import { FileError } from "../domain/errors";
3
+ /**
4
+ * FileSystem Service Layer
5
+ *
6
+ * Abstraction for file I/O operations
7
+ * Integrates with Effect's dependency injection
8
+ */
9
+ export interface FileSystem {
10
+ readonly readFile: (path: string) => Effect.Effect<Buffer, FileError>;
11
+ readonly writeFile: (path: string, content: Buffer | string) => Effect.Effect<void, FileError>;
12
+ readonly exists: (path: string) => Effect.Effect<boolean>;
13
+ }
14
+ export declare const FileSystem: Context.Tag<FileSystem, FileSystem>;
15
+ export declare const Live: Layer.Layer<FileSystem, never, never>;
16
+ //# sourceMappingURL=filesystem.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filesystem.d.ts","sourceRoot":"","sources":["../../src/layers/filesystem.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAChD,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAE7C;;;;;GAKG;AAEH,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACtE,QAAQ,CAAC,SAAS,EAAE,CAClB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,GAAG,MAAM,KACrB,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IACpC,QAAQ,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;CAC3D;AAED,eAAO,MAAM,UAAU,qCAA+C,CAAC;AAkCvE,eAAO,MAAM,IAAI,uCAAmB,CAAC"}
@@ -0,0 +1,16 @@
1
+ import { Context, Effect, Layer } from "effect";
2
+ /**
3
+ * Logger Service Layer
4
+ *
5
+ * Provides structured logging with different levels (debug, info, warn, error)
6
+ * Integrates with Effect's dependency injection system
7
+ */
8
+ export interface Logger {
9
+ readonly debug: (message: string) => Effect.Effect<void>;
10
+ readonly info: (message: string) => Effect.Effect<void>;
11
+ readonly warn: (message: string) => Effect.Effect<void>;
12
+ readonly error: (message: string, error?: Error) => Effect.Effect<void>;
13
+ }
14
+ export declare const Logger: Context.Tag<Logger, Logger>;
15
+ export declare const LoggerLive: Layer.Layer<Logger, never, never>;
16
+ //# sourceMappingURL=logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/layers/logger.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAEhD;;;;;GAKG;AAEH,MAAM,WAAW,MAAM;IACrB,QAAQ,CAAC,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACzD,QAAQ,CAAC,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACxD,QAAQ,CAAC,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACxD,QAAQ,CAAC,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,KAAK,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;CACzE;AAED,eAAO,MAAM,MAAM,6BAAuC,CAAC;AAmC3D,eAAO,MAAM,UAAU,mCAAgB,CAAC"}