Skip to content

Commit cf43359

Browse files
committed
Refactor Pine AI authentication flow: update skill documentation, remove required fields in plugin config, and enhance error handling in auth tools. Introduce CLI commands for manual authentication fallback.
1 parent 5890ed9 commit cf43359

4 files changed

Lines changed: 305 additions & 43 deletions

File tree

‎openclaw.plugin.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"default": "https://www.19pine.ai"
2222
}
2323
},
24-
"required": ["access_token", "user_id"]
24+
"required": []
2525
},
2626
"uiHints": {
2727
"access_token": {

‎skills/pine-ai-auth/SKILL.md‎

Lines changed: 84 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,92 @@
11
---
2-
name: Pine AI Auth
3-
description: Authenticate with Pine AI using email verification
4-
tools: [pine_auth_request, pine_auth_verify]
2+
name: pine-ai-auth
3+
description: Set up or refresh Pine AI authentication via the built-in auth tools.
4+
metadata:
5+
{ "openclaw": { "emoji": "🔑" } }
56
---
67

7-
# Pine AI Authentication
8+
# Pine AI Auth Setup
89

9-
To use Pine AI, the user needs to authenticate with their email address.
10+
## When to use
1011

11-
## Steps
12+
Use this skill when **any** of these are true:
1213

13-
1. Ask the user for their email address.
14-
2. Call `pine_auth_request` with their email.
15-
3. Tell the user to check their email for a 6-digit verification code.
16-
4. When they provide the code, call `pine_auth_verify` with email, code, and request_token.
17-
5. Save the returned `access_token` and `user_id` in the plugin configuration.
14+
- The user asks to set up Pine AI, configure Pine, or authenticate
15+
- A Pine AI tool invocation returns "not authenticated" or credentials are missing
16+
- A Pine AI operation fails with `TOKEN_EXPIRED`, `UNAUTHORIZED`, or a 401 response
17+
- The user says their Pine AI token isn't working
1818

19-
## When to use
19+
Do **not** use this skill for creating sessions or starting tasks — see the `pine-ai` skill for that.
20+
21+
## Prerequisites
22+
23+
- The user must have a **Pine AI account** (sign up at https://19pine.ai)
24+
25+
## Important: email verification requires user presence
26+
27+
This auth flow sends a verification code to the user's email inbox. The user **must be available** to check their email and tell you the code. This cannot be automated.
28+
29+
**Recommended timing:** Run this flow right after plugin installation or when the user explicitly asks to set up Pine AI — not during an unattended or automated workflow.
30+
31+
## Step-by-step instructions
32+
33+
### Step 1: Ask the user for their Pine AI email
34+
35+
Ask: "What email address is your Pine AI account registered with?"
36+
37+
Do not proceed until you have the email.
38+
39+
### Step 2: Send a verification code
40+
41+
Call the `pine_auth_request` tool with the user's email:
42+
43+
```
44+
pine_auth_request({ email: "user@example.com" })
45+
```
46+
47+
This sends a verification code to their email. If the request fails with a 400/422, the email may not be registered — ask the user to check their email or sign up at https://19pine.ai.
48+
49+
### Step 3: Ask the user for the verification code
50+
51+
Tell the user: "I've sent a verification code to your email. Please check your inbox (and spam folder) and tell me the code."
52+
53+
Wait for the user to provide the code. Do not guess or skip this step.
54+
55+
### Step 4: Verify the code and save credentials
56+
57+
Call the `pine_auth_verify` tool with the email and code:
58+
59+
```
60+
pine_auth_verify({ email: "user@example.com", code: "123456" })
61+
```
62+
63+
The tool verifies the code, saves the credentials to `~/.openclaw/openclaw.json` automatically, and returns a success message.
64+
65+
**If verification fails:**
66+
- Invalid code — ask the user to double-check the code and try again (call `pine_auth_verify` with the corrected code).
67+
- Expired token — go back to step 2 to send a new code.
68+
69+
### Step 5: Restart the gateway
70+
71+
Tell the user to restart the gateway for the new credentials to take effect:
72+
73+
"Credentials saved! Please run this command to activate them:"
74+
75+
```
76+
openclaw gateway restart
77+
```
78+
79+
The gateway **must be restarted** after authentication for the new credentials to take effect.
80+
81+
## Token refresh
82+
83+
Access tokens expire periodically. When a Pine AI operation fails with `TOKEN_EXPIRED` or a 401 error:
84+
85+
1. Inform the user their token has expired and needs to be refreshed
86+
2. Re-run this auth flow starting from step 1
87+
88+
## Security notes
2089

21-
Use this when:
22-
- The user wants to use Pine AI but hasn't authenticated yet.
23-
- The user's token has expired.
24-
- The user explicitly asks to log in or authenticate.
90+
- Never log or echo the access token in plaintext beyond what is needed
91+
- The token is stored in a local config file with the same permissions as the user's home directory
92+
- Do not commit config files containing tokens to version control

‎src/auth.ts‎

Lines changed: 215 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,245 @@
11
/**
2-
* Auth tools for Pine AI — conversational email verification.
2+
* Auth flow for Pine AI plugin.
3+
*
4+
* Provides two surfaces:
5+
* 1. Tools (pine_auth_request / pine_auth_verify) — the primary,
6+
* conversational path where the AI agent drives the flow.
7+
* 2. CLI commands (openclaw pine auth setup/verify) — a manual fallback.
8+
*
9+
* Delegates to the pine-ai SDK for actual API calls.
310
*/
411

512
import { Type } from '@sinclair/typebox';
6-
import { PineAI } from 'pine-ai';
13+
import { PineAI, AuthError } from 'pine-ai';
14+
15+
// ---------------------------------------------------------------------------
16+
// Module-level state: stores requestToken between the request and verify steps
17+
// so the AI agent never needs to pass it explicitly.
18+
// ---------------------------------------------------------------------------
19+
const pendingAuth = new Map<string, string>(); // email → requestToken
20+
21+
// ---------------------------------------------------------------------------
22+
// Tool registration (primary path)
23+
// ---------------------------------------------------------------------------
724

825
export function registerAuthTools(api: any): void {
26+
// --- Tool: pine_auth_request ---
927
api.registerTool({
1028
name: 'pine_auth_request',
11-
description: 'Request a Pine AI verification code. Sends an email with a 6-digit code.',
29+
description:
30+
'Start Pine AI authentication. Sends a verification code to the user\'s ' +
31+
'Pine AI account email. After calling this, ask the user to check their email ' +
32+
'(including spam) and provide the code, then call pine_auth_verify.',
1233
parameters: Type.Object({
13-
email: Type.String({ description: 'Email address to verify' }),
34+
email: Type.String({ description: "The user's Pine AI account email address" }),
1435
}),
15-
execute: async (args: { email: string }) => {
36+
async execute(_toolCallId: string, params: { email: string }) {
1637
try {
1738
const client = new PineAI();
18-
const result = await client.auth.requestCode(args.email);
39+
const result = await client.auth.requestCode(params.email);
40+
pendingAuth.set(params.email, result.request_token);
41+
42+
api.log?.info?.(`pine: auth code requested for ${params.email}`);
43+
1944
return {
20-
success: true,
21-
request_token: result.request_token,
22-
message: `Verification code sent to ${args.email}. Ask the user for the 6-digit code.`,
45+
content: [
46+
{
47+
type: 'text',
48+
text:
49+
`Verification code sent to ${params.email}. ` +
50+
'Ask the user to check their email (including spam folder) and provide the code. ' +
51+
'Then call pine_auth_verify with the email and code.',
52+
},
53+
],
54+
isError: false,
55+
};
56+
} catch (err: unknown) {
57+
const message = err instanceof Error ? err.message : String(err);
58+
api.log?.error?.(`pine: auth request failed: ${message}`);
59+
60+
const hint =
61+
err instanceof AuthError
62+
? ' The email may not be registered — the user can sign up at https://19pine.ai.'
63+
: '';
64+
65+
return {
66+
content: [{ type: 'text', text: `Pine AI auth request failed: ${message}.${hint}` }],
67+
isError: true,
2368
};
24-
} catch (err) {
25-
return { error: `Failed to request code: ${(err as Error).message}` };
2669
}
2770
},
2871
});
2972

73+
// --- Tool: pine_auth_verify ---
3074
api.registerTool({
3175
name: 'pine_auth_verify',
32-
description: 'Verify a Pine AI email code and get access credentials.',
76+
description:
77+
'Complete Pine AI authentication. Verifies the code the user received by email, ' +
78+
'saves the credentials to openclaw.json, and tells the user to restart the gateway. ' +
79+
'Must be called after pine_auth_request.',
3380
parameters: Type.Object({
34-
email: Type.String({ description: 'Email address' }),
35-
code: Type.String({ description: '6-digit verification code' }),
36-
request_token: Type.String({ description: 'Token from pine_auth_request' }),
81+
email: Type.String({ description: 'The same email used in pine_auth_request' }),
82+
code: Type.String({ description: "The verification code from the user's email" }),
83+
request_token: Type.Optional(
84+
Type.String({ description: 'Request token from pine_auth_request (usually not needed — resolved automatically)' }),
85+
),
3786
}),
38-
execute: async (args: { email: string; code: string; request_token: string }) => {
87+
async execute(_toolCallId: string, params: { email: string; code: string; request_token?: string }) {
88+
const requestToken = params.request_token || pendingAuth.get(params.email);
89+
90+
if (!requestToken) {
91+
return {
92+
content: [
93+
{
94+
type: 'text',
95+
text:
96+
'No pending auth request found for this email. ' +
97+
'Call pine_auth_request first to send a new verification code.',
98+
},
99+
],
100+
isError: true,
101+
};
102+
}
103+
39104
try {
40105
const client = new PineAI();
41-
const result = await client.auth.verifyCode(args.email, args.code, args.request_token);
106+
const result = await client.auth.verifyCode(params.email, params.code, requestToken);
107+
108+
// Write credentials to openclaw.json
109+
const cfg = api.runtime.config.loadConfig();
110+
const plugins = (cfg.plugins ?? {}) as Record<string, any>;
111+
const entries = (plugins.entries ?? {}) as Record<string, any>;
112+
const pluginEntry = (entries['openclaw-pine'] ?? {}) as Record<string, any>;
113+
114+
const updatedConfig = {
115+
...cfg,
116+
plugins: {
117+
...plugins,
118+
entries: {
119+
...entries,
120+
'openclaw-pine': {
121+
...pluginEntry,
122+
config: {
123+
...(pluginEntry.config ?? {}),
124+
access_token: result.access_token,
125+
user_id: result.id,
126+
},
127+
},
128+
},
129+
},
130+
};
131+
132+
await api.runtime.config.writeConfigFile(updatedConfig);
133+
pendingAuth.delete(params.email);
134+
135+
api.log?.info?.(`pine: auth successful for ${params.email}, credentials saved`);
136+
42137
return {
43-
success: true,
44-
user_id: result.id,
45-
email: result.email,
46-
access_token: result.access_token,
47-
message: 'Authentication successful. Save access_token and user_id in plugin config.',
138+
content: [
139+
{
140+
type: 'text',
141+
text:
142+
'Authentication successful! Credentials have been saved to openclaw.json. ' +
143+
'Tell the user to restart the gateway for the changes to take effect:\n\n' +
144+
' openclaw gateway restart',
145+
},
146+
],
147+
isError: false,
148+
};
149+
} catch (err: unknown) {
150+
const message = err instanceof Error ? err.message : String(err);
151+
api.log?.error?.(`pine: auth verify failed: ${message}`);
152+
153+
const isExpired = message.toLowerCase().includes('expired');
154+
const hint = isExpired
155+
? ' The request token has expired — call pine_auth_request again to send a new code.'
156+
: ' Ask the user to double-check the code and try again.';
157+
158+
return {
159+
content: [{ type: 'text', text: `Pine AI auth verification failed: ${message}.${hint}` }],
160+
isError: true,
48161
};
49-
} catch (err) {
50-
return { error: `Verification failed: ${(err as Error).message}` };
51162
}
52163
},
53164
});
54165
}
166+
167+
// ---------------------------------------------------------------------------
168+
// CLI registration (manual fallback)
169+
// ---------------------------------------------------------------------------
170+
171+
export function registerAuthCommands(api: any): void {
172+
api.registerCli?.(
173+
({ program }: any) => {
174+
const pine = program.command('pine').description('Pine AI plugin');
175+
const auth = pine.command('auth').description('Pine AI authentication');
176+
177+
auth
178+
.command('setup')
179+
.description('Set up Pine AI authentication')
180+
.option('--email <email>', 'Your Pine AI account email')
181+
.action(async (opts: any) => {
182+
if (!opts.email) {
183+
console.log('Usage: openclaw pine auth setup --email you@example.com');
184+
return;
185+
}
186+
187+
console.log(`Requesting verification code for ${opts.email}...`);
188+
189+
try {
190+
const client = new PineAI();
191+
const { request_token } = await client.auth.requestCode(opts.email);
192+
193+
console.log('Verification code sent! Check your email.');
194+
console.log(`Then run: openclaw pine auth verify --email ${opts.email} --request-token ${request_token} --code <code>`);
195+
} catch (err: unknown) {
196+
const message = err instanceof Error ? err.message : String(err);
197+
console.error(`Error: ${message}`);
198+
}
199+
});
200+
201+
auth
202+
.command('verify')
203+
.description('Verify email code and get access token')
204+
.option('--email <email>', 'Your Pine AI account email')
205+
.option('--request-token <token>', 'Request token from auth setup step')
206+
.option('--code <code>', 'Verification code from email')
207+
.action(async (opts: any) => {
208+
if (!opts.code || !opts.email || !opts.requestToken) {
209+
console.log('Usage: openclaw pine auth verify --email you@example.com --request-token <token> --code 1234');
210+
return;
211+
}
212+
213+
try {
214+
const client = new PineAI();
215+
const result = await client.auth.verifyCode(
216+
opts.email,
217+
opts.code,
218+
opts.requestToken || '',
219+
);
220+
221+
console.log('Authentication successful!');
222+
console.log('Add this to your plugin config in ~/.openclaw/openclaw.json:');
223+
console.log('');
224+
console.log(' "plugins": {');
225+
console.log(' "entries": {');
226+
console.log(' "openclaw-pine": {');
227+
console.log(' "config": {');
228+
console.log(` "access_token": "${result.access_token}",`);
229+
console.log(` "user_id": "${result.id}"`);
230+
console.log(' }');
231+
console.log(' }');
232+
console.log(' }');
233+
console.log(' }');
234+
console.log('');
235+
console.log('Then restart the gateway:');
236+
console.log(' openclaw gateway restart');
237+
} catch (err: unknown) {
238+
const message = err instanceof Error ? err.message : String(err);
239+
console.error(`Error: ${message}`);
240+
}
241+
});
242+
},
243+
{ commands: ['pine'] },
244+
);
245+
}

0 commit comments

Comments
 (0)