pi-ynabro 2.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 +1 -1
- package/package.json +1 -1
- package/skills/ynabro/prompts/ynabro.md +1 -1
- package/src/index.ts +138 -28
package/README.md
CHANGED
|
@@ -28,4 +28,4 @@ pi install npm:pi-ynabro
|
|
|
28
28
|
|
|
29
29
|
## Requirements
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
Run `ynabro_setup` once to complete interactive onboarding. The tool will prompt for a YNAB Personal Access Token (generate at https://app.ynab.com/settings/developer) and let you select a default plan. Both are stored securely in pi's `AuthStorage` (`~/.pi/agent/auth.json`).
|
package/package.json
CHANGED
|
@@ -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 (
|
|
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,7 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { AuthStorage } from "@earendil-works/pi-coding-agent";
|
|
2
3
|
import { Type } from "typebox";
|
|
4
|
+
import type { YnabroConfigAdapter } from "ynabro";
|
|
3
5
|
import {
|
|
4
6
|
approveTransaction,
|
|
5
7
|
getPendingTransactions,
|
|
@@ -11,18 +13,42 @@ import {
|
|
|
11
13
|
YnabroClient,
|
|
12
14
|
} from "ynabro";
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
const authStorage = AuthStorage.create();
|
|
17
|
+
|
|
18
|
+
const piConfigAdapter: YnabroConfigAdapter = {
|
|
19
|
+
async getDefaultPlanId(): Promise<string | undefined> {
|
|
20
|
+
const credential = authStorage.get("ynab-plan");
|
|
21
|
+
if (credential?.type === "api_key") {
|
|
22
|
+
return credential.key;
|
|
23
|
+
}
|
|
24
|
+
return undefined;
|
|
25
|
+
},
|
|
26
|
+
async setDefaultPlanId(planId: string): Promise<void> {
|
|
27
|
+
authStorage.set("ynab-plan", { type: "api_key", key: planId });
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
async function getClient(): Promise<YnabroClient> {
|
|
32
|
+
const token = await authStorage.getApiKey("ynab");
|
|
33
|
+
if (!token) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
"YNAB token not configured. Run ynabro_setup to complete onboarding.",
|
|
36
|
+
);
|
|
37
|
+
}
|
|
17
38
|
return new YnabroClient(token);
|
|
18
39
|
}
|
|
19
40
|
|
|
20
|
-
|
|
21
|
-
planId
|
|
22
|
-
|
|
41
|
+
async function getDefaultPlanId(): Promise<string> {
|
|
42
|
+
const planId = await piConfigAdapter.getDefaultPlanId();
|
|
43
|
+
if (!planId) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
"No default plan configured. Run ynabro_setup to complete onboarding.",
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
return planId;
|
|
49
|
+
}
|
|
23
50
|
|
|
24
51
|
const approveSchema = Type.Object({
|
|
25
|
-
planId: Type.String({ description: "The ID of the YNAB plan" }),
|
|
26
52
|
transactionId: Type.String({
|
|
27
53
|
description: "The ID of the transaction to approve",
|
|
28
54
|
}),
|
|
@@ -52,10 +78,15 @@ export default function ynabroExtension(api: ExtensionAPI): void {
|
|
|
52
78
|
api.registerTool({
|
|
53
79
|
name: "ynabro_get_pending_transactions",
|
|
54
80
|
label: "Get Pending Transactions",
|
|
55
|
-
description:
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
81
|
+
description:
|
|
82
|
+
"Get all pending (uncategorized) transactions for the default plan",
|
|
83
|
+
parameters: Type.Object({}),
|
|
84
|
+
async execute(_toolCallId, _params) {
|
|
85
|
+
const [client, planId] = await Promise.all([
|
|
86
|
+
getClient(),
|
|
87
|
+
getDefaultPlanId(),
|
|
88
|
+
]);
|
|
89
|
+
const result = await getPendingTransactions(client, planId);
|
|
59
90
|
return {
|
|
60
91
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
61
92
|
details: undefined,
|
|
@@ -66,10 +97,14 @@ export default function ynabroExtension(api: ExtensionAPI): void {
|
|
|
66
97
|
api.registerTool({
|
|
67
98
|
name: "ynabro_get_recent_transactions",
|
|
68
99
|
label: "Get Recent Transactions",
|
|
69
|
-
description: "Get recent transactions for
|
|
70
|
-
parameters:
|
|
71
|
-
async execute(_toolCallId,
|
|
72
|
-
const
|
|
100
|
+
description: "Get recent transactions for the default plan",
|
|
101
|
+
parameters: Type.Object({}),
|
|
102
|
+
async execute(_toolCallId, _params) {
|
|
103
|
+
const [client, planId] = await Promise.all([
|
|
104
|
+
getClient(),
|
|
105
|
+
getDefaultPlanId(),
|
|
106
|
+
]);
|
|
107
|
+
const result = await getRecentTransactions(client, planId);
|
|
73
108
|
return {
|
|
74
109
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
75
110
|
details: undefined,
|
|
@@ -80,14 +115,14 @@ export default function ynabroExtension(api: ExtensionAPI): void {
|
|
|
80
115
|
api.registerTool({
|
|
81
116
|
name: "ynabro_approve_transaction",
|
|
82
117
|
label: "Approve Transaction",
|
|
83
|
-
description: "Approve a specific transaction",
|
|
118
|
+
description: "Approve a specific transaction in the default plan",
|
|
84
119
|
parameters: approveSchema,
|
|
85
120
|
async execute(_toolCallId, params) {
|
|
86
|
-
await
|
|
121
|
+
const [client, planId] = await Promise.all([
|
|
87
122
|
getClient(),
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
);
|
|
123
|
+
getDefaultPlanId(),
|
|
124
|
+
]);
|
|
125
|
+
await approveTransaction(client, planId, params.transactionId);
|
|
91
126
|
return {
|
|
92
127
|
content: [{ type: "text", text: JSON.stringify({ success: true }) }],
|
|
93
128
|
details: undefined,
|
|
@@ -98,10 +133,14 @@ export default function ynabroExtension(api: ExtensionAPI): void {
|
|
|
98
133
|
api.registerTool({
|
|
99
134
|
name: "ynabro_get_plan_info",
|
|
100
135
|
label: "Get Plan Info",
|
|
101
|
-
description: "Get basic information about
|
|
102
|
-
parameters:
|
|
103
|
-
async execute(_toolCallId,
|
|
104
|
-
const
|
|
136
|
+
description: "Get basic information about the default plan",
|
|
137
|
+
parameters: Type.Object({}),
|
|
138
|
+
async execute(_toolCallId, _params) {
|
|
139
|
+
const [client, planId] = await Promise.all([
|
|
140
|
+
getClient(),
|
|
141
|
+
getDefaultPlanId(),
|
|
142
|
+
]);
|
|
143
|
+
const result = await getPlanInfo(client, planId);
|
|
105
144
|
return {
|
|
106
145
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
107
146
|
details: undefined,
|
|
@@ -113,12 +152,83 @@ export default function ynabroExtension(api: ExtensionAPI): void {
|
|
|
113
152
|
name: "ynabro_setup",
|
|
114
153
|
label: "Setup YNAB",
|
|
115
154
|
description:
|
|
116
|
-
"Set up YNAB integration —
|
|
155
|
+
"Set up or reconfigure YNAB integration — stores API token and selects a default plan",
|
|
117
156
|
parameters: Type.Object({}),
|
|
118
|
-
async execute() {
|
|
119
|
-
|
|
157
|
+
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
158
|
+
// Step 1: Token check
|
|
159
|
+
let token = await authStorage.getApiKey("ynab");
|
|
160
|
+
if (!token) {
|
|
161
|
+
const input = await ctx.ui.input(
|
|
162
|
+
"YNAB Personal Access Token",
|
|
163
|
+
"Paste your token from https://app.ynab.com/settings/developer",
|
|
164
|
+
);
|
|
165
|
+
if (!input) {
|
|
166
|
+
return {
|
|
167
|
+
content: [
|
|
168
|
+
{
|
|
169
|
+
type: "text",
|
|
170
|
+
text: JSON.stringify({
|
|
171
|
+
error: "Setup cancelled — no token provided.",
|
|
172
|
+
}),
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
details: undefined,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
authStorage.set("ynab", { type: "api_key", key: input });
|
|
179
|
+
token = input;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Step 2: Fetch plans
|
|
183
|
+
const client = new YnabroClient(token);
|
|
184
|
+
const plans = await client.getPlans();
|
|
185
|
+
if (plans.length === 0) {
|
|
186
|
+
return {
|
|
187
|
+
content: [
|
|
188
|
+
{
|
|
189
|
+
type: "text",
|
|
190
|
+
text: JSON.stringify({
|
|
191
|
+
error: "No plans found in your YNAB account.",
|
|
192
|
+
}),
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
details: undefined,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Step 3: Plan selector
|
|
200
|
+
const planOptions = plans.map((p) => `${p.name} (${p.id})`);
|
|
201
|
+
const selected = await ctx.ui.select(
|
|
202
|
+
"Select your default YNAB plan",
|
|
203
|
+
planOptions,
|
|
204
|
+
);
|
|
205
|
+
if (!selected) {
|
|
206
|
+
return {
|
|
207
|
+
content: [
|
|
208
|
+
{
|
|
209
|
+
type: "text",
|
|
210
|
+
text: JSON.stringify({
|
|
211
|
+
error: "Setup cancelled — no plan selected.",
|
|
212
|
+
}),
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
details: undefined,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Step 4: Store and return
|
|
220
|
+
const selectedPlan = plans[planOptions.indexOf(selected)];
|
|
221
|
+
await setupYnab(client, plans, selectedPlan.id, piConfigAdapter);
|
|
120
222
|
return {
|
|
121
|
-
content: [
|
|
223
|
+
content: [
|
|
224
|
+
{
|
|
225
|
+
type: "text",
|
|
226
|
+
text: JSON.stringify({
|
|
227
|
+
message: `Setup complete! Default plan set to: ${selectedPlan.name}`,
|
|
228
|
+
defaultPlanId: selectedPlan.id,
|
|
229
|
+
}),
|
|
230
|
+
},
|
|
231
|
+
],
|
|
122
232
|
details: undefined,
|
|
123
233
|
};
|
|
124
234
|
},
|