financialclaw 1.0.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.
- package/README.es.md +72 -0
- package/README.md +65 -0
- package/bin/financialclaw-setup.mjs +139 -0
- package/bin/postinstall-message.mjs +17 -0
- package/dist/src/db/database.d.ts +3 -0
- package/dist/src/db/database.js +58 -0
- package/dist/src/db/schema.d.ts +16 -0
- package/dist/src/db/schema.js +154 -0
- package/dist/src/index.d.ts +8 -0
- package/dist/src/index.js +112 -0
- package/dist/src/services/daily-sync.d.ts +16 -0
- package/dist/src/services/daily-sync.js +135 -0
- package/dist/src/tools/add-recurring-expense.d.ts +16 -0
- package/dist/src/tools/add-recurring-expense.js +126 -0
- package/dist/src/tools/get-financial-summary.d.ts +8 -0
- package/dist/src/tools/get-financial-summary.js +158 -0
- package/dist/src/tools/helpers/currency-utils.d.ts +17 -0
- package/dist/src/tools/helpers/currency-utils.js +47 -0
- package/dist/src/tools/helpers/date-utils.d.ts +8 -0
- package/dist/src/tools/helpers/date-utils.js +102 -0
- package/dist/src/tools/list-expenses.d.ts +16 -0
- package/dist/src/tools/list-expenses.js +142 -0
- package/dist/src/tools/list-incomes.d.ts +12 -0
- package/dist/src/tools/list-incomes.js +124 -0
- package/dist/src/tools/log-expense-from-receipt.d.ts +13 -0
- package/dist/src/tools/log-expense-from-receipt.js +91 -0
- package/dist/src/tools/log-expense-manual.d.ts +12 -0
- package/dist/src/tools/log-expense-manual.js +60 -0
- package/dist/src/tools/log-income-receipt.d.ts +11 -0
- package/dist/src/tools/log-income-receipt.js +103 -0
- package/dist/src/tools/log-income.d.ts +13 -0
- package/dist/src/tools/log-income.js +103 -0
- package/dist/src/tools/manage-currency.d.ts +10 -0
- package/dist/src/tools/manage-currency.js +104 -0
- package/dist/src/tools/mark-expense-paid.d.ts +8 -0
- package/dist/src/tools/mark-expense-paid.js +44 -0
- package/dist/src/tools/run-daily-sync.d.ts +5 -0
- package/dist/src/tools/run-daily-sync.js +85 -0
- package/dist/src/version.d.ts +2 -0
- package/dist/src/version.js +2 -0
- package/openclaw.plugin.json +24 -0
- package/package.json +52 -0
- package/skills/financialclaw/SKILL.md +257 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { Type } from "@sinclair/typebox";
|
|
3
|
+
import { Value } from "@sinclair/typebox/value";
|
|
4
|
+
import { getDb } from "../db/database.js";
|
|
5
|
+
import { computeNextDate } from "./helpers/date-utils.js";
|
|
6
|
+
import { formatAmount, isPlaceholderCurrency, resolveCurrency, } from "./helpers/currency-utils.js";
|
|
7
|
+
const ISO_DATE_PATTERN = "^\\d{4}-\\d{2}-\\d{2}$";
|
|
8
|
+
function isValidCalendarDate(dateStr) {
|
|
9
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(dateStr);
|
|
10
|
+
if (match === null)
|
|
11
|
+
return false;
|
|
12
|
+
const year = Number.parseInt(match[1], 10);
|
|
13
|
+
const month = Number.parseInt(match[2], 10);
|
|
14
|
+
const day = Number.parseInt(match[3], 10);
|
|
15
|
+
const d = new Date(Date.UTC(year, month - 1, day));
|
|
16
|
+
return (d.getUTCFullYear() === year &&
|
|
17
|
+
d.getUTCMonth() === month - 1 &&
|
|
18
|
+
d.getUTCDate() === day);
|
|
19
|
+
}
|
|
20
|
+
export const InputSchema = Type.Object({
|
|
21
|
+
reason: Type.String({ minLength: 1 }),
|
|
22
|
+
expected_amount: Type.Number({ minimum: 0 }),
|
|
23
|
+
currency: Type.Optional(Type.String()),
|
|
24
|
+
date: Type.String({ pattern: ISO_DATE_PATTERN }),
|
|
25
|
+
recurring: Type.Optional(Type.Boolean()),
|
|
26
|
+
frequency: Type.Optional(Type.Union([
|
|
27
|
+
Type.Literal("WEEKLY"),
|
|
28
|
+
Type.Literal("BIWEEKLY"),
|
|
29
|
+
Type.Literal("MONTHLY"),
|
|
30
|
+
Type.Literal("INTERVAL_DAYS"),
|
|
31
|
+
])),
|
|
32
|
+
interval_days: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
33
|
+
}, { additionalProperties: false });
|
|
34
|
+
export function executeLogIncome(input, db = getDb()) {
|
|
35
|
+
if (!Value.Check(InputSchema, input)) {
|
|
36
|
+
throw new Error("Invalid parameters: reason must not be empty, expected_amount must be >= 0, and date must be in YYYY-MM-DD format.");
|
|
37
|
+
}
|
|
38
|
+
const trimmedReason = input.reason.trim();
|
|
39
|
+
if (trimmedReason.length === 0) {
|
|
40
|
+
throw new Error("The reason field must not be empty or blank.");
|
|
41
|
+
}
|
|
42
|
+
if (!isValidCalendarDate(input.date)) {
|
|
43
|
+
throw new Error(`The date "${input.date}" is not a valid calendar date.`);
|
|
44
|
+
}
|
|
45
|
+
const isRecurring = input.recurring ?? false;
|
|
46
|
+
if (isRecurring && !input.frequency) {
|
|
47
|
+
throw new Error("The frequency field is required for recurring income.");
|
|
48
|
+
}
|
|
49
|
+
if (input.frequency === "INTERVAL_DAYS" && !input.interval_days) {
|
|
50
|
+
throw new Error("The interval_days field is required when frequency is INTERVAL_DAYS.");
|
|
51
|
+
}
|
|
52
|
+
const currency = resolveCurrency(input.currency, db);
|
|
53
|
+
const nextDate = isRecurring && input.frequency
|
|
54
|
+
? computeNextDate(input.date, input.frequency, input.interval_days ?? 0)
|
|
55
|
+
: null;
|
|
56
|
+
const now = new Date().toISOString();
|
|
57
|
+
const incomeId = randomUUID();
|
|
58
|
+
const receiptId = randomUUID();
|
|
59
|
+
db.exec("BEGIN");
|
|
60
|
+
try {
|
|
61
|
+
db.prepare(`
|
|
62
|
+
INSERT INTO incomes (
|
|
63
|
+
id,
|
|
64
|
+
reason,
|
|
65
|
+
expected_amount,
|
|
66
|
+
currency,
|
|
67
|
+
date,
|
|
68
|
+
frequency,
|
|
69
|
+
interval_days,
|
|
70
|
+
is_recurring,
|
|
71
|
+
next_expected_receipt_date,
|
|
72
|
+
is_active,
|
|
73
|
+
created_at
|
|
74
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
75
|
+
`).run(incomeId, trimmedReason, input.expected_amount, currency.code, input.date, input.frequency ?? null, input.interval_days ?? null, isRecurring ? 1 : 0, nextDate, 1, now);
|
|
76
|
+
db.prepare(`
|
|
77
|
+
INSERT INTO income_receipts (
|
|
78
|
+
id,
|
|
79
|
+
income_id,
|
|
80
|
+
amount,
|
|
81
|
+
currency,
|
|
82
|
+
date,
|
|
83
|
+
notes,
|
|
84
|
+
created_at
|
|
85
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
86
|
+
`).run(receiptId, incomeId, input.expected_amount, currency.code, input.date, null, now);
|
|
87
|
+
db.exec("COMMIT");
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
db.exec("ROLLBACK");
|
|
91
|
+
throw err;
|
|
92
|
+
}
|
|
93
|
+
const formattedAmount = formatAmount(input.expected_amount, currency);
|
|
94
|
+
let message = `Income logged: ${formattedAmount} · ${trimmedReason} · ${input.date} (ID: ${incomeId})`;
|
|
95
|
+
if (nextDate !== null) {
|
|
96
|
+
message += `\nNext expected receipt: ${nextDate}`;
|
|
97
|
+
}
|
|
98
|
+
if (isPlaceholderCurrency(db)) {
|
|
99
|
+
message +=
|
|
100
|
+
"\n\nHint: you haven't configured a real currency yet. Use manage_currency to add yours and set it as default.";
|
|
101
|
+
}
|
|
102
|
+
return message;
|
|
103
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
import { type Static } from "@sinclair/typebox";
|
|
3
|
+
export declare const InputSchema: import("@sinclair/typebox").TObject<{
|
|
4
|
+
action: import("@sinclair/typebox").TUnion<import("@sinclair/typebox").TLiteral<"add" | "list" | "set_default">[]>;
|
|
5
|
+
code: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
6
|
+
name: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
7
|
+
symbol: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
8
|
+
}>;
|
|
9
|
+
export type ManageCurrencyInput = Static<typeof InputSchema>;
|
|
10
|
+
export declare function executeManageCurrency(input: ManageCurrencyInput, db?: DatabaseSync): string;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import { getDb } from "../db/database.js";
|
|
3
|
+
const ACTIONS = ["add", "list", "set_default"];
|
|
4
|
+
export const InputSchema = Type.Object({
|
|
5
|
+
action: Type.Union(ACTIONS.map((action) => Type.Literal(action))),
|
|
6
|
+
code: Type.Optional(Type.String()),
|
|
7
|
+
name: Type.Optional(Type.String()),
|
|
8
|
+
symbol: Type.Optional(Type.String()),
|
|
9
|
+
}, { additionalProperties: false });
|
|
10
|
+
function requireText(value, fieldName, action) {
|
|
11
|
+
const normalized = value?.trim();
|
|
12
|
+
if (normalized === undefined || normalized === "") {
|
|
13
|
+
throw new Error(`Action ${action} requires the "${fieldName}" field.`);
|
|
14
|
+
}
|
|
15
|
+
return normalized;
|
|
16
|
+
}
|
|
17
|
+
function normalizeCode(code, action) {
|
|
18
|
+
return requireText(code, "code", action).toUpperCase();
|
|
19
|
+
}
|
|
20
|
+
function addCurrency(input, db) {
|
|
21
|
+
const code = normalizeCode(input.code, "add");
|
|
22
|
+
const name = requireText(input.name, "name", "add");
|
|
23
|
+
const symbol = requireText(input.symbol, "symbol", "add");
|
|
24
|
+
const existingCurrency = db
|
|
25
|
+
.prepare(`
|
|
26
|
+
SELECT code
|
|
27
|
+
FROM currencies
|
|
28
|
+
WHERE code = ?
|
|
29
|
+
LIMIT 1
|
|
30
|
+
`)
|
|
31
|
+
.get(code);
|
|
32
|
+
if (existingCurrency !== undefined) {
|
|
33
|
+
throw new Error(`Currency ${code} already exists. Use a different code or choose set_default.`);
|
|
34
|
+
}
|
|
35
|
+
db.prepare(`
|
|
36
|
+
INSERT INTO currencies (code, name, symbol, is_default)
|
|
37
|
+
VALUES (?, ?, ?, 0)
|
|
38
|
+
`).run(code, name, symbol);
|
|
39
|
+
return `Currency ${code} added successfully.`;
|
|
40
|
+
}
|
|
41
|
+
function listCurrencies(db) {
|
|
42
|
+
const currencies = db
|
|
43
|
+
.prepare(`
|
|
44
|
+
SELECT code, name, symbol, is_default
|
|
45
|
+
FROM currencies
|
|
46
|
+
ORDER BY is_default DESC, created_at ASC, code ASC
|
|
47
|
+
`)
|
|
48
|
+
.all();
|
|
49
|
+
if (currencies.length === 0) {
|
|
50
|
+
return "No currencies registered.";
|
|
51
|
+
}
|
|
52
|
+
const lines = currencies.map((currency) => {
|
|
53
|
+
const prefix = currency.is_default === 1 ? "*" : "-";
|
|
54
|
+
const suffix = currency.is_default === 1 ? " [default]" : "";
|
|
55
|
+
return `${prefix} ${currency.code} - ${currency.name} (${currency.symbol})${suffix}`;
|
|
56
|
+
});
|
|
57
|
+
return ["Registered currencies:", ...lines].join("\n");
|
|
58
|
+
}
|
|
59
|
+
function setDefaultCurrency(input, db) {
|
|
60
|
+
const code = normalizeCode(input.code, "set_default");
|
|
61
|
+
const existingCurrency = db
|
|
62
|
+
.prepare(`
|
|
63
|
+
SELECT code
|
|
64
|
+
FROM currencies
|
|
65
|
+
WHERE code = ?
|
|
66
|
+
LIMIT 1
|
|
67
|
+
`)
|
|
68
|
+
.get(code);
|
|
69
|
+
if (existingCurrency === undefined) {
|
|
70
|
+
throw new Error(`No registered currency found with code ${code}.`);
|
|
71
|
+
}
|
|
72
|
+
db.exec("BEGIN");
|
|
73
|
+
try {
|
|
74
|
+
db.prepare(`
|
|
75
|
+
UPDATE currencies
|
|
76
|
+
SET is_default = 0
|
|
77
|
+
`).run();
|
|
78
|
+
db.prepare(`
|
|
79
|
+
UPDATE currencies
|
|
80
|
+
SET is_default = 1
|
|
81
|
+
WHERE code = ?
|
|
82
|
+
`).run(code);
|
|
83
|
+
db.exec("COMMIT");
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
db.exec("ROLLBACK");
|
|
87
|
+
throw err;
|
|
88
|
+
}
|
|
89
|
+
return `Currency ${code} set as default.`;
|
|
90
|
+
}
|
|
91
|
+
export function executeManageCurrency(input, db = getDb()) {
|
|
92
|
+
switch (input.action) {
|
|
93
|
+
case "add":
|
|
94
|
+
return addCurrency(input, db);
|
|
95
|
+
case "list":
|
|
96
|
+
return listCurrencies(db);
|
|
97
|
+
case "set_default":
|
|
98
|
+
return setDefaultCurrency(input, db);
|
|
99
|
+
default: {
|
|
100
|
+
const exhaustiveCheck = input.action;
|
|
101
|
+
throw new Error(`Unsupported action: ${exhaustiveCheck}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
import { type Static } from "@sinclair/typebox";
|
|
3
|
+
export declare const InputSchema: import("@sinclair/typebox").TObject<{
|
|
4
|
+
expense_id: import("@sinclair/typebox").TString;
|
|
5
|
+
payment_date: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
6
|
+
}>;
|
|
7
|
+
export type MarkExpensePaidInput = Static<typeof InputSchema>;
|
|
8
|
+
export declare function executeMarkExpensePaid(input: MarkExpensePaidInput, db?: DatabaseSync): string;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import { Value } from "@sinclair/typebox/value";
|
|
3
|
+
import { getDb } from "../db/database.js";
|
|
4
|
+
const ISO_DATE_PATTERN = "^\\d{4}-\\d{2}-\\d{2}$";
|
|
5
|
+
export const InputSchema = Type.Object({
|
|
6
|
+
expense_id: Type.String({ minLength: 1 }),
|
|
7
|
+
payment_date: Type.Optional(Type.String({ pattern: ISO_DATE_PATTERN })),
|
|
8
|
+
}, { additionalProperties: false });
|
|
9
|
+
function assertValidInput(input) {
|
|
10
|
+
if (!Value.Check(InputSchema, input)) {
|
|
11
|
+
throw new Error("Invalid parameters: expense_id is required and payment_date must be in YYYY-MM-DD format.");
|
|
12
|
+
}
|
|
13
|
+
if (input.expense_id.trim() === "") {
|
|
14
|
+
throw new Error("The expense_id field is required.");
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export function executeMarkExpensePaid(input, db = getDb()) {
|
|
18
|
+
assertValidInput(input);
|
|
19
|
+
const expenseId = input.expense_id;
|
|
20
|
+
const expense = db
|
|
21
|
+
.prepare(`
|
|
22
|
+
SELECT id, status, payment_date, updated_at
|
|
23
|
+
FROM expenses
|
|
24
|
+
WHERE id = ?
|
|
25
|
+
`)
|
|
26
|
+
.get(expenseId);
|
|
27
|
+
if (expense === undefined) {
|
|
28
|
+
throw new Error(`No expense found with ID "${expenseId}".`);
|
|
29
|
+
}
|
|
30
|
+
if (expense.status === "PAID") {
|
|
31
|
+
return `Expense "${expenseId}" was already marked as paid.`;
|
|
32
|
+
}
|
|
33
|
+
const paymentDate = input.payment_date ?? new Date().toISOString().slice(0, 10);
|
|
34
|
+
const updatedAt = new Date().toISOString();
|
|
35
|
+
db.prepare(`
|
|
36
|
+
UPDATE expenses
|
|
37
|
+
SET status = 'PAID',
|
|
38
|
+
payment_date = ?,
|
|
39
|
+
updated_at = ?
|
|
40
|
+
WHERE id = ?
|
|
41
|
+
AND status != 'PAID'
|
|
42
|
+
`).run(paymentDate, updatedAt, expenseId);
|
|
43
|
+
return `Expense "${expenseId}" marked as paid on ${paymentDate}.`;
|
|
44
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Static } from "@sinclair/typebox";
|
|
2
|
+
import { DatabaseSync } from "node:sqlite";
|
|
3
|
+
export declare const InputSchema: import("@sinclair/typebox").TObject<{}>;
|
|
4
|
+
export type Input = Static<typeof InputSchema>;
|
|
5
|
+
export declare function executeRunDailySync(_params: Input, db?: DatabaseSync): Promise<string>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import { getDb } from "../db/database.js";
|
|
3
|
+
import { dailySync } from "../services/daily-sync.js";
|
|
4
|
+
import { formatAmount, resolveCurrency } from "./helpers/currency-utils.js";
|
|
5
|
+
import { PACKAGE_NAME, PACKAGE_VERSION } from "../version.js";
|
|
6
|
+
export const InputSchema = Type.Object({});
|
|
7
|
+
const UPDATE_CHECK_TIMEOUT_MS = 3_000;
|
|
8
|
+
async function fetchLatestVersion(packageName) {
|
|
9
|
+
const controller = new AbortController();
|
|
10
|
+
const timeout = setTimeout(() => { controller.abort(); }, UPDATE_CHECK_TIMEOUT_MS);
|
|
11
|
+
try {
|
|
12
|
+
const encoded = encodeURIComponent(packageName).replace(/^%40/, "@");
|
|
13
|
+
const response = await fetch(`https://registry.npmjs.org/${encoded}/latest`, { signal: controller.signal });
|
|
14
|
+
if (!response.ok) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
const data = await response.json();
|
|
18
|
+
return data.version ?? null;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
clearTimeout(timeout);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function isNewerVersion(current, candidate) {
|
|
28
|
+
const parse = (v) => {
|
|
29
|
+
const [major = 0, minor = 0, patch = 0] = v.split(".").map(Number);
|
|
30
|
+
return [major, minor, patch];
|
|
31
|
+
};
|
|
32
|
+
const [cMaj, cMin, cPat] = parse(current);
|
|
33
|
+
const [lMaj, lMin, lPat] = parse(candidate);
|
|
34
|
+
if (lMaj !== cMaj)
|
|
35
|
+
return lMaj > cMaj;
|
|
36
|
+
if (lMin !== cMin)
|
|
37
|
+
return lMin > cMin;
|
|
38
|
+
return lPat > cPat;
|
|
39
|
+
}
|
|
40
|
+
function markRemindersSent(db, reminderIds) {
|
|
41
|
+
if (reminderIds.length === 0) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const stmt = db.prepare(`UPDATE reminders SET sent = 1, sent_at = ? WHERE id = ?`);
|
|
45
|
+
const now = new Date().toISOString();
|
|
46
|
+
for (const id of reminderIds) {
|
|
47
|
+
stmt.run(now, id);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function formatReminderLine(description, amount, currency, dueDate, daysBefore, db) {
|
|
51
|
+
const resolvedCurrency = resolveCurrency(currency, db);
|
|
52
|
+
const formattedAmount = formatAmount(amount, resolvedCurrency, db);
|
|
53
|
+
const timing = daysBefore > 0
|
|
54
|
+
? `due in ${daysBefore} day${daysBefore > 1 ? "s" : ""} (${dueDate})`
|
|
55
|
+
: `due today (${dueDate})`;
|
|
56
|
+
return `• ${description}: ${formattedAmount} — ${timing}`;
|
|
57
|
+
}
|
|
58
|
+
export async function executeRunDailySync(_params, db = getDb()) {
|
|
59
|
+
const result = dailySync(db);
|
|
60
|
+
markRemindersSent(db, result.remindersDue.map((r) => r.reminder_id));
|
|
61
|
+
const lines = [];
|
|
62
|
+
lines.push(`Daily sync completed:`);
|
|
63
|
+
lines.push(`• Recurring expenses generated: ${result.expensesGenerated}`);
|
|
64
|
+
lines.push(`• Expenses marked as overdue: ${result.expensesMarkedOverdue}`);
|
|
65
|
+
if (result.remindersDue.length === 0) {
|
|
66
|
+
lines.push(`• No pending reminders — finances up to date ✓`);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
lines.push(`\nPending reminders (${result.remindersDue.length}):`);
|
|
70
|
+
for (const reminder of result.remindersDue) {
|
|
71
|
+
lines.push(formatReminderLine(reminder.description, reminder.amount, reminder.currency, reminder.due_date, reminder.days_before, db));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const latestVersion = await fetchLatestVersion(PACKAGE_NAME);
|
|
76
|
+
if (latestVersion !== null && isNewerVersion(PACKAGE_VERSION, latestVersion)) {
|
|
77
|
+
lines.push(`\n⚠️ Update available: v${PACKAGE_VERSION} → v${latestVersion}`);
|
|
78
|
+
lines.push(` To update: openclaw plugins update financialclaw && openclaw gateway restart`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// Update check is non-critical — must never break the sync
|
|
83
|
+
}
|
|
84
|
+
return lines.join("\n");
|
|
85
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "financialclaw",
|
|
3
|
+
"name": "FinancialClaw",
|
|
4
|
+
"description": "Personal finance plugin: expenses, income, recurring payments, and receipt OCR",
|
|
5
|
+
"configSchema": {
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {
|
|
8
|
+
"dbPath": {
|
|
9
|
+
"type": "string"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"additionalProperties": false
|
|
13
|
+
},
|
|
14
|
+
"uiHints": {
|
|
15
|
+
"dbPath": {
|
|
16
|
+
"label": "Database path",
|
|
17
|
+
"help": "Absolute path to the SQLite database file. Defaults to ~/.openclaw/workspace/financialclaw.db",
|
|
18
|
+
"placeholder": "/home/user/.openclaw/workspace/financialclaw.db"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"skills": [
|
|
22
|
+
"skills/financialclaw"
|
|
23
|
+
]
|
|
24
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "financialclaw",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Personal finance plugin for OpenClaw: expenses, income, recurring payments, and receipt OCR",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"financialclaw-setup": "bin/financialclaw-setup.mjs"
|
|
8
|
+
},
|
|
9
|
+
"openclaw": {
|
|
10
|
+
"extensions": [
|
|
11
|
+
"./dist/src/index.js"
|
|
12
|
+
],
|
|
13
|
+
"compat": {
|
|
14
|
+
"pluginApi": ">=2026.3.24",
|
|
15
|
+
"minGatewayVersion": "2026.3.24"
|
|
16
|
+
},
|
|
17
|
+
"build": {
|
|
18
|
+
"openclawVersion": "2026.3.24",
|
|
19
|
+
"pluginSdkVersion": "2026.3.24"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc",
|
|
24
|
+
"typecheck": "tsc --noEmit",
|
|
25
|
+
"postinstall": "node bin/postinstall-message.mjs",
|
|
26
|
+
"test": "tsx --test tests/**/*.test.ts",
|
|
27
|
+
"test:unit": "tsx --test tests/unit/**/*.test.ts",
|
|
28
|
+
"test:integration": "tsx --test tests/integration/**/*.test.ts"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@sinclair/typebox": "^0.34.0"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"openclaw": ">=2026.3.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^24.0.0",
|
|
38
|
+
"openclaw": "^2026.3.0",
|
|
39
|
+
"tsx": "^4.19.0",
|
|
40
|
+
"typescript": "^5.5.0",
|
|
41
|
+
"vitest": "^4.1.2"
|
|
42
|
+
},
|
|
43
|
+
"files": [
|
|
44
|
+
"bin/",
|
|
45
|
+
"dist/src/",
|
|
46
|
+
"skills/",
|
|
47
|
+
"openclaw.plugin.json"
|
|
48
|
+
],
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=24.0.0"
|
|
51
|
+
}
|
|
52
|
+
}
|