Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions public/icons/plivo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import fitbitRoutes from './routes/fitbit.js';
import braveRoutes from './routes/brave.js';
import googleSearchRoutes from './routes/google-search.js';
import homeassistantRoutes from './routes/homeassistant.js';
import plivoRoutes from './routes/plivo.js';
import queueRoutes from './routes/queue.js';
import agentsRoutes from './routes/agents.js';
import mementoRoutes from './routes/memento.js';
Expand Down Expand Up @@ -80,6 +81,7 @@ app.use('/api/fitbit', apiKeyAuth, serviceAccessCheck('fitbit'), writeProxy('fit
app.use('/api/brave', apiKeyAuth, serviceAccessCheck('brave'), writeProxy('brave'), braveRoutes);
app.use('/api/google_search', apiKeyAuth, serviceAccessCheck('google_search'), writeProxy('google_search'), googleSearchRoutes);
app.use('/api/homeassistant', apiKeyAuth, serviceAccessCheck('homeassistant'), writeProxy('homeassistant'), homeassistantRoutes);
app.use('/api/plivo', apiKeyAuth, serviceAccessCheck('plivo'), writeProxy('plivo'), plivoRoutes);

// Service access management - admin API (requires auth)
app.use('/api/services', apiKeyAuth, servicesRoutes);
Expand Down
14 changes: 12 additions & 2 deletions src/lib/queueExecutor.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ const SERVICE_URLS = {
linkedin: 'https://api.linkedin.com/v2',
jira: null, // Dynamic: https://{domain}/rest/api/3
fitbit: 'https://api.fitbit.com',
homeassistant: null // Dynamic: {host}/api
homeassistant: null, // Dynamic: {host}/api
plivo: null // Dynamic: https://api.plivo.com/v1/Account/{auth_id}
};

// Get access token for a service, refreshing if needed
Expand Down Expand Up @@ -55,6 +56,9 @@ export async function getAccessToken(service, accountName) {
case 'homeassistant':
return creds.token || null;

case 'plivo':
return creds;

default:
return null;
}
Expand Down Expand Up @@ -245,6 +249,9 @@ export function buildUrl(service, accountName, path) {
const host = creds.host.replace(/\/+$/, '');
return `${host}/api/${path.replace(/^\//, '')}`;
}
if (service === 'plivo' && creds?.authId) {
return `https://api.plivo.com/v1/Account/${creds.authId}/${path.replace(/^\//, '')}`;
}

const baseUrl = SERVICE_URLS[service];
if (!baseUrl) return null;
Expand All @@ -260,7 +267,10 @@ export function buildHeaders(service, token, customHeaders = {}) {
'Content-Type': 'application/json'
};

if (service === 'jira' && token?.email && token?.apiToken) {
if (service === 'plivo' && token?.authId && token?.authToken) {
const basicAuth = Buffer.from(`${token.authId}:${token.authToken}`).toString('base64');
defaults['Authorization'] = `Basic ${basicAuth}`;
} else if (service === 'jira' && token?.email && token?.apiToken) {
const basicAuth = Buffer.from(`${token.email}:${token.apiToken}`).toString('base64');
defaults['Authorization'] = `Basic ${basicAuth}`;
} else if (token && typeof token === 'string') {
Expand Down
8 changes: 6 additions & 2 deletions src/lib/serviceRegistry.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { serviceInfo as fitbitInfo, readService as fitbitRead } from '../routes/
import { serviceInfo as braveInfo, readService as braveRead } from '../routes/brave.js';
import { serviceInfo as googleSearchInfo, readService as googleSearchRead } from '../routes/google-search.js';
import { serviceInfo as homeassistantInfo, readService as homeassistantRead } from '../routes/homeassistant.js';
import { serviceInfo as plivoInfo, readService as plivoRead } from '../routes/plivo.js';

// Aggregate service metadata from all routes
const SERVICE_REGISTRY = {
Expand All @@ -24,7 +25,8 @@ const SERVICE_REGISTRY = {
[fitbitInfo.key]: fitbitInfo,
[braveInfo.key]: braveInfo,
[googleSearchInfo.key]: googleSearchInfo,
[homeassistantInfo.key]: homeassistantInfo
[homeassistantInfo.key]: homeassistantInfo,
[plivoInfo.key]: plivoInfo
};

/**
Expand All @@ -50,7 +52,8 @@ export const SERVICE_READERS = {
[fitbitInfo.key]: fitbitRead,
[braveInfo.key]: braveRead,
[googleSearchInfo.key]: googleSearchRead,
[homeassistantInfo.key]: homeassistantRead
[homeassistantInfo.key]: homeassistantRead,
[plivoInfo.key]: plivoRead
};

// Category mapping for MCP tool registration
Expand All @@ -59,6 +62,7 @@ export const SERVICE_CATEGORIES = {
social: { name: 'Social', description: 'Social networks — posts, profiles, timelines', services: ['bluesky', 'mastodon', 'reddit', 'linkedin'], hasWrite: true },
code: { name: 'Code', description: 'Code repos, issues, PRs, projects', services: ['github', 'jira'], hasWrite: true },
personal: { name: 'Personal', description: 'Health, calendar, and media', services: ['fitbit', 'calendar', 'google_calendar', 'youtube'], hasWrite: true },
messaging: { name: 'Messaging', description: 'SMS and messaging services', services: ['plivo'], hasWrite: true },
iot: { name: 'IoT', description: 'Smart home and IoT devices', services: ['homeassistant'], hasWrite: true }
};

Expand Down
101 changes: 101 additions & 0 deletions src/routes/plivo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { Router } from 'express';
import { getAccountCredentials } from '../lib/db.js';

const router = Router();
const PLIVO_API = 'https://api.plivo.com/v1';

// Service metadata - exported for /api/agent_start_here and /api/skill
export const serviceInfo = {
key: 'plivo',
name: 'Plivo',
shortDesc: 'SMS messaging via Plivo',
description: 'Plivo SMS API proxy for sending and reading messages',
authType: 'basic',
authMethods: ['basic'],
docs: 'https://www.plivo.com/docs/sms/',
examples: [
'GET /api/plivo/{accountName}/messages',
'GET /api/plivo/{accountName}/messages/{messageUuid}',
'GET /api/plivo/{accountName}/account'
]
};

// Core read function - used by both Express routes and MCP
export async function readService(accountName, path, { query = {}, raw: _raw = false } = {}) {
const creds = getAccountCredentials('plivo', accountName);
if (!creds?.authId || !creds?.authToken) {
return { status: 401, data: { error: 'Plivo credentials not configured', hint: `Configure Auth ID and Auth Token for account "${accountName}" in the AgentGate UI` } };
}

const basicAuth = Buffer.from(`${creds.authId}:${creds.authToken}`).toString('base64');
const queryString = new URLSearchParams(query).toString();
const url = `${PLIVO_API}/Account/${creds.authId}/${path}${queryString ? '?' + queryString : ''}`;

const response = await fetch(url, {
headers: {
'Authorization': `Basic ${basicAuth}`,
'Content-Type': 'application/json'
}
});

const data = await response.json();
return { status: response.status, data };
}

// List messages
router.get('/:accountName/messages', async (req, res) => {
try {
const result = await readService(req.params.accountName, 'Message/', { query: req.query });
res.status(result.status).json(result.data);
} catch (error) {
res.status(500).json({ error: 'Plivo API request failed', message: error.message });
}
});

// Get single message
router.get('/:accountName/messages/:messageUuid', async (req, res) => {
try {
const result = await readService(req.params.accountName, `Message/${req.params.messageUuid}/`);
res.status(result.status).json(result.data);
} catch (error) {
res.status(500).json({ error: 'Plivo API request failed', message: error.message });
}
});

// Account info endpoint
router.get('/:accountName/account', async (req, res) => {
try {
const result = await readService(req.params.accountName, '');
res.status(result.status).json(result.data);
} catch (error) {
res.status(500).json({ error: 'Plivo API request failed', message: error.message });
}
});

// Wildcard GET - proxy any other Plivo API path
router.get('/:accountName/*', async (req, res) => {
try {
const path = req.params[0];
const result = await readService(req.params.accountName, path, { query: req.query });
res.status(result.status).json(result.data);
} catch (error) {
res.status(500).json({ error: 'Plivo API request failed', message: error.message });
}
});

// Account info
router.get('/:accountName', async (req, res) => {
res.json({
service: 'plivo',
account: req.params.accountName,
description: 'Plivo SMS API proxy. Send and read SMS messages.',
examples: [
`GET /api/plivo/${req.params.accountName}/messages`,
`GET /api/plivo/${req.params.accountName}/messages/{messageUuid}`,
`GET /api/plivo/${req.params.accountName}/account`
],
docs: 'https://www.plivo.com/docs/sms/'
});
});

export default router;
63 changes: 63 additions & 0 deletions src/routes/ui/plivo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { setAccountCredentials, deleteAccount } from '../../lib/db.js';
import { escapeHtml } from './shared.js';

export function registerRoutes(router) {
router.post('/plivo/setup', (req, res) => {
const { accountName, authId, authToken } = req.body;
if (!accountName || !authId || !authToken) {
return res.status(400).send('Account name, Auth ID, and Auth Token required');
}
setAccountCredentials('plivo', accountName, { authId, authToken });
res.redirect('/ui');
});

router.post('/plivo/delete', (req, res) => {
const { accountName } = req.body;
deleteAccount('plivo', accountName);
res.redirect('/ui');
});
}

export function renderCard(accounts, _baseUrl) {
const serviceAccounts = accounts.filter(a => a.service === 'plivo');

const renderAccounts = () => {
if (serviceAccounts.length === 0) return '';
return serviceAccounts.map(acc => `
<div class="account-item">
<span><strong>${escapeHtml(acc.name)}</strong></span>
<form method="POST" action="/ui/plivo/delete" style="margin:0;">
<input type="hidden" name="accountName" value="${escapeHtml(acc.name)}" autocomplete="off">
<button type="submit" class="btn-sm btn-danger">Remove</button>
</form>
</div>
`).join('');
};

return `
<div class="card">
<div class="service-header">
<img class="service-icon" src="/public/icons/plivo.svg" alt="Plivo">
<h3>Plivo</h3>
</div>
${renderAccounts()}
<details>
<summary>Add Plivo Account</summary>
<div style="margin-top: 15px;">
<p class="help">Get your Auth ID and Auth Token from the <a href="https://console.plivo.com/dashboard/" target="_blank">Plivo Console</a></p>
<form method="POST" action="/ui/plivo/setup">
<label>Account Name</label>
<input type="text" name="accountName" placeholder="default, work, etc." required autocomplete="off">
<label>Auth ID</label>
<input type="text" name="authId" placeholder="Your Plivo Auth ID" required autocomplete="off">
<label>Auth Token</label>
<input type="password" name="authToken" placeholder="Your Plivo Auth Token" required autocomplete="off">
<button type="submit" class="btn-primary">Add Account</button>
</form>
</div>
</details>
</div>`;
}

export const serviceName = 'plivo';
export const displayName = 'Plivo';
14 changes: 13 additions & 1 deletion src/routes/ui/service-detail.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,8 @@ function getServiceIcon(service) {
jira: '/public/icons/jira.svg',
fitbit: '/public/icons/fitbit.svg',
brave: '/public/icons/brave.svg',
google_search: '/public/icons/google-search.svg'
google_search: '/public/icons/google-search.svg',
plivo: '/public/icons/plivo.svg'
};
return icons[service] || '/public/favicon.svg';
}
Expand Down Expand Up @@ -326,6 +327,17 @@ function getServiceFormFields(serviceName) {
<p class="help">Set up at <a href="https://programmablesearchengine.google.com/" target="_blank">Programmable Search Engine</a></p>
</div>`,

plivo: `
<div class="form-group">
<label>Auth ID</label>
<input type="text" name="authId" placeholder="Your Plivo Auth ID" required autocomplete="off">
</div>
<div class="form-group">
<label>Auth Token</label>
<input type="password" name="authToken" placeholder="Your Plivo Auth Token" required autocomplete="off">
<p class="help">Get your credentials from the <a href="https://console.plivo.com/dashboard/" target="_blank">Plivo Console</a></p>
</div>`,

homeassistant: `
<div class="form-group">
<label>Host URL</label>
Expand Down
13 changes: 11 additions & 2 deletions src/routes/ui/services.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import * as fitbit from './fitbit.js';
import * as brave from './brave.js';
import * as googleSearch from './google-search.js';
import * as homeassistant from './homeassistant.js';
import * as plivo from './plivo.js';

// Export all implemented services in display order
export const services = [
Expand All @@ -36,7 +37,8 @@ export const services = [
linkedin,
brave,
googleSearch,
homeassistant
homeassistant,
plivo
];

// Full service catalog with categories — includes both implemented and coming-soon services
Expand Down Expand Up @@ -78,6 +80,12 @@ export const catalog = [
{ id: 'stripe', name: 'Stripe', icon: '💳', implemented: false }
]
},
{
category: 'Messaging',
services: [
{ id: 'plivo', name: 'Plivo', icon: '💬', implemented: true }
]
},
{
category: 'IoT & Smart Home',
services: [
Expand Down Expand Up @@ -207,7 +215,8 @@ function getServiceIcon(service) {
fitbit: '/public/icons/fitbit.svg',
brave: '/public/icons/brave.svg',
google_search: '/public/icons/google-search.svg',
homeassistant: '/public/icons/homeassistant.svg'
homeassistant: '/public/icons/homeassistant.svg',
plivo: '/public/icons/plivo.svg'
};
return icons[service] || '/public/favicon.svg';
}
Expand Down
2 changes: 1 addition & 1 deletion src/services/queueService.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { executeQueueEntry } from '../lib/queueExecutor.js';
import { notifyAgentQueueWarning } from '../lib/agentNotifier.js';

// Valid services that support write operations
const VALID_SERVICES = ['github', 'bluesky', 'reddit', 'mastodon', 'calendar', 'google_calendar', 'youtube', 'linkedin', 'jira', 'fitbit', 'homeassistant'];
const VALID_SERVICES = ['github', 'bluesky', 'reddit', 'mastodon', 'calendar', 'google_calendar', 'youtube', 'linkedin', 'jira', 'fitbit', 'homeassistant', 'plivo'];

// Valid HTTP methods for write operations
const VALID_METHODS = ['POST', 'PUT', 'PATCH', 'DELETE'];
Expand Down
Loading
Loading