mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-07-12 21:02:17 +02:00
feat(mini-apps): real scoped Composio bridge + GitHub Dashboard
Phase 2 makes the Mini App bridge real instead of stubbed. - New composio:execute-tool and composio:search-tools IPC channels (shared schema + main handlers reusing the agent's execute path) - Bridge refactored to a scoped RPC: callAction/searchTools/isConnected/ connect, enforced against each app's declared scope at the host - Auth prompt: acting on a disconnected toolkit triggers Composio OAuth - GitHub Dashboard sample: track repos, view their 20 most-recent open PRs with descriptions, plus the authenticated user's profile + contribution graph — all live via the bridge - Twitter sample reverted to a local UI demo (X has no managed OAuth2)
This commit is contained in:
parent
7acbf67cd4
commit
30a46178d1
9 changed files with 496 additions and 63 deletions
|
|
@ -315,3 +315,50 @@ export async function listToolkits() {
|
||||||
totalItems: filtered.length,
|
totalItems: filtered.length,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a Composio tool by slug on behalf of a Mini App. The toolkit must be
|
||||||
|
* connected (ACTIVE). Mirrors the agent's composio-execute-tool builtin.
|
||||||
|
*/
|
||||||
|
export async function executeTool(
|
||||||
|
toolkitSlug: string,
|
||||||
|
toolSlug: string,
|
||||||
|
args?: Record<string, unknown>,
|
||||||
|
): Promise<{ successful: boolean; data?: unknown; error?: string }> {
|
||||||
|
const account = composioAccountsRepo.getAccount(toolkitSlug);
|
||||||
|
if (!account || account.status !== 'ACTIVE') {
|
||||||
|
return { successful: false, error: `Toolkit "${toolkitSlug}" is not connected.` };
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = await composioClient.executeAction(toolSlug, {
|
||||||
|
connected_account_id: account.id,
|
||||||
|
user_id: 'rowboat-user',
|
||||||
|
version: 'latest',
|
||||||
|
arguments: args ?? {},
|
||||||
|
});
|
||||||
|
return { successful: result.successful, data: result.data, error: result.error ?? undefined };
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
console.error(`[Composio] Mini App tool execution failed for ${toolSlug}:`, message);
|
||||||
|
return { successful: false, error: `Failed to execute ${toolSlug}: ${message}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search Composio tools within a toolkit so a Mini App can discover the right
|
||||||
|
* tool slug + input schema at runtime (how generated apps will wire actions).
|
||||||
|
*/
|
||||||
|
export async function searchToolsInToolkit(
|
||||||
|
toolkitSlug: string,
|
||||||
|
query: string,
|
||||||
|
): Promise<{ tools: Array<{ slug: string; name: string; description?: string }>; error?: string }> {
|
||||||
|
try {
|
||||||
|
const { items } = await composioClient.searchTools(query, [toolkitSlug]);
|
||||||
|
return {
|
||||||
|
tools: items.map((t) => ({ slug: t.slug, name: t.name, description: t.description })),
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
return { tools: [], error: message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1304,6 +1304,12 @@ export function setupIpcHandlers() {
|
||||||
'composio:list-toolkits': async () => {
|
'composio:list-toolkits': async () => {
|
||||||
return composioHandler.listToolkits();
|
return composioHandler.listToolkits();
|
||||||
},
|
},
|
||||||
|
'composio:execute-tool': async (_event, args) => {
|
||||||
|
return composioHandler.executeTool(args.toolkitSlug, args.toolSlug, args.arguments);
|
||||||
|
},
|
||||||
|
'composio:search-tools': async (_event, args) => {
|
||||||
|
return composioHandler.searchToolsInToolkit(args.toolkitSlug, args.query);
|
||||||
|
},
|
||||||
'migration:check-composio-google': async () => {
|
'migration:check-composio-google': async () => {
|
||||||
return qualifyAndDisconnectComposioGoogle();
|
return qualifyAndDisconnectComposioGoogle();
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -5,37 +5,62 @@ import { MINI_APP_MESSAGE } from '@/mini-apps/types'
|
||||||
// Host side of the Mini App bridge. Renders the app's self-contained HTML in a
|
// Host side of the Mini App bridge. Renders the app's self-contained HTML in a
|
||||||
// sandboxed iframe and answers the postMessage protocol in mini-apps/types.ts.
|
// sandboxed iframe and answers the postMessage protocol in mini-apps/types.ts.
|
||||||
//
|
//
|
||||||
// Phase 1: data is delivered from the static app.data, per-app state lives in
|
// Phase 2: the bridge is real. App rpc calls (callAction/searchTools/
|
||||||
// memory only, and callAction is stubbed (returns a friendly demo result). Later
|
// isConnected/connect) are scope-checked against the app's declared scope and
|
||||||
// phases replace the action/state handlers with real IPC to the agent + Composio.
|
// routed to Composio over IPC. Per-app state is still in-memory for now.
|
||||||
|
|
||||||
// Sandbox intentionally omits allow-same-origin: the app gets an opaque origin so
|
// Sandbox intentionally omits allow-same-origin: the app gets an opaque origin so
|
||||||
// it cannot reach host cookies/storage. The bridge is the only channel out.
|
// it cannot reach host cookies/storage. The bridge is the only channel out.
|
||||||
const SANDBOX = 'allow-scripts allow-popups allow-popups-to-escape-sandbox allow-forms allow-modals allow-downloads'
|
const SANDBOX = 'allow-scripts allow-popups allow-popups-to-escape-sandbox allow-forms allow-modals allow-downloads'
|
||||||
|
|
||||||
// Phase 1 stub: pretend the Composio action succeeded.
|
|
||||||
function stubActionResult(action: string): { message: string } {
|
|
||||||
switch (action) {
|
|
||||||
case 'repost': return { message: 'Reposted (demo)' }
|
|
||||||
case 'reply': return { message: 'Reply sent (demo)' }
|
|
||||||
case 'mark_read': return { message: 'Dismissed' }
|
|
||||||
default: return { message: action + ' done (demo)' }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function MiniAppFrame({ app }: { app: MiniApp }) {
|
export function MiniAppFrame({ app }: { app: MiniApp }) {
|
||||||
const iframeRef = useRef<HTMLIFrameElement | null>(null)
|
const iframeRef = useRef<HTMLIFrameElement | null>(null)
|
||||||
// In-memory per-app state for Phase 1 (resets when the frame unmounts).
|
// In-memory per-app state for Phase 1/2 (resets when the frame unmounts).
|
||||||
const stateRef = useRef<Record<string, unknown>>({})
|
const stateRef = useRef<Record<string, unknown>>({})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Reset state when switching apps.
|
|
||||||
stateRef.current = {}
|
stateRef.current = {}
|
||||||
|
|
||||||
function postToFrame(message: unknown) {
|
function postToFrame(message: unknown) {
|
||||||
iframeRef.current?.contentWindow?.postMessage(message, '*')
|
iframeRef.current?.contentWindow?.postMessage(message, '*')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dispatch an app rpc call to real Composio IPC. Throws on scope violation
|
||||||
|
// or failure; the caller turns that into an rpc-result error.
|
||||||
|
async function handleRpc(method: string, params: Record<string, unknown>): Promise<unknown> {
|
||||||
|
const scope = typeof params.scope === 'string' ? params.scope : ''
|
||||||
|
if (!scope || !app.scope.includes(scope)) {
|
||||||
|
throw new Error(`This app is not allowed to use "${scope || '(none)'}".`)
|
||||||
|
}
|
||||||
|
switch (method) {
|
||||||
|
case 'isConnected': {
|
||||||
|
const r = await window.ipc.invoke('composio:get-connection-status', { toolkitSlug: scope })
|
||||||
|
return r.isConnected
|
||||||
|
}
|
||||||
|
case 'connect': {
|
||||||
|
const r = await window.ipc.invoke('composio:initiate-connection', { toolkitSlug: scope })
|
||||||
|
if (!r.success && r.error) throw new Error(r.error)
|
||||||
|
return { started: r.success }
|
||||||
|
}
|
||||||
|
case 'searchTools': {
|
||||||
|
const query = typeof params.query === 'string' ? params.query : ''
|
||||||
|
const r = await window.ipc.invoke('composio:search-tools', { toolkitSlug: scope, query })
|
||||||
|
if (r.error) throw new Error(r.error)
|
||||||
|
return r.tools
|
||||||
|
}
|
||||||
|
case 'callAction': {
|
||||||
|
const tool = typeof params.tool === 'string' ? params.tool : ''
|
||||||
|
if (!tool) throw new Error('No tool specified.')
|
||||||
|
const args = (params.args && typeof params.args === 'object' ? params.args : {}) as Record<string, unknown>
|
||||||
|
const r = await window.ipc.invoke('composio:execute-tool', { toolkitSlug: scope, toolSlug: tool, arguments: args })
|
||||||
|
if (!r.successful) throw new Error(r.error || 'Action failed.')
|
||||||
|
return r.data
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown bridge method "${method}".`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleMessage(event: MessageEvent) {
|
function handleMessage(event: MessageEvent) {
|
||||||
const frameWindow = iframeRef.current?.contentWindow
|
const frameWindow = iframeRef.current?.contentWindow
|
||||||
if (!frameWindow || event.source !== frameWindow) return
|
if (!frameWindow || event.source !== frameWindow) return
|
||||||
|
|
@ -49,27 +74,16 @@ export function MiniAppFrame({ app }: { app: MiniApp }) {
|
||||||
postToFrame({ type: MINI_APP_MESSAGE.state, state: stateRef.current })
|
postToFrame({ type: MINI_APP_MESSAGE.state, state: stateRef.current })
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case MINI_APP_MESSAGE.action: {
|
case MINI_APP_MESSAGE.rpc: {
|
||||||
// Phase 1: stubbed. (Phase 2 enforces app.scope and calls Composio.)
|
const { id, method, params } = msg
|
||||||
const allowed = app.scope.includes(msg.scope)
|
handleRpc(method, (params ?? {}) as Record<string, unknown>)
|
||||||
if (!allowed) {
|
.then((result) => postToFrame({ type: MINI_APP_MESSAGE.rpcResult, id, ok: true, result }))
|
||||||
postToFrame({
|
.catch((err) => postToFrame({
|
||||||
type: MINI_APP_MESSAGE.actionResult,
|
type: MINI_APP_MESSAGE.rpcResult,
|
||||||
id: msg.id,
|
id,
|
||||||
ok: false,
|
ok: false,
|
||||||
error: 'Action scope "' + msg.scope + '" not granted to this app',
|
error: err instanceof Error ? err.message : String(err),
|
||||||
})
|
}))
|
||||||
break
|
|
||||||
}
|
|
||||||
// Small delay so the UI's busy state is visible.
|
|
||||||
window.setTimeout(() => {
|
|
||||||
postToFrame({
|
|
||||||
type: MINI_APP_MESSAGE.actionResult,
|
|
||||||
id: msg.id,
|
|
||||||
ok: true,
|
|
||||||
result: stubActionResult(msg.action),
|
|
||||||
})
|
|
||||||
}, 350)
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case MINI_APP_MESSAGE.setState: {
|
case MINI_APP_MESSAGE.setState: {
|
||||||
|
|
|
||||||
314
apps/x/apps/renderer/src/mini-apps/apps/github-radar.ts
Normal file
314
apps/x/apps/renderer/src/mini-apps/apps/github-radar.ts
Normal file
|
|
@ -0,0 +1,314 @@
|
||||||
|
// Sample Mini App: GitHub Dashboard.
|
||||||
|
//
|
||||||
|
// A custom dashboard: pick repos to track, then view their open pull requests
|
||||||
|
// (title, author, description) — fetched LIVE from GitHub through the Phase 2
|
||||||
|
// bridge (searchTools -> callAction against a Composio managed-OAuth2 toolkit).
|
||||||
|
// Tracked repos persist via the per-app state store.
|
||||||
|
|
||||||
|
import { buildMiniAppHtml } from '../runtime'
|
||||||
|
import type { MiniApp } from '../types'
|
||||||
|
|
||||||
|
// Seed repos shown on first open (the user can add/remove their own).
|
||||||
|
const data = {
|
||||||
|
seedRepos: [
|
||||||
|
{ owner: 'vercel', repo: 'next.js' },
|
||||||
|
{ owner: 'rowboatlabs', repo: 'rowboat' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const style = `
|
||||||
|
.app { height:100%; overflow:auto; background:#0d1117; color:#e6edf3; }
|
||||||
|
.wrap { max-width:760px; margin:0 auto; padding:24px 16px 64px; }
|
||||||
|
.h { font-size:22px; font-weight:600; letter-spacing:-0.02em; margin:0 0 4px; color:#f0f6fc; }
|
||||||
|
.sub { font-size:13px; color:#8b949e; margin:0 0 18px; }
|
||||||
|
.banner { display:flex; align-items:center; justify-content:space-between; gap:12px; margin:0 0 16px; padding:12px 14px; border:1px solid #1f6feb; background:rgba(31,111,235,0.12); border-radius:10px; }
|
||||||
|
.banner.ok { border-color:#238636; background:rgba(35,134,54,0.12); }
|
||||||
|
.banner-text { font-size:13px; color:#e6edf3; }
|
||||||
|
.banner-btn { background:#238636; color:#fff; border:0; border-radius:8px; padding:7px 14px; font-size:13px; font-weight:600; cursor:pointer; }
|
||||||
|
.banner-btn:disabled { opacity:.6; cursor:default; }
|
||||||
|
.profile { display:flex; gap:14px; align-items:center; margin-bottom:16px; }
|
||||||
|
.avatar { width:64px; height:64px; border-radius:50%; border:1px solid #30363d; flex:0 0 auto; object-fit:cover; }
|
||||||
|
.avatar-fb { width:64px; height:64px; border-radius:50%; background:#21262d; display:none; align-items:center; justify-content:center; font-size:24px; font-weight:600; color:#8b949e; flex:0 0 auto; }
|
||||||
|
.pname { font-size:17px; font-weight:600; color:#f0f6fc; }
|
||||||
|
.plogin { font-size:13px; color:#8b949e; }
|
||||||
|
.pbio { font-size:13px; color:#adbac7; margin-top:4px; }
|
||||||
|
.pstats { display:flex; gap:16px; margin-top:8px; font-size:12px; color:#8b949e; }
|
||||||
|
.pstats b { color:#e6edf3; }
|
||||||
|
.contrib { border:1px solid #30363d; background:#161b22; border-radius:10px; padding:14px; margin-bottom:18px; }
|
||||||
|
.contrib-title { font-size:13px; font-weight:600; color:#f0f6fc; margin-bottom:10px; }
|
||||||
|
.contrib-img { width:100%; height:auto; display:block; border-radius:6px; }
|
||||||
|
.contrib-note { font-size:12px; color:#6e7681; }
|
||||||
|
.adder { display:flex; gap:8px; margin-bottom:18px; }
|
||||||
|
.adder input { flex:1; background:#0d1117; border:1px solid #30363d; color:#e6edf3; border-radius:8px; padding:8px 11px; font-size:13px; outline:none; }
|
||||||
|
.adder input:focus { border-color:#1f6feb; }
|
||||||
|
.adder button { background:#21262d; border:1px solid #30363d; color:#e6edf3; border-radius:8px; padding:0 14px; font-size:13px; font-weight:600; cursor:pointer; }
|
||||||
|
.adder button:hover { background:#30363d; }
|
||||||
|
.repo { border:1px solid #30363d; background:#161b22; border-radius:10px; margin-bottom:14px; overflow:hidden; }
|
||||||
|
.repo-head { display:flex; align-items:center; justify-content:space-between; gap:10px; padding:12px 14px; border-bottom:1px solid #21262d; }
|
||||||
|
.repo-name { font-size:14px; font-weight:600; color:#2f81f7; }
|
||||||
|
.repo-actions { display:flex; gap:8px; align-items:center; }
|
||||||
|
.repo-count { font-size:12px; color:#8b949e; }
|
||||||
|
.icon-btn { background:none; border:0; color:#8b949e; cursor:pointer; font-size:13px; padding:2px 6px; border-radius:6px; }
|
||||||
|
.icon-btn:hover { background:#21262d; color:#e6edf3; }
|
||||||
|
.pr { border-bottom:1px solid #21262d; }
|
||||||
|
.pr:last-child { border-bottom:0; }
|
||||||
|
.pr-row { display:flex; gap:10px; align-items:flex-start; padding:10px 14px; cursor:pointer; }
|
||||||
|
.pr-row:hover { background:#1c2128; }
|
||||||
|
.pr-icon { color:#3fb950; font-size:13px; line-height:1.5; flex:0 0 auto; }
|
||||||
|
.pr-main { min-width:0; flex:1; }
|
||||||
|
.pr-title { font-size:13px; color:#e6edf3; }
|
||||||
|
.pr-sub { font-size:12px; color:#8b949e; margin-top:2px; }
|
||||||
|
.pr-body { padding:0 14px 12px 38px; font-size:13px; line-height:1.5; color:#adbac7; white-space:pre-wrap; word-wrap:break-word; }
|
||||||
|
.pr-body.empty { color:#6e7681; font-style:italic; }
|
||||||
|
.state { padding:14px; font-size:13px; color:#8b949e; }
|
||||||
|
.state.err { color:#f85149; }
|
||||||
|
.toast { position:fixed; bottom:20px; left:50%; transform:translateX(-50%); background:#238636; color:#fff; font-size:14px; font-weight:600; padding:10px 18px; border-radius:8px; opacity:0; transition:opacity .2s; pointer-events:none; }
|
||||||
|
.toast.show { opacity:1; }
|
||||||
|
.toast.err { background:#da3633; }
|
||||||
|
.empty-state { padding:32px 14px; text-align:center; color:#6e7681; font-size:13px; }
|
||||||
|
.loading { padding:48px 16px; text-align:center; color:#8b949e; }
|
||||||
|
`
|
||||||
|
|
||||||
|
const script = `
|
||||||
|
var root = document.getElementById('root');
|
||||||
|
var seed = [];
|
||||||
|
var repos = null; // [{owner,repo}], persisted
|
||||||
|
var expanded = {}; // pr key -> bool
|
||||||
|
var prCache = {}; // 'owner/repo' -> { loading, error, prs }
|
||||||
|
var profile = null; // authenticated user profile
|
||||||
|
var profileState = null; // { loading, error }
|
||||||
|
var repoInput = '';
|
||||||
|
var connected = null;
|
||||||
|
var connecting = false;
|
||||||
|
var toastTimer = null;
|
||||||
|
|
||||||
|
function esc(s){ return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
||||||
|
function key(o,r){ return o + '/' + r; }
|
||||||
|
function persist(){ window.rowboat.setState({ repos: repos }); }
|
||||||
|
|
||||||
|
// ---- live GitHub reads via the bridge -------------------------------------
|
||||||
|
function asArray(d){
|
||||||
|
if (Array.isArray(d)) return d;
|
||||||
|
if (d && typeof d === 'object') {
|
||||||
|
var keys = ['details','data','items','pull_requests','pullRequests','response_data','result'];
|
||||||
|
for (var i=0;i<keys.length;i++){ if (Array.isArray(d[keys[i]])) return d[keys[i]]; }
|
||||||
|
for (var k in d){ if (Array.isArray(d[k]) && d[k].length && typeof d[k][0] === 'object') return d[k]; }
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
function normPRs(d){
|
||||||
|
return asArray(d).map(function(p){
|
||||||
|
return {
|
||||||
|
number: (p.number != null ? p.number : (p.id != null ? p.id : '?')),
|
||||||
|
title: p.title || '(untitled)',
|
||||||
|
body: (p.body || '').trim(),
|
||||||
|
author: (p.user && (p.user.login || p.user.name)) || p.author || '',
|
||||||
|
url: p.html_url || p.url || ''
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Pick the *list pull requests* tool, excluding review/comment/file/commit
|
||||||
|
// variants that happen to contain PULL + LIST but need pull_number/review_id.
|
||||||
|
function pickPRTool(tools){
|
||||||
|
var bad = ['REVIEW','COMMENT','FILE','COMMIT','REQUESTED'];
|
||||||
|
function ok(s){ for (var i=0;i<bad.length;i++) if (s.indexOf(bad[i])>=0) return false; return true; }
|
||||||
|
for (var i=0;i<tools.length;i++){ if ((tools[i].slug||'').toUpperCase() === 'GITHUB_LIST_PULL_REQUESTS') return tools[i]; }
|
||||||
|
for (var j=0;j<tools.length;j++){ var s=(tools[j].slug||'').toUpperCase(); if (/LIST_PULL_REQUESTS$/.test(s) && ok(s)) return tools[j]; }
|
||||||
|
for (var m=0;m<tools.length;m++){ var t=(tools[m].slug||'').toUpperCase(); if (t.indexOf('PULL_REQUEST')>=0 && t.indexOf('LIST')>=0 && ok(t)) return tools[m]; }
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function fetchPRs(o, r){
|
||||||
|
var k = key(o,r);
|
||||||
|
prCache[k] = { loading:true }; render();
|
||||||
|
var args = { owner:o, repo:r, state:'open', sort:'updated', direction:'desc', per_page:20 };
|
||||||
|
// Canonical slug first; fall back to a strict search only if it's unknown.
|
||||||
|
window.rowboat.callAction('github', 'GITHUB_LIST_PULL_REQUESTS', args).catch(function(){
|
||||||
|
return window.rowboat.searchTools('github','list pull requests for a repository').then(function(tools){
|
||||||
|
var tool = pickPRTool(tools || []);
|
||||||
|
if (!tool) throw new Error('could not find a list-pull-requests tool');
|
||||||
|
return window.rowboat.callAction('github', tool.slug, args);
|
||||||
|
});
|
||||||
|
}).then(function(d){
|
||||||
|
prCache[k] = { loading:false, prs: normPRs(d) }; render();
|
||||||
|
}).catch(function(e){
|
||||||
|
prCache[k] = { loading:false, error: (e && e.message ? e.message : String(e)) }; render();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function fetchAll(){ if (!connected) return; loadProfile(); if (!repos) return; repos.forEach(function(rp){ var k=key(rp.owner,rp.repo); if (!prCache[k]) fetchPRs(rp.owner, rp.repo); }); }
|
||||||
|
|
||||||
|
// ---- authenticated user profile -------------------------------------------
|
||||||
|
function asObj(d){
|
||||||
|
if (d && typeof d === 'object'){
|
||||||
|
if (d.login) return d;
|
||||||
|
var ks = ['data','details','response_data','result','user'];
|
||||||
|
for (var i=0;i<ks.length;i++){ var v=d[ks[i]]; if (v && typeof v === 'object' && v.login) return v; }
|
||||||
|
}
|
||||||
|
return d || {};
|
||||||
|
}
|
||||||
|
function loadProfile(){
|
||||||
|
if (!connected || profile || (profileState && profileState.loading)) return;
|
||||||
|
profileState = { loading:true }; render();
|
||||||
|
window.rowboat.callAction('github','GITHUB_GET_THE_AUTHENTICATED_USER',{}).catch(function(){
|
||||||
|
return window.rowboat.searchTools('github','get the authenticated user').then(function(tools){
|
||||||
|
var pick = null;
|
||||||
|
for (var i=0;i<(tools||[]).length;i++){ if (/AUTHENTICATED_USER$/.test((tools[i].slug||'').toUpperCase())){ pick = tools[i]; break; } }
|
||||||
|
if (!pick && tools && tools.length) pick = tools[0];
|
||||||
|
if (!pick) throw new Error('no profile tool found');
|
||||||
|
return window.rowboat.callAction('github', pick.slug, {});
|
||||||
|
});
|
||||||
|
}).then(function(d){
|
||||||
|
var u = asObj(d);
|
||||||
|
profile = { login:u.login||'', name:u.name||u.login||'', avatar:u.avatar_url||'', bio:u.bio||'', repos:u.public_repos||0, followers:u.followers||0, following:u.following||0 };
|
||||||
|
profileState = { loading:false }; render();
|
||||||
|
}).catch(function(e){
|
||||||
|
profileState = { loading:false, error:(e && e.message ? e.message : String(e)) }; render();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- connection -----------------------------------------------------------
|
||||||
|
function bannerHtml(){
|
||||||
|
if (connecting) return '<div class="banner"><span class="banner-text">Connecting GitHub — finish in your browser…</span><button class="banner-btn" disabled>Connecting…</button></div>';
|
||||||
|
if (connected === false) return '<div class="banner"><span class="banner-text">Connect GitHub to load pull requests.</span><button class="banner-btn" data-connect="1">Connect GitHub</button></div>';
|
||||||
|
if (connected === true) return '<div class="banner ok"><span class="banner-text">GitHub connected — pull requests are live.</span></div>';
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
function checkConn(){ window.rowboat.isConnected('github').then(function(c){ connected = c; render(); fetchAll(); }); }
|
||||||
|
function connect(){
|
||||||
|
connecting = true; render();
|
||||||
|
window.rowboat.connect('github').then(function(){
|
||||||
|
var tries = 0;
|
||||||
|
var iv = setInterval(function(){
|
||||||
|
tries++;
|
||||||
|
window.rowboat.isConnected('github').then(function(c){
|
||||||
|
if (c){ connected = true; connecting = false; clearInterval(iv); render(); flash('GitHub connected'); fetchAll(); }
|
||||||
|
else if (tries > 30){ connecting = false; clearInterval(iv); render(); }
|
||||||
|
});
|
||||||
|
}, 2000);
|
||||||
|
}).catch(function(e){ connecting = false; render(); flash('Connect failed: ' + (e && e.message ? e.message : e), true); });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- repo management ------------------------------------------------------
|
||||||
|
function addRepo(){
|
||||||
|
var v = (repoInput || '').trim().replace(/^https?:\\/\\/github.com\\//i, '');
|
||||||
|
var m = v.match(/^([\\w.-]+)\\/([\\w.-]+)$/);
|
||||||
|
if (!m){ flash('Enter as owner/repo', true); return; }
|
||||||
|
var o = m[1], r = m[2];
|
||||||
|
if (repos.some(function(x){ return x.owner === o && x.repo === r; })){ flash('Already tracked'); return; }
|
||||||
|
repos.push({ owner:o, repo:r }); repoInput = ''; persist(); render();
|
||||||
|
if (connected) fetchPRs(o, r);
|
||||||
|
}
|
||||||
|
function removeRepo(o, r){
|
||||||
|
repos = repos.filter(function(x){ return !(x.owner === o && x.repo === r); });
|
||||||
|
delete prCache[key(o,r)]; persist(); render();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- render ---------------------------------------------------------------
|
||||||
|
function prHtml(o, r, pr){
|
||||||
|
var k = key(o,r) + '#' + pr.number;
|
||||||
|
var open = !!expanded[k];
|
||||||
|
var body = open
|
||||||
|
? (pr.body ? '<div class="pr-body">' + esc(pr.body) + '</div>' : '<div class="pr-body empty">No description.</div>')
|
||||||
|
: '';
|
||||||
|
return '<div class="pr"><div class="pr-row" data-pr="' + esc(k) + '">'
|
||||||
|
+ '<span class="pr-icon">⌥</span><div class="pr-main">'
|
||||||
|
+ '<div class="pr-title">' + esc(pr.title) + '</div>'
|
||||||
|
+ '<div class="pr-sub">#' + esc(pr.number) + (pr.author ? ' · ' + esc(pr.author) : '') + '</div>'
|
||||||
|
+ '</div></div>' + body + '</div>';
|
||||||
|
}
|
||||||
|
function repoHtml(rp){
|
||||||
|
var k = key(rp.owner, rp.repo);
|
||||||
|
var c = prCache[k];
|
||||||
|
var inner;
|
||||||
|
if (!connected) inner = '<div class="state">Connect GitHub to load PRs.</div>';
|
||||||
|
else if (!c || c.loading) inner = '<div class="state">Loading pull requests…</div>';
|
||||||
|
else if (c.error) inner = '<div class="state err">' + esc(c.error) + '</div>';
|
||||||
|
else if (!c.prs.length) inner = '<div class="empty-state">No open pull requests 🎉</div>';
|
||||||
|
else inner = c.prs.map(function(pr){ return prHtml(rp.owner, rp.repo, pr); }).join('');
|
||||||
|
var count = (c && c.prs) ? '<span class="repo-count">' + c.prs.length + ' open</span>' : '';
|
||||||
|
return '<div class="repo"><div class="repo-head">'
|
||||||
|
+ '<span class="repo-name">' + esc(rp.owner) + '/' + esc(rp.repo) + '</span>'
|
||||||
|
+ '<span class="repo-actions">' + count
|
||||||
|
+ '<button class="icon-btn" data-refresh="' + esc(k) + '">↻</button>'
|
||||||
|
+ '<button class="icon-btn" data-remove="' + esc(k) + '">✕</button>'
|
||||||
|
+ '</span></div>' + inner + '</div>';
|
||||||
|
}
|
||||||
|
function profileHtml(){
|
||||||
|
if (!connected) return '';
|
||||||
|
if (!profile) return (profileState && profileState.loading) ? '<div class="state">Loading profile…</div>' : '';
|
||||||
|
var initials = (profile.name || profile.login || '?').slice(0,1).toUpperCase();
|
||||||
|
var img = profile.avatar ? '<img class="avatar" src="' + esc(profile.avatar) + '" alt="" />' : '';
|
||||||
|
var fbStyle = profile.avatar ? '' : ' style="display:flex"';
|
||||||
|
return '<div class="profile">' + img
|
||||||
|
+ '<div id="avfb" class="avatar-fb"' + fbStyle + '>' + esc(initials) + '</div>'
|
||||||
|
+ '<div><div class="pname">' + esc(profile.name) + '</div><div class="plogin">@' + esc(profile.login) + '</div>'
|
||||||
|
+ (profile.bio ? '<div class="pbio">' + esc(profile.bio) + '</div>' : '')
|
||||||
|
+ '<div class="pstats"><span><b>' + profile.repos + '</b> repos</span><span><b>' + profile.followers + '</b> followers</span><span><b>' + profile.following + '</b> following</span></div>'
|
||||||
|
+ '</div></div>'
|
||||||
|
+ '<div class="contrib"><div class="contrib-title">Contributions</div>'
|
||||||
|
+ '<img class="contrib-img" src="https://ghchart.rshah.org/' + encodeURIComponent(profile.login) + '" alt="contributions" />'
|
||||||
|
+ '<div id="contribnote" class="contrib-note" style="display:none">Contribution graph could not load.</div>'
|
||||||
|
+ '</div>';
|
||||||
|
}
|
||||||
|
function render(){
|
||||||
|
var list = repos
|
||||||
|
? (repos.length ? repos.map(repoHtml).join('') : '<div class="empty-state">No repos yet — add one above.</div>')
|
||||||
|
: '<div class="loading">Loading…</div>';
|
||||||
|
root.innerHTML = '<div class="app"><div class="wrap">'
|
||||||
|
+ '<h1 class="h">GitHub Dashboard</h1><p class="sub">Track repos and read their open pull requests.</p>'
|
||||||
|
+ bannerHtml()
|
||||||
|
+ profileHtml()
|
||||||
|
+ '<div class="adder"><input id="repo-input" placeholder="owner/repo (e.g. vercel/next.js)" /><button data-add="1">Add</button></div>'
|
||||||
|
+ list
|
||||||
|
+ '</div><div class="toast" id="toast"></div></div>';
|
||||||
|
var el = document.getElementById('repo-input');
|
||||||
|
if (el) el.value = repoInput;
|
||||||
|
var av = root.querySelector('.avatar');
|
||||||
|
if (av) av.onerror = function(){ this.style.display='none'; var f=document.getElementById('avfb'); if (f) f.style.display='flex'; };
|
||||||
|
var ci = root.querySelector('.contrib-img');
|
||||||
|
if (ci) ci.onerror = function(){ this.style.display='none'; var n=document.getElementById('contribnote'); if (n) n.style.display='block'; };
|
||||||
|
}
|
||||||
|
|
||||||
|
function flash(msg, isErr){
|
||||||
|
var el = document.getElementById('toast'); if (!el) return;
|
||||||
|
el.textContent = msg; el.className = 'toast show' + (isErr ? ' err' : '');
|
||||||
|
if (toastTimer) clearTimeout(toastTimer);
|
||||||
|
toastTimer = setTimeout(function(){ el.className = 'toast' + (isErr ? ' err' : ''); }, 2400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- events ---------------------------------------------------------------
|
||||||
|
root.addEventListener('input', function(e){ if (e.target && e.target.id === 'repo-input') repoInput = e.target.value; });
|
||||||
|
root.addEventListener('keydown', function(e){ if (e.target && e.target.id === 'repo-input' && e.key === 'Enter') addRepo(); });
|
||||||
|
root.addEventListener('click', function(e){
|
||||||
|
var t = e.target;
|
||||||
|
if (!t || !t.closest) return;
|
||||||
|
if (t.closest('[data-connect]')) { connect(); return; }
|
||||||
|
if (t.closest('[data-add]')) { addRepo(); return; }
|
||||||
|
var rm = t.closest('[data-remove]');
|
||||||
|
if (rm) { var p = rm.getAttribute('data-remove').split('/'); removeRepo(p[0], p[1]); return; }
|
||||||
|
var rf = t.closest('[data-refresh]');
|
||||||
|
if (rf) { var q = rf.getAttribute('data-refresh').split('/'); fetchPRs(q[0], q[1]); return; }
|
||||||
|
var pr = t.closest('[data-pr]');
|
||||||
|
if (pr) { var pk = pr.getAttribute('data-pr'); expanded[pk] = !expanded[pk]; render(); return; }
|
||||||
|
});
|
||||||
|
|
||||||
|
window.rowboat.onData(function(d){ seed = (d && d.seedRepos) || []; if (repos === null) { repos = seed.slice(); render(); } });
|
||||||
|
window.rowboat.onState(function(st){
|
||||||
|
if (st && st.repos) { repos = st.repos; render(); fetchAll(); }
|
||||||
|
else if (repos === null) { repos = seed.slice(); render(); }
|
||||||
|
});
|
||||||
|
render();
|
||||||
|
window.rowboat.ready();
|
||||||
|
checkConn();
|
||||||
|
`
|
||||||
|
|
||||||
|
export const githubRadarApp: MiniApp = {
|
||||||
|
id: 'github-radar',
|
||||||
|
name: 'GitHub Dashboard',
|
||||||
|
description: 'Track repos and read their open pull requests — live via GitHub.',
|
||||||
|
source: 'GitHub',
|
||||||
|
active: true,
|
||||||
|
lastRun: 'just now',
|
||||||
|
scope: ['github'],
|
||||||
|
data,
|
||||||
|
html: buildMiniAppHtml({ title: 'GitHub Dashboard', style, script }),
|
||||||
|
}
|
||||||
|
|
@ -109,6 +109,10 @@ const style = `
|
||||||
.action.like.on { color:#f91880; }
|
.action.like.on { color:#f91880; }
|
||||||
.action.repost.on { color:#00ba7c; }
|
.action.repost.on { color:#00ba7c; }
|
||||||
.empty { padding:48px 16px; text-align:center; color:#71767b; font-size:15px; }
|
.empty { padding:48px 16px; text-align:center; color:#71767b; font-size:15px; }
|
||||||
|
.banner { display:flex; align-items:center; justify-content:space-between; gap:12px; margin:14px 16px 0; padding:12px 14px; border:1px solid #1d9bf0; background:rgba(29,155,240,0.12); border-radius:12px; }
|
||||||
|
.banner-text { font-size:13px; color:#e7e9ea; }
|
||||||
|
.banner-btn { background:#1d9bf0; color:#fff; border:0; border-radius:999px; padding:7px 16px; font-size:13px; font-weight:700; cursor:pointer; }
|
||||||
|
.banner-btn:disabled { opacity:.6; cursor:default; }
|
||||||
.toast { position:fixed; bottom:20px; left:50%; transform:translateX(-50%); background:#1d9bf0; color:#fff; font-size:14px; font-weight:600; padding:10px 18px; border-radius:999px; opacity:0; transition:opacity .2s; pointer-events:none; }
|
.toast { position:fixed; bottom:20px; left:50%; transform:translateX(-50%); background:#1d9bf0; color:#fff; font-size:14px; font-weight:600; padding:10px 18px; border-radius:999px; opacity:0; transition:opacity .2s; pointer-events:none; }
|
||||||
.toast.show { opacity:1; }
|
.toast.show { opacity:1; }
|
||||||
.loading { padding:48px 16px; text-align:center; color:#71767b; font-size:15px; }
|
.loading { padding:48px 16px; text-align:center; color:#71767b; font-size:15px; }
|
||||||
|
|
@ -133,6 +137,17 @@ function persist() {
|
||||||
window.rowboat.setState({ topics: selected, liked: liked, reposted: reposted });
|
window.rowboat.setState({ topics: selected, liked: liked, reposted: reposted });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// X/Twitter has no Composio managed-OAuth2 flow, so this sample stays a local UI
|
||||||
|
// demo (see GitHub Radar for the real connect -> execute bridge).
|
||||||
|
function act(action, id) {
|
||||||
|
if (action === 'reply') { flash('Reply drafted (demo)'); return; }
|
||||||
|
if (action === 'like') liked[id] = !liked[id];
|
||||||
|
if (action === 'repost') reposted[id] = !reposted[id];
|
||||||
|
persist(); render();
|
||||||
|
if (action === 'repost') flash(reposted[id] ? 'Reposted (demo)' : 'Removed');
|
||||||
|
if (action === 'like') flash(liked[id] ? 'Liked (demo)' : 'Unliked');
|
||||||
|
}
|
||||||
|
|
||||||
function postHtml(p) {
|
function postHtml(p) {
|
||||||
var likeOn = liked[p.id] ? ' on' : '';
|
var likeOn = liked[p.id] ? ' on' : '';
|
||||||
var repoOn = reposted[p.id] ? ' on' : '';
|
var repoOn = reposted[p.id] ? ' on' : '';
|
||||||
|
|
@ -193,18 +208,7 @@ root.addEventListener('click', function (e) {
|
||||||
}
|
}
|
||||||
var btn = t && t.closest ? t.closest('.action') : null;
|
var btn = t && t.closest ? t.closest('.action') : null;
|
||||||
if (!btn) return;
|
if (!btn) return;
|
||||||
var action = btn.getAttribute('data-action');
|
act(btn.getAttribute('data-action'), btn.getAttribute('data-id'));
|
||||||
var id = btn.getAttribute('data-id');
|
|
||||||
if (action === 'reply') { flash('Reply drafted (demo)'); return; }
|
|
||||||
if (action === 'like') { liked[id] = !liked[id]; }
|
|
||||||
if (action === 'repost') { reposted[id] = !reposted[id]; }
|
|
||||||
persist();
|
|
||||||
render();
|
|
||||||
window.rowboat.callAction('twitter', action, { id: id }).then(function (res) {
|
|
||||||
flash((res && res.message) || 'Done');
|
|
||||||
}).catch(function (err) {
|
|
||||||
flash('Failed: ' + (err && err.message ? err.message : err));
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
window.rowboat.onData(function (d) {
|
window.rowboat.onData(function (d) {
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,11 @@
|
||||||
// from ~/.rowboat/apps/<id>/ over IPC.
|
// from ~/.rowboat/apps/<id>/ over IPC.
|
||||||
|
|
||||||
import type { MiniApp } from './types'
|
import type { MiniApp } from './types'
|
||||||
|
import { githubRadarApp } from './apps/github-radar'
|
||||||
import { twitterClientApp } from './apps/twitter-client'
|
import { twitterClientApp } from './apps/twitter-client'
|
||||||
import { newsletterDigestApp, competitorWatchApp } from './apps/digests'
|
import { newsletterDigestApp, competitorWatchApp } from './apps/digests'
|
||||||
|
|
||||||
export const MINI_APPS: MiniApp[] = [twitterClientApp, newsletterDigestApp, competitorWatchApp]
|
export const MINI_APPS: MiniApp[] = [githubRadarApp, twitterClientApp, newsletterDigestApp, competitorWatchApp]
|
||||||
|
|
||||||
export function getMiniApp(id: string): MiniApp | undefined {
|
export function getMiniApp(id: string): MiniApp | undefined {
|
||||||
return MINI_APPS.find((app) => app.id === id)
|
return MINI_APPS.find((app) => app.id === id)
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,13 @@ const BRIDGE_SHIM = /* js */ `
|
||||||
var dataCbs = [], stateCbs = [];
|
var dataCbs = [], stateCbs = [];
|
||||||
var pending = {}, seq = 0;
|
var pending = {}, seq = 0;
|
||||||
function post(msg) { parent.postMessage(msg, '*'); }
|
function post(msg) { parent.postMessage(msg, '*'); }
|
||||||
|
function rpc(method, params) {
|
||||||
|
var id = 'r' + (++seq);
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
pending[id] = { resolve: resolve, reject: reject };
|
||||||
|
post({ type: 'rowboat:mini-app:rpc', id: id, method: method, params: params });
|
||||||
|
});
|
||||||
|
}
|
||||||
window.addEventListener('message', function (e) {
|
window.addEventListener('message', function (e) {
|
||||||
var m = e.data;
|
var m = e.data;
|
||||||
if (!m || typeof m !== 'object') return;
|
if (!m || typeof m !== 'object') return;
|
||||||
|
|
@ -28,11 +35,11 @@ const BRIDGE_SHIM = /* js */ `
|
||||||
} else if (m.type === 'rowboat:mini-app:state') {
|
} else if (m.type === 'rowboat:mini-app:state') {
|
||||||
state = m.state;
|
state = m.state;
|
||||||
stateCbs.forEach(function (cb) { try { cb(state); } catch (_) {} });
|
stateCbs.forEach(function (cb) { try { cb(state); } catch (_) {} });
|
||||||
} else if (m.type === 'rowboat:mini-app:action-result') {
|
} else if (m.type === 'rowboat:mini-app:rpc-result') {
|
||||||
var p = pending[m.id];
|
var p = pending[m.id];
|
||||||
if (p) {
|
if (p) {
|
||||||
delete pending[m.id];
|
delete pending[m.id];
|
||||||
if (m.ok) p.resolve(m.result); else p.reject(new Error(m.error || 'action failed'));
|
if (m.ok) p.resolve(m.result); else p.reject(new Error(m.error || 'request failed'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -54,13 +61,15 @@ const BRIDGE_SHIM = /* js */ `
|
||||||
post({ type: 'rowboat:mini-app:setState', patch: patch });
|
post({ type: 'rowboat:mini-app:setState', patch: patch });
|
||||||
stateCbs.forEach(function (cb) { try { cb(state); } catch (_) {} });
|
stateCbs.forEach(function (cb) { try { cb(state); } catch (_) {} });
|
||||||
},
|
},
|
||||||
callAction: function (scope, action, args) {
|
// Execute a Composio tool by slug within the app's scope. Resolves to the
|
||||||
var id = 'a' + (++seq);
|
// tool result, rejects with an Error (e.g. not connected / out of scope).
|
||||||
return new Promise(function (resolve, reject) {
|
callAction: function (scope, tool, args) { return rpc('callAction', { scope: scope, tool: tool, args: args }); },
|
||||||
pending[id] = { resolve: resolve, reject: reject };
|
// Find tool slugs within a toolkit. Resolves to [{ slug, name, description }].
|
||||||
post({ type: 'rowboat:mini-app:action', id: id, scope: scope, action: action, args: args });
|
searchTools: function (scope, query) { return rpc('searchTools', { scope: scope, query: query }); },
|
||||||
});
|
// Resolve to true/false whether the toolkit is connected.
|
||||||
},
|
isConnected: function (scope) { return rpc('isConnected', { scope: scope }); },
|
||||||
|
// Trigger the Composio OAuth flow for the toolkit. Resolves when started.
|
||||||
|
connect: function (scope) { return rpc('connect', { scope: scope }); },
|
||||||
ready: function () { post({ type: 'rowboat:mini-app:ready' }); },
|
ready: function () { post({ type: 'rowboat:mini-app:ready' }); },
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -46,12 +46,22 @@ export type MiniApp = {
|
||||||
// surface the app codes against and the security boundary.
|
// surface the app codes against and the security boundary.
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Host RPC methods the app can call (all scope-checked against the app's
|
||||||
|
* declared integration scope by the host before they run):
|
||||||
|
* - callAction: execute a Composio tool by slug params: { scope, tool, args? }
|
||||||
|
* - searchTools: find tool slugs within a toolkit params: { scope, query }
|
||||||
|
* - isConnected: is the toolkit connected? params: { scope }
|
||||||
|
* - connect: trigger the Composio OAuth flow params: { scope }
|
||||||
|
*/
|
||||||
|
export type MiniAppRpcMethod = 'callAction' | 'searchTools' | 'isConnected' | 'connect'
|
||||||
|
|
||||||
/** Messages sent from the iframe (app) up to the host (renderer). */
|
/** Messages sent from the iframe (app) up to the host (renderer). */
|
||||||
export type MiniAppOutboundMessage =
|
export type MiniAppOutboundMessage =
|
||||||
/** Handshake: app is mounted and wants its initial data + state. */
|
/** Handshake: app is mounted and wants its initial data + state. */
|
||||||
| { type: 'rowboat:mini-app:ready' }
|
| { type: 'rowboat:mini-app:ready' }
|
||||||
/** App requests a scoped Composio action. Host replies with action-result. */
|
/** A request/response call into the host, correlated by id. */
|
||||||
| { type: 'rowboat:mini-app:action'; id: string; scope: string; action: string; args?: unknown }
|
| { type: 'rowboat:mini-app:rpc'; id: string; method: MiniAppRpcMethod; params?: unknown }
|
||||||
/** App wants to persist a patch to its per-app state store. */
|
/** App wants to persist a patch to its per-app state store. */
|
||||||
| { type: 'rowboat:mini-app:setState'; patch: unknown }
|
| { type: 'rowboat:mini-app:setState'; patch: unknown }
|
||||||
|
|
||||||
|
|
@ -61,14 +71,14 @@ export type MiniAppInboundMessage =
|
||||||
| { type: 'rowboat:mini-app:data'; data: unknown }
|
| { type: 'rowboat:mini-app:data'; data: unknown }
|
||||||
/** Current per-app state; sent on ready and after setState. */
|
/** Current per-app state; sent on ready and after setState. */
|
||||||
| { type: 'rowboat:mini-app:state'; state: unknown }
|
| { type: 'rowboat:mini-app:state'; state: unknown }
|
||||||
/** Result of a previously requested action, correlated by id. */
|
/** Result of a previously requested rpc call, correlated by id. */
|
||||||
| { type: 'rowboat:mini-app:action-result'; id: string; ok: boolean; result?: unknown; error?: string }
|
| { type: 'rowboat:mini-app:rpc-result'; id: string; ok: boolean; result?: unknown; error?: string }
|
||||||
|
|
||||||
export const MINI_APP_MESSAGE = {
|
export const MINI_APP_MESSAGE = {
|
||||||
ready: 'rowboat:mini-app:ready',
|
ready: 'rowboat:mini-app:ready',
|
||||||
action: 'rowboat:mini-app:action',
|
rpc: 'rowboat:mini-app:rpc',
|
||||||
setState: 'rowboat:mini-app:setState',
|
setState: 'rowboat:mini-app:setState',
|
||||||
data: 'rowboat:mini-app:data',
|
data: 'rowboat:mini-app:data',
|
||||||
state: 'rowboat:mini-app:state',
|
state: 'rowboat:mini-app:state',
|
||||||
actionResult: 'rowboat:mini-app:action-result',
|
rpcResult: 'rowboat:mini-app:rpc-result',
|
||||||
} as const
|
} as const
|
||||||
|
|
|
||||||
|
|
@ -1020,6 +1020,34 @@ const ipcSchemas = {
|
||||||
req: z.object({}),
|
req: z.object({}),
|
||||||
res: ZListToolkitsResponse,
|
res: ZListToolkitsResponse,
|
||||||
},
|
},
|
||||||
|
// Mini Apps: execute a Composio tool by slug (scoped to a connected toolkit).
|
||||||
|
'composio:execute-tool': {
|
||||||
|
req: z.object({
|
||||||
|
toolkitSlug: z.string(),
|
||||||
|
toolSlug: z.string(),
|
||||||
|
arguments: z.record(z.string(), z.unknown()).optional(),
|
||||||
|
}),
|
||||||
|
res: z.object({
|
||||||
|
successful: z.boolean(),
|
||||||
|
data: z.unknown().optional(),
|
||||||
|
error: z.string().optional(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
// Mini Apps: search Composio tools within a toolkit (returns slugs + schemas).
|
||||||
|
'composio:search-tools': {
|
||||||
|
req: z.object({
|
||||||
|
toolkitSlug: z.string(),
|
||||||
|
query: z.string(),
|
||||||
|
}),
|
||||||
|
res: z.object({
|
||||||
|
tools: z.array(z.object({
|
||||||
|
slug: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
description: z.string().optional(),
|
||||||
|
})),
|
||||||
|
error: z.string().optional(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
// Agent schedule channels
|
// Agent schedule channels
|
||||||
'agent-schedule:getConfig': {
|
'agent-schedule:getConfig': {
|
||||||
req: z.null(),
|
req: z.null(),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue