Skill: bojieli/pine-voice (version 1.2.2)

--- name: pine-voice description: Give your agent a real phone. It dials, waits on hold, negotiates your bills, and returns a full transcript. homepage: https://19pine.ai metadata: {"openclaw":{"emoji":"📞","requires":{"bins":["node"]}}} command-tool: exec --- # Pine Voice Make real phone calls via Pine AI's voice agent. The agent calls the specified number, navigates IVR systems, handles verification, conducts negotiations, and returns a full transcript. ## Authentication Credentials persist in `~/.pine-voice/credentials.json` — users only need to authenticate once. Before making calls, check if already authenticated: ```bash node {baseDir}/scripts/auth-check.mjs ``` If `authenticated` is `true`, skip straight to **How to make a call**. If `false`, run the auth flow below. **Ask the user for their Pine AI account email** (sign up at https://19pine.ai). ### Step 1: Request verification code ```bash node {baseDir}/scripts/auth-request.mjs "[REDACTED_SECRET]" ``` Returns `{"request_token": "...", "email": "..."}`. Save the `request_token`. Tell the user: *"A verification code has been sent to your email. Check your inbox (and spam) and give me the code."* ### Step 2: Verify and save credentials ```bash node {baseDir}/scripts/auth-verify.mjs "[REDACTED_SECRET]" "REQUEST_TOKEN" "CODE" ``` Returns `{"status": "authenticated", "credentials_path": "..."}`. Credentials are saved automatically. ## When to use Use this skill when the user wants you to **make a phone call** on their behalf. **Important:** The voice agent can only speak English. Supported countries: US/CA/PR (+1), UK (+44), AU (+61), NZ (+64), SG (+65), IE (+353), HK (+852). ## Best for - Calling customer service to negotiate bills, request credits, or resolve issues - Scheduling meetings or appointments by phone - Making restaurant reservations - Calling businesses to inquire about services or availability - Following up with contacts on behalf of the user ## How to make a call ### Step 1: Gather all required information Before calling, you **must** collect every piece of information the callee might need. The voice agent **cannot ask a human for missing information during the call**. Anticipate what will be required: authentication details, payment info, negotiation targets, relevant context. ### Step 2: Initiate the call Pass call parameters as JSON via stdin: ```bash node {baseDir}/scripts/call.mjs <<'EOF' { "dialed_number": "+14155551234", "callee_name": "Comcast Customer Service", "callee_context": "Cable and internet provider. Account holder: Jane Doe, account #12345.", "call_objective": "Negotiate monthly bill down to $50/mo. Do not accept above $65/mo.", "detailed_instructions": "Mention 10-year customer loyalty. If no reduction, ask for retention department.", "caller": "negotiator", "voice": "female", "max_duration_minutes": 60, "enable_summary": false } EOF ``` Returns `{"call_id": "..."}`. The call is now active. ### Step 3: Poll for results Poll every 30 seconds until `is_terminal` is `true`: ```bash node {baseDir}/scripts/call-status.mjs "CALL_ID" ``` When complete, the response includes `transcript`, `duration_seconds`, and `credits_charged`. The `is_terminal` field tells you when the call is done. **IMPORTANT: Use `sessions_spawn` to run this in a background sub-agent** so you remain available to the user during the call (which can take 5-60+ minutes). Example task for sessions_spawn: > Make a phone call using the Pine Voice scripts. Run: node {baseDir}/scripts/call.mjs with stdin JSON: {"dialed_number": "+14155551234", "callee_name": "The Restaurant", "callee_context": "Italian restaurant, making a dinner reservation", "call_objective": "Reserve a table for 4 at 7pm tonight", "caller": "communicator"}. Then poll with: node {baseDir}/scripts/call-status.mjs "CALL_ID" every 30 seconds until is_terminal is true. Report the full transcript and outcome. ### Step 4: Evaluate the transcript **Do NOT rely on the `status` field** to judge success. Read what the OTHER party actually said. **Treat the call as a FAILURE if:** - Only Pine's agent speaks and the other side is silent - The other party's responses are automated/recorded (voicemail, IVR-only) - Extended silence from both sides - The callee hung up before the objective was discussed ## Call parameters | Parameter | Required | Description | |---|---|---| | `dialed_number` | Yes | Phone number in E.164 format (e.g. `+14155551234`) | | `callee_name` | Yes | Name of the person or business | | `callee_context` | Yes | All context the agent needs: who they are, auth details, verification info | | `call_objective` | Yes | Specific goal with targets and constraints | | `detailed_instructions` | No | Strategy, approach, behavioral instructions | | `caller` | No | `"negotiator"` (default) or `"communicator"` | | `voice` | No | `"male"` or `"female"` (default: `"female"`) | | `max_duration_minutes` | No | 1-120 (default: 120) | | `enable_summary` | No | `true`/`false` (default: `false`) | ## Negotiation calls For negotiations, set `caller` to `"negotiator"` and provide a thorough strategy: - **Target outcome**: "Reduce monthly bill to $50/mo" - **Acceptable range**: "Will accept up to $65/mo" - **Hard constraints**: "Do not change plan tier" - **Leverage points**: "10-year customer, competitor offers $45/mo" - **Fallback**: "Request one-time credit of $100" - **Walk-away**: "Ask for retention department" ## Examples **Test call:** "Call my phone at +1XXXXXXXXXX. Tell me that Pine Voice is set up and working." **Restaurant reservation:** "Call +14155559876 and make a reservation for 4 tonight at 7pm. If unavailable, try 7:30 or 8pm. Name: Jane Doe." ## Model requirements Pine Voice works best with models that have thinking/reasoning capabilities. - **Recommended:** Claude Sonnet/Opus 4.5+, GPT-5.2+, Gemini 3 Pro - **Not recommended:** Gemini 3 Flash, or models without thinking capabilities ## Privacy Pine Voice processes call data on Pine AI's servers. Credentials are stored locally in `~/.pine-voice/credentials.json` with restricted permissions (600). Call transcripts are returned in the API response and are not stored locally. See https://www.19pine.ai/page/privacy-policy for Pine AI's data handling policies.

path: scripts/auth-check.mjs
content: #!/usr/bin/env node

import { readFileSync } from "node:fs";
import { join } from "node:path";
import { homedir } from "node:os";

const credPath = join(homedir(), ".pine-voice", "credentials.json");

try {
  const creds = JSON.parse(readFileSync(credPath, "utf-8"));
  if (!creds.access_token || !creds.user_id) {
    console.log(JSON.stringify({ authenticated: false, reason: "incomplete credentials" }));
    process.exit(1);
  }
  console.log(JSON.stringify({ authenticated: true, user_id: creds.user_id, credentials_path: credPath }));
} catch {
  console.log(JSON.stringify({ authenticated: false, reason: "no credentials file" }));
  process.exit(1);
}


path: scripts/auth-request.mjs
content: #!/usr/bin/env node

const email = (process.argv[2] ?? "").trim();
if (!email) {
  console.error("Usage: auth-request.mjs <email>");
  process.exit(2);
}

const resp = await fetch("https://www.19pine.ai/api/v2/auth/email/request", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ email }),
});

if (!resp.ok) {
  const text = await resp.text().catch(() => "");
  console.error(`Auth request failed (${resp.status}): ${text}`);
  process.exit(1);
}

const data = await resp.json();
const requestToken = data?.data?.request_token;

if (!requestToken) {
  console.error("Unexpected response — no request_token returned.");
  console.error(JSON.stringify(data, null, 2));
  process.exit(1);
}

console.log(JSON.stringify({ request_token: requestToken, email }));


path: scripts/auth-verify.mjs
content: #!/usr/bin/env node

import { writeFileSync, mkdirSync, chmodSync } from "node:fs";
import { join } from "node:path";
import { homedir } from "node:os";

const email = (process.argv[2] ?? "").trim();
const requestToken = (process.argv[3] ?? "").trim();
const code = (process.argv[4] ?? "").trim();

if (!email || !requestToken || !code) {
  console.error("Usage: auth-verify.mjs <email> <request_token> <code>");
  process.exit(2);
}

const resp = await fetch("https://www.19pine.ai/api/v2/auth/email/verify", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ email, request_token: requestToken, code }),
});

if (!resp.ok) {
  const text = await resp.text().catch(() => "");
  console.error(`Auth verification failed (${resp.status}): ${text}`);
  process.exit(1);
}

const data = await resp.json();
const accessToken = data?.data?.access_token;
const userId = data?.data?.id;

if (!accessToken || !userId) {
  console.error("Unexpected response — missing access_token or id.");
  console.error(JSON.stringify(data, null, 2));
  process.exit(1);
}

const dir = join(homedir(), ".pine-voice");
const credPath = join(dir, "credentials.json");

mkdirSync(dir, { recursive: true });
writeFileSync(credPath, JSON.stringify({ access_token: accessToken, user_id: userId }, null, 2) + "\n");
chmodSync(credPath, 0o600);

console.log(JSON.stringify({
  status: "authenticated",
  credentials_path: credPath,
  user_id: userId,
}));


path: scripts/call-status.mjs
content: #!/usr/bin/env node

import { readFileSync } from "node:fs";
import { join } from "node:path";
import { homedir } from "node:os";

const GATEWAY = "https://agent3-api-gateway-staging.19pine.ai";

const TERMINAL_STATUSES = new Set([
  "completed", "Completed",
  "HungupByPeer", "HungupByLocal",
  "Timeout", "TooLongSilence",
  "failed", "Failed", "DialFailed", "NoAnswer", "Declined",
  "cancelled",
]);

function loadCredentials() {
  const credPath = join(homedir(), ".pine-voice", "credentials.json");
  try {
    return JSON.parse(readFileSync(credPath, "utf-8"));
  } catch {
    console.error("Not authenticated. Run auth-request.mjs and auth-verify.mjs first.");
    process.exit(1);
  }
}

const callId = (process.argv[2] ?? "").trim();
if (!callId) {
  console.error("Usage: call-status.mjs <call_id>");
  process.exit(2);
}

const creds = loadCredentials();

const resp = await fetch(`${GATEWAY}/api/v2/voice/call/${encodeURIComponent(callId)}`, {
  headers: {
    "Authorization": `Bearer ${creds.access_token}`,
    "X-Pine-User-Id": creds.user_id,
  },
});

if (!resp.ok) {
  const text = await resp.text().catch(() => "");
  console.error(`Status check failed (${resp.status}): ${text}`);
  process.exit(1);
}

const data = await resp.json();
const status = data?.status ?? "unknown";
const isTerminal = TERMINAL_STATUSES.has(status);

console.log(JSON.stringify({ ...data, is_terminal: isTerminal }, null, 2));


path: scripts/call.mjs
content: #!/usr/bin/env node

import { readFileSync } from "node:fs";
import { join } from "node:path";
import { homedir } from "node:os";

const GATEWAY = "https://agent3-api-gateway-staging.19pine.ai";

function loadCredentials() {
  const credPath = join(homedir(), ".pine-voice", "credentials.json");
  try {
    return JSON.parse(readFileSync(credPath, "utf-8"));
  } catch {
    console.error("Not authenticated. Run auth-request.mjs and auth-verify.mjs first.");
    process.exit(1);
  }
}

function readStdin() {
  return new Promise((resolve) => {
    let data = "";
    process.stdin.setEncoding("utf-8");
    process.stdin.on("data", (chunk) => (data += chunk));
    process.stdin.on("end", () => resolve(data));
    if (process.stdin.isTTY) resolve("");
  });
}

const input = await readStdin();
if (!input.trim()) {
  console.error("Usage: echo '{...}' | node call.mjs");
  console.error("Pass call parameters as JSON via stdin.");
  console.error("Required: dialed_number, callee_name, callee_context, call_objective");
  process.exit(2);
}

let params;
try {
  params = JSON.parse(input);
} catch (e) {
  console.error(`Invalid JSON: ${e.message}`);
  process.exit(2);
}

const required = ["dialed_number", "callee_name", "callee_context", "call_objective"];
for (const field of required) {
  if (!params[field]) {
    console.error(`Missing required field: ${field}`);
    process.exit(2);
  }
}

const allowed = [
  "dialed_number", "callee_name", "callee_context", "call_objective",
  "detailed_instructions", "caller", "voice", "max_duration_minutes", "enable_summary",
];
const body = {};
for (const key of allowed) {
  if (params[key] !== undefined) body[key] = params[key];
}

const creds = loadCredentials();

const resp = await fetch(`${GATEWAY}/api/v2/voice/call`, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": `Bearer ${creds.access_token}`,
    "X-Pine-User-Id": creds.user_id,
  },
  body: JSON.stringify(body),
});

if (!resp.ok) {
  const text = await resp.text().catch(() => "");
  console.error(`Call failed (${resp.status}): ${text}`);
  process.exit(1);
}

const data = await resp.json();
console.log(JSON.stringify(data, null, 2));


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)