openclaw-ynabro 1.0.0 → 2.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.
package/README.md CHANGED
@@ -1,6 +1,14 @@
1
+ ![YNABro Logo](full_logo.png)
2
+
3
+ <p align="center">
4
+ <a href="https://www.npmjs.com/package/openclaw-ynabro"><img src="https://img.shields.io/npm/v/openclaw-ynabro.svg?style=flat-square" alt="npm version"></a>
5
+ <a href="https://github.com/jmcombs/ynabro/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-MIT-3465a4?style=flat-square" alt="MIT License"></a>
6
+ <a href="https://github.com/sponsors/jmcombs"><img src="https://img.shields.io/badge/Sponsor-GitHub%20Sponsors-3465a4?style=flat-square" alt="GitHub Sponsors"></a>
7
+ </p>
8
+
1
9
  # openclaw-ynabro
2
10
 
3
- OpenClaw plugin that registers [YNABro](https://github.com/jmcombs/ynabro) tools for YNAB budget management.
11
+ OpenClaw plugin that registers [ynabro](https://www.npmjs.com/package/ynabro) tools for YNAB integration.
4
12
 
5
13
  ## Installation
6
14
 
@@ -8,18 +16,28 @@ OpenClaw plugin that registers [YNABro](https://github.com/jmcombs/ynabro) tools
8
16
  openclaw plugins install openclaw-ynabro
9
17
  ```
10
18
 
19
+ ## Available Tools
20
+
21
+ - `ynabro_setup`
22
+ - `ynabro_save_default_plan`
23
+ - `ynabro_get_pending_transactions`
24
+ - `ynabro_get_recent_transactions`
25
+ - `ynabro_approve_transaction`
26
+ - `ynabro_get_plan_info`
27
+ - `ynabro_get_skill_state`
28
+ - `ynabro_update_skill_state`
29
+
11
30
  ## Configuration
12
31
 
13
- Set your YNAB Personal Access Token:
32
+ Set your YNAB Personal Access Token in `openclaw.json`:
14
33
 
15
34
  ```json
16
35
  {
17
- "skills": {
36
+ "plugins": {
18
37
  "entries": {
19
- "match-transactions": {
20
- "enabled": true,
21
- "env": {
22
- "YNAB_TOKEN": "your-token-here"
38
+ "openclaw-ynabro": {
39
+ "config": {
40
+ "token": "your-ynab-personal-access-token"
23
41
  }
24
42
  }
25
43
  }
@@ -27,20 +45,16 @@ Set your YNAB Personal Access Token:
27
45
  }
28
46
  ```
29
47
 
30
- Or set the `YNAB_TOKEN` environment variable directly.
48
+ OpenClaw also surfaces this as a sensitive field in its settings UI (labeled "YNAB Personal Access Token"). Generate a token at https://app.ynab.com/settings/developer.
49
+
50
+ No environment variable fallback is supported.
31
51
 
32
- ## Tools
52
+ ## Onboarding
33
53
 
34
- | Tool | Description |
35
- |------|-------------|
36
- | `ynabro_setup` | Set up YNAB integration (token + plan selection) |
37
- | `ynabro_get_pending_transactions` | Get pending (uncategorized) transactions |
38
- | `ynabro_get_recent_transactions` | Get recent transactions |
39
- | `ynabro_approve_transaction` | Approve a transaction |
40
- | `ynabro_get_plan_info` | Get plan details |
41
- | `ynabro_get_skill_state` | Read skill state (memory, auto-approve flag) |
42
- | `ynabro_update_skill_state` | Update skill state |
54
+ Run `ynabro_setup` to fetch your available YNAB plans, then `ynabro_save_default_plan` with the plan ID you want to use as the default:
43
55
 
44
- ## Skills
56
+ 1. `ynabro_setup` — returns `{ plans: [{ id, name }] }`
57
+ 2. User (or agent) selects a plan from the list
58
+ 3. `ynabro_save_default_plan` with `{ planId: "<selected-id>" }` — persists the default
45
59
 
46
- This plugin ships the `match-transactions` skill for reviewing YNAB's automatic transaction matching.
60
+ After onboarding, all plan-dependent tools (`ynabro_get_pending_transactions`, `ynabro_get_recent_transactions`, `ynabro_approve_transaction`, `ynabro_get_plan_info`) resolve the plan automatically — no `planId` parameter required.
package/full_logo.png ADDED
Binary file
package/logo.png ADDED
Binary file
@@ -5,6 +5,44 @@
5
5
  "skills": ["skills"],
6
6
  "configSchema": {
7
7
  "type": "object",
8
- "additionalProperties": false
8
+ "additionalProperties": false,
9
+ "properties": {
10
+ "token": {
11
+ "type": "string",
12
+ "description": "YNAB Personal Access Token"
13
+ },
14
+ "defaultPlanId": {
15
+ "type": "string",
16
+ "description": "Default YNAB plan ID selected during onboarding"
17
+ }
18
+ }
19
+ },
20
+ "uiHints": {
21
+ "token": {
22
+ "label": "YNAB Personal Access Token",
23
+ "placeholder": "your-ynab-personal-access-token",
24
+ "sensitive": true,
25
+ "help": "Generate at https://app.ynab.com/settings/developer. Falls back to YNAB_TOKEN env var if not set here."
26
+ }
27
+ },
28
+ "setup": {
29
+ "providers": [
30
+ {
31
+ "id": "ynab",
32
+ "envVars": ["YNAB_TOKEN"]
33
+ }
34
+ ]
35
+ },
36
+ "contracts": {
37
+ "tools": [
38
+ "ynabro_setup",
39
+ "ynabro_save_default_plan",
40
+ "ynabro_get_pending_transactions",
41
+ "ynabro_get_recent_transactions",
42
+ "ynabro_approve_transaction",
43
+ "ynabro_get_plan_info",
44
+ "ynabro_get_skill_state",
45
+ "ynabro_update_skill_state"
46
+ ]
9
47
  }
10
48
  }
package/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "openclaw-ynabro",
3
- "version": "1.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "OpenClaw plugin that registers YNABro tools for YNAB integration",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
7
7
  "files": [
8
8
  "src",
9
9
  "openclaw.plugin.json",
10
+ "full_logo.png",
11
+ "logo.png",
10
12
  "skills",
11
13
  "README.md"
12
14
  ],
@@ -10,7 +10,7 @@ You are **YNABro**, a friendly and reliable YNAB assistant designed to help user
10
10
 
11
11
  Before performing any YNAB operations, check whether YNAB access has been set up:
12
12
 
13
- 1. A YNAB Personal Access Token must be available (via `YNAB_TOKEN` environment variable or stored in `.ynabro/config.json`).
13
+ 1. A YNAB Personal Access Token must be available. In OpenClaw, the preferred storage location is `plugins.entries.openclaw-ynabro.config.token` in `openclaw.json` (surfaced as a sensitive field in the settings UI). The `YNAB_TOKEN` environment variable is also accepted as a fallback.
14
14
  2. A default plan must be selected and saved.
15
15
 
16
16
  If either is missing, call the `setupYnab()` tool first. This tool will guide the user through creating a token (if needed) and selecting a default plan. Do not attempt to read or modify transactions until setup is complete.
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Type } from "@sinclair/typebox";
2
2
  import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
3
+ import type { YnabroConfigAdapter } from "ynabro";
3
4
  import {
4
5
  approveTransaction,
5
6
  getPendingTransactions,
@@ -11,12 +12,6 @@ import {
11
12
  YnabroClient,
12
13
  } from "ynabro";
13
14
 
14
- function getClient(): YnabroClient {
15
- const token = process.env.YNAB_TOKEN;
16
- if (!token) throw new Error("YNAB_TOKEN environment variable is not set");
17
- return new YnabroClient(token);
18
- }
19
-
20
15
  function params(raw: unknown): Record<string, unknown> {
21
16
  return raw as Record<string, unknown>;
22
17
  }
@@ -28,17 +23,18 @@ function ok(text: string) {
28
23
  };
29
24
  }
30
25
 
31
- const planIdSchema = Type.Object({
32
- planId: Type.String({ description: "The ID of the YNAB plan" }),
33
- });
34
-
35
26
  const approveSchema = Type.Object({
36
- planId: Type.String({ description: "The ID of the YNAB plan" }),
37
27
  transactionId: Type.String({
38
28
  description: "The ID of the transaction to approve",
39
29
  }),
40
30
  });
41
31
 
32
+ const saveDefaultPlanSchema = Type.Object({
33
+ planId: Type.String({
34
+ description: "The ID of the YNAB plan to set as the default",
35
+ }),
36
+ });
37
+
42
38
  const skillStateSchema = Type.Object({
43
39
  skillSlug: Type.String({
44
40
  description: "The slug identifier for the skill",
@@ -64,29 +60,121 @@ export default definePluginEntry({
64
60
  name: "YNABro",
65
61
  description: "YNAB budget management tools for OpenClaw agents",
66
62
  register(api) {
63
+ let cachedPlanId: string | undefined;
64
+
65
+ const openClawAdapter: YnabroConfigAdapter = {
66
+ async getDefaultPlanId(): Promise<string | undefined> {
67
+ if (cachedPlanId !== undefined) return cachedPlanId;
68
+ return (api.pluginConfig as { defaultPlanId?: string } | undefined)
69
+ ?.defaultPlanId;
70
+ },
71
+ async setDefaultPlanId(planId: string): Promise<void> {
72
+ cachedPlanId = planId;
73
+ await api.runtime.config.mutateConfigFile({
74
+ afterWrite: { mode: "auto" },
75
+ mutate: (draft) => {
76
+ // Ensure the path exists before writing
77
+ if (!draft.plugins) {
78
+ (draft as { plugins?: unknown }).plugins = {
79
+ entries: {},
80
+ } as unknown;
81
+ }
82
+ // TypeScript flow analysis requires explicit check after conditional assignment
83
+ if (draft.plugins) {
84
+ if (!draft.plugins.entries) {
85
+ draft.plugins.entries = {};
86
+ }
87
+ const existing = draft.plugins.entries["openclaw-ynabro"] ?? {};
88
+ const existingConfig =
89
+ (existing.config as Record<string, unknown> | undefined) ?? {};
90
+ existingConfig.defaultPlanId = planId;
91
+ draft.plugins.entries["openclaw-ynabro"] = {
92
+ ...existing,
93
+ config: existingConfig,
94
+ };
95
+ }
96
+ },
97
+ });
98
+ },
99
+ };
100
+
101
+ function getClient(): YnabroClient {
102
+ const token = (api.pluginConfig as { token?: string } | undefined)?.token;
103
+ if (!token) {
104
+ throw new Error(
105
+ "YNAB token not configured. " +
106
+ "Set it via plugins.entries.openclaw-ynabro.config.token in openclaw.json.",
107
+ );
108
+ }
109
+ return new YnabroClient(token);
110
+ }
111
+
112
+ async function getDefaultPlanId(): Promise<string> {
113
+ const planId = await openClawAdapter.getDefaultPlanId();
114
+ if (!planId) {
115
+ throw new Error(
116
+ "No default plan configured. Run ynabro_setup then ynabro_save_default_plan to complete onboarding.",
117
+ );
118
+ }
119
+ return planId;
120
+ }
121
+
67
122
  api.registerTool({
68
123
  name: "ynabro_setup",
69
124
  label: "Setup YNAB",
70
125
  description:
71
- "Set up YNAB integration checks for token and selects a default plan",
126
+ "Fetch available YNAB plans for onboarding. Returns a list of plans. " +
127
+ "After the user selects one, call ynabro_save_default_plan with the chosen plan ID.",
72
128
  parameters: Type.Object({}),
73
129
  async execute() {
74
- const result = await setupYnab();
75
- return ok(JSON.stringify(result, null, 2));
130
+ const client = getClient();
131
+ const plans = await client.getPlans();
132
+ if (plans.length === 0) {
133
+ return ok(
134
+ JSON.stringify({ error: "No plans found in your YNAB account." }),
135
+ );
136
+ }
137
+ return ok(
138
+ JSON.stringify({
139
+ plans: plans.map((p) => ({ id: p.id, name: p.name })),
140
+ }),
141
+ );
76
142
  },
77
143
  });
78
144
 
79
145
  api.registerTool({
80
- name: "ynabro_get_pending_transactions",
81
- label: "Get Pending Transactions",
82
- description: "Get all pending (uncategorized) transactions for a plan",
83
- parameters: planIdSchema,
146
+ name: "ynabro_save_default_plan",
147
+ label: "Save Default Plan",
148
+ description:
149
+ "Save a YNAB plan as the default for all subsequent tool calls. " +
150
+ "Call ynabro_setup first to get the list of available plan IDs.",
151
+ parameters: saveDefaultPlanSchema,
84
152
  async execute(_id, raw) {
85
153
  const p = params(raw);
86
- const result = await getPendingTransactions(
87
- getClient(),
88
- p.planId as string,
154
+ const client = getClient();
155
+ const plans = await client.getPlans();
156
+ await setupYnab(client, plans, p.planId as string, openClawAdapter);
157
+ const saved = plans.find((plan) => plan.id === p.planId);
158
+ return ok(
159
+ JSON.stringify({
160
+ message: `Default plan set to: ${saved?.name ?? p.planId}`,
161
+ defaultPlanId: p.planId,
162
+ }),
89
163
  );
164
+ },
165
+ });
166
+
167
+ api.registerTool({
168
+ name: "ynabro_get_pending_transactions",
169
+ label: "Get Pending Transactions",
170
+ description: "Get all pending (uncategorized) transactions for a plan",
171
+ parameters: Type.Object({}),
172
+ async execute() {
173
+ const [client, planId] = await Promise.all([
174
+ Promise.resolve(getClient()),
175
+ getDefaultPlanId(),
176
+ ]);
177
+ const result = await getPendingTransactions(client, planId);
90
178
  return ok(JSON.stringify(result, null, 2));
91
179
  },
92
180
  });
@@ -95,13 +183,13 @@ export default definePluginEntry({
95
183
  name: "ynabro_get_recent_transactions",
96
184
  label: "Get Recent Transactions",
97
185
  description: "Get recent transactions for a plan",
98
- parameters: planIdSchema,
99
- async execute(_id, raw) {
100
- const p = params(raw);
101
- const result = await getRecentTransactions(
102
- getClient(),
103
- p.planId as string,
104
- );
186
+ parameters: Type.Object({}),
187
+ async execute() {
188
+ const [client, planId] = await Promise.all([
189
+ Promise.resolve(getClient()),
190
+ getDefaultPlanId(),
191
+ ]);
192
+ const result = await getRecentTransactions(client, planId);
105
193
  return ok(JSON.stringify(result, null, 2));
106
194
  },
107
195
  });
@@ -113,11 +201,11 @@ export default definePluginEntry({
113
201
  parameters: approveSchema,
114
202
  async execute(_id, raw) {
115
203
  const p = params(raw);
116
- await approveTransaction(
117
- getClient(),
118
- p.planId as string,
119
- p.transactionId as string,
120
- );
204
+ const [client, planId] = await Promise.all([
205
+ Promise.resolve(getClient()),
206
+ getDefaultPlanId(),
207
+ ]);
208
+ await approveTransaction(client, planId, p.transactionId as string);
121
209
  return ok(JSON.stringify({ success: true }));
122
210
  },
123
211
  });
@@ -126,10 +214,13 @@ export default definePluginEntry({
126
214
  name: "ynabro_get_plan_info",
127
215
  label: "Get Plan Info",
128
216
  description: "Get basic information about a plan",
129
- parameters: planIdSchema,
130
- async execute(_id, raw) {
131
- const p = params(raw);
132
- const result = await getPlanInfo(getClient(), p.planId as string);
217
+ parameters: Type.Object({}),
218
+ async execute() {
219
+ const [client, planId] = await Promise.all([
220
+ Promise.resolve(getClient()),
221
+ getDefaultPlanId(),
222
+ ]);
223
+ const result = await getPlanInfo(client, planId);
133
224
  return ok(JSON.stringify(result, null, 2));
134
225
  },
135
226
  });