openclaw-ynabro 2.0.0 → 2.2.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 +31 -2
- package/openclaw.plugin.json +40 -1
- package/package.json +1 -1
- package/skills/ynabro/prompts/ynabro.md +21 -4
- package/src/index.ts +199 -42
package/README.md
CHANGED
|
@@ -19,6 +19,7 @@ openclaw plugins install openclaw-ynabro
|
|
|
19
19
|
## Available Tools
|
|
20
20
|
|
|
21
21
|
- `ynabro_setup`
|
|
22
|
+
- `ynabro_save_default_plan`
|
|
22
23
|
- `ynabro_get_pending_transactions`
|
|
23
24
|
- `ynabro_get_recent_transactions`
|
|
24
25
|
- `ynabro_approve_transaction`
|
|
@@ -26,6 +27,34 @@ openclaw plugins install openclaw-ynabro
|
|
|
26
27
|
- `ynabro_get_skill_state`
|
|
27
28
|
- `ynabro_update_skill_state`
|
|
28
29
|
|
|
29
|
-
##
|
|
30
|
+
## Configuration
|
|
31
|
+
|
|
32
|
+
Set your YNAB Personal Access Token in `openclaw.json`:
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"plugins": {
|
|
37
|
+
"entries": {
|
|
38
|
+
"openclaw-ynabro": {
|
|
39
|
+
"config": {
|
|
40
|
+
"token": "your-ynab-personal-access-token"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
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.
|
|
51
|
+
|
|
52
|
+
## Onboarding
|
|
53
|
+
|
|
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:
|
|
55
|
+
|
|
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
|
|
30
59
|
|
|
31
|
-
- `
|
|
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/openclaw.plugin.json
CHANGED
|
@@ -5,6 +5,45 @@
|
|
|
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_onboarding_status",
|
|
39
|
+
"ynabro_setup",
|
|
40
|
+
"ynabro_save_default_plan",
|
|
41
|
+
"ynabro_get_pending_transactions",
|
|
42
|
+
"ynabro_get_recent_transactions",
|
|
43
|
+
"ynabro_approve_transaction",
|
|
44
|
+
"ynabro_get_plan_info",
|
|
45
|
+
"ynabro_get_skill_state",
|
|
46
|
+
"ynabro_update_skill_state"
|
|
47
|
+
]
|
|
9
48
|
}
|
|
10
49
|
}
|
package/package.json
CHANGED
|
@@ -8,12 +8,29 @@ You are **YNABro**, a friendly and reliable YNAB assistant designed to help user
|
|
|
8
8
|
|
|
9
9
|
## Onboarding & Access
|
|
10
10
|
|
|
11
|
-
Before performing any YNAB operations,
|
|
11
|
+
Before performing any YNAB operations, call `ynabro_onboarding_status` to check
|
|
12
|
+
whether YNAB access is configured.
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
2. A default plan must be selected and saved.
|
|
14
|
+
If `ready` is `false`, walk the user through setup:
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
1. **Missing token:** Share the `tokenInstructions` field from the status response.
|
|
17
|
+
The token must never be entered into the chat.
|
|
18
|
+
- **pi:** Call `ynabro_setup` — it presents a native TUI input popup where the
|
|
19
|
+
user enters the token directly. It goes straight to pi's AuthStorage and
|
|
20
|
+
never appears in the conversation.
|
|
21
|
+
- **OpenClaw:** Instruct the user to add the token to `openclaw.json` or via
|
|
22
|
+
the OpenClaw settings UI, then ask them to confirm when done.
|
|
23
|
+
|
|
24
|
+
2. **Missing plan:** Call `ynabro_setup` to list available plans. Help the user
|
|
25
|
+
pick one. On OpenClaw, follow up with `ynabro_save_default_plan`.
|
|
26
|
+
|
|
27
|
+
3. **After onboarding completes:** If the user's original message was a functional
|
|
28
|
+
request (e.g., "show my pending transactions"), fulfill it immediately.
|
|
29
|
+
Do not make them repeat themselves.
|
|
30
|
+
|
|
31
|
+
If a tool returns `{ "error": "onboarding_required" }` during a conversation,
|
|
32
|
+
treat it the same as a failed status check — initiate onboarding, then retry
|
|
33
|
+
the original operation.
|
|
17
34
|
|
|
18
35
|
## Core Capabilities
|
|
19
36
|
- Help users view, update, and manage all aspects of their YNAB budget, including:
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
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,
|
|
6
|
+
checkOnboardingStatus,
|
|
5
7
|
getPendingTransactions,
|
|
6
8
|
getPlanInfo,
|
|
7
9
|
getRecentTransactions,
|
|
@@ -11,12 +13,6 @@ import {
|
|
|
11
13
|
YnabroClient,
|
|
12
14
|
} from "ynabro";
|
|
13
15
|
|
|
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
16
|
function params(raw: unknown): Record<string, unknown> {
|
|
21
17
|
return raw as Record<string, unknown>;
|
|
22
18
|
}
|
|
@@ -28,17 +24,18 @@ function ok(text: string) {
|
|
|
28
24
|
};
|
|
29
25
|
}
|
|
30
26
|
|
|
31
|
-
const planIdSchema = Type.Object({
|
|
32
|
-
planId: Type.String({ description: "The ID of the YNAB plan" }),
|
|
33
|
-
});
|
|
34
|
-
|
|
35
27
|
const approveSchema = Type.Object({
|
|
36
|
-
planId: Type.String({ description: "The ID of the YNAB plan" }),
|
|
37
28
|
transactionId: Type.String({
|
|
38
29
|
description: "The ID of the transaction to approve",
|
|
39
30
|
}),
|
|
40
31
|
});
|
|
41
32
|
|
|
33
|
+
const saveDefaultPlanSchema = Type.Object({
|
|
34
|
+
planId: Type.String({
|
|
35
|
+
description: "The ID of the YNAB plan to set as the default",
|
|
36
|
+
}),
|
|
37
|
+
});
|
|
38
|
+
|
|
42
39
|
const skillStateSchema = Type.Object({
|
|
43
40
|
skillSlug: Type.String({
|
|
44
41
|
description: "The slug identifier for the skill",
|
|
@@ -64,15 +61,132 @@ export default definePluginEntry({
|
|
|
64
61
|
name: "YNABro",
|
|
65
62
|
description: "YNAB budget management tools for OpenClaw agents",
|
|
66
63
|
register(api) {
|
|
64
|
+
let cachedPlanId: string | undefined;
|
|
65
|
+
|
|
66
|
+
const openClawAdapter: YnabroConfigAdapter = {
|
|
67
|
+
async getDefaultPlanId(): Promise<string | undefined> {
|
|
68
|
+
if (cachedPlanId !== undefined) return cachedPlanId;
|
|
69
|
+
return (api.pluginConfig as { defaultPlanId?: string } | undefined)
|
|
70
|
+
?.defaultPlanId;
|
|
71
|
+
},
|
|
72
|
+
async setDefaultPlanId(planId: string): Promise<void> {
|
|
73
|
+
cachedPlanId = planId;
|
|
74
|
+
await api.runtime.config.mutateConfigFile({
|
|
75
|
+
afterWrite: { mode: "auto" },
|
|
76
|
+
mutate: (draft) => {
|
|
77
|
+
// Ensure the path exists before writing
|
|
78
|
+
if (!draft.plugins) {
|
|
79
|
+
(draft as { plugins?: unknown }).plugins = {
|
|
80
|
+
entries: {},
|
|
81
|
+
} as unknown;
|
|
82
|
+
}
|
|
83
|
+
// TypeScript flow analysis requires explicit check after conditional assignment
|
|
84
|
+
if (draft.plugins) {
|
|
85
|
+
if (!draft.plugins.entries) {
|
|
86
|
+
draft.plugins.entries = {};
|
|
87
|
+
}
|
|
88
|
+
const existing = draft.plugins.entries["openclaw-ynabro"] ?? {};
|
|
89
|
+
const existingConfig =
|
|
90
|
+
(existing.config as Record<string, unknown> | undefined) ?? {};
|
|
91
|
+
existingConfig.defaultPlanId = planId;
|
|
92
|
+
draft.plugins.entries["openclaw-ynabro"] = {
|
|
93
|
+
...existing,
|
|
94
|
+
config: existingConfig,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
},
|
|
100
|
+
async hasToken(): Promise<boolean> {
|
|
101
|
+
return !!(api.pluginConfig as { token?: string } | undefined)?.token;
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
function getClient(): YnabroClient {
|
|
106
|
+
const token = (api.pluginConfig as { token?: string } | undefined)?.token;
|
|
107
|
+
if (!token) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
"YNAB token not configured. " +
|
|
110
|
+
"Set it via plugins.entries.openclaw-ynabro.config.token in openclaw.json.",
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
return new YnabroClient(token);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function getDefaultPlanId(): Promise<string> {
|
|
117
|
+
const planId = await openClawAdapter.getDefaultPlanId();
|
|
118
|
+
if (!planId) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
"No default plan configured. Run ynabro_setup then ynabro_save_default_plan to complete onboarding.",
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
return planId;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
api.registerTool({
|
|
127
|
+
name: "ynabro_onboarding_status",
|
|
128
|
+
label: "Onboarding Status",
|
|
129
|
+
description:
|
|
130
|
+
"Check whether YNABro is fully configured. Returns ready status, any missing configuration, and token generation instructions.",
|
|
131
|
+
parameters: Type.Object({}),
|
|
132
|
+
async execute() {
|
|
133
|
+
const status = await checkOnboardingStatus(openClawAdapter);
|
|
134
|
+
return ok(JSON.stringify(status));
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
|
|
67
138
|
api.registerTool({
|
|
68
139
|
name: "ynabro_setup",
|
|
69
140
|
label: "Setup YNAB",
|
|
70
141
|
description:
|
|
71
|
-
"
|
|
142
|
+
"Fetch available YNAB plans for onboarding. Returns a list of plans. " +
|
|
143
|
+
"After the user selects one, call ynabro_save_default_plan with the chosen plan ID.",
|
|
72
144
|
parameters: Type.Object({}),
|
|
73
145
|
async execute() {
|
|
74
|
-
const
|
|
75
|
-
|
|
146
|
+
const client = getClient();
|
|
147
|
+
const plans = await client.getPlans();
|
|
148
|
+
if (plans.length === 0) {
|
|
149
|
+
return ok(
|
|
150
|
+
JSON.stringify({ error: "No plans found in your YNAB account." }),
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
return ok(
|
|
154
|
+
JSON.stringify({
|
|
155
|
+
plans: plans.map((p) => ({ id: p.id, name: p.name })),
|
|
156
|
+
}),
|
|
157
|
+
);
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
api.registerTool({
|
|
162
|
+
name: "ynabro_save_default_plan",
|
|
163
|
+
label: "Save Default Plan",
|
|
164
|
+
description:
|
|
165
|
+
"Save a YNAB plan as the default for all subsequent tool calls. " +
|
|
166
|
+
"Call ynabro_setup first to get the list of available plan IDs.",
|
|
167
|
+
parameters: saveDefaultPlanSchema,
|
|
168
|
+
async execute(_id, raw) {
|
|
169
|
+
try {
|
|
170
|
+
const p = params(raw);
|
|
171
|
+
const client = getClient();
|
|
172
|
+
const plans = await client.getPlans();
|
|
173
|
+
await setupYnab(client, plans, p.planId as string, openClawAdapter);
|
|
174
|
+
const saved = plans.find((plan) => plan.id === p.planId);
|
|
175
|
+
return ok(
|
|
176
|
+
JSON.stringify({
|
|
177
|
+
message: `Default plan set to: ${saved?.name ?? p.planId}`,
|
|
178
|
+
defaultPlanId: p.planId,
|
|
179
|
+
}),
|
|
180
|
+
);
|
|
181
|
+
} catch (_error) {
|
|
182
|
+
const status = await checkOnboardingStatus(openClawAdapter);
|
|
183
|
+
return ok(
|
|
184
|
+
JSON.stringify({
|
|
185
|
+
error: "onboarding_required",
|
|
186
|
+
...status,
|
|
187
|
+
}),
|
|
188
|
+
);
|
|
189
|
+
}
|
|
76
190
|
},
|
|
77
191
|
});
|
|
78
192
|
|
|
@@ -80,14 +194,24 @@ export default definePluginEntry({
|
|
|
80
194
|
name: "ynabro_get_pending_transactions",
|
|
81
195
|
label: "Get Pending Transactions",
|
|
82
196
|
description: "Get all pending (uncategorized) transactions for a plan",
|
|
83
|
-
parameters:
|
|
84
|
-
async execute(
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
197
|
+
parameters: Type.Object({}),
|
|
198
|
+
async execute() {
|
|
199
|
+
try {
|
|
200
|
+
const [client, planId] = await Promise.all([
|
|
201
|
+
Promise.resolve(getClient()),
|
|
202
|
+
getDefaultPlanId(),
|
|
203
|
+
]);
|
|
204
|
+
const result = await getPendingTransactions(client, planId);
|
|
205
|
+
return ok(JSON.stringify(result, null, 2));
|
|
206
|
+
} catch (_error) {
|
|
207
|
+
const status = await checkOnboardingStatus(openClawAdapter);
|
|
208
|
+
return ok(
|
|
209
|
+
JSON.stringify({
|
|
210
|
+
error: "onboarding_required",
|
|
211
|
+
...status,
|
|
212
|
+
}),
|
|
213
|
+
);
|
|
214
|
+
}
|
|
91
215
|
},
|
|
92
216
|
});
|
|
93
217
|
|
|
@@ -95,14 +219,24 @@ export default definePluginEntry({
|
|
|
95
219
|
name: "ynabro_get_recent_transactions",
|
|
96
220
|
label: "Get Recent Transactions",
|
|
97
221
|
description: "Get recent transactions for a plan",
|
|
98
|
-
parameters:
|
|
99
|
-
async execute(
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
222
|
+
parameters: Type.Object({}),
|
|
223
|
+
async execute() {
|
|
224
|
+
try {
|
|
225
|
+
const [client, planId] = await Promise.all([
|
|
226
|
+
Promise.resolve(getClient()),
|
|
227
|
+
getDefaultPlanId(),
|
|
228
|
+
]);
|
|
229
|
+
const result = await getRecentTransactions(client, planId);
|
|
230
|
+
return ok(JSON.stringify(result, null, 2));
|
|
231
|
+
} catch (_error) {
|
|
232
|
+
const status = await checkOnboardingStatus(openClawAdapter);
|
|
233
|
+
return ok(
|
|
234
|
+
JSON.stringify({
|
|
235
|
+
error: "onboarding_required",
|
|
236
|
+
...status,
|
|
237
|
+
}),
|
|
238
|
+
);
|
|
239
|
+
}
|
|
106
240
|
},
|
|
107
241
|
});
|
|
108
242
|
|
|
@@ -112,13 +246,23 @@ export default definePluginEntry({
|
|
|
112
246
|
description: "Approve a specific transaction",
|
|
113
247
|
parameters: approveSchema,
|
|
114
248
|
async execute(_id, raw) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
249
|
+
try {
|
|
250
|
+
const p = params(raw);
|
|
251
|
+
const [client, planId] = await Promise.all([
|
|
252
|
+
Promise.resolve(getClient()),
|
|
253
|
+
getDefaultPlanId(),
|
|
254
|
+
]);
|
|
255
|
+
await approveTransaction(client, planId, p.transactionId as string);
|
|
256
|
+
return ok(JSON.stringify({ success: true }));
|
|
257
|
+
} catch (_error) {
|
|
258
|
+
const status = await checkOnboardingStatus(openClawAdapter);
|
|
259
|
+
return ok(
|
|
260
|
+
JSON.stringify({
|
|
261
|
+
error: "onboarding_required",
|
|
262
|
+
...status,
|
|
263
|
+
}),
|
|
264
|
+
);
|
|
265
|
+
}
|
|
122
266
|
},
|
|
123
267
|
});
|
|
124
268
|
|
|
@@ -126,11 +270,24 @@ export default definePluginEntry({
|
|
|
126
270
|
name: "ynabro_get_plan_info",
|
|
127
271
|
label: "Get Plan Info",
|
|
128
272
|
description: "Get basic information about a plan",
|
|
129
|
-
parameters:
|
|
130
|
-
async execute(
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
273
|
+
parameters: Type.Object({}),
|
|
274
|
+
async execute() {
|
|
275
|
+
try {
|
|
276
|
+
const [client, planId] = await Promise.all([
|
|
277
|
+
Promise.resolve(getClient()),
|
|
278
|
+
getDefaultPlanId(),
|
|
279
|
+
]);
|
|
280
|
+
const result = await getPlanInfo(client, planId);
|
|
281
|
+
return ok(JSON.stringify(result, null, 2));
|
|
282
|
+
} catch (_error) {
|
|
283
|
+
const status = await checkOnboardingStatus(openClawAdapter);
|
|
284
|
+
return ok(
|
|
285
|
+
JSON.stringify({
|
|
286
|
+
error: "onboarding_required",
|
|
287
|
+
...status,
|
|
288
|
+
}),
|
|
289
|
+
);
|
|
290
|
+
}
|
|
134
291
|
},
|
|
135
292
|
});
|
|
136
293
|
|