pi-ynabro 2.1.0 → 2.2.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-ynabro",
3
- "version": "2.1.0",
3
+ "version": "2.2.1",
4
4
  "description": "Pi extension that registers YNABro tools for YNAB integration",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -25,7 +25,7 @@
25
25
  ]
26
26
  },
27
27
  "dependencies": {
28
- "ynabro": "file:../ynabro"
28
+ "ynabro": "^2.2.0"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@earendil-works/pi-coding-agent": "^0.74.0",
@@ -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, check whether YNAB access has been set up:
11
+ Before performing any YNAB operations, call `ynabro_onboarding_status` to check
12
+ whether YNAB access is configured.
12
13
 
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
- 2. A default plan must be selected and saved.
14
+ If `ready` is `false`, walk the user through setup:
15
15
 
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.
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,9 +1,10 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { AuthStorage } from "@earendil-works/pi-coding-agent";
3
3
  import { Type } from "typebox";
4
- import type { YnabroConfigAdapter } from "ynabro";
4
+ import type { OnboardingStatus, YnabroConfigAdapter } from "ynabro";
5
5
  import {
6
6
  approveTransaction,
7
+ checkOnboardingStatus,
7
8
  getPendingTransactions,
8
9
  getPlanInfo,
9
10
  getRecentTransactions,
@@ -26,28 +27,31 @@ const piConfigAdapter: YnabroConfigAdapter = {
26
27
  async setDefaultPlanId(planId: string): Promise<void> {
27
28
  authStorage.set("ynab-plan", { type: "api_key", key: planId });
28
29
  },
30
+ async hasToken(): Promise<boolean> {
31
+ return !!(await authStorage.getApiKey("ynab"));
32
+ },
29
33
  };
30
34
 
31
35
  async function getClient(): Promise<YnabroClient> {
32
36
  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
- }
37
+ if (!token) throw new Error("YNAB token not configured");
38
38
  return new YnabroClient(token);
39
39
  }
40
40
 
41
41
  async function getDefaultPlanId(): Promise<string> {
42
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
- }
43
+ if (!planId) throw new Error("No default plan configured");
48
44
  return planId;
49
45
  }
50
46
 
47
+ async function checkConfigured(): Promise<OnboardingStatus | null> {
48
+ const status = await checkOnboardingStatus(piConfigAdapter);
49
+ if (!status.ready) {
50
+ return status;
51
+ }
52
+ return null;
53
+ }
54
+
51
55
  const approveSchema = Type.Object({
52
56
  transactionId: Type.String({
53
57
  description: "The ID of the transaction to approve",
@@ -75,6 +79,21 @@ const updateSkillStateSchema = Type.Object({
75
79
  });
76
80
 
77
81
  export default function ynabroExtension(api: ExtensionAPI): void {
82
+ api.registerTool({
83
+ name: "ynabro_onboarding_status",
84
+ label: "Onboarding Status",
85
+ description:
86
+ "Check whether YNABro is fully configured. Returns ready status, any missing configuration, and token generation instructions.",
87
+ parameters: Type.Object({}),
88
+ async execute(_toolCallId, _params) {
89
+ const status = await checkOnboardingStatus(piConfigAdapter);
90
+ return {
91
+ content: [{ type: "text", text: JSON.stringify(status) }],
92
+ details: undefined,
93
+ };
94
+ },
95
+ });
96
+
78
97
  api.registerTool({
79
98
  name: "ynabro_get_pending_transactions",
80
99
  label: "Get Pending Transactions",
@@ -82,6 +101,22 @@ export default function ynabroExtension(api: ExtensionAPI): void {
82
101
  "Get all pending (uncategorized) transactions for the default plan",
83
102
  parameters: Type.Object({}),
84
103
  async execute(_toolCallId, _params) {
104
+ const onboardingError = await checkConfigured();
105
+ if (onboardingError) {
106
+ return {
107
+ content: [
108
+ {
109
+ type: "text",
110
+ text: JSON.stringify({
111
+ error: "onboarding_required",
112
+ ...onboardingError,
113
+ }),
114
+ },
115
+ ],
116
+ details: undefined,
117
+ };
118
+ }
119
+
85
120
  const [client, planId] = await Promise.all([
86
121
  getClient(),
87
122
  getDefaultPlanId(),
@@ -100,6 +135,22 @@ export default function ynabroExtension(api: ExtensionAPI): void {
100
135
  description: "Get recent transactions for the default plan",
101
136
  parameters: Type.Object({}),
102
137
  async execute(_toolCallId, _params) {
138
+ const onboardingError = await checkConfigured();
139
+ if (onboardingError) {
140
+ return {
141
+ content: [
142
+ {
143
+ type: "text",
144
+ text: JSON.stringify({
145
+ error: "onboarding_required",
146
+ ...onboardingError,
147
+ }),
148
+ },
149
+ ],
150
+ details: undefined,
151
+ };
152
+ }
153
+
103
154
  const [client, planId] = await Promise.all([
104
155
  getClient(),
105
156
  getDefaultPlanId(),
@@ -118,6 +169,22 @@ export default function ynabroExtension(api: ExtensionAPI): void {
118
169
  description: "Approve a specific transaction in the default plan",
119
170
  parameters: approveSchema,
120
171
  async execute(_toolCallId, params) {
172
+ const onboardingError = await checkConfigured();
173
+ if (onboardingError) {
174
+ return {
175
+ content: [
176
+ {
177
+ type: "text",
178
+ text: JSON.stringify({
179
+ error: "onboarding_required",
180
+ ...onboardingError,
181
+ }),
182
+ },
183
+ ],
184
+ details: undefined,
185
+ };
186
+ }
187
+
121
188
  const [client, planId] = await Promise.all([
122
189
  getClient(),
123
190
  getDefaultPlanId(),
@@ -136,6 +203,22 @@ export default function ynabroExtension(api: ExtensionAPI): void {
136
203
  description: "Get basic information about the default plan",
137
204
  parameters: Type.Object({}),
138
205
  async execute(_toolCallId, _params) {
206
+ const onboardingError = await checkConfigured();
207
+ if (onboardingError) {
208
+ return {
209
+ content: [
210
+ {
211
+ type: "text",
212
+ text: JSON.stringify({
213
+ error: "onboarding_required",
214
+ ...onboardingError,
215
+ }),
216
+ },
217
+ ],
218
+ details: undefined,
219
+ };
220
+ }
221
+
139
222
  const [client, planId] = await Promise.all([
140
223
  getClient(),
141
224
  getDefaultPlanId(),