openclaw-pine-voice 0.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/LICENSE +21 -0
- package/README.md +291 -0
- package/dist/auth.d.ts +7 -0
- package/dist/auth.js +57 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +26 -0
- package/dist/tool.d.ts +10 -0
- package/dist/tool.js +272 -0
- package/dist/types.d.ts +6 -0
- package/dist/types.js +1 -0
- package/openclaw.plugin.json +40 -0
- package/package.json +50 -0
- package/skills/pine-voice/SKILL.md +131 -0
- package/skills/pine-voice-auth/SKILL.md +148 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pine AI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
# Pine AI Voice Call - OpenClaw Plugin
|
|
2
|
+
|
|
3
|
+
Make phone calls via Pine AI's voice agent from OpenClaw. The AI agent calls the specified number, carries out the conversation based on your instructions, and returns a full transcript (and an optional LLM-generated summary if requested). The voice agent can only speak English.
|
|
4
|
+
|
|
5
|
+
**Powered by [Pine AI](https://19pine.ai). Subject to [Pine AI Voice Terms of Service](https://19pine.ai/legal/voice-tos).**
|
|
6
|
+
|
|
7
|
+
## How is this different from the built-in voice-call plugin?
|
|
8
|
+
|
|
9
|
+
| | Built-in `voice_call` | Pine `pine_voice_call` |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| Who is the voice agent? | Your OpenClaw agent | Pine's purpose-trained agent |
|
|
12
|
+
| Requires webhook URL? | Yes (ngrok/Tailscale) | No |
|
|
13
|
+
| Requires Twilio/Telnyx account? | Yes | No (Pine handles telephony) |
|
|
14
|
+
| Real-time conversation control? | Yes | No (delegated) |
|
|
15
|
+
| Best for | Custom real-time voice bots | High-quality delegated calls |
|
|
16
|
+
|
|
17
|
+
## Prerequisites
|
|
18
|
+
|
|
19
|
+
- Pine AI Pro subscription ([19pine.ai](https://19pine.ai))
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
openclaw plugins install openclaw-pine-voice
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Restart the gateway after installation:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
openclaw gateway restart
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Configure
|
|
34
|
+
|
|
35
|
+
Configuration has two parts: enabling the tool for your agent, and authenticating with Pine AI.
|
|
36
|
+
|
|
37
|
+
### Step 1: Enable the tool for your agent
|
|
38
|
+
|
|
39
|
+
The plugin provides three tools (all registered as optional):
|
|
40
|
+
|
|
41
|
+
| Tool | Description |
|
|
42
|
+
|---|---|
|
|
43
|
+
| `pine_voice_call_and_wait` | **Recommended.** Initiates a call and blocks until it completes, returning the full transcript in one tool call. Uses SSE to wait for the final result with automatic polling fallback. |
|
|
44
|
+
| `pine_voice_call` | Initiates a call and returns immediately with a `call_id`. Use with `pine_voice_call_status` for manual polling. |
|
|
45
|
+
| `pine_voice_call_status` | Checks the status of a call initiated by `pine_voice_call`. |
|
|
46
|
+
|
|
47
|
+
Your agent won't see these tools until you explicitly allow them. Add them to your agent's tool allowlist in `openclaw.json`:
|
|
48
|
+
|
|
49
|
+
**To enable for all agents globally**, add the tools to the top-level `tools.allow`:
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"tools": {
|
|
54
|
+
"allow": ["pine_voice_call_and_wait"]
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
> **Tip:** `pine_voice_call_and_wait` is all most agents need. If you want the manual initiate+poll pattern as well, add `"pine_voice_call"` and `"pine_voice_call_status"` to the list.
|
|
60
|
+
|
|
61
|
+
**To enable for a specific agent only**, add it under that agent's config in `agents.list`:
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"agents": {
|
|
66
|
+
"list": [
|
|
67
|
+
{
|
|
68
|
+
"id": "main",
|
|
69
|
+
"tools": {
|
|
70
|
+
"allow": ["pine_voice_call_and_wait"]
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
> **Note:** If your agent already has a `tools.allow` list with other tools, just append the tool names to the existing array. If you're using `tools.profile` (e.g., `"coding"` or `"messaging"`), adding tools to `tools.allow` will make them available alongside your profile's default tools — the profile won't be overridden.
|
|
79
|
+
|
|
80
|
+
### Step 2: Restart the gateway
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
openclaw gateway restart
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Your agent now has access to the Pine Voice call tools.
|
|
87
|
+
|
|
88
|
+
### Step 3: Authenticate with Pine AI
|
|
89
|
+
|
|
90
|
+
You have two options for when to authenticate:
|
|
91
|
+
|
|
92
|
+
**Option A: Authenticate now (recommended)**
|
|
93
|
+
|
|
94
|
+
We recommend authenticating right after installation. The auth flow requires an email verification code, so it's best done while you're actively setting things up — not later when the agent tries to make a call (which could be at any time).
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
# 1. Request a verification code (sent to your Pine AI account email)
|
|
98
|
+
openclaw pine-voice auth setup --email you@example.com
|
|
99
|
+
|
|
100
|
+
# 2. Check your email for the code, then verify (use the request-token from setup output)
|
|
101
|
+
openclaw pine-voice auth verify --email you@example.com --request-token <TOKEN> --code 1234
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The command prints your access token. Add it to your plugin config in `openclaw.json`:
|
|
105
|
+
|
|
106
|
+
```json
|
|
107
|
+
{
|
|
108
|
+
"plugins": {
|
|
109
|
+
"entries": {
|
|
110
|
+
"pine-voice": {
|
|
111
|
+
"enabled": true,
|
|
112
|
+
"config": {
|
|
113
|
+
"access_token": "PASTE_YOUR_TOKEN_HERE"
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Then restart the gateway again:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
openclaw gateway restart
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**Option B: Let the agent handle it on first use**
|
|
128
|
+
|
|
129
|
+
If you skip authentication, the plugin still loads and the tool is visible to your agent. The first time the agent tries to make a call, it will receive an error explaining that authentication is needed. The agent will then guide you through the email verification flow — it will ask for your email, run the auth commands, and configure the token for you.
|
|
130
|
+
|
|
131
|
+
This works, but keep in mind: the email verification code arrives in your inbox, so you need to be available to provide it. If the agent tries to make a call while you're away (e.g., in an automated workflow or overnight), it will be blocked until you complete verification.
|
|
132
|
+
|
|
133
|
+
## Try it out
|
|
134
|
+
|
|
135
|
+
After setup, test that everything works by making a quick call to your own phone:
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
"Call my phone at +1XXXXXXXXXX. Tell me that Pine Voice is set up and working.
|
|
139
|
+
Just confirm the setup is complete and say goodbye."
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Replace `+1XXXXXXXXXX` with your actual phone number. You'll receive a call from Pine's voice agent, hear it speak, and get a transcript back — this confirms your token, subscription, and the full end-to-end flow are all working.
|
|
143
|
+
|
|
144
|
+
Then try real tasks:
|
|
145
|
+
|
|
146
|
+
- "Call John at +14155551234 and schedule a meeting for Tuesday"
|
|
147
|
+
- "Phone the restaurant at +14155559876 to make a reservation for tonight at 7pm for 4 people"
|
|
148
|
+
- "Call Comcast at +18001234567 and negotiate my bill down to $60/mo. My account is 1234567890, current plan is $89.99/mo. I've been a customer for 8 years. Don't change the plan tier."
|
|
149
|
+
|
|
150
|
+
### Supported countries
|
|
151
|
+
|
|
152
|
+
The voice agent can only speak English. Calls can be placed to phone numbers in the following countries: US, Canada (+1), UK (+44), Australia (+61), New Zealand (+64), and Ireland (+353). Calls to numbers outside these country codes will be rejected.
|
|
153
|
+
|
|
154
|
+
### Caller personality
|
|
155
|
+
|
|
156
|
+
The plugin supports two caller personalities:
|
|
157
|
+
|
|
158
|
+
- **Negotiator** (`caller: "negotiator"`): For complex negotiations like bill reductions, insurance claims, and formal business matters. **You must provide a thorough negotiation strategy** in the context and instructions — including target outcome, acceptable range, leverage points, fallback positions, and constraints. The negotiator agent thinks things through deliberately and confirms important details multiple times.
|
|
159
|
+
- **Communicator** (`caller: "communicator"`): For general-purpose routine tasks like scheduling appointments, making reservations, and inquiries. A specific objective is sufficient — no elaborate strategy needed.
|
|
160
|
+
|
|
161
|
+
If not specified, the caller defaults to "negotiator".
|
|
162
|
+
|
|
163
|
+
### Important: Gather all required information first
|
|
164
|
+
|
|
165
|
+
The voice agent **cannot ask a human for missing information mid-call**. There is no way for the AI to pause and request details during the conversation. Before making a call, make sure you have gathered all information the callee may need, including any authentication, verification, or payment details relevant to the task.
|
|
166
|
+
|
|
167
|
+
The exact requirements vary depending on the type of call — anticipate what the callee will ask for and include it upfront. If calling customer service or any entity that verifies identity, **you must include sufficient verification information** — the call will fail without it.
|
|
168
|
+
|
|
169
|
+
## What happens
|
|
170
|
+
|
|
171
|
+
When using `pine_voice_call_and_wait` (recommended):
|
|
172
|
+
|
|
173
|
+
1. The tool sends your instructions to Pine's voice agent
|
|
174
|
+
2. An SSE connection waits for the final result (with automatic polling fallback)
|
|
175
|
+
3. You receive the full transcript (and an optional summary if requested) as soon as the call completes
|
|
176
|
+
|
|
177
|
+
> **Note:** No real-time intermediate updates are available during the call. You will not receive "call connected" events, partial transcripts, or live conversation updates. The only result is the final transcript delivered after the call ends.
|
|
178
|
+
|
|
179
|
+
When using `pine_voice_call` + `pine_voice_call_status` (manual):
|
|
180
|
+
|
|
181
|
+
1. The tool sends your instructions and returns a `call_id` immediately
|
|
182
|
+
2. Your agent polls `pine_voice_call_status` every 30 seconds
|
|
183
|
+
3. You receive the full transcript once the status reaches a terminal state
|
|
184
|
+
|
|
185
|
+
> **Note:** While a call is in progress, your agent is waiting for the result. If you need to do other tasks simultaneously, use OpenClaw's sub-agents (`sessions_spawn`) to run the call in a background session.
|
|
186
|
+
|
|
187
|
+
## Limits and pricing
|
|
188
|
+
|
|
189
|
+
- Each call costs **50 base credits + 20 credits per minute** of duration
|
|
190
|
+
- 5 concurrent calls per user
|
|
191
|
+
- Pro subscription required
|
|
192
|
+
- MCP voice calls use your existing Pine AI credit balance. All credit sources apply — daily login rewards (300 credits/day), subscription plan credits, and purchased add-ons. Credit policies are governed by the Pine AI app. See [Pine AI Pricing FAQ](https://www.19pine.ai/pricing-faqs) for details.
|
|
193
|
+
|
|
194
|
+
## Troubleshooting
|
|
195
|
+
|
|
196
|
+
| Error | Cause | Fix |
|
|
197
|
+
|---|---|---|
|
|
198
|
+
| TOKEN_EXPIRED | Access token expired | Re-run `openclaw pine-voice auth setup` |
|
|
199
|
+
| SUBSCRIPTION_REQUIRED | Not a Pro subscriber | Subscribe at 19pine.ai |
|
|
200
|
+
| RATE_LIMITED | Too many calls | Wait and try again |
|
|
201
|
+
| INSUFFICIENT_DETAIL | Objective too vague | Provide a more specific call objective |
|
|
202
|
+
| DND_BLOCKED | Number on do-not-call list | Cannot call this number |
|
|
203
|
+
|
|
204
|
+
## Development
|
|
205
|
+
|
|
206
|
+
### Testing locally (before publishing)
|
|
207
|
+
|
|
208
|
+
**Option A: Link local folder (recommended for development)**
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
openclaw plugins install -l /path/to/openclaw-pine-voice
|
|
212
|
+
openclaw gateway restart
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
The `-l` flag creates a symlink instead of copying, so edits to your source files take effect after a gateway restart. No need to re-install.
|
|
216
|
+
|
|
217
|
+
**Option B: Load path in config**
|
|
218
|
+
|
|
219
|
+
Add the plugin path directly to `openclaw.json` — no install command needed:
|
|
220
|
+
|
|
221
|
+
```json
|
|
222
|
+
{
|
|
223
|
+
"plugins": {
|
|
224
|
+
"load": {
|
|
225
|
+
"paths": ["/path/to/openclaw-pine-voice"]
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Restart the gateway to load the plugin.
|
|
232
|
+
|
|
233
|
+
### Development workflow
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
# 1. Link your local plugin
|
|
237
|
+
openclaw plugins install -l ./
|
|
238
|
+
|
|
239
|
+
# 2. Add config to openclaw.json (token, gateway_url, tools.allow)
|
|
240
|
+
|
|
241
|
+
# 3. Restart gateway
|
|
242
|
+
openclaw gateway restart
|
|
243
|
+
|
|
244
|
+
# 4. Test by chatting with your agent
|
|
245
|
+
# "Call +14155551234 and ask about store hours"
|
|
246
|
+
|
|
247
|
+
# 5. Edit src/*.ts, restart gateway, test again
|
|
248
|
+
|
|
249
|
+
# 6. Verify the plugin loads correctly
|
|
250
|
+
openclaw plugins list
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Publishing to npm
|
|
254
|
+
|
|
255
|
+
Once tested locally:
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
# 1. Login to npm
|
|
259
|
+
npm login
|
|
260
|
+
|
|
261
|
+
# 2. Publish the package
|
|
262
|
+
npm publish
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
After publishing, users can install with:
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
openclaw plugins install openclaw-pine-voice
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Updating a published version
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
# Bump version in package.json, then:
|
|
275
|
+
npm publish
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Users update with:
|
|
279
|
+
|
|
280
|
+
```bash
|
|
281
|
+
openclaw plugins update openclaw-pine-voice
|
|
282
|
+
openclaw gateway restart
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
## Terms of Service
|
|
286
|
+
|
|
287
|
+
This plugin connects to Pine AI's voice calling service. Pine AI is the service provider. All calls are recorded and transcribed. By using this plugin, you agree to [Pine AI's Voice Terms of Service](https://19pine.ai/legal/voice-tos).
|
|
288
|
+
|
|
289
|
+
## License
|
|
290
|
+
|
|
291
|
+
MIT
|
package/dist/auth.d.ts
ADDED
package/dist/auth.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth flow for Pine Voice plugin.
|
|
3
|
+
* Registers CLI commands for email-based authentication.
|
|
4
|
+
*
|
|
5
|
+
* Delegates to the pine-voice SDK for actual API calls.
|
|
6
|
+
*/
|
|
7
|
+
import { PineVoice } from "pine-voice";
|
|
8
|
+
export function registerAuthCommands(api) {
|
|
9
|
+
api.registerCli?.(({ program }) => {
|
|
10
|
+
const pineVoice = program.command("pine-voice").description("Pine AI Voice Call plugin");
|
|
11
|
+
const auth = pineVoice.command("auth").description("Pine AI authentication");
|
|
12
|
+
auth
|
|
13
|
+
.command("setup")
|
|
14
|
+
.description("Set up Pine AI authentication")
|
|
15
|
+
.option("--email <email>", "Your Pine AI account email")
|
|
16
|
+
.action(async (opts) => {
|
|
17
|
+
if (!opts.email) {
|
|
18
|
+
console.log("Usage: openclaw pine-voice auth setup --email you@example.com");
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
console.log(`Requesting verification code for ${opts.email}...`);
|
|
22
|
+
try {
|
|
23
|
+
const { requestToken } = await PineVoice.auth.requestCode(opts.email);
|
|
24
|
+
console.log("Verification code sent! Check your email.");
|
|
25
|
+
console.log(`Then run: openclaw pine-voice auth verify --email ${opts.email} --request-token ${requestToken} --code <code>`);
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
29
|
+
console.error(`Error: ${message}`);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
auth
|
|
33
|
+
.command("verify")
|
|
34
|
+
.description("Verify email code and get access token")
|
|
35
|
+
.option("--email <email>", "Your Pine AI account email")
|
|
36
|
+
.option("--request-token <token>", "Request token from auth setup step")
|
|
37
|
+
.option("--code <code>", "Verification code from email")
|
|
38
|
+
.action(async (opts) => {
|
|
39
|
+
if (!opts.code || !opts.email || !opts.requestToken) {
|
|
40
|
+
console.log("Usage: openclaw pine-voice auth verify --email you@example.com --request-token <token> --code 1234");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const { accessToken, userId } = await PineVoice.auth.verifyCode(opts.email, opts.requestToken || "", opts.code);
|
|
45
|
+
console.log("Authentication successful!");
|
|
46
|
+
console.log(`Add this to your pine-voice config:`);
|
|
47
|
+
console.log(` access_token: "${accessToken}"`);
|
|
48
|
+
console.log(` user_id: "${userId}"`);
|
|
49
|
+
console.log("\nOr set it in openclaw.json under plugins.entries.pine-voice.config");
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
53
|
+
console.error(`Error: ${message}`);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}, { commands: ["pine-voice"] });
|
|
57
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pine AI Voice Call plugin for OpenClaw.
|
|
3
|
+
*
|
|
4
|
+
* Registers:
|
|
5
|
+
* - pine_voice_call tool (initiate a phone call, returns immediately)
|
|
6
|
+
* - pine_voice_call_status tool (check call progress / get results)
|
|
7
|
+
* - pine-voice CLI commands (auth setup/verify)
|
|
8
|
+
*
|
|
9
|
+
* Delegates all API calls to the pine-voice SDK. All safety, billing,
|
|
10
|
+
* and prompt logic lives on Pine's side.
|
|
11
|
+
*
|
|
12
|
+
* For non-blocking calls, the pine-voice skill instructs the AI to use
|
|
13
|
+
* sessions_spawn so the main agent stays responsive during long calls.
|
|
14
|
+
*/
|
|
15
|
+
export default function register(api: any): void;
|
|
16
|
+
export declare const id = "pine-voice";
|
|
17
|
+
export declare const name = "Pine AI Voice Call";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { registerVoiceCallTools } from "./tool.js";
|
|
2
|
+
import { registerAuthCommands } from "./auth.js";
|
|
3
|
+
/**
|
|
4
|
+
* Pine AI Voice Call plugin for OpenClaw.
|
|
5
|
+
*
|
|
6
|
+
* Registers:
|
|
7
|
+
* - pine_voice_call tool (initiate a phone call, returns immediately)
|
|
8
|
+
* - pine_voice_call_status tool (check call progress / get results)
|
|
9
|
+
* - pine-voice CLI commands (auth setup/verify)
|
|
10
|
+
*
|
|
11
|
+
* Delegates all API calls to the pine-voice SDK. All safety, billing,
|
|
12
|
+
* and prompt logic lives on Pine's side.
|
|
13
|
+
*
|
|
14
|
+
* For non-blocking calls, the pine-voice skill instructs the AI to use
|
|
15
|
+
* sessions_spawn so the main agent stays responsive during long calls.
|
|
16
|
+
*/
|
|
17
|
+
export default function register(api) {
|
|
18
|
+
// Register voice call tools (initiate + status)
|
|
19
|
+
registerVoiceCallTools(api);
|
|
20
|
+
// Register CLI commands for authentication
|
|
21
|
+
registerAuthCommands(api);
|
|
22
|
+
api.log?.info?.("pine-voice: plugin loaded");
|
|
23
|
+
}
|
|
24
|
+
// Also export as named for module compatibility
|
|
25
|
+
export const id = "pine-voice";
|
|
26
|
+
export const name = "Pine AI Voice Call";
|
package/dist/tool.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Register voice call tools with OpenClaw:
|
|
3
|
+
* - pine_voice_call: initiate a call (returns immediately)
|
|
4
|
+
* - pine_voice_call_status: poll a call's status
|
|
5
|
+
* - pine_voice_call_and_wait: initiate + block until complete (SSE + polling fallback)
|
|
6
|
+
*
|
|
7
|
+
* All tools are optional (user must add them to tools.allow).
|
|
8
|
+
* Delegates all API calls to the pine-voice SDK.
|
|
9
|
+
*/
|
|
10
|
+
export declare function registerVoiceCallTools(api: any): void;
|
package/dist/tool.js
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import { PineVoice, AuthError } from "pine-voice";
|
|
3
|
+
/** Auth error message returned when credentials are missing. */
|
|
4
|
+
const AUTH_MISSING_MESSAGE = [
|
|
5
|
+
"Pine Voice is not authenticated yet. Both a user ID and access token are required before making calls.",
|
|
6
|
+
"",
|
|
7
|
+
"To set up authentication, run these commands in the terminal:",
|
|
8
|
+
"",
|
|
9
|
+
" # Step 1: Request a verification code (sent to your Pine AI account email)",
|
|
10
|
+
" openclaw pine-voice auth setup --email <USER_EMAIL>",
|
|
11
|
+
"",
|
|
12
|
+
" # Step 2: Enter the code and request token from Step 1 to get your user ID and access token",
|
|
13
|
+
" openclaw pine-voice auth verify --email <USER_EMAIL> --request-token <TOKEN> --code <CODE>",
|
|
14
|
+
"",
|
|
15
|
+
" # Step 3: Add both values to your plugin config in openclaw.json:",
|
|
16
|
+
' # plugins.entries.pine-voice.config.user_id = "<USER_ID>"',
|
|
17
|
+
' # plugins.entries.pine-voice.config.access_token = "<TOKEN>"',
|
|
18
|
+
"",
|
|
19
|
+
" # Step 4: Restart the gateway",
|
|
20
|
+
" openclaw gateway restart",
|
|
21
|
+
"",
|
|
22
|
+
"Ask the user for their Pine AI account email to begin. If they don't have a Pine AI account, they can sign up at https://19pine.ai.",
|
|
23
|
+
].join("\n");
|
|
24
|
+
const AUTH_EXPIRED_MESSAGE = [
|
|
25
|
+
"Pine Voice authentication has expired or is invalid.",
|
|
26
|
+
"",
|
|
27
|
+
"To re-authenticate, run:",
|
|
28
|
+
" openclaw pine-voice auth setup --email <USER_EMAIL>",
|
|
29
|
+
" openclaw pine-voice auth verify --email <USER_EMAIL> --request-token <TOKEN> --code <CODE>",
|
|
30
|
+
"",
|
|
31
|
+
"Then update user_id and access_token in openclaw.json and restart the gateway.",
|
|
32
|
+
"Ask the user for their Pine AI account email to begin.",
|
|
33
|
+
].join("\n");
|
|
34
|
+
/** Read plugin config and create a PineVoice SDK client. Returns error response if not authenticated. */
|
|
35
|
+
function getClientOrError(api) {
|
|
36
|
+
const config = api.config?.plugins?.entries?.["pine-voice"]?.config;
|
|
37
|
+
if (!config?.access_token || !config?.user_id) {
|
|
38
|
+
return { content: [{ type: "text", text: AUTH_MISSING_MESSAGE }], isError: true };
|
|
39
|
+
}
|
|
40
|
+
return new PineVoice({
|
|
41
|
+
accessToken: config.access_token,
|
|
42
|
+
userId: config.user_id,
|
|
43
|
+
gatewayUrl: config.gateway_url,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
/** Convert a PineVoiceError into an OpenClaw tool error response. */
|
|
47
|
+
function handleError(api, err) {
|
|
48
|
+
if (err instanceof AuthError) {
|
|
49
|
+
api.log?.error?.(`pine-voice: auth error: ${err.message}`);
|
|
50
|
+
return { content: [{ type: "text", text: AUTH_EXPIRED_MESSAGE }], isError: true };
|
|
51
|
+
}
|
|
52
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
53
|
+
api.log?.error?.(`pine-voice: error: ${message}`);
|
|
54
|
+
return { content: [{ type: "text", text: `Pine Voice Call Error: ${message}` }], isError: true };
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Register voice call tools with OpenClaw:
|
|
58
|
+
* - pine_voice_call: initiate a call (returns immediately)
|
|
59
|
+
* - pine_voice_call_status: poll a call's status
|
|
60
|
+
* - pine_voice_call_and_wait: initiate + block until complete (SSE + polling fallback)
|
|
61
|
+
*
|
|
62
|
+
* All tools are optional (user must add them to tools.allow).
|
|
63
|
+
* Delegates all API calls to the pine-voice SDK.
|
|
64
|
+
*/
|
|
65
|
+
export function registerVoiceCallTools(api) {
|
|
66
|
+
// --- Tool 1: pine_voice_call (initiate) ---
|
|
67
|
+
api.registerTool({
|
|
68
|
+
name: "pine_voice_call",
|
|
69
|
+
description: "Make a phone call via Pine AI voice agent. The agent calls the specified number and handles the " +
|
|
70
|
+
"conversation (including IVR navigation, negotiation, and verification) based on your instructions. " +
|
|
71
|
+
"Important: the voice agent can only speak English, so calls can only be delivered to English-speaking " +
|
|
72
|
+
"countries and recipients who understand English. " +
|
|
73
|
+
"BEFORE calling this tool, you MUST gather from the user all information that may be needed during " +
|
|
74
|
+
"the call, including any authentication, verification, or payment details the callee may require. " +
|
|
75
|
+
"The voice agent has no way to contact a human for missing information mid-call — anticipate what " +
|
|
76
|
+
"the callee will ask for and include it upfront. " +
|
|
77
|
+
"For negotiations, include target outcome, acceptable range, constraints, and leverage points. " +
|
|
78
|
+
"Returns immediately with a call_id. Use pine_voice_call_status to check progress and get results. " +
|
|
79
|
+
"Powered by Pine AI.",
|
|
80
|
+
parameters: Type.Object({
|
|
81
|
+
to: Type.String({ description: "Phone number to call (E.164 format, e.g. +14155551234). Must be a number in an English-speaking country, as the voice agent can only speak English." }),
|
|
82
|
+
callee_name: Type.String({ description: "Name of the person or business being called" }),
|
|
83
|
+
callee_context: Type.String({ description: "Comprehensive context about the callee and all information needed for the call. Include: who they are, your relationship, and any authentication, verification, or payment details the callee may require. The voice agent CANNOT ask a human for missing information mid-call, so you must anticipate what will be needed and include everything upfront." }),
|
|
84
|
+
objective: Type.String({ description: "Specific goal the call should accomplish. For negotiations, include your target outcome, acceptable range, and constraints (e.g. 'Negotiate monthly bill down to $50/mo, do not accept above $65/mo, do not change plan tier')." }),
|
|
85
|
+
instructions: Type.Optional(Type.String({ description: "Detailed strategy and instructions for the voice agent. For negotiations, describe: what leverage points to use, what offers to accept/reject, fallback positions, and when to walk away. The more thorough the strategy, the better the outcome." })),
|
|
86
|
+
caller: Type.Optional(Type.String({ enum: ["negotiator", "communicator"], description: "Caller personality. 'negotiator' for complex negotiations — requires a thorough negotiation strategy in callee_context and instructions (target outcome, acceptable range, leverage points, fallback positions, walk-away conditions). 'communicator' for general-purpose routine tasks (scheduling, inquiries, reservations)." })),
|
|
87
|
+
voice: Type.Optional(Type.String({ enum: ["male", "female"], description: "Voice gender" })),
|
|
88
|
+
max_duration_minutes: Type.Optional(Type.Number({ default: 120, minimum: 1, maximum: 120, description: "Maximum call duration in minutes" })),
|
|
89
|
+
enable_summary: Type.Optional(Type.Boolean({ default: false, description: "Request an LLM-generated summary after the call. Default: false. Most AI agents can process the full transcript directly, so the summary is opt-in to save latency and cost." })),
|
|
90
|
+
}),
|
|
91
|
+
async execute(_toolCallId, params) {
|
|
92
|
+
const clientOrErr = getClientOrError(api);
|
|
93
|
+
if (!(clientOrErr instanceof PineVoice))
|
|
94
|
+
return clientOrErr;
|
|
95
|
+
try {
|
|
96
|
+
const { callId } = await clientOrErr.calls.create({
|
|
97
|
+
to: params.to,
|
|
98
|
+
name: params.callee_name,
|
|
99
|
+
context: params.callee_context,
|
|
100
|
+
objective: params.objective,
|
|
101
|
+
instructions: params.instructions,
|
|
102
|
+
caller: params.caller,
|
|
103
|
+
voice: params.voice,
|
|
104
|
+
maxDurationMinutes: params.max_duration_minutes,
|
|
105
|
+
enableSummary: params.enable_summary,
|
|
106
|
+
});
|
|
107
|
+
api.log?.info?.(`pine-voice: call initiated, call_id=${callId}`);
|
|
108
|
+
return {
|
|
109
|
+
content: [
|
|
110
|
+
{
|
|
111
|
+
type: "text",
|
|
112
|
+
text: `Call initiated (call_id: ${callId}).\n\nUse pine_voice_call_status with call_id "${callId}" to check progress. Poll every 30 seconds until the call completes.`,
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
isError: false,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
return handleError(api, err);
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
}, { optional: true });
|
|
123
|
+
// --- Tool 2: pine_voice_call_status (query) ---
|
|
124
|
+
api.registerTool({
|
|
125
|
+
name: "pine_voice_call_status",
|
|
126
|
+
description: "Check the status of a phone call initiated by pine_voice_call. " +
|
|
127
|
+
"Returns the current status while in progress and the full transcript when the call is complete " +
|
|
128
|
+
"(plus an LLM-generated summary if enable_summary was set to true). " +
|
|
129
|
+
"Poll this tool every 30 seconds after initiating a call until a transcript is present. " +
|
|
130
|
+
"Note: no real-time intermediate updates are available — you will not receive partial transcripts " +
|
|
131
|
+
"or 'call connected' events. Simply poll until the call reaches a terminal state. " +
|
|
132
|
+
"CRITICAL: Do NOT rely on the status field to judge whether the call succeeded. " +
|
|
133
|
+
"You MUST read the full transcript — especially what the OTHER party said. " +
|
|
134
|
+
"If the other side was silent, responded with automated/voicemail messages, or never gave a meaningful human response, the call FAILED regardless of the technical status. " +
|
|
135
|
+
"Powered by Pine AI.",
|
|
136
|
+
parameters: Type.Object({
|
|
137
|
+
call_id: Type.String({ description: "The call_id returned by pine_voice_call" }),
|
|
138
|
+
}),
|
|
139
|
+
async execute(_toolCallId, params) {
|
|
140
|
+
const clientOrErr = getClientOrError(api);
|
|
141
|
+
if (!(clientOrErr instanceof PineVoice))
|
|
142
|
+
return clientOrErr;
|
|
143
|
+
try {
|
|
144
|
+
const result = await clientOrErr.calls.get(params.call_id);
|
|
145
|
+
// Terminal states: return formatted result
|
|
146
|
+
if (result.status === "completed" || result.status === "failed" || result.status === "cancelled") {
|
|
147
|
+
return formatResult(result);
|
|
148
|
+
}
|
|
149
|
+
// Non-terminal: return simple progress message.
|
|
150
|
+
// Note: Real-time intermediate updates (phase, partial transcript) are
|
|
151
|
+
// NOT currently available. The transcript is only delivered after the
|
|
152
|
+
// call completes.
|
|
153
|
+
const elapsed = result.durationSeconds ? formatDuration(result.durationSeconds) : "unknown";
|
|
154
|
+
const progressLines = [];
|
|
155
|
+
progressLines.push(`Call is still in progress (${elapsed} elapsed). No intermediate updates are available — the transcript will be delivered when the call completes.`);
|
|
156
|
+
progressLines.push("", "Poll again in 30 seconds to check status.");
|
|
157
|
+
return {
|
|
158
|
+
content: [
|
|
159
|
+
{
|
|
160
|
+
type: "text",
|
|
161
|
+
text: progressLines.join("\n"),
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
isError: false,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
return handleError(api, err);
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
}, { optional: true });
|
|
172
|
+
// --- Tool 3: pine_voice_call_and_wait (initiate + wait for result) ---
|
|
173
|
+
api.registerTool({
|
|
174
|
+
name: "pine_voice_call_and_wait",
|
|
175
|
+
description: "Make a phone call via Pine AI voice agent and wait for the result. This is the PREFERRED tool " +
|
|
176
|
+
"for making calls — it blocks until the call completes and returns the full transcript " +
|
|
177
|
+
"in a single tool call. No manual polling needed. Uses SSE under the hood to wait for " +
|
|
178
|
+
"the final result, with automatic fallback to polling if SSE is unavailable. " +
|
|
179
|
+
"No real-time intermediate updates are available — you simply wait for the final transcript. " +
|
|
180
|
+
"Important: the voice agent can only speak English, so calls can only be delivered to English-speaking " +
|
|
181
|
+
"countries and recipients who understand English. " +
|
|
182
|
+
"BEFORE calling this tool, you MUST gather from the user all information that may be needed during " +
|
|
183
|
+
"the call, including any authentication, verification, or payment details the callee may require. " +
|
|
184
|
+
"The voice agent has no way to contact a human for missing information mid-call — anticipate what " +
|
|
185
|
+
"the callee will ask for and include it upfront. " +
|
|
186
|
+
"For negotiations, include target outcome, acceptable range, constraints, and leverage points. " +
|
|
187
|
+
"Note: this tool blocks for the duration of the call (typically 1-30 minutes). " +
|
|
188
|
+
"Powered by Pine AI.",
|
|
189
|
+
parameters: Type.Object({
|
|
190
|
+
to: Type.String({ description: "Phone number to call (E.164 format, e.g. +14155551234). Must be a number in an English-speaking country, as the voice agent can only speak English." }),
|
|
191
|
+
callee_name: Type.String({ description: "Name of the person or business being called" }),
|
|
192
|
+
callee_context: Type.String({ description: "Comprehensive context about the callee and all information needed for the call. Include: who they are, your relationship, and any authentication, verification, or payment details the callee may require. The voice agent CANNOT ask a human for missing information mid-call, so you must anticipate what will be needed and include everything upfront." }),
|
|
193
|
+
objective: Type.String({ description: "Specific goal the call should accomplish. For negotiations, include your target outcome, acceptable range, and constraints (e.g. 'Negotiate monthly bill down to $50/mo, do not accept above $65/mo, do not change plan tier')." }),
|
|
194
|
+
instructions: Type.Optional(Type.String({ description: "Detailed strategy and instructions for the voice agent. For negotiations, describe: what leverage points to use, what offers to accept/reject, fallback positions, and when to walk away. The more thorough the strategy, the better the outcome." })),
|
|
195
|
+
caller: Type.Optional(Type.String({ enum: ["negotiator", "communicator"], description: "Caller personality. 'negotiator' for complex negotiations — requires a thorough negotiation strategy in callee_context and instructions (target outcome, acceptable range, leverage points, fallback positions, walk-away conditions). 'communicator' for general-purpose routine tasks (scheduling, inquiries, reservations)." })),
|
|
196
|
+
voice: Type.Optional(Type.String({ enum: ["male", "female"], description: "Voice gender" })),
|
|
197
|
+
max_duration_minutes: Type.Optional(Type.Number({ default: 120, minimum: 1, maximum: 120, description: "Maximum call duration in minutes" })),
|
|
198
|
+
enable_summary: Type.Optional(Type.Boolean({ default: false, description: "Request an LLM-generated summary after the call. Default: false. Most AI agents can process the full transcript directly, so the summary is opt-in to save latency and cost." })),
|
|
199
|
+
}),
|
|
200
|
+
async execute(_toolCallId, params) {
|
|
201
|
+
const clientOrErr = getClientOrError(api);
|
|
202
|
+
if (!(clientOrErr instanceof PineVoice))
|
|
203
|
+
return clientOrErr;
|
|
204
|
+
try {
|
|
205
|
+
api.log?.info?.(`pine-voice: initiating call and waiting for result...`);
|
|
206
|
+
const result = await clientOrErr.calls.createAndWait({
|
|
207
|
+
to: params.to,
|
|
208
|
+
name: params.callee_name,
|
|
209
|
+
context: params.callee_context,
|
|
210
|
+
objective: params.objective,
|
|
211
|
+
instructions: params.instructions,
|
|
212
|
+
caller: params.caller,
|
|
213
|
+
voice: params.voice,
|
|
214
|
+
maxDurationMinutes: params.max_duration_minutes,
|
|
215
|
+
enableSummary: params.enable_summary,
|
|
216
|
+
});
|
|
217
|
+
api.log?.info?.(`pine-voice: call completed, status=${result.status}`);
|
|
218
|
+
return formatResult(result);
|
|
219
|
+
}
|
|
220
|
+
catch (err) {
|
|
221
|
+
return handleError(api, err);
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
}, { optional: true });
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Format a CallResult into an OpenClaw tool response.
|
|
228
|
+
*
|
|
229
|
+
* NOTE: We intentionally do NOT include structuredContent here because
|
|
230
|
+
* these tools have no outputSchema. Per MCP spec 2025-03-26, returning
|
|
231
|
+
* structuredContent without a corresponding outputSchema is a protocol
|
|
232
|
+
* violation and causes schema validation errors in strict MCP clients
|
|
233
|
+
* (e.g., Claude). All relevant data is included in the text content.
|
|
234
|
+
*/
|
|
235
|
+
function formatResult(result) {
|
|
236
|
+
if (result.status === "failed") {
|
|
237
|
+
return {
|
|
238
|
+
content: [{ type: "text", text: `Call failed: ${result.summary || "Unknown error"}` }],
|
|
239
|
+
isError: true,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
if (result.status === "cancelled") {
|
|
243
|
+
return {
|
|
244
|
+
content: [{ type: "text", text: "Call was cancelled." }],
|
|
245
|
+
isError: false,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
const durationMin = Math.floor((result.durationSeconds || 0) / 60);
|
|
249
|
+
const durationSec = (result.durationSeconds || 0) % 60;
|
|
250
|
+
const lines = [
|
|
251
|
+
`**Call ${result.status}**`,
|
|
252
|
+
`Duration: ${durationMin}m ${durationSec}s | Credits charged: ${result.creditsCharged}`,
|
|
253
|
+
];
|
|
254
|
+
if (result.summary) {
|
|
255
|
+
lines.push("", `**Summary:** ${result.summary}`);
|
|
256
|
+
}
|
|
257
|
+
if (result.transcript?.length > 0) {
|
|
258
|
+
lines.push("", "**Transcript:**");
|
|
259
|
+
for (const entry of result.transcript) {
|
|
260
|
+
lines.push(`- **${entry.speaker}:** ${entry.text}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
265
|
+
isError: false,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
function formatDuration(seconds) {
|
|
269
|
+
const m = Math.floor(seconds / 60);
|
|
270
|
+
const s = seconds % 60;
|
|
271
|
+
return m > 0 ? `${m}m ${s}s` : `${s}s`;
|
|
272
|
+
}
|
package/dist/types.d.ts
ADDED
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "pine-voice",
|
|
3
|
+
"name": "Pine AI Voice Call",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "Make phone calls via Pine AI voice agent. The AI agent calls the specified number and carries out the conversation based on your instructions. The voice agent can only speak English, so calls can only be delivered to English-speaking countries. Before calling, gather all information the callee may need for authentication and verification — the agent cannot ask a human for missing info mid-call. Returns the full transcript. Powered by Pine AI.",
|
|
6
|
+
"configSchema": {
|
|
7
|
+
"type": "object",
|
|
8
|
+
"properties": {
|
|
9
|
+
"gateway_url": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"description": "Pine Voice API Gateway URL",
|
|
12
|
+
"default": "https://agent3-api-gateway-staging.19pine.ai"
|
|
13
|
+
},
|
|
14
|
+
"access_token": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"description": "Pine access token (from email verification)"
|
|
17
|
+
},
|
|
18
|
+
"user_id": {
|
|
19
|
+
"type": "string",
|
|
20
|
+
"description": "Pine user ID (the \"id\" field from email verification)"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"required": []
|
|
24
|
+
},
|
|
25
|
+
"uiHints": {
|
|
26
|
+
"gateway_url": {
|
|
27
|
+
"label": "Gateway URL",
|
|
28
|
+
"placeholder": "https://agent3-api-gateway-staging.19pine.ai"
|
|
29
|
+
},
|
|
30
|
+
"access_token": {
|
|
31
|
+
"label": "Pine Access Token",
|
|
32
|
+
"sensitive": true
|
|
33
|
+
},
|
|
34
|
+
"user_id": {
|
|
35
|
+
"label": "Pine User ID",
|
|
36
|
+
"placeholder": "1234567890"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"skills": ["skills/pine-voice", "skills/pine-voice-auth"]
|
|
40
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "openclaw-pine-voice",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Make phone calls via Pine AI voice agent from OpenClaw",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"openclaw": {
|
|
15
|
+
"extensions": [
|
|
16
|
+
"./dist/index.js"
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"openclaw.plugin.json",
|
|
22
|
+
"skills",
|
|
23
|
+
"README.md",
|
|
24
|
+
"LICENSE"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsc",
|
|
28
|
+
"prepublishOnly": "npm run build"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"openclaw",
|
|
32
|
+
"plugin",
|
|
33
|
+
"voice",
|
|
34
|
+
"phone",
|
|
35
|
+
"pine"
|
|
36
|
+
],
|
|
37
|
+
"author": "Pine AI",
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"homepage": "https://pineclaw.com",
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=18"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@sinclair/typebox": "^0.32.0",
|
|
45
|
+
"pine-voice": "^0.1.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"typescript": "^5.0.0"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Pine AI Voice Call
|
|
2
|
+
|
|
3
|
+
## When to use
|
|
4
|
+
Use `pine_voice_call_and_wait` (preferred) or `pine_voice_call` when the user wants you to **make a phone call** on their behalf. The Pine AI voice agent will call the specified number, navigate IVR systems, handle verification, conduct negotiations, and carry out the conversation autonomously.
|
|
5
|
+
|
|
6
|
+
**Important:** The voice agent can only speak English. Calls can be placed to the US, Canada (+1), UK (+44), Australia (+61), New Zealand (+64), and Ireland (+353). Do not use this tool for calls to numbers outside these countries.
|
|
7
|
+
|
|
8
|
+
## Best for
|
|
9
|
+
- Calling customer service to negotiate bills, request credits, or resolve issues
|
|
10
|
+
- Scheduling meetings or appointments by phone
|
|
11
|
+
- Making restaurant reservations
|
|
12
|
+
- Calling businesses to inquire about services or availability
|
|
13
|
+
- Following up with contacts on behalf of the user
|
|
14
|
+
|
|
15
|
+
## How to make a call
|
|
16
|
+
|
|
17
|
+
**IMPORTANT: Phone calls are long-running (5-120 minutes). ALWAYS use `sessions_spawn` to run the call in a background sub-agent so you remain available to the user.**
|
|
18
|
+
|
|
19
|
+
### Step 1: Gather all required information
|
|
20
|
+
|
|
21
|
+
Before initiating a call, you **must** ask the user for every piece of information the callee might need. The voice agent **cannot ask a human for missing information during the call**. There is no way for the AI agent to pause and request details mid-conversation.
|
|
22
|
+
|
|
23
|
+
Anticipate what the callee will require based on the type of call. This may include authentication or verification details, payment information, negotiation targets and constraints, relevant background, or any other context specific to the task.
|
|
24
|
+
|
|
25
|
+
If the user hasn't provided sufficient information for the callee to process the request (e.g., a customer service call with no verification details), **ask the user for this information before proceeding**. Do not invoke the tool hoping it will work without it.
|
|
26
|
+
|
|
27
|
+
### Step 2: Spawn a background sub-agent
|
|
28
|
+
|
|
29
|
+
Use `sessions_spawn` to run the call in the background.
|
|
30
|
+
|
|
31
|
+
**Preferred approach — `pine_voice_call_and_wait` (single tool, blocks until done):**
|
|
32
|
+
|
|
33
|
+
- **tool**: `sessions_spawn`
|
|
34
|
+
- **task**: Write a clear task that includes ALL call parameters. Example:
|
|
35
|
+
|
|
36
|
+
> Make a phone call using the pine_voice_call_and_wait tool. Call details: Call the restaurant at +14155559876. Callee name: The Italian Place. Callee context: Italian restaurant on Main Street. Making a dinner reservation. Objective: Make a reservation for 4 people tonight at 7pm. Instructions: If 7pm is not available, try 7:30 or 8pm. Prefer a booth if possible. Name for the reservation: Jane Doe. When the call completes, report the full summary, transcript, and outcome.
|
|
37
|
+
|
|
38
|
+
- **label**: Short description, e.g. `call-restaurant-reservation`
|
|
39
|
+
- **runTimeoutSeconds**: `(max_duration_minutes + 5) * 60` — give extra buffer beyond the call's max duration
|
|
40
|
+
|
|
41
|
+
**Alternative approach — `pine_voice_call` + `pine_voice_call_status` (manual polling):**
|
|
42
|
+
|
|
43
|
+
If `pine_voice_call_and_wait` is not available, use `pine_voice_call` to initiate, then poll `pine_voice_call_status` every 30 seconds. Include polling instructions in the task description.
|
|
44
|
+
|
|
45
|
+
### Step 3: Tell the user the call is active
|
|
46
|
+
|
|
47
|
+
After spawning, tell the user that **the call is already active** — Pine's voice agent has dialed the number and is handling the conversation in the background. The call is NOT "connecting" or "being set up". Example:
|
|
48
|
+
> "The call is now active — Pine's voice agent is on the line with [callee] handling the conversation in the background. This typically takes a few minutes. I'll let you know the results when it's done. Feel free to ask me anything else in the meantime."
|
|
49
|
+
|
|
50
|
+
### Step 4: Evaluate the transcript and summarize the result
|
|
51
|
+
|
|
52
|
+
When the sub-agent announces the result, you MUST read the full transcript carefully to determine the actual outcome. **Do NOT rely on the `status` field** — a status like `HungupByPeer` only means the other party hung up, not that the call succeeded.
|
|
53
|
+
|
|
54
|
+
**How to evaluate the transcript:**
|
|
55
|
+
|
|
56
|
+
Read what the OTHER party (not Pine's agent) actually said. The callee's responses are the only way to know if the objective was achieved.
|
|
57
|
+
|
|
58
|
+
**Treat the call as a FAILURE if:**
|
|
59
|
+
- Only Pine's agent speaks and the other side is silent or gives no meaningful response
|
|
60
|
+
- The other party's responses are automated/recorded messages (e.g. voicemail greetings, "leave a message after the beep", "your call cannot be completed as dialed")
|
|
61
|
+
- System messages report extended silence from both sides (e.g. "silence from both sides for 25 seconds")
|
|
62
|
+
- The callee hung up before the objective could be discussed
|
|
63
|
+
- The callee never acknowledged or responded to the request
|
|
64
|
+
|
|
65
|
+
These patterns mean the call reached voicemail, an automated system, or the callee was unavailable. Report this honestly to the user.
|
|
66
|
+
|
|
67
|
+
**Summarize for the user:**
|
|
68
|
+
- Whether the objective was actually achieved (based on the transcript, not the status)
|
|
69
|
+
- If it failed: why (voicemail, no answer, hung up, etc.) and suggest a retry or alternative
|
|
70
|
+
- Key details from what the callee actually said
|
|
71
|
+
- Any follow-up actions needed
|
|
72
|
+
- Credits charged
|
|
73
|
+
|
|
74
|
+
## Tools
|
|
75
|
+
|
|
76
|
+
### pine_voice_call_and_wait (preferred — initiate + wait)
|
|
77
|
+
Initiates a phone call and blocks until it completes, returning the full result in a single tool call. Uses SSE to wait for the final result with automatic fallback to polling. No manual polling needed.
|
|
78
|
+
|
|
79
|
+
**Important:** No real-time intermediate updates are available. You will NOT receive "call connected" events, partial transcripts, or live conversation progress. The only result is the final complete transcript delivered after the call ends.
|
|
80
|
+
|
|
81
|
+
Parameters:
|
|
82
|
+
|
|
83
|
+
- `to` (required): Phone number in E.164 format (e.g., +14155551234). Must be in a supported country: US/CA (+1), UK (+44), AU (+61), NZ (+64), IE (+353).
|
|
84
|
+
- `callee_name` (required): Name of the person or business being called
|
|
85
|
+
- `callee_context` (required): Comprehensive context — include all authentication, verification, and payment info the agent may need during the call
|
|
86
|
+
- `objective` (required): Specific goal with negotiation targets and constraints if applicable
|
|
87
|
+
- `instructions` (optional): Detailed strategy, approach, and behavioral instructions
|
|
88
|
+
- `caller` (optional): "negotiator" or "communicator", default "negotiator". Negotiator requires a thorough negotiation strategy in context/instructions (target outcome, acceptable range, leverage points, fallback positions). Communicator is for general-purpose routine tasks.
|
|
89
|
+
- `voice` (optional): "male" or "female", default "female"
|
|
90
|
+
- `max_duration_minutes` (optional): 1-120, default 120
|
|
91
|
+
- `enable_summary` (optional): boolean, default false. Request an LLM-generated summary after the call. Most AI agents can process the full transcript directly, so the summary is opt-in.
|
|
92
|
+
|
|
93
|
+
### pine_voice_call (initiate)
|
|
94
|
+
Initiates a phone call and returns immediately with a `call_id`. At this point, the call is ALREADY ACTIVE — the voice agent has dialed and is on the line. Same parameters as `pine_voice_call_and_wait`. Use with `pine_voice_call_status` to poll for results.
|
|
95
|
+
|
|
96
|
+
### pine_voice_call_status (poll)
|
|
97
|
+
Checks call progress using the `call_id` from pine_voice_call. Poll every 30 seconds until a transcript is present in the response. When the status is `in_progress`, the voice agent is ACTIVELY on the call speaking with the callee — it is NOT connecting or waiting. Returns the full transcript and billing info when complete (plus summary if `enable_summary` was set to true).
|
|
98
|
+
|
|
99
|
+
**CRITICAL:** When you receive the final result, do NOT assume the call succeeded just because the status says "completed" or "HungupByPeer". You MUST read the full transcript. If the other party never responded meaningfully (voicemail, silence, automated messages), the call failed.
|
|
100
|
+
|
|
101
|
+
- `call_id` (required): The call_id returned by pine_voice_call
|
|
102
|
+
|
|
103
|
+
**Note:** No real-time intermediate updates are available. The `phase` and `partial_transcript` fields are defined in the schema but are NOT currently populated. You will not receive "call connected" events or live transcript turns. The only way to get the transcript is to wait for the call to complete. Simply poll until the status is terminal and the full transcript is present.
|
|
104
|
+
|
|
105
|
+
## Negotiation calls
|
|
106
|
+
For calls involving negotiation (bill reduction, rate matching, fee waiver), provide a **thorough negotiation strategy**, not just a target:
|
|
107
|
+
|
|
108
|
+
- **Target outcome**: "Reduce monthly bill to $50/mo"
|
|
109
|
+
- **Acceptable range**: "Will accept up to $65/mo"
|
|
110
|
+
- **Hard constraints**: "Do not change plan tier, do not remove any features"
|
|
111
|
+
- **Leverage points**: "Mention 10-year customer loyalty", "Competitor offers $45/mo"
|
|
112
|
+
- **Fallback**: "If no reduction, request one-time credit of $100"
|
|
113
|
+
- **Walk-away point**: "If nothing offered, ask for retention department"
|
|
114
|
+
|
|
115
|
+
## Examples
|
|
116
|
+
|
|
117
|
+
**Test call to yourself:**
|
|
118
|
+
"Call my phone at +1XXXXXXXXXX. Tell me that Pine Voice is set up and working. Confirm the setup is complete and say goodbye."
|
|
119
|
+
|
|
120
|
+
**Restaurant reservation:**
|
|
121
|
+
"Call the restaurant at +14155559876 and make a reservation for 4 people tonight at 7pm. If 7pm is not available, try 7:30 or 8pm. Name for the reservation: Jane Doe."
|
|
122
|
+
|
|
123
|
+
## HTTP API reference
|
|
124
|
+
|
|
125
|
+
The plugin uses these REST endpoints on the Pine Voice gateway (for transparency — the tools handle this automatically):
|
|
126
|
+
|
|
127
|
+
- **POST /api/v2/voice/call** — Initiate a call. Returns `{ "call_id": "...", "status": "in_progress" }`.
|
|
128
|
+
- **GET /api/v2/voice/call/{call_id}** — Poll call status. Returns full transcript and billing info when complete (plus summary if requested).
|
|
129
|
+
- **GET /api/v2/voice/call/{call_id}/stream** — SSE stream that waits for the final call result. Used by `pine_voice_call_and_wait` under the hood. No intermediate events are delivered; only the final transcript after call completion.
|
|
130
|
+
|
|
131
|
+
Auth: `Authorization: Bearer <token>` + `X-Pine-User-Id: <user_id>` headers.
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pine-voice-auth
|
|
3
|
+
description: Set up or refresh Pine Voice authentication. Obtains an access token via email verification and writes it to the plugin config.
|
|
4
|
+
metadata:
|
|
5
|
+
{
|
|
6
|
+
"openclaw":
|
|
7
|
+
{ "emoji": "🔑", "requires": { "bins": ["curl", "jq"] } },
|
|
8
|
+
}
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Pine Voice Auth Setup
|
|
12
|
+
|
|
13
|
+
## When to use
|
|
14
|
+
|
|
15
|
+
Use this skill when **any** of these are true:
|
|
16
|
+
|
|
17
|
+
- The user asks to set up Pine Voice, configure Pine AI, or authenticate for voice calls
|
|
18
|
+
- A `pine_voice_call` or `pine_voice_call_and_wait` invocation returns "Pine Voice is not authenticated yet"
|
|
19
|
+
- A `pine_voice_call` or `pine_voice_call_and_wait` invocation fails with `TOKEN_EXPIRED`, `UNAUTHORIZED`, or a 401 response
|
|
20
|
+
- The user says their Pine Voice token isn't working
|
|
21
|
+
|
|
22
|
+
Do **not** use this skill for making phone calls — see the `pine-voice` skill for that.
|
|
23
|
+
|
|
24
|
+
## Prerequisites
|
|
25
|
+
|
|
26
|
+
- The user must have a **Pine AI account** with a Pro subscription (sign up at https://19pine.ai)
|
|
27
|
+
- `curl` and `jq` must be available in the shell
|
|
28
|
+
|
|
29
|
+
## Important: email verification requires user presence
|
|
30
|
+
|
|
31
|
+
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.
|
|
32
|
+
|
|
33
|
+
**Recommended timing:** Run this flow right after plugin installation or when the user explicitly asks to set up Pine Voice — not during an unattended or automated workflow.
|
|
34
|
+
|
|
35
|
+
## Auth flow overview
|
|
36
|
+
|
|
37
|
+
Pine Voice uses email-based verification. The flow has two API calls with a human step in between:
|
|
38
|
+
|
|
39
|
+
1. **Request** — send the user's email to get a `request_token`
|
|
40
|
+
2. **Wait** — user checks their email for a verification code
|
|
41
|
+
3. **Verify** — send email + request_token + code to get a `user_id` and `access_token`
|
|
42
|
+
4. **Store & restart** — write both values via `openclaw config set` and restart the gateway
|
|
43
|
+
5. **Test** — make a test call to verify everything works
|
|
44
|
+
|
|
45
|
+
## Step-by-step instructions
|
|
46
|
+
|
|
47
|
+
### Step 1: Ask the user for their Pine AI email
|
|
48
|
+
|
|
49
|
+
Ask: "What email address is your Pine AI account registered with?"
|
|
50
|
+
|
|
51
|
+
Do not proceed until you have the email.
|
|
52
|
+
|
|
53
|
+
### Step 2: Request a verification code
|
|
54
|
+
|
|
55
|
+
Run this command, replacing `USER_EMAIL` with the email from step 1:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
curl -s -X POST https://www.19pine.ai/api/v2/auth/email/request \
|
|
59
|
+
-H "Content-Type: application/json" \
|
|
60
|
+
-d '{"email": "USER_EMAIL"}' | jq .
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Expected response:**
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{"status": "success", "data": {"request_token": "abc123..."}}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Save the `request_token` value — you need it in step 4.
|
|
70
|
+
|
|
71
|
+
**If the request fails:**
|
|
72
|
+
- `400` or `422` — the email may not be registered. Ask the user to check their email or sign up at https://19pine.ai.
|
|
73
|
+
- Network error — check connectivity and retry.
|
|
74
|
+
|
|
75
|
+
### Step 3: Ask the user for the verification code
|
|
76
|
+
|
|
77
|
+
Tell the user: "I've sent a verification code to your email. Please check your inbox (and spam folder) and tell me the code."
|
|
78
|
+
|
|
79
|
+
Wait for the user to provide the code. Do not guess or skip this step.
|
|
80
|
+
|
|
81
|
+
### Step 4: Verify the code and obtain the access token
|
|
82
|
+
|
|
83
|
+
Run this command, replacing `USER_EMAIL`, `THE_REQUEST_TOKEN`, and `THE_CODE` with the actual values:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
curl -s -X POST https://www.19pine.ai/api/v2/auth/email/verify \
|
|
87
|
+
-H "Content-Type: application/json" \
|
|
88
|
+
-d '{"email": "USER_EMAIL", "request_token": "THE_REQUEST_TOKEN", "code": "THE_CODE"}' | jq .
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Expected response:**
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{"id": "1234567890", "access_token": "eyJ...", ...}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Save **both** values:
|
|
98
|
+
- **`id`** — the user's Pine user ID
|
|
99
|
+
- **`access_token`** — the access token
|
|
100
|
+
|
|
101
|
+
**If verification fails:**
|
|
102
|
+
- `401` or `400` with "invalid code" — ask the user to double-check the code and try again.
|
|
103
|
+
- `410` or "expired" — the request_token expired. Go back to step 2 to start over.
|
|
104
|
+
|
|
105
|
+
### Step 5: Store the credentials and restart
|
|
106
|
+
|
|
107
|
+
Use `openclaw config set` to write both values. Replace `THE_ACCESS_TOKEN` and `THE_USER_ID` with the actual values from step 4.
|
|
108
|
+
|
|
109
|
+
**Important:** The user ID is all digits, so the CLI will parse it as a number unless you force it to be a JSON string with `--json` and explicit quotes.
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
openclaw config set plugins.entries.pine-voice.config.access_token "THE_ACCESS_TOKEN"
|
|
113
|
+
openclaw config set plugins.entries.pine-voice.config.user_id '"THE_USER_ID"' --json
|
|
114
|
+
openclaw gateway restart
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
You can verify the stored values with:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
openclaw config get plugins.entries.pine-voice.config
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Step 6: Verify with a test call
|
|
124
|
+
|
|
125
|
+
After authentication is complete, suggest the user make a test call to their own phone number to verify everything works end-to-end:
|
|
126
|
+
|
|
127
|
+
"Would you like to test it? I can call your phone to confirm everything is working. Just tell me your phone number."
|
|
128
|
+
|
|
129
|
+
Use the `pine_voice_call_and_wait` tool (or `pine_voice_call` if unavailable) with:
|
|
130
|
+
- `to`: the user's phone number
|
|
131
|
+
- `callee_name`: the user's name
|
|
132
|
+
- `callee_context`: "This is a test call to verify Pine Voice setup."
|
|
133
|
+
- `objective`: "Confirm that Pine Voice is set up and working correctly. Say hello, confirm the setup is complete, and say goodbye."
|
|
134
|
+
|
|
135
|
+
This verifies the token, subscription, and full end-to-end flow.
|
|
136
|
+
|
|
137
|
+
## Token refresh
|
|
138
|
+
|
|
139
|
+
Access tokens expire periodically. When a call fails with `TOKEN_EXPIRED` or a 401 error:
|
|
140
|
+
|
|
141
|
+
1. Inform the user their token has expired and needs to be refreshed
|
|
142
|
+
2. Re-run this auth flow starting from step 1
|
|
143
|
+
|
|
144
|
+
## Security notes
|
|
145
|
+
|
|
146
|
+
- Never log or echo the access token in plaintext beyond what is needed to write the config file
|
|
147
|
+
- The token is stored in a local config file with the same permissions as the user's home directory
|
|
148
|
+
- Do not commit config files containing tokens to version control
|