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 CHANGED
@@ -28,4 +28,4 @@ pi install npm:pi-ynabro
28
28
 
29
29
  ## Requirements
30
30
 
31
- - `YNAB_TOKEN` environment variable must be set
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-ynabro",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Pi extension that registers YNABro tools for YNAB integration",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -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,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
- function getClient(): YnabroClient {
15
- const token = process.env.YNAB_TOKEN;
16
- if (!token) throw new Error("YNAB_TOKEN environment variable is not set");
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
- const planIdSchema = Type.Object({
21
- planId: Type.String({ description: "The ID of the YNAB plan" }),
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: "Get all pending (uncategorized) transactions for a plan",
56
- parameters: planIdSchema,
57
- async execute(_toolCallId, params) {
58
- const result = await getPendingTransactions(getClient(), params.planId);
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 a plan",
70
- parameters: planIdSchema,
71
- async execute(_toolCallId, params) {
72
- const result = await getRecentTransactions(getClient(), params.planId);
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 approveTransaction(
121
+ const [client, planId] = await Promise.all([
87
122
  getClient(),
88
- params.planId,
89
- params.transactionId,
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 a plan",
102
- parameters: planIdSchema,
103
- async execute(_toolCallId, params) {
104
- const result = await getPlanInfo(getClient(), params.planId);
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 — checks for token and selects a default plan",
155
+ "Set up or reconfigure YNAB integration — stores API token and selects a default plan",
117
156
  parameters: Type.Object({}),
118
- async execute() {
119
- const result = await setupYnab();
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: [{ type: "text", text: JSON.stringify(result, null, 2) }],
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
  },