appoiint 4.18.0 → 4.18.1
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/dist/commands/ice.js +1 -1
- package/dist/commands/schema.js +1 -1
- package/dist/lib/turnProbe.js +1 -1
- package/dist/mcp-tools.js +1 -1
- package/package.json +1 -1
package/dist/commands/ice.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const{getFlag:getFlag,hasFlag:hasFlag}=require("../config"),fmt=require("../format"),{probe:probe,runControl:runControl,verdict:verdict,parseIceUrl:parseIceUrl,
|
|
1
|
+
const{getFlag:getFlag,hasFlag:hasFlag}=require("../config"),fmt=require("../format"),{probe:probe,runControl:runControl,verdict:verdict,parseIceUrl:parseIceUrl,flattenIceServers:flattenIceServers,relayTargets:relayTargets,relayCheck:relayCheck}=require("../lib/turnProbe"),{rawApi:rawApi}=require("../api"),STATUS_STYLE={pass:["[32m","PASS"],fail:["[31m","FAIL"],warn:["[33m","WARN"],inconclusive:["[2m","INCONCLUSIVE"]};function badge(e){const[r,o]=STATUS_STYLE[e]||["",e.toUpperCase()];return`${r}${o.padEnd(12)}[0m`}async function resolveIceServers(e,r){const o=getFlag(r,"url");if(o){const e=getFlag(r,"user")||process.env.TURN_SERVER_USERNAME,s=getFlag(r,"pass")||process.env.TURN_SERVER_CREDENTIAL;return{source:"command line",servers:o.split(",").map(r=>({urls:r.trim(),username:e,credential:s}))}}try{const r=await rawApi(e)("GET","/stt/config"),o=r&&r.iceServers||[];if(o.length)return{source:`${e.baseUrl}/stt/config`,servers:o}}catch(e){hasFlag(r,"json")||fmt.info(`Could not read /stt/config (${e.message}) — falling back to local env`)}const s=[],t=process.env.TURN_SERVER_USERNAME,n=process.env.TURN_SERVER_CREDENTIAL;for(const[e,r]of[["STUN_SERVERS",!1],["TURN_SERVERS",!0]])if(process.env[e])try{JSON.parse(process.env[e]).forEach(e=>{s.push(r?{urls:e.urls||e,username:t,credential:n}:{urls:e.urls||e})})}catch{}return{source:"local environment",servers:s}}const flatten=flattenIceServers;async function runRelay(e,r,o,s,t,n){const a=getFlag(r,"transport"),l=(getFlag(r,"hosts")||"").split(",").map(e=>e.trim()).filter(Boolean),c=relayTargets(t,{transport:a,hosts:l});c.length||(fmt.err("No TURN servers with credentials to relay through"+(a?` over ${a}`:"")+"."),process.exit(1));const{results:i,verdict:u}=await relayCheck(c,{timeoutMs:n});o?console.log(JSON.stringify({source:s,mode:"relay",results:i,verdict:u},null,2)):(console.log(`\n${fmt.bold("TURN relay path")} ${fmt.dim("source: "+s)}\n`),i.forEach(e=>{if(console.log(` ${badge(e.status)} ${e.a.server} <-> ${e.z.server}`),console.log(` ${fmt.dim("relay "+(e.a.relayAddress||e.a.allocate)+" <-> "+(e.z.relayAddress||e.z.allocate))}`),"done"===e.stage){const r=e=>e.delivered?`delivered ${e.ms}ms`:"DROPPED";console.log(` ${fmt.dim("A->Z "+r(e.aToZ)+" Z->A "+r(e.zToA))}`)}console.log(` ${fmt.dim(e.reason)}`)}),console.log("")),"fail"===u&&process.exit(1)}async function run(e,r,o){const s=parseInt(getFlag(r,"timeout")||"6000",10),{source:t,servers:n}=await resolveIceServers(e,r),a=flatten(n);if(hasFlag(r,"relay"))return a.length||(fmt.err("No ICE servers found."),process.exit(1)),runRelay(e,r,o,t,a,s);a.length||(fmt.err("No ICE servers found. Pass --url turn:HOST:PORT [--user U --pass P], or check STUN/TURN env."),process.exit(1));const l=a.map(e=>({...e,ice:parseIceUrl(e.url)})).filter(e=>!!e.ice||(o||fmt.err(`Unparseable ICE url, skipped: ${e.url}`),!1)),c=[...new Set(l.filter(e=>"udp"===e.ice.transport).map(e=>e.ice.port))],i={};for(const e of c)i[e]=await runControl({port:e,timeoutMs:s});const u=[];for(const e of l){const r=e.ice.needsAuth;if(r&&(!e.username||!e.credential)){u.push({url:e.url,status:"warn",reason:"TURN url has no credentials — cannot test allocate",probe:null});continue}const o=await probe({host:e.ice.host,port:e.ice.port,transport:e.ice.transport,username:r?e.username:void 0,password:r?e.credential:void 0,timeoutMs:s}),t=verdict({...o,needsAuth:r},i[e.ice.port]||null);u.push({url:e.url,status:t.status,reason:t.reason,probe:o})}const m=u.reduce((e,r)=>({...e,[r.status]:(e[r.status]||0)+1}),{});if(o)return console.log(JSON.stringify({source:t,controls:i,results:u,counts:m,verdict:m.fail?"fail":m.inconclusive?"inconclusive":"pass"},null,2));console.log(`\n${fmt.bold("ICE reachability")} ${fmt.dim("source: "+t)}\n`),console.log(fmt.bold(" Control")),c.length||console.log(` ${fmt.dim("no UDP targets — nothing to control against")}`);for(const e of c){const r=i[e];r.valid&&r.samePort?console.log(` udp/${e} ${STATUS_STYLE.pass[0]}ok[0m via ${r.via} ${fmt.dim("you appear as "+r.mappedAddress)}`):r.valid?console.log(` udp/${e} ${STATUS_STYLE.warn[0]}weak[0m — only udp/${r.port} reachable (${r.via}); this port is unproven`):console.log(` udp/${e} ${STATUS_STYLE.fail[0]}none[0m — no public STUN server answered. This host is blind.`)}console.log(`\n${fmt.bold(" Targets")}`),u.forEach(e=>{const r=e.probe,o=r&&null!=r.ms?fmt.dim(` ${r.ms}ms`):"";console.log(` ${badge(e.status)} ${e.url}${o}`),console.log(` ${fmt.dim(e.reason)}`),r&&r.relayAddress&&console.log(` ${fmt.dim("relay "+r.relayAddress+(r.realm?" realm "+r.realm:""))}`)});const p=["pass","fail","warn","inconclusive"].filter(e=>m[e]).map(e=>`${m[e]} ${e}`).join(", ");console.log(`\n ${p}`),m.inconclusive&&(console.log(fmt.dim("\n INCONCLUSIVE means this machine could not prove it can reach the port at all.")),console.log(fmt.dim(" Do NOT report an outage on that basis — re-run from a host with a working control."))),console.log(""),m.fail&&process.exit(1)}module.exports={run:run};
|
package/dist/commands/schema.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const pkg=require("../../package.json"),GLOBAL_FLAGS={"--json":"JSON output (for agents)","--key/-k":"override API key","--agent":"one-off identity override (handle)","--help/-h":"help"},COMMANDS={signup:{syntax:"apo signup --email E --password P",positional:[],flags:{"--email/-e":"email","--password/-p":"password","--agent/-a":"agent name","--type/-t":"agent type"},description:"Sign up (free 7-day trial, card required)"},signin:{syntax:"apo signin --email E --password P",positional:[],flags:{"--email/-e":"email","--password/-p":"password","--local":"save to ./.appoiint"},description:"Sign in as a human seat"},activate:{syntax:"apo activate [SUBSCRIPTION_ID]",positional:["subscriptionId?"],flags:{},description:"Get API key after card setup"},login:{syntax:"apo login --key KEY [--user USERID]",positional:[],flags:{"--key/-k":"API key","--user/-u":"user id","--local":"save to ./.appoiint"},description:"Save API key to config"},logout:{syntax:"apo logout",positional:[],flags:{},description:"Clear config"},config:{syntax:"apo config",positional:[],flags:{},description:"Show active config"},status:{syntax:"apo status",positional:[],flags:{},description:"API health + auth status"},version:{syntax:"apo version",positional:[],flags:{},description:"CLI version"},init:{syntax:"apo init [--agent-schema] [--json]",positional:[],flags:{"--agent-schema":"emit machine-readable command schema"},description:"Quickstart guide; --agent-schema dumps this schema as JSON"},room:{syntax:'apo room "Title"',positional:["title"],flags:{},description:"Create meeting room"},rooms:{syntax:"apo rooms",positional:[],flags:{},description:"List active rooms"},"room-status":{syntax:"apo room-status ROOM_ID",positional:["roomId"],flags:{},description:"Room status + participants"},"room-end":{syntax:"apo room-end ROOM_ID",positional:["roomId"],flags:{},description:"End meeting"},invite:{syntax:"apo invite ROOM_ID --email E --name N",positional:["roomId"],flags:{"--email/-e":"invitee email","--name/-n":"invitee name","--type/-t":"human|agent"},description:"Invite human or agent"},transcript:{syntax:"apo transcript ROOM_ID [--format F]",positional:["roomId"],flags:{"--format/-f":"json|text|srt"},description:"Get transcript"},join:{syntax:'apo join ROOM_ID|BOOKING_ID [--name N] [--say "msg"] [--voice V]',positional:["roomIdOrBookingId"],flags:{"--name":"display name","--role":"agent role","--say":"one message then leave","--voice":"TTS voice"},description:"Join room (bkg_ id resolves to room), stream transcript"},say:{syntax:'apo say ROOM_ID "text"',positional:["roomId","text"],flags:{},description:"Speak into a live join session (external-brain channel)"},hear:{syntax:"apo hear ROOM_ID [--wait]",positional:["roomId"],flags:{"--wait":"block until next utterance","--since":"timestamp","--speaker":"filter speaker","--timeout":"seconds"},description:"Print next utterance (external-brain channel)"},leave:{syntax:"apo leave ROOM_ID",positional:["roomId"],flags:{},description:"Leave room"},book:{syntax:"apo book --date D --email E",positional:[],flags:{"--event/-e":"event type id","--date/-d":"ISO datetime","--email":"guest email","--name/-n":"guest name","--timezone/-z":"tz","--host":"host email"},description:"Create booking"},bookings:{syntax:"apo bookings [--status S] [--host ID]",positional:[],flags:{"--status/-s":"status filter","--host":"host user id"},description:"List bookings (as host)"},booking:{syntax:"apo booking BOOKING_ID",positional:["bookingId"],flags:{},description:"Show booking (room + join links)"},cancel:{syntax:"apo cancel BOOKING_ID [--reason R]",positional:["bookingId"],flags:{"--reason/-r":"reason"},description:"Cancel a booking"},reschedule:{syntax:"apo reschedule BOOKING_ID --date D",positional:["bookingId"],flags:{"--date/-d":"new ISO datetime","--reason/-r":"reason"},description:"Move a booking to a new time"},events:{syntax:"apo events",positional:[],flags:{},description:"List event types"},"event-create":{syntax:'apo event-create --name "Title"',positional:[],flags:{"--name/-n":"title","--duration/-d":"minutes","--slug":"url slug","--description":"text","--timezone/-z":"tz","--price":"amount","--currency":"ISO currency"},description:"Create an event type"},"event-delete":{syntax:"apo event-delete EVENT_TYPE_ID [--soft] [--force]",positional:["eventTypeId"],flags:{"--id":"event type id","--soft":"soft delete","--force/-f":"skip booking check"},description:"Delete an event type (blocks if bookings exist)"},availability:{subcommands:{get:{syntax:"apo availability get [--user ID]",positional:[],flags:{"--user":"user id"},description:"Show weekly availability"},set:{syntax:"apo availability set --days mon-fri [--from 09:00 --to 17:00]",positional:[],flags:{"--days":"e.g. mon-fri","--from":"HH:MM","--to":"HH:MM","--timezone/-z":"tz","--user":"user id","--org":"organization id"},description:"Set availability"}}},slots:{syntax:"apo slots --email E --date D",positional:[],flags:{"--email/-e":"host email","--date/-d":"YYYY-MM-DD","--timezone/-z":"tz"},description:"Check availability"},domains:{syntax:"apo domains",positional:[],flags:{},description:"List your custom domains"},domain:{subcommands:{add:{syntax:"apo domain add HOSTNAME",positional:["hostname"],flags:{},description:"Link a domain (returns CNAME)"},verify:{syntax:"apo domain verify HOSTNAME",positional:["hostname"],flags:{},description:"Trigger DNS verification"},status:{syntax:"apo domain status HOSTNAME",positional:["hostname"],flags:{},description:"DNS + SSL status"},show:{syntax:"apo domain show HOSTNAME",positional:["hostname"],flags:{},description:"Domain details"},remove:{syntax:"apo domain remove HOSTNAME --yes",positional:["hostname"],flags:{"--yes":"confirm deletion"},description:"Unlink a domain"}}},page:{subcommands:{show:{syntax:"apo page",positional:[],flags:{},description:"Show your meetings-page profile"},set:{syntax:"apo page set --title T --company C",positional:[],flags:{"--first":"first name","--last":"last name","--company":"company","--title":"title","--phone":"phone","--website":"url"},description:"Update page fields"},cover:{syntax:"apo page cover <URL|file> [--type cover|avatar]",positional:["source"],flags:{"--type":"cover|avatar"},description:"Set meetings-page cover image"},check:{syntax:"apo page check [HOSTNAME]",positional:["hostname?"],flags:{},description:"Audit live page: routing, OG tags, image, profile"}}},tts:{syntax:'apo tts "text" --voice V',positional:["text"],flags:{"--voice/-v":"voice id","--role/-r":"role voice"},description:"Synthesize speech"},voices:{syntax:"apo voices",positional:[],flags:{},description:"List available voices"},admin:{subcommands:{tenants:{syntax:"apo admin tenants",positional:[],flags:{},description:"List all tenants"},"tenant-view":{syntax:"apo admin tenant-view SLUG",positional:["slug"],flags:{},description:"Full tenant detail"},"tenant-create":{syntax:"apo admin tenant-create SLUG",positional:["slug"],flags:{"--plan":"plan","--email":"owner email"},description:"Create tenant"},"tenant-suspend":{syntax:"apo admin tenant-suspend SLUG",positional:["slug"],flags:{},description:"Suspend tenant"},"tenant-activate":{syntax:"apo admin tenant-activate SLUG",positional:["slug"],flags:{"--plan":"plan"},description:"Reactivate tenant"},"tenant-delete":{syntax:"apo admin tenant-delete SLUG",positional:["slug"],flags:{},description:"Delete tenant"},users:{syntax:"apo admin users",positional:[],flags:{},description:"List all users"},"user-create":{syntax:"apo admin user-create --email E -p PASS",positional:[],flags:{"--email":"email","--password/-p":"password","--tenant":"tenant","--role":"role"},description:"Create user"},"user-reset":{syntax:"apo admin user-reset EMAIL -p PASS",positional:["email"],flags:{"--password/-p":"new password"},description:"Reset user password"},"profile-cover":{syntax:"apo admin profile-cover [EMAIL] <SRC> -p PASS [--tenants a@x,b@y]",positional:["emailOrSource","source?"],flags:{"--type":"cover|avatar","--password/-p":"password","--tenants":"comma list","--tenants-file":"file of tenants"},description:"Set tenant cover image (bulk = gated snapshot→apply→verify→rollback→ledger)"},"profile-avatar":{syntax:"apo admin profile-avatar [EMAIL] <SRC> -p PASS [--tenants a@x,b@y]",positional:["emailOrSource","source?"],flags:{"--password/-p":"password","--tenants":"comma list","--tenants-file":"file of tenants"},description:"Set tenant avatar (bulk gated)"},"profile-website":{syntax:"apo admin profile-website [EMAIL URL] -p PASS [--tenants a@x=url,b@y=url]",positional:["email?","url?"],flags:{"--password/-p":"password","--tenants":"a@x=url pairs","--tenants-file":"file of pairs"},description:"Set vCard VISIT CTA (bulk gated)"},billing:{syntax:"apo admin billing",positional:[],flags:{"--plan":"plan","--months":"n"},description:"MRR, ARR, revenue"},costs:{syntax:"apo admin costs [ROOM_ID]",positional:["roomId?"],flags:{},description:"Platform or per-meeting cost breakdown"},models:{syntax:"apo admin models",positional:[],flags:{},description:"Spend by provider/model + tokens"},usage:{syntax:"apo admin usage",positional:[],flags:{},description:"Spend by user"},timeseries:{syntax:"apo admin timeseries [--bucket day]",positional:[],flags:{"--bucket":"hour|day"},description:"Spend over time"},pricing:{syntax:"apo admin pricing",positional:[],flags:{},description:"Provider pricing table"},ice:{syntax:"apo admin ice [--relay [--transport tcp|udp]] [--url turn:IP:3478 --user U --pass P]",positional:[],flags:{"--relay":"relay datagrams between TURN hosts' relay ports (tests the relay port range; result independent of this host's network)","--transport":"control-channel transport for --relay (default tcp)","--url":"stun:/turn: url","--user":"turn user","--pass":"turn credential","--timeout":"ms"},description:"Probe live STUN/TURN (control-gated; INCONCLUSIVE ≠ fault)"},profile:{syntax:"apo admin profile EMAIL -p PASS",positional:["email"],flags:{"--password/-p":"password"},description:"Superadmin-orchestrated profile view"}}},stats:{syntax:"apo stats",positional:[],flags:{},description:"Platform overview"},funnel:{syntax:"apo funnel",positional:[],flags:{},description:"Signup funnel"},signups:{syntax:"apo signups [--days 7]",positional:[],flags:{"--days":"window"},description:"Recent signups"},revenue:{syntax:"apo revenue",positional:[],flags:{},description:"Revenue breakdown"},tickets:{syntax:"apo tickets [--status open]",positional:[],flags:{"--status":"filter"},description:"List support tickets"},ticket:{subcommands:{show:{syntax:"apo ticket show NUMBER",positional:["number"],flags:{},description:"Ticket detail"},reply:{syntax:"apo ticket reply NUMBER MESSAGE",positional:["number","message"],flags:{},description:"Reply to ticket"},close:{syntax:"apo ticket close NUMBER",positional:["number"],flags:{},description:"Close ticket"}}},audit:{syntax:"apo audit [export] [--action X] [--limit N] [--format csv]",positional:["export?"],flags:{"--action":"filter","--limit":"n","--format":"json|csv (export)"},description:"Activity trail / export"},helpdesk:{subcommands:{tickets:{syntax:"apo helpdesk tickets [--status S]",positional:[],flags:{"--status":"filter"},description:"List tickets"},ticket:{subcommands:{show:{syntax:"apo helpdesk ticket show ID",positional:["id"],flags:{},description:"Ticket detail + messages"},create:{syntax:'apo helpdesk ticket create "Subject"',positional:["subject"],flags:{"--subject":"subject","--priority":"low|medium|high","--category":"category"},description:"Create ticket"},reply:{syntax:'apo helpdesk ticket reply ID "msg"',positional:["id","message"],flags:{"--message":"body"},description:"Reply to ticket"},close:{syntax:"apo helpdesk ticket close ID",positional:["id"],flags:{},description:"Close ticket"},assign:{syntax:"apo helpdesk ticket assign ID --agent X",positional:["id"],flags:{"--agent":"agent"},description:"Assign ticket"}}},kb:{syntax:"apo helpdesk kb",positional:[],flags:{},description:"List KB articles"},stats:{syntax:"apo helpdesk stats",positional:[],flags:{},description:"Helpdesk statistics"}}},crm:{subcommands:{leads:{syntax:"apo crm leads [--status S]",positional:[],flags:{"--status":"filter"},description:"List leads"},lead:{subcommands:{show:{syntax:"apo crm lead show ID",positional:["id"],flags:{},description:"Lead detail"},create:{syntax:'apo crm lead create "Name" --email X',positional:["name"],flags:{"--name":"name","--email":"email","--source":"source"},description:"Create lead"},convert:{syntax:"apo crm lead convert ID",positional:["id"],flags:{},description:"Convert lead → opportunity"}}},opps:{syntax:"apo crm opps [--stage S]",positional:[],flags:{"--stage":"filter"},description:"List opportunities"},opp:{subcommands:{show:{syntax:"apo crm opp show ID",positional:["id"],flags:{},description:"Opportunity detail"},create:{syntax:'apo crm opp create "Deal" --value X',positional:["name"],flags:{"--name":"name","--value":"amount","--stage":"stage"},description:"Create opportunity"}}},pipelines:{syntax:"apo crm pipelines",positional:[],flags:{},description:"List pipelines"},stats:{syntax:"apo crm stats",positional:[],flags:{},description:"CRM statistics"}}},agent:{subcommands:{create:{syntax:"apo agent create HANDLE",positional:["handle"],flags:{"--type":"agent type","--global":"save to ~/.appoiint"},description:"Create named agent"},list:{syntax:"apo agent list",positional:[],flags:{},description:"List your agents"},use:{syntax:"apo agent use HANDLE | --account",positional:["handle?"],flags:{"--account":"switch back to account identity","--global":"save to ~/.appoiint"},description:"Act as @HANDLE"},whoami:{syntax:"apo agent whoami",positional:[],flags:{},description:"Show active identity"},revoke:{syntax:"apo agent revoke HANDLE",positional:["handle"],flags:{"--global":"save to ~/.appoiint"},description:"Revoke agent key"}}},wall:{syntax:"apo wall",positional:[],flags:{},description:"All threads (DMs + channels) w/ unread badges"},dm:{syntax:'apo dm @AGENT ["msg"] [--watch]',positional:["handle","message?"],flags:{"--message/-m":"body","--watch/-w":"live stream","--limit":"history size"},description:"Read/send a DM"},notifications:{syntax:"apo notifications",positional:[],flags:{},description:"Unread digest across all threads"},channel:{subcommands:{create:{syntax:"apo channel create NAME [--topic T]",positional:["name"],flags:{"--topic":"topic"},description:"Create a named channel"},join:{syntax:"apo channel join NAME",positional:["name"],flags:{},description:"Join a channel"},leave:{syntax:"apo channel leave NAME",positional:["name"],flags:{},description:"Leave a channel"},list:{syntax:"apo channel list",positional:[],flags:{},description:"List channels"},post:{syntax:'apo channel post NAME "msg"',positional:["name","message"],flags:{"--message/-m":"body"},description:"Post to a channel"},history:{syntax:"apo channel history <NAME|@AGENT>",positional:["target"],flags:{"--limit":"n"},description:"Channel or DM history (reading = ack)"},send:{syntax:'apo channel send @AGENT "msg"',positional:["handle","message"],flags:{"--message/-m":"body","--action/-a":"action","--payload":"json payload"},description:"Send 1:1 message"},inbox:{syntax:"apo channel inbox",positional:[],flags:{},description:"Unread messages"},agents:{syntax:"apo channel agents",positional:[],flags:{},description:"List agent contacts"}}},diagnose:{syntax:"apo diagnose ROOM_ID",positional:["roomId"],flags:{},description:"Full room diagnostic"},man:{syntax:"apo man renderer",positional:["page"],flags:{},description:"Manual pages (renderer)"},renderer:{syntax:"apo renderer",positional:[],flags:{},description:"Chat bubble renderer spec"},"mcp-serve":{syntax:"apo mcp-serve",positional:[],flags:{},description:"Run stdio MCP server (95 tools)"},help:{syntax:"apo help",positional:[],flags:{},description:"Full reference"}};function schema(){return{$schema:"appoiint-cli-commands",version:pkg.version,description:"apo CLI command schema. Load this into agent context BEFORE using any command. If a command, subcommand or flag is not listed here, DO NOT USE it — it does not exist. Never invent verbs or flags from memory; re-run `apo init --agent-schema --json` to refresh.",globalFlags:GLOBAL_FLAGS,commands:COMMANDS}}module.exports={schema:schema};
|
|
1
|
+
const pkg=require("../../package.json"),GLOBAL_FLAGS={"--json":"JSON output (for agents)","--key/-k":"override API key","--agent":"one-off identity override (handle)","--help/-h":"help"},COMMANDS={signup:{syntax:"apo signup --email E --password P",positional:[],flags:{"--email/-e":"email","--password/-p":"password","--agent/-a":"agent name","--type/-t":"agent type"},description:"Sign up (free 7-day trial, card required)"},signin:{syntax:"apo signin --email E --password P",positional:[],flags:{"--email/-e":"email","--password/-p":"password","--local":"save to ./.appoiint"},description:"Sign in as a human seat"},activate:{syntax:"apo activate [SUBSCRIPTION_ID]",positional:["subscriptionId?"],flags:{},description:"Get API key after card setup"},login:{syntax:"apo login --key KEY [--user USERID]",positional:[],flags:{"--key/-k":"API key","--user/-u":"user id","--local":"save to ./.appoiint"},description:"Save API key to config"},logout:{syntax:"apo logout",positional:[],flags:{},description:"Clear config"},config:{syntax:"apo config",positional:[],flags:{},description:"Show active config"},status:{syntax:"apo status",positional:[],flags:{},description:"API health + auth status"},version:{syntax:"apo version",positional:[],flags:{},description:"CLI version"},init:{syntax:"apo init [--agent-schema] [--json]",positional:[],flags:{"--agent-schema":"emit machine-readable command schema"},description:"Quickstart guide; --agent-schema dumps this schema as JSON"},room:{syntax:'apo room "Title"',positional:["title"],flags:{},description:"Create meeting room"},rooms:{syntax:"apo rooms",positional:[],flags:{},description:"List active rooms"},"room-status":{syntax:"apo room-status ROOM_ID",positional:["roomId"],flags:{},description:"Room status + participants"},"room-end":{syntax:"apo room-end ROOM_ID",positional:["roomId"],flags:{},description:"End meeting"},invite:{syntax:"apo invite ROOM_ID --email E --name N",positional:["roomId"],flags:{"--email/-e":"invitee email","--name/-n":"invitee name","--type/-t":"human|agent"},description:"Invite human or agent"},transcript:{syntax:"apo transcript ROOM_ID [--format F]",positional:["roomId"],flags:{"--format/-f":"json|text|srt"},description:"Get transcript"},join:{syntax:'apo join ROOM_ID|BOOKING_ID [--name N] [--say "msg"] [--voice V]',positional:["roomIdOrBookingId"],flags:{"--name":"display name","--role":"agent role","--say":"one message then leave","--voice":"TTS voice"},description:"Join room (bkg_ id resolves to room), stream transcript"},say:{syntax:'apo say ROOM_ID "text"',positional:["roomId","text"],flags:{},description:"Speak into a live join session (external-brain channel)"},hear:{syntax:"apo hear ROOM_ID [--wait]",positional:["roomId"],flags:{"--wait":"block until next utterance","--since":"timestamp","--speaker":"filter speaker","--timeout":"seconds"},description:"Print next utterance (external-brain channel)"},leave:{syntax:"apo leave ROOM_ID",positional:["roomId"],flags:{},description:"Leave room"},book:{syntax:"apo book --date D --email E",positional:[],flags:{"--event/-e":"event type id","--date/-d":"ISO datetime","--email":"guest email","--name/-n":"guest name","--timezone/-z":"tz","--host":"host email"},description:"Create booking"},bookings:{syntax:"apo bookings [--status S] [--host ID]",positional:[],flags:{"--status/-s":"status filter","--host":"host user id"},description:"List bookings (as host)"},booking:{syntax:"apo booking BOOKING_ID",positional:["bookingId"],flags:{},description:"Show booking (room + join links)"},cancel:{syntax:"apo cancel BOOKING_ID [--reason R]",positional:["bookingId"],flags:{"--reason/-r":"reason"},description:"Cancel a booking"},reschedule:{syntax:"apo reschedule BOOKING_ID --date D",positional:["bookingId"],flags:{"--date/-d":"new ISO datetime","--reason/-r":"reason"},description:"Move a booking to a new time"},events:{syntax:"apo events",positional:[],flags:{},description:"List event types"},"event-create":{syntax:'apo event-create --name "Title"',positional:[],flags:{"--name/-n":"title","--duration/-d":"minutes","--slug":"url slug","--description":"text","--timezone/-z":"tz","--price":"amount","--currency":"ISO currency"},description:"Create an event type"},"event-delete":{syntax:"apo event-delete EVENT_TYPE_ID [--soft] [--force]",positional:["eventTypeId"],flags:{"--id":"event type id","--soft":"soft delete","--force/-f":"skip booking check"},description:"Delete an event type (blocks if bookings exist)"},availability:{subcommands:{get:{syntax:"apo availability get [--user ID]",positional:[],flags:{"--user":"user id"},description:"Show weekly availability"},set:{syntax:"apo availability set --days mon-fri [--from 09:00 --to 17:00]",positional:[],flags:{"--days":"e.g. mon-fri","--from":"HH:MM","--to":"HH:MM","--timezone/-z":"tz","--user":"user id","--org":"organization id"},description:"Set availability"}}},slots:{syntax:"apo slots --email E --date D",positional:[],flags:{"--email/-e":"host email","--date/-d":"YYYY-MM-DD","--timezone/-z":"tz"},description:"Check availability"},domains:{syntax:"apo domains",positional:[],flags:{},description:"List your custom domains"},domain:{subcommands:{add:{syntax:"apo domain add HOSTNAME",positional:["hostname"],flags:{},description:"Link a domain (returns CNAME)"},verify:{syntax:"apo domain verify HOSTNAME",positional:["hostname"],flags:{},description:"Trigger DNS verification"},status:{syntax:"apo domain status HOSTNAME",positional:["hostname"],flags:{},description:"DNS + SSL status"},show:{syntax:"apo domain show HOSTNAME",positional:["hostname"],flags:{},description:"Domain details"},remove:{syntax:"apo domain remove HOSTNAME --yes",positional:["hostname"],flags:{"--yes":"confirm deletion"},description:"Unlink a domain"}}},page:{subcommands:{show:{syntax:"apo page",positional:[],flags:{},description:"Show your meetings-page profile"},set:{syntax:"apo page set --title T --company C",positional:[],flags:{"--first":"first name","--last":"last name","--company":"company","--title":"title","--phone":"phone","--website":"url"},description:"Update page fields"},cover:{syntax:"apo page cover <URL|file> [--type cover|avatar]",positional:["source"],flags:{"--type":"cover|avatar"},description:"Set meetings-page cover image"},check:{syntax:"apo page check [HOSTNAME]",positional:["hostname?"],flags:{},description:"Audit live page: routing, OG tags, image, profile"}}},tts:{syntax:'apo tts "text" --voice V',positional:["text"],flags:{"--voice/-v":"voice id","--role/-r":"role voice"},description:"Synthesize speech"},voices:{syntax:"apo voices",positional:[],flags:{},description:"List available voices"},admin:{subcommands:{tenants:{syntax:"apo admin tenants",positional:[],flags:{},description:"List all tenants"},"tenant-view":{syntax:"apo admin tenant-view SLUG",positional:["slug"],flags:{},description:"Full tenant detail"},"tenant-create":{syntax:"apo admin tenant-create SLUG",positional:["slug"],flags:{"--plan":"plan","--email":"owner email"},description:"Create tenant"},"tenant-suspend":{syntax:"apo admin tenant-suspend SLUG",positional:["slug"],flags:{},description:"Suspend tenant"},"tenant-activate":{syntax:"apo admin tenant-activate SLUG",positional:["slug"],flags:{"--plan":"plan"},description:"Reactivate tenant"},"tenant-delete":{syntax:"apo admin tenant-delete SLUG",positional:["slug"],flags:{},description:"Delete tenant"},users:{syntax:"apo admin users",positional:[],flags:{},description:"List all users"},"user-create":{syntax:"apo admin user-create --email E -p PASS",positional:[],flags:{"--email":"email","--password/-p":"password","--tenant":"tenant","--role":"role"},description:"Create user"},"user-reset":{syntax:"apo admin user-reset EMAIL -p PASS",positional:["email"],flags:{"--password/-p":"new password"},description:"Reset user password"},"profile-cover":{syntax:"apo admin profile-cover [EMAIL] <SRC> -p PASS [--tenants a@x,b@y]",positional:["emailOrSource","source?"],flags:{"--type":"cover|avatar","--password/-p":"password","--tenants":"comma list","--tenants-file":"file of tenants"},description:"Set tenant cover image (bulk = gated snapshot→apply→verify→rollback→ledger)"},"profile-avatar":{syntax:"apo admin profile-avatar [EMAIL] <SRC> -p PASS [--tenants a@x,b@y]",positional:["emailOrSource","source?"],flags:{"--password/-p":"password","--tenants":"comma list","--tenants-file":"file of tenants"},description:"Set tenant avatar (bulk gated)"},"profile-website":{syntax:"apo admin profile-website [EMAIL URL] -p PASS [--tenants a@x=url,b@y=url]",positional:["email?","url?"],flags:{"--password/-p":"password","--tenants":"a@x=url pairs","--tenants-file":"file of pairs"},description:"Set vCard VISIT CTA (bulk gated)"},billing:{syntax:"apo admin billing",positional:[],flags:{"--plan":"plan","--months":"n"},description:"MRR, ARR, revenue"},costs:{syntax:"apo admin costs [ROOM_ID]",positional:["roomId?"],flags:{},description:"Platform or per-meeting cost breakdown"},models:{syntax:"apo admin models",positional:[],flags:{},description:"Spend by provider/model + tokens"},usage:{syntax:"apo admin usage",positional:[],flags:{},description:"Spend by user"},timeseries:{syntax:"apo admin timeseries [--bucket day]",positional:[],flags:{"--bucket":"hour|day"},description:"Spend over time"},pricing:{syntax:"apo admin pricing",positional:[],flags:{},description:"Provider pricing table"},ice:{syntax:"apo admin ice [--relay [--transport tcp|udp] [--hosts IP,IP]] [--url turn:IP:3478 --user U --pass P]",positional:[],flags:{"--relay":"relay datagrams between TURN hosts' relay ports (tests the relay port range; result independent of this host's network)","--transport":"control-channel transport for --relay (default tcp)","--hosts":"with --relay: also relay through these hosts from /stt/config (e.g. a coturn box advertised only as STUN), using the advertised TURN creds","--url":"stun:/turn: url","--user":"turn user","--pass":"turn credential","--timeout":"ms"},description:"Probe live STUN/TURN (control-gated; INCONCLUSIVE ≠ fault)"},profile:{syntax:"apo admin profile EMAIL -p PASS",positional:["email"],flags:{"--password/-p":"password"},description:"Superadmin-orchestrated profile view"}}},stats:{syntax:"apo stats",positional:[],flags:{},description:"Platform overview"},funnel:{syntax:"apo funnel",positional:[],flags:{},description:"Signup funnel"},signups:{syntax:"apo signups [--days 7]",positional:[],flags:{"--days":"window"},description:"Recent signups"},revenue:{syntax:"apo revenue",positional:[],flags:{},description:"Revenue breakdown"},tickets:{syntax:"apo tickets [--status open]",positional:[],flags:{"--status":"filter"},description:"List support tickets"},ticket:{subcommands:{show:{syntax:"apo ticket show NUMBER",positional:["number"],flags:{},description:"Ticket detail"},reply:{syntax:"apo ticket reply NUMBER MESSAGE",positional:["number","message"],flags:{},description:"Reply to ticket"},close:{syntax:"apo ticket close NUMBER",positional:["number"],flags:{},description:"Close ticket"}}},audit:{syntax:"apo audit [export] [--action X] [--limit N] [--format csv]",positional:["export?"],flags:{"--action":"filter","--limit":"n","--format":"json|csv (export)"},description:"Activity trail / export"},helpdesk:{subcommands:{tickets:{syntax:"apo helpdesk tickets [--status S]",positional:[],flags:{"--status":"filter"},description:"List tickets"},ticket:{subcommands:{show:{syntax:"apo helpdesk ticket show ID",positional:["id"],flags:{},description:"Ticket detail + messages"},create:{syntax:'apo helpdesk ticket create "Subject"',positional:["subject"],flags:{"--subject":"subject","--priority":"low|medium|high","--category":"category"},description:"Create ticket"},reply:{syntax:'apo helpdesk ticket reply ID "msg"',positional:["id","message"],flags:{"--message":"body"},description:"Reply to ticket"},close:{syntax:"apo helpdesk ticket close ID",positional:["id"],flags:{},description:"Close ticket"},assign:{syntax:"apo helpdesk ticket assign ID --agent X",positional:["id"],flags:{"--agent":"agent"},description:"Assign ticket"}}},kb:{syntax:"apo helpdesk kb",positional:[],flags:{},description:"List KB articles"},stats:{syntax:"apo helpdesk stats",positional:[],flags:{},description:"Helpdesk statistics"}}},crm:{subcommands:{leads:{syntax:"apo crm leads [--status S]",positional:[],flags:{"--status":"filter"},description:"List leads"},lead:{subcommands:{show:{syntax:"apo crm lead show ID",positional:["id"],flags:{},description:"Lead detail"},create:{syntax:'apo crm lead create "Name" --email X',positional:["name"],flags:{"--name":"name","--email":"email","--source":"source"},description:"Create lead"},convert:{syntax:"apo crm lead convert ID",positional:["id"],flags:{},description:"Convert lead → opportunity"}}},opps:{syntax:"apo crm opps [--stage S]",positional:[],flags:{"--stage":"filter"},description:"List opportunities"},opp:{subcommands:{show:{syntax:"apo crm opp show ID",positional:["id"],flags:{},description:"Opportunity detail"},create:{syntax:'apo crm opp create "Deal" --value X',positional:["name"],flags:{"--name":"name","--value":"amount","--stage":"stage"},description:"Create opportunity"}}},pipelines:{syntax:"apo crm pipelines",positional:[],flags:{},description:"List pipelines"},stats:{syntax:"apo crm stats",positional:[],flags:{},description:"CRM statistics"}}},agent:{subcommands:{create:{syntax:"apo agent create HANDLE",positional:["handle"],flags:{"--type":"agent type","--global":"save to ~/.appoiint"},description:"Create named agent"},list:{syntax:"apo agent list",positional:[],flags:{},description:"List your agents"},use:{syntax:"apo agent use HANDLE | --account",positional:["handle?"],flags:{"--account":"switch back to account identity","--global":"save to ~/.appoiint"},description:"Act as @HANDLE"},whoami:{syntax:"apo agent whoami",positional:[],flags:{},description:"Show active identity"},revoke:{syntax:"apo agent revoke HANDLE",positional:["handle"],flags:{"--global":"save to ~/.appoiint"},description:"Revoke agent key"}}},wall:{syntax:"apo wall",positional:[],flags:{},description:"All threads (DMs + channels) w/ unread badges"},dm:{syntax:'apo dm @AGENT ["msg"] [--watch]',positional:["handle","message?"],flags:{"--message/-m":"body","--watch/-w":"live stream","--limit":"history size"},description:"Read/send a DM"},notifications:{syntax:"apo notifications",positional:[],flags:{},description:"Unread digest across all threads"},channel:{subcommands:{create:{syntax:"apo channel create NAME [--topic T]",positional:["name"],flags:{"--topic":"topic"},description:"Create a named channel"},join:{syntax:"apo channel join NAME",positional:["name"],flags:{},description:"Join a channel"},leave:{syntax:"apo channel leave NAME",positional:["name"],flags:{},description:"Leave a channel"},list:{syntax:"apo channel list",positional:[],flags:{},description:"List channels"},post:{syntax:'apo channel post NAME "msg"',positional:["name","message"],flags:{"--message/-m":"body"},description:"Post to a channel"},history:{syntax:"apo channel history <NAME|@AGENT>",positional:["target"],flags:{"--limit":"n"},description:"Channel or DM history (reading = ack)"},send:{syntax:'apo channel send @AGENT "msg"',positional:["handle","message"],flags:{"--message/-m":"body","--action/-a":"action","--payload":"json payload"},description:"Send 1:1 message"},inbox:{syntax:"apo channel inbox",positional:[],flags:{},description:"Unread messages"},agents:{syntax:"apo channel agents",positional:[],flags:{},description:"List agent contacts"}}},diagnose:{syntax:"apo diagnose ROOM_ID",positional:["roomId"],flags:{},description:"Full room diagnostic"},man:{syntax:"apo man renderer",positional:["page"],flags:{},description:"Manual pages (renderer)"},renderer:{syntax:"apo renderer",positional:[],flags:{},description:"Chat bubble renderer spec"},"mcp-serve":{syntax:"apo mcp-serve",positional:[],flags:{},description:"Run stdio MCP server (95 tools)"},help:{syntax:"apo help",positional:[],flags:{},description:"Full reference"}};function schema(){return{$schema:"appoiint-cli-commands",version:pkg.version,description:"apo CLI command schema. Load this into agent context BEFORE using any command. If a command, subcommand or flag is not listed here, DO NOT USE it — it does not exist. Never invent verbs or flags from memory; re-run `apo init --agent-schema --json` to refresh.",globalFlags:GLOBAL_FLAGS,commands:COMMANDS}}module.exports={schema:schema};
|
package/dist/lib/turnProbe.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";const dgram=require("dgram"),net=require("net"),tls=require("tls"),crypto=require("crypto"),MAGIC_COOKIE=554869826,BINDING_REQUEST=1,BINDING_SUCCESS=257,ALLOCATE_REQUEST=3,ALLOCATE_SUCCESS=259,REFRESH_REQUEST=4,CREATE_PERMISSION_REQUEST=8,CREATE_PERMISSION_SUCCESS=264,SEND_INDICATION=22,DATA_INDICATION=23,ATTR_MAPPED_ADDRESS=1,ATTR_USERNAME=6,ATTR_MESSAGE_INTEGRITY=8,ATTR_ERROR_CODE=9,ATTR_LIFETIME=13,ATTR_XOR_PEER_ADDRESS=18,ATTR_DATA=19,ATTR_REALM=20,ATTR_NONCE=21,ATTR_XOR_RELAYED_ADDRESS=22,ATTR_REQUESTED_TRANSPORT=25,ATTR_XOR_MAPPED_ADDRESS=32,UDP_CONTROLS=[{host:"stun.cloudflare.com",port:3478},{host:"stun.nextcloud.com",port:3478},{host:"stun.sipgate.net",port:3478},{host:"stun.l.google.com",port:19302},{host:"stun1.l.google.com",port:19302}];function buildHeader(e,r,t){const s=Buffer.alloc(20);return s.writeUInt16BE(e,0),s.writeUInt16BE(r,2),s.writeUInt32BE(554869826,4),t.copy(s,8),s}function buildAttr(e,r){const t=(4-r.length%4)%4,s=Buffer.alloc(4+r.length+t);return s.writeUInt16BE(e,0),s.writeUInt16BE(r.length,2),r.copy(s,4),s}function buildBindingRequest(e){return buildHeader(1,0,e)}function signedMessage(e,r,t,s){const o=Buffer.concat([...t,buildAttr(6,Buffer.from(s.username,"utf8")),buildAttr(20,s.realm),buildAttr(21,s.nonce)]),n=crypto.createHash("md5").update(`${s.username}:${s.realm.toString("utf8")}:${s.password}`).digest(),a=buildHeader(e,o.length+24,r),l=crypto.createHmac("sha1",n).update(Buffer.concat([a,o])).digest(),i=Buffer.concat([o,buildAttr(8,l)]);return Buffer.concat([buildHeader(e,i.length,r),i])}function unsignedMessage(e,r,t){const s=Buffer.concat(t);return Buffer.concat([buildHeader(e,s.length,r),s])}function buildAllocateRequest(e,r){const t=[buildAttr(25,Buffer.from([17,0,0,0]))];return r?signedMessage(3,e,t,r):unsignedMessage(3,e,t)}function encodeXorAddress(e,r){const t=Buffer.alloc(8);t.writeUInt8(1,1),t.writeUInt16BE(8466^r,2);const s=Buffer.alloc(4);return s.writeUInt32BE(554869826,0),e.split(".").forEach((e,r)=>t.writeUInt8(parseInt(e,10)^s[r],4+r)),t}function parseMessage(e){if(e.length<20)return null;const r={type:e.readUInt16BE(0),txId:e.slice(8,20),attrs:{}},t=Math.min(20+e.readUInt16BE(2),e.length);let s=20;for(;s+4<=t;){const t=e.readUInt16BE(s),o=e.readUInt16BE(s+2);if(s+4+o>e.length)break;r.attrs[t]=e.slice(s+4,s+4+o),s+=4+o+(4-o%4)%4}return r}function decodeAddress(e,r){if(!e||e.length<8)return null;if(1!==e.readUInt8(1))return"(ipv6)";const t=r?8466^e.readUInt16BE(2):e.readUInt16BE(2),s=e.slice(4,8);let o;if(r){const e=Buffer.alloc(4);e.writeUInt32BE(554869826,0),o=Array.from(s).map((r,t)=>r^e[t]).join(".")}else o=Array.from(s).join(".");return`${o}:${t}`}function decodeError(e){return!e||e.length<4?null:{code:100*e.readUInt8(2)+e.readUInt8(3),reason:e.slice(4).toString("utf8")}}function createTransport(e,r,t,s){if("udp"===e){const e=dgram.createSocket("udp4");return{send:s=>e.send(s,t,r),onMessage:r=>e.on("message",r),onError:r=>e.on("error",r),close:()=>{try{e.close()}catch{}},connect:e=>e()}}const o=0!==net.isIP(r),n="tls"===e?tls.connect({host:r,port:t,...o?{}:{servername:r},rejectUnauthorized:!1}):new net.Socket;let a=Buffer.alloc(0),l=()=>{};return n.setTimeout(s),n.on("data",e=>{for(a=Buffer.concat([a,e]);a.length>=20;){const e=20+a.readUInt16BE(2);if(a.length<e)return;const r=a.slice(0,e);a=a.slice(e),l(r)}}),{send:e=>n.write(e),onMessage:e=>{l=e},onError:e=>{n.on("error",r=>e(r)),n.on("timeout",()=>e(Object.assign(new Error("timeout"),{code:"ETIMEDOUT"})))},close:()=>{try{n.destroy()}catch{}},connect:s=>{"tls"===e?n.on("secureConnect",s):n.connect(t,r,s)},peerCert:()=>{if("tls"!==e)return null;try{const e=n.getPeerCertificate();return e&&e.subject?{subject:e.subject.CN||null,issuer:e.issuer&&e.issuer.CN,validTo:e.valid_to}:null}catch{return null}}}}function probe({host:e,port:r,transport:t="udp",username:s,password:o,timeoutMs:n=5e3}){return new Promise(a=>{const l={host:e,port:r,transport:t,reachable:!1,binding:null,mappedAddress:null,realm:null,allocate:null,relayAddress:null,tlsCert:null,error:null,ms:null},i=Date.now();let d,c="binding",u=crypto.randomBytes(12),p=!1;try{d=createTransport(t,e,r,n)}catch(e){return l.ms=Date.now()-i,l.error=e.code||e.message,a(l)}const f=e=>{p||(p=!0,clearTimeout(m),d.peerCert&&(l.tlsCert=d.peerCert()),d.close(),l.ms=Date.now()-i,a(Object.assign(l,e||{})))},m=setTimeout(()=>f({error:l.reachable?`timeout during ${c}`:"timeout (no response)"}),n);d.onError(e=>f({error:e.code||e.message})),d.onMessage(e=>{const r=parseMessage(e);if(r){if(l.reachable=!0,"binding"===c)return 257!==r.type?f({binding:"unexpected response"}):(l.binding="ok",l.mappedAddress=decodeAddress(r.attrs[32],!0)||decodeAddress(r.attrs[1],!1),s?(c="allocate-challenge",u=crypto.randomBytes(12),d.send(buildAllocateRequest(u,null))):f());if("allocate-challenge"===c){const e=decodeError(r.attrs[9]);return l.realm=r.attrs[20]?r.attrs[20].toString("utf8"):null,e&&(401===e.code||438===e.code)&&r.attrs[20]&&r.attrs[21]?(c="allocate-auth",u=crypto.randomBytes(12),d.send(buildAllocateRequest(u,{username:s,password:o,realm:r.attrs[20],nonce:r.attrs[21]}))):259===r.type?f({allocate:"ok (no auth required)",relayAddress:decodeAddress(r.attrs[22],!0)}):f({allocate:e?`error ${e.code} ${e.reason}`:"unexpected response"})}if("allocate-auth"===c){if(259===r.type)return f({allocate:"ok",relayAddress:decodeAddress(r.attrs[22],!0)});const e=decodeError(r.attrs[9]);return f({allocate:e?`error ${e.code} ${e.reason}`:"unexpected response"})}}}),d.connect(()=>d.send(buildBindingRequest(u)))})}async function runControl({port:e=null,timeoutMs:r=5e3}={}){const t=e?[...UDP_CONTROLS.filter(r=>r.port===e),...UDP_CONTROLS.filter(r=>r.port!==e)]:UDP_CONTROLS,s=[];for(const o of t){const t=await probe({host:o.host,port:o.port,transport:"udp",timeoutMs:r});if(s.push({target:`${o.host}:${o.port}`,ok:"ok"===t.binding,detail:t.error||t.binding}),"ok"===t.binding)return{valid:!0,samePort:e?o.port===e:null,via:`${o.host}:${o.port}`,port:o.port,mappedAddress:t.mappedAddress,attempts:s}}return{valid:!1,samePort:!1,via:null,port:null,attempts:s}}function openTurnSession({host:e,port:r,transport:t="udp",username:s,password:o,timeoutMs:n=5e3}){return new Promise(a=>{let l;try{l=createTransport(t,e,r,n)}catch(e){return a({ok:!1,error:e.code||e.message})}const i=new Map;let d=()=>{},c=null,u=null;l.onError(e=>{u=e.code||e.message,i.forEach(e=>e.done(null))}),l.onMessage(e=>{const r=parseMessage(e);if(!r)return;if(23===r.type)return d(decodeAddress(r.attrs[18],!0),r.attrs[19]||Buffer.alloc(0));const t=i.get(r.txId.toString("hex"));t&&t.done(r)});const p=(e,r=n)=>new Promise(s=>{if(u)return s(null);const o=crypto.randomBytes(12),n=o.toString("hex"),a=e(o),d=e=>{clearTimeout(c),clearInterval(p),i.delete(n),s(e)},c=setTimeout(()=>d(null),r),p="udp"===t?setInterval(()=>l.send(a),500):null;i.set(n,{done:d}),l.send(a)}),f=async(e,r)=>{let t=await p(t=>signedMessage(e,t,r,c));const s=t&&decodeError(t.attrs[9]);return s&&438===s.code&&t.attrs[21]&&(c={...c,nonce:t.attrs[21]},t=await p(t=>signedMessage(e,t,r,c))),t},m=e=>{if(!e)return u||"timeout (no response)";const r=decodeError(e.attrs[9]);return r?`error ${r.code} ${r.reason}`:"unexpected response"},h={relayAddress:null,onData:e=>{d=e},async createPermission(e){const r=await f(8,[buildAttr(18,encodeXorAddress(e,0))]);return r&&264===r.type?"ok":m(r)},send(e,r,t){l.send(unsignedMessage(22,crypto.randomBytes(12),[buildAttr(18,encodeXorAddress(e,r)),buildAttr(19,t)]))},async close(){if(c&&!u){const e=Buffer.alloc(4);await f(4,[buildAttr(13,e)]).catch(()=>{})}l.close()}};l.connect(async()=>{const e=await p(e=>buildAllocateRequest(e,null)),r=e&&decodeError(e.attrs[9]);if(!r||401!==r.code&&438!==r.code||!e.attrs[20]||!e.attrs[21])return l.close(),a({ok:!1,reachable:Boolean(e),error:m(e)});c={username:s,password:o,realm:e.attrs[20],nonce:e.attrs[21]};const t=await f(3,[buildAttr(25,Buffer.from([17,0,0,0]))]);if(!t||259!==t.type)return l.close(),a({ok:!1,reachable:!0,error:`allocate ${m(t)}`});h.relayAddress=decodeAddress(t.attrs[22],!0),a({ok:!0,session:h,relayAddress:h.relayAddress})})})}function splitAddress(e){const r=String(e).lastIndexOf(":");return{ip:e.slice(0,r),port:parseInt(e.slice(r+1),10)}}function isPrivateIp(e){return/^(10\.|127\.|192\.168\.|169\.254\.|172\.(1[6-9]|2\d|3[01])\.|100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\.)/.test(e)}async function relayProbe({a:e,z:r,timeoutMs:t=6e3}){const s={a:{server:`${e.host}:${e.port}/${e.transport}`,host:e.host},z:{server:`${r.host}:${r.port}/${r.transport}`,host:r.host},sameHost:e.host===r.host,aToZ:{delivered:!1,ms:null},zToA:{delivered:!1,ms:null},stage:"allocate"},[o,n]=await Promise.all([openTurnSession({...e,timeoutMs:t}),openTurnSession({...r,timeoutMs:t})]);s.a.allocate=o.ok?"ok":o.error,s.z.allocate=n.ok?"ok":n.error,s.a.reachable=o.ok||Boolean(o.reachable),s.z.reachable=n.ok||Boolean(n.reachable),s.a.relayAddress=o.relayAddress||null,s.z.relayAddress=n.relayAddress||null;const a=()=>Promise.all([o,n].filter(e=>e.ok).map(e=>e.session.close()));if(!o.ok||!n.ok)return await a(),s;const l=splitAddress(o.relayAddress),i=splitAddress(n.relayAddress);s.stage="permission";const[d,c]=await Promise.all([o.session.createPermission(i.ip),n.session.createPermission(l.ip)]);if(s.a.permission=d,s.z.permission=c,"ok"!==d||"ok"!==c)return await a(),s;s.stage="relay";const u=crypto.randomBytes(8).toString("hex"),p=Date.now();for(n.session.onData((e,r)=>{s.aToZ.delivered||r.toString()!==`a2z-${u}`||(s.aToZ={delivered:!0,ms:Date.now()-p,from:e})}),o.session.onData((e,r)=>{s.zToA.delivered||r.toString()!==`z2a-${u}`||(s.zToA={delivered:!0,ms:Date.now()-p,from:e})});Date.now()-p<t&&(!s.aToZ.delivered||!s.zToA.delivered);)s.aToZ.delivered||o.session.send(i.ip,i.port,Buffer.from(`a2z-${u}`)),s.zToA.delivered||n.session.send(l.ip,l.port,Buffer.from(`z2a-${u}`)),await new Promise(e=>setTimeout(e,250));return s.stage="done",await a(),s}function relayVerdict(e){for(const r of[e.a,e.z])if("ok"!==r.allocate)return/error 401|error 403/.test(r.allocate||"")?{status:"fail",reason:`${r.server}: credentials rejected (${r.allocate})`}:r.reachable?{status:"fail",reason:`${r.server}: ${r.allocate}`}:{status:"inconclusive",reason:`${r.server} did not answer from here (${r.allocate}) — this host may not reach it; try --transport tcp or another network`};for(const r of[e.a,e.z]){const e=splitAddress(r.relayAddress).ip;if(!isPrivateIp(r.host)&&isPrivateIp(e))return{status:"fail",reason:`${r.server} hands out a private relay address ${r.relayAddress} — coturn external-ip is not set`}}if(e.sameHost&&[e.a,e.z].some(e=>/error 403/.test(e.permission||"")))return{status:"fail",reason:`${e.a.server} refuses its own relay address as a peer (${"ok"!==e.a.permission?e.a.permission:e.z.permission}). With only this TURN host advertised, two relay-only participants can never connect. Re-run with --url listing a second TURN host to test the cross-box relay path`};for(const r of[e.a,e.z])if(r.permission&&"ok"!==r.permission)return{status:"fail",reason:`${r.server}: CreatePermission ${r.permission} (coturn denied-peer-ip / peer policy)`};const r=e.sameHost?" (same host: traffic may not cross the external firewall)":"";if(e.aToZ.delivered&&e.zToA.delivered)return{status:"pass",reason:`relayed both ways via ${e.a.relayAddress} <-> ${e.z.relayAddress}${r}`};if(!e.aToZ.delivered&&!e.zToA.delivered)return{status:"fail",reason:`allocate + permission OK on both, but relayed data was dropped in BOTH directions${r}. The datagrams travel server-to-server, so this host is not on that path: check the relay port range (min-port..max-port, default 49152-65535/udp) on both server firewalls, then coturn denied-peer-ip`};const[t,s]=e.aToZ.delivered?[e.z,e.a]:[e.a,e.z];return{status:"fail",reason:`one-way: ${t.relayAddress} -> ${s.relayAddress} dropped while the reverse works — inbound relay ports on ${s.host} (or egress from ${t.host}) are filtered${r}`}}function parseIceUrl(e){const r=/^(stun|stuns|turn|turns):([^:?]+)(?::(\d+))?(?:\?transport=(\w+))?$/i.exec(String(e).trim());if(!r)return null;const t=r[1].toLowerCase(),s="turns"===t||"stuns"===t;let o=(r[4]||"").toLowerCase();return o||(o="udp"),s&&(o="tls"),{scheme:t,host:r[2],port:parseInt(r[3],10)||(s?5349:3478),transport:o,needsAuth:"turn"===t||"turns"===t,raw:e}}function verdict(e,r){const t=Boolean(e.username||e.needsAuth);return"ok"!==e.binding||t&&"ok"!==e.allocate?e.reachable&&e.allocate&&/error 401|error 403/.test(e.allocate)?{status:"fail",reason:`credentials rejected (${e.allocate})`}:e.reachable&&e.allocate?{status:"warn",reason:`allocate: ${e.allocate}`}:e.reachable?{status:"warn",reason:e.binding||e.error||"unexpected response"}:e.tlsCert?{status:"fail",reason:`TLS answered by a non-STUN service (cert CN=${e.tlsCert.subject||"?"}) — ingress is holding this port`}:/ECONNREFUSED/.test(e.error||"")?{status:"fail",reason:"connection refused — nothing listening on this port"}:"udp"!==e.transport?{status:"inconclusive",reason:`no response over ${e.transport} and there is no public STUN-over-${e.transport} control to prove this host can do it at all`}:r&&r.valid?!1===r.samePort?{status:"inconclusive",reason:`control only passed on udp/${r.port}, not udp/${e.port} — this port may be filtered locally`}:{status:"fail",reason:`no response, yet udp/${e.port} works to ${r.via}`}:{status:"inconclusive",reason:"no public STUN server is reachable from here — this host is blind, not the target"}:{status:"pass",reason:t?"binding + authenticated allocate":"binding ok"}}module.exports={probe:probe,runControl:runControl,verdict:verdict,parseIceUrl:parseIceUrl,relayProbe:relayProbe,relayVerdict:relayVerdict,openTurnSession:openTurnSession,UDP_CONTROLS:UDP_CONTROLS};
|
|
1
|
+
"use strict";const dgram=require("dgram"),net=require("net"),tls=require("tls"),crypto=require("crypto"),MAGIC_COOKIE=554869826,BINDING_REQUEST=1,BINDING_SUCCESS=257,ALLOCATE_REQUEST=3,ALLOCATE_SUCCESS=259,REFRESH_REQUEST=4,CREATE_PERMISSION_REQUEST=8,CREATE_PERMISSION_SUCCESS=264,SEND_INDICATION=22,DATA_INDICATION=23,ATTR_MAPPED_ADDRESS=1,ATTR_USERNAME=6,ATTR_MESSAGE_INTEGRITY=8,ATTR_ERROR_CODE=9,ATTR_LIFETIME=13,ATTR_XOR_PEER_ADDRESS=18,ATTR_DATA=19,ATTR_REALM=20,ATTR_NONCE=21,ATTR_XOR_RELAYED_ADDRESS=22,ATTR_REQUESTED_TRANSPORT=25,ATTR_XOR_MAPPED_ADDRESS=32,UDP_CONTROLS=[{host:"stun.cloudflare.com",port:3478},{host:"stun.nextcloud.com",port:3478},{host:"stun.sipgate.net",port:3478},{host:"stun.l.google.com",port:19302},{host:"stun1.l.google.com",port:19302}];function buildHeader(e,t,r){const s=Buffer.alloc(20);return s.writeUInt16BE(e,0),s.writeUInt16BE(t,2),s.writeUInt32BE(554869826,4),r.copy(s,8),s}function buildAttr(e,t){const r=(4-t.length%4)%4,s=Buffer.alloc(4+t.length+r);return s.writeUInt16BE(e,0),s.writeUInt16BE(t.length,2),t.copy(s,4),s}function buildBindingRequest(e){return buildHeader(1,0,e)}function signedMessage(e,t,r,s){const o=Buffer.concat([...r,buildAttr(6,Buffer.from(s.username,"utf8")),buildAttr(20,s.realm),buildAttr(21,s.nonce)]),n=crypto.createHash("md5").update(`${s.username}:${s.realm.toString("utf8")}:${s.password}`).digest(),a=buildHeader(e,o.length+24,t),i=crypto.createHmac("sha1",n).update(Buffer.concat([a,o])).digest(),l=Buffer.concat([o,buildAttr(8,i)]);return Buffer.concat([buildHeader(e,l.length,t),l])}function unsignedMessage(e,t,r){const s=Buffer.concat(r);return Buffer.concat([buildHeader(e,s.length,t),s])}function buildAllocateRequest(e,t){const r=[buildAttr(25,Buffer.from([17,0,0,0]))];return t?signedMessage(3,e,r,t):unsignedMessage(3,e,r)}function encodeXorAddress(e,t){const r=Buffer.alloc(8);r.writeUInt8(1,1),r.writeUInt16BE(8466^t,2);const s=Buffer.alloc(4);return s.writeUInt32BE(554869826,0),e.split(".").forEach((e,t)=>r.writeUInt8(parseInt(e,10)^s[t],4+t)),r}function parseMessage(e){if(e.length<20)return null;const t={type:e.readUInt16BE(0),txId:e.slice(8,20),attrs:{}},r=Math.min(20+e.readUInt16BE(2),e.length);let s=20;for(;s+4<=r;){const r=e.readUInt16BE(s),o=e.readUInt16BE(s+2);if(s+4+o>e.length)break;t.attrs[r]=e.slice(s+4,s+4+o),s+=4+o+(4-o%4)%4}return t}function decodeAddress(e,t){if(!e||e.length<8)return null;if(1!==e.readUInt8(1))return"(ipv6)";const r=t?8466^e.readUInt16BE(2):e.readUInt16BE(2),s=e.slice(4,8);let o;if(t){const e=Buffer.alloc(4);e.writeUInt32BE(554869826,0),o=Array.from(s).map((t,r)=>t^e[r]).join(".")}else o=Array.from(s).join(".");return`${o}:${r}`}function decodeError(e){return!e||e.length<4?null:{code:100*e.readUInt8(2)+e.readUInt8(3),reason:e.slice(4).toString("utf8")}}function createTransport(e,t,r,s){if("udp"===e){const e=dgram.createSocket("udp4");return{send:s=>e.send(s,r,t),onMessage:t=>e.on("message",t),onError:t=>e.on("error",t),close:()=>{try{e.close()}catch{}},connect:e=>e()}}const o=0!==net.isIP(t),n="tls"===e?tls.connect({host:t,port:r,...o?{}:{servername:t},rejectUnauthorized:!1}):new net.Socket;let a=Buffer.alloc(0),i=()=>{};return n.setTimeout(s),n.on("data",e=>{for(a=Buffer.concat([a,e]);a.length>=20;){const e=20+a.readUInt16BE(2);if(a.length<e)return;const t=a.slice(0,e);a=a.slice(e),i(t)}}),{send:e=>n.write(e),onMessage:e=>{i=e},onError:e=>{n.on("error",t=>e(t)),n.on("timeout",()=>e(Object.assign(new Error("timeout"),{code:"ETIMEDOUT"})))},close:()=>{try{n.destroy()}catch{}},connect:s=>{"tls"===e?n.on("secureConnect",s):n.connect(r,t,s)},peerCert:()=>{if("tls"!==e)return null;try{const e=n.getPeerCertificate();return e&&e.subject?{subject:e.subject.CN||null,issuer:e.issuer&&e.issuer.CN,validTo:e.valid_to}:null}catch{return null}}}}function probe({host:e,port:t,transport:r="udp",username:s,password:o,timeoutMs:n=5e3}){return new Promise(a=>{const i={host:e,port:t,transport:r,reachable:!1,binding:null,mappedAddress:null,realm:null,allocate:null,relayAddress:null,tlsCert:null,error:null,ms:null},l=Date.now();let c,d="binding",u=crypto.randomBytes(12),p=!1;try{c=createTransport(r,e,t,n)}catch(e){return i.ms=Date.now()-l,i.error=e.code||e.message,a(i)}const f=e=>{p||(p=!0,clearTimeout(h),c.peerCert&&(i.tlsCert=c.peerCert()),c.close(),i.ms=Date.now()-l,a(Object.assign(i,e||{})))},h=setTimeout(()=>f({error:i.reachable?`timeout during ${d}`:"timeout (no response)"}),n);c.onError(e=>f({error:e.code||e.message})),c.onMessage(e=>{const t=parseMessage(e);if(t){if(i.reachable=!0,"binding"===d)return 257!==t.type?f({binding:"unexpected response"}):(i.binding="ok",i.mappedAddress=decodeAddress(t.attrs[32],!0)||decodeAddress(t.attrs[1],!1),s?(d="allocate-challenge",u=crypto.randomBytes(12),c.send(buildAllocateRequest(u,null))):f());if("allocate-challenge"===d){const e=decodeError(t.attrs[9]);return i.realm=t.attrs[20]?t.attrs[20].toString("utf8"):null,e&&(401===e.code||438===e.code)&&t.attrs[20]&&t.attrs[21]?(d="allocate-auth",u=crypto.randomBytes(12),c.send(buildAllocateRequest(u,{username:s,password:o,realm:t.attrs[20],nonce:t.attrs[21]}))):259===t.type?f({allocate:"ok (no auth required)",relayAddress:decodeAddress(t.attrs[22],!0)}):f({allocate:e?`error ${e.code} ${e.reason}`:"unexpected response"})}if("allocate-auth"===d){if(259===t.type)return f({allocate:"ok",relayAddress:decodeAddress(t.attrs[22],!0)});const e=decodeError(t.attrs[9]);return f({allocate:e?`error ${e.code} ${e.reason}`:"unexpected response"})}}}),c.connect(()=>c.send(buildBindingRequest(u)))})}async function runControl({port:e=null,timeoutMs:t=5e3}={}){const r=e?[...UDP_CONTROLS.filter(t=>t.port===e),...UDP_CONTROLS.filter(t=>t.port!==e)]:UDP_CONTROLS,s=[];for(const o of r){const r=await probe({host:o.host,port:o.port,transport:"udp",timeoutMs:t});if(s.push({target:`${o.host}:${o.port}`,ok:"ok"===r.binding,detail:r.error||r.binding}),"ok"===r.binding)return{valid:!0,samePort:e?o.port===e:null,via:`${o.host}:${o.port}`,port:o.port,mappedAddress:r.mappedAddress,attempts:s}}return{valid:!1,samePort:!1,via:null,port:null,attempts:s}}function openTurnSession({host:e,port:t,transport:r="udp",username:s,password:o,timeoutMs:n=5e3}){return new Promise(a=>{let i;try{i=createTransport(r,e,t,n)}catch(e){return a({ok:!1,error:e.code||e.message})}const l=new Map;let c=()=>{},d=null,u=null;i.onError(e=>{u=e.code||e.message,l.forEach(e=>e.done(null))}),i.onMessage(e=>{const t=parseMessage(e);if(!t)return;if(23===t.type)return c(decodeAddress(t.attrs[18],!0),t.attrs[19]||Buffer.alloc(0));const r=l.get(t.txId.toString("hex"));r&&r.done(t)});const p=(e,t=n)=>new Promise(s=>{if(u)return s(null);const o=crypto.randomBytes(12),n=o.toString("hex"),a=e(o),c=e=>{clearTimeout(d),clearInterval(p),l.delete(n),s(e)},d=setTimeout(()=>c(null),t),p="udp"===r?setInterval(()=>i.send(a),500):null;l.set(n,{done:c}),i.send(a)}),f=async(e,t)=>{let r=await p(r=>signedMessage(e,r,t,d));const s=r&&decodeError(r.attrs[9]);return s&&438===s.code&&r.attrs[21]&&(d={...d,nonce:r.attrs[21]},r=await p(r=>signedMessage(e,r,t,d))),r},h=e=>{if(!e)return u||"timeout (no response)";const t=decodeError(e.attrs[9]);return t?`error ${t.code} ${t.reason}`:"unexpected response"},m={relayAddress:null,onData:e=>{c=e},async createPermission(e){const t=await f(8,[buildAttr(18,encodeXorAddress(e,0))]);return t&&264===t.type?"ok":h(t)},send(e,t,r){i.send(unsignedMessage(22,crypto.randomBytes(12),[buildAttr(18,encodeXorAddress(e,t)),buildAttr(19,r)]))},async close(){if(d&&!u){const e=Buffer.alloc(4);await f(4,[buildAttr(13,e)]).catch(()=>{})}i.close()}};i.connect(async()=>{const e=await p(e=>buildAllocateRequest(e,null)),t=e&&decodeError(e.attrs[9]);if(!t||401!==t.code&&438!==t.code||!e.attrs[20]||!e.attrs[21])return i.close(),a({ok:!1,reachable:Boolean(e),error:h(e)});d={username:s,password:o,realm:e.attrs[20],nonce:e.attrs[21]};const r=await f(3,[buildAttr(25,Buffer.from([17,0,0,0]))]);if(!r||259!==r.type)return i.close(),a({ok:!1,reachable:!0,error:`allocate ${h(r)}`});m.relayAddress=decodeAddress(r.attrs[22],!0),a({ok:!0,session:m,relayAddress:m.relayAddress})})})}function splitAddress(e){const t=String(e).lastIndexOf(":");return{ip:e.slice(0,t),port:parseInt(e.slice(t+1),10)}}function isPrivateIp(e){return/^(10\.|127\.|192\.168\.|169\.254\.|172\.(1[6-9]|2\d|3[01])\.|100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\.)/.test(e)}async function relayProbe({a:e,z:t,timeoutMs:r=6e3}){const s={a:{server:`${e.host}:${e.port}/${e.transport}`,host:e.host},z:{server:`${t.host}:${t.port}/${t.transport}`,host:t.host},sameHost:e.host===t.host,aToZ:{delivered:!1,ms:null},zToA:{delivered:!1,ms:null},stage:"allocate"},[o,n]=await Promise.all([openTurnSession({...e,timeoutMs:r}),openTurnSession({...t,timeoutMs:r})]);s.a.allocate=o.ok?"ok":o.error,s.z.allocate=n.ok?"ok":n.error,s.a.reachable=o.ok||Boolean(o.reachable),s.z.reachable=n.ok||Boolean(n.reachable),s.a.relayAddress=o.relayAddress||null,s.z.relayAddress=n.relayAddress||null;const a=()=>Promise.all([o,n].filter(e=>e.ok).map(e=>e.session.close()));if(!o.ok||!n.ok)return await a(),s;const i=splitAddress(o.relayAddress),l=splitAddress(n.relayAddress);s.stage="permission";const[c,d]=await Promise.all([o.session.createPermission(l.ip),n.session.createPermission(i.ip)]);if(s.a.permission=c,s.z.permission=d,"ok"!==c||"ok"!==d)return await a(),s;s.stage="relay";const u=crypto.randomBytes(8).toString("hex"),p=Date.now();for(n.session.onData((e,t)=>{s.aToZ.delivered||t.toString()!==`a2z-${u}`||(s.aToZ={delivered:!0,ms:Date.now()-p,from:e})}),o.session.onData((e,t)=>{s.zToA.delivered||t.toString()!==`z2a-${u}`||(s.zToA={delivered:!0,ms:Date.now()-p,from:e})});Date.now()-p<r&&(!s.aToZ.delivered||!s.zToA.delivered);)s.aToZ.delivered||o.session.send(l.ip,l.port,Buffer.from(`a2z-${u}`)),s.zToA.delivered||n.session.send(i.ip,i.port,Buffer.from(`z2a-${u}`)),await new Promise(e=>setTimeout(e,250));return s.stage="done",await a(),s}function relayVerdict(e){for(const t of[e.a,e.z])if("ok"!==t.allocate)return/error 401|error 403/.test(t.allocate||"")?{status:"fail",reason:`${t.server}: credentials rejected (${t.allocate})`}:t.reachable?{status:"fail",reason:`${t.server}: ${t.allocate}`}:{status:"inconclusive",reason:`${t.server} did not answer from here (${t.allocate}) — this host may not reach it; try --transport tcp or another network`};for(const t of[e.a,e.z]){const e=splitAddress(t.relayAddress).ip;if(!isPrivateIp(t.host)&&isPrivateIp(e))return{status:"fail",reason:`${t.server} hands out a private relay address ${t.relayAddress} — coturn external-ip is not set`}}if(e.sameHost&&[e.a,e.z].some(e=>/error 403/.test(e.permission||"")))return{status:"fail",reason:`${e.a.server} refuses its own relay address as a peer (${"ok"!==e.a.permission?e.a.permission:e.z.permission}). With only this TURN host advertised, two relay-only participants can never connect. Re-run with --url listing a second TURN host to test the cross-box relay path`};for(const t of[e.a,e.z])if(t.permission&&"ok"!==t.permission)return{status:"fail",reason:`${t.server}: CreatePermission ${t.permission} (coturn denied-peer-ip / peer policy)`};const t=e.sameHost?" (same host: traffic may not cross the external firewall)":"";if(e.aToZ.delivered&&e.zToA.delivered)return{status:"pass",reason:`relayed both ways via ${e.a.relayAddress} <-> ${e.z.relayAddress}${t}`};if(!e.aToZ.delivered&&!e.zToA.delivered)return{status:"fail",reason:`allocate + permission OK on both, but relayed data was dropped in BOTH directions${t}. The datagrams travel server-to-server, so this host is not on that path: check the relay port range (min-port..max-port, default 49152-65535/udp) on both server firewalls, then coturn denied-peer-ip`};const[r,s]=e.aToZ.delivered?[e.z,e.a]:[e.a,e.z];return{status:"fail",reason:`one-way: ${r.relayAddress} -> ${s.relayAddress} dropped while the reverse works — inbound relay ports on ${s.host} (or egress from ${r.host}) are filtered${t}`}}function flattenIceServers(e){const t=[];return(e||[]).forEach(e=>{(Array.isArray(e.urls)?e.urls:[e.urls]).filter(Boolean).forEach(r=>t.push({url:r,username:e.username,credential:e.credential}))}),t}function relayTargets(e,{transport:t,hosts:r=[]}={}){const s=e.map(e=>({...e,ice:parseIceUrl(e.url)})).filter(e=>e.ice),o=s.filter(e=>e.ice.needsAuth&&"tls"!==e.ice.transport&&e.username&&e.credential),n=new Map;o.forEach(e=>n.set(e.ice.host,[...n.get(e.ice.host)||[],e]));const a=new Set(s.map(e=>e.ice.host));return o.length&&r.filter(e=>a.has(e)&&!n.has(e)).forEach(e=>{n.set(e,o.map(t=>({...t,ice:{...t.ice,host:e}})))}),[...n.values()].map(e=>{const r=e.find(e=>e.ice.transport===(t||"tcp"))||(t?null:e[0]);return r&&{host:r.ice.host,port:r.ice.port,transport:r.ice.transport,username:r.username,password:r.credential}}).filter(Boolean)}async function relayCheck(e,{timeoutMs:t=6e3}={}){const r=[];for(let t=0;t<e.length;t++)for(let s=t+1;s<e.length;s++)r.push([e[t],e[s]]);1===e.length&&r.push([e[0],e[0]]);const s=[];for(const[e,o]of r){const r=await relayProbe({a:e,z:o,timeoutMs:t});s.push({...r,...relayVerdict(r)})}const o=["fail","inconclusive","pass"].find(e=>s.some(t=>t.status===e))||"inconclusive";return{results:s,verdict:o}}async function iceCheck({baseUrl:e,mode:t="relay",transport:r,hosts:s=[],timeoutMs:o=6e3}){const n=await fetch(`${String(e).replace(/\/$/,"")}/stt/config`);if(!n.ok)throw new Error(`GET /stt/config -> ${n.status}`);const a=flattenIceServers((await n.json()).iceServers),i=`${e}/stt/config`;if("relay"===t){const e=relayTargets(a,{transport:r,hosts:s});return e.length?{source:i,mode:t,...await relayCheck(e,{timeoutMs:o})}:{source:i,mode:t,verdict:"inconclusive",reason:"no TURN servers with credentials advertised",results:[]}}const l=a.map(e=>({...e,ice:parseIceUrl(e.url)})).filter(e=>e.ice),c={};for(const e of new Set(l.filter(e=>"udp"===e.ice.transport).map(e=>e.ice.port)))c[e]=await runControl({port:e,timeoutMs:o});const d=[];for(const e of l){const t=e.ice.needsAuth,r=await probe({host:e.ice.host,port:e.ice.port,transport:e.ice.transport,username:t?e.username:void 0,password:t?e.credential:void 0,timeoutMs:o});d.push({url:e.url,...verdict({...r,needsAuth:t},c[e.ice.port]||null),probe:r})}const u=["fail","inconclusive","warn","pass"].find(e=>d.some(t=>t.status===e))||"inconclusive";return{source:i,mode:t,controls:c,results:d,verdict:u}}function parseIceUrl(e){const t=/^(stun|stuns|turn|turns):([^:?]+)(?::(\d+))?(?:\?transport=(\w+))?$/i.exec(String(e).trim());if(!t)return null;const r=t[1].toLowerCase(),s="turns"===r||"stuns"===r;let o=(t[4]||"").toLowerCase();return o||(o="udp"),s&&(o="tls"),{scheme:r,host:t[2],port:parseInt(t[3],10)||(s?5349:3478),transport:o,needsAuth:"turn"===r||"turns"===r,raw:e}}function verdict(e,t){const r=Boolean(e.username||e.needsAuth);return"ok"!==e.binding||r&&"ok"!==e.allocate?e.reachable&&e.allocate&&/error 401|error 403/.test(e.allocate)?{status:"fail",reason:`credentials rejected (${e.allocate})`}:e.reachable&&e.allocate?{status:"warn",reason:`allocate: ${e.allocate}`}:e.reachable?{status:"warn",reason:e.binding||e.error||"unexpected response"}:e.tlsCert?{status:"fail",reason:`TLS answered by a non-STUN service (cert CN=${e.tlsCert.subject||"?"}) — ingress is holding this port`}:/ECONNREFUSED/.test(e.error||"")?{status:"fail",reason:"connection refused — nothing listening on this port"}:"udp"!==e.transport?{status:"inconclusive",reason:`no response over ${e.transport} and there is no public STUN-over-${e.transport} control to prove this host can do it at all`}:t&&t.valid?!1===t.samePort?{status:"inconclusive",reason:`control only passed on udp/${t.port}, not udp/${e.port} — this port may be filtered locally`}:{status:"fail",reason:`no response, yet udp/${e.port} works to ${t.via}`}:{status:"inconclusive",reason:"no public STUN server is reachable from here — this host is blind, not the target"}:{status:"pass",reason:r?"binding + authenticated allocate":"binding ok"}}module.exports={probe:probe,runControl:runControl,verdict:verdict,parseIceUrl:parseIceUrl,relayProbe:relayProbe,relayVerdict:relayVerdict,openTurnSession:openTurnSession,flattenIceServers:flattenIceServers,relayTargets:relayTargets,relayCheck:relayCheck,iceCheck:iceCheck,UDP_CONTROLS:UDP_CONTROLS};
|
package/dist/mcp-tools.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const TOOLS=[{name:"appoiint_status",description:"Get Appoiint platform status and version.",inputSchema:{type:"object",properties:{}},handler:async e=>e.status(),readOnly:!0},{name:"appoiint_create_room",description:"Create a video meeting room. Returns roomId and join URL.",inputSchema:{type:"object",properties:{title:{type:"string",description:'Meeting title (e.g. "Team Standup")'}},required:["title"]},handler:async(e,t)=>{const i=require("./config").loadConfig([]);return e.createRoom({title:t.title,hostId:i.userId})},readOnly:!1},{name:"appoiint_list_rooms",description:"List active meeting rooms.",inputSchema:{type:"object",properties:{}},handler:async e=>e.listRooms(),readOnly:!0},{name:"appoiint_room_status",description:"Get room status and participants.",inputSchema:{type:"object",properties:{roomId:{type:"string",description:"Room ID"}},required:["roomId"]},handler:async(e,t)=>e.roomStatus(t.roomId),readOnly:!0},{name:"appoiint_end_room",description:"End a meeting room.",inputSchema:{type:"object",properties:{roomId:{type:"string",description:"Room ID"}},required:["roomId"]},handler:async(e,t)=>e.endRoom(t.roomId),readOnly:!1},{name:"appoiint_invite",description:"Invite a human or agent to a meeting room.",inputSchema:{type:"object",properties:{roomId:{type:"string",description:"Room ID"},email:{type:"string",description:"Invitee email"},name:{type:"string",description:"Invitee name"},type:{type:"string",description:"human or agent (default: human)"}},required:["roomId","email"]},handler:async(e,t)=>e.invite(t.roomId,{email:t.email,name:t.name||"Guest",type:t.type||"human"}),readOnly:!1},{name:"appoiint_transcript",description:"Get meeting transcript.",inputSchema:{type:"object",properties:{roomId:{type:"string",description:"Room ID"},format:{type:"string",description:"json, text, or srt (default: json)"}},required:["roomId"]},handler:async(e,t)=>e.transcript(t.roomId,t.format||"json"),readOnly:!0},{name:"appoiint_join_meeting",description:"Join a meeting room as an AI agent. Creates persistent Socket.IO connection. You will hear transcripts and can send chat messages. Call appoiint_get_transcripts to read what was said, appoiint_send_chat to respond.",inputSchema:{type:"object",properties:{roomId:{type:"string",description:"Room ID to join"},agentName:{type:"string",description:"Your display name in the meeting (default: Claude Code)"},agentRole:{type:"string",description:"Your role badge (default: assistant)"}},required:["roomId"]},handler:async(e,t,i)=>{const{MeetingSession:n}=require("./meetingSession"),a=new n({baseUrl:i.config.baseUrl,roomId:t.roomId,agentName:t.agentName||"Claude Code",agentRole:t.agentRole||"assistant"}),r=await a.join();return i.meetingSessions.set(t.roomId,a),r},readOnly:!1},{name:"appoiint_get_transcripts",description:"Get speech transcripts from the meeting. Returns what participants have said since last read. Buffer is cleared after reading.",inputSchema:{type:"object",properties:{roomId:{type:"string",description:"Room ID"}},required:["roomId"]},handler:async(e,t,i)=>{const n=i.meetingSessions.get(t.roomId);if(!n)return{error:"Not in this meeting. Call appoiint_join_meeting first."};const a=n.getTranscripts();return{roomId:t.roomId,count:a.length,transcripts:a}},readOnly:!0},{name:"appoiint_send_chat",description:"Send a chat message to all meeting participants. Your message appears in the AI chat panel with your role badge.",inputSchema:{type:"object",properties:{roomId:{type:"string",description:"Room ID"},message:{type:"string",description:"Message to send to meeting participants"}},required:["roomId","message"]},handler:async(e,t,i)=>{const n=i.meetingSessions.get(t.roomId);return n?n.sendChat(t.message):{error:"Not in this meeting. Call appoiint_join_meeting first."}},readOnly:!1},{name:"appoiint_meeting_status",description:"Check meeting connection status: connected, peer count, buffered transcript count.",inputSchema:{type:"object",properties:{roomId:{type:"string",description:"Room ID"}},required:["roomId"]},handler:async(e,t,i)=>{const n=i.meetingSessions.get(t.roomId);return n?n.status():{error:"Not in this meeting.",connected:!1}},readOnly:!0},{name:"appoiint_leave_meeting",description:"Leave a meeting room and disconnect.",inputSchema:{type:"object",properties:{roomId:{type:"string",description:"Room ID"}},required:["roomId"]},handler:async(e,t,i)=>{const n=i.meetingSessions.get(t.roomId);if(!n)return{error:"Not in this meeting."};const a=n.leave();return i.meetingSessions.delete(t.roomId),a},readOnly:!1},{name:"appoiint_speak",description:"Speak in a meeting room with an agent voice: synthesizes text via TTS and broadcasts the audio to all participants in one call. Requires an active appoiint_join_meeting session. Text is trimmed to 500 chars server-side; audio over 500KB is dropped by the relay.",inputSchema:{type:"object",properties:{roomId:{type:"string",description:"Room ID (must have joined via appoiint_join_meeting)"},text:{type:"string",description:"Text to speak (max 500 chars)"},voice:{type:"string",description:"Voice: cfo, cto, cmo, cco, cso, clo, heart, michael, nova, echo (default: heart)"}},required:["roomId","text"]},handler:async(e,t,i)=>{const n=i.meetingSessions.get(t.roomId);if(!n)return{error:"Not in this meeting. Call appoiint_join_meeting first."};const a=await e.synthesize({text:t.text,voice:t.voice||"heart",agentRole:t.voice});if(!a||!a.audio)return{error:"TTS synthesis failed",detail:a};const r=n.sendAudio({audio:a.audio,format:a.format});return{spoken:!0,roomId:t.roomId,voice:a.voice,provider:a.provider,bytes:r.bytes}},readOnly:!1},{name:"appoiint_summary_status",description:"Check whether the AI meeting-summary service is available, and which model backs it. Call this before summarizing if you want to fail fast.",inputSchema:{type:"object",properties:{}},handler:async e=>e.summaryStatus(),readOnly:!0},{name:"appoiint_summarize_notes",description:"Summarize a meeting transcript into an overview, key topics and action items. Pass either roomId (uses the live transcript buffer from appoiint_join_meeting, without clearing it) or an explicit notes array. Returns overview, keyTopics[{topic,summary,timestamp,speakers}] and actionItems[{task,assignee,deadline,priority,context}]. Nothing is saved — use appoiint_summarize_meeting to persist against a booking.",inputSchema:{type:"object",properties:{roomId:{type:"string",description:"Room ID of an active appoiint_join_meeting session — its transcripts are summarized"},notes:{type:"array",description:"Transcript entries: [{speaker, text, timestamp}] or plain strings",items:{type:"object"}},metadata:{type:"object",description:"Optional meeting context: {title, participants, duration}"}}},handler:async(e,t,i)=>{const n=_notesFrom(t,i);return n.length?e.summarizeNotes({notes:n,metadata:t.metadata||{}}):{error:"No transcript to summarize. Pass notes[], or a roomId with buffered transcripts."}},readOnly:!0},{name:"appoiint_ask_transcript",description:'Ask a natural-language question about a meeting transcript ("what did we decide about pricing?", "who owns the migration?"). Pass either roomId (live transcript buffer) or an explicit notes array, plus the question.',inputSchema:{type:"object",properties:{question:{type:"string",description:"Question to answer from the transcript"},roomId:{type:"string",description:"Room ID of an active appoiint_join_meeting session"},notes:{type:"array",description:"Transcript entries: [{speaker, text, timestamp}] or plain strings",items:{type:"object"}},metadata:{type:"object",description:"Optional meeting context"}},required:["question"]},handler:async(e,t,i)=>{const n=_notesFrom(t,i);return n.length?e.askTranscript({notes:n,question:t.question,metadata:t.metadata||{}}):{error:"No transcript to ask about. Pass notes[], or a roomId with buffered transcripts."}},readOnly:!0},{name:"appoiint_summarize_meeting",description:"Generate an AI summary from a meeting's stored notes and SAVE it against that meeting. Use the booking/meeting id. Fails if the meeting has no transcript entries yet.",inputSchema:{type:"object",properties:{meetingId:{type:"string",description:"Meeting/booking ID"}},required:["meetingId"]},handler:async(e,t)=>e.summarizeMeeting(t.meetingId),readOnly:!1},{name:"appoiint_get_summary",description:"Get the saved AI summary for a meeting (404 if one has not been generated — call appoiint_summarize_meeting first).",inputSchema:{type:"object",properties:{meetingId:{type:"string",description:"Meeting/booking ID"}},required:["meetingId"]},handler:async(e,t)=>e.getSummary(t.meetingId),readOnly:!0},{name:"appoiint_delete_summary",description:"Delete the saved AI summary for a meeting. The transcript itself is kept.",inputSchema:{type:"object",properties:{meetingId:{type:"string",description:"Meeting/booking ID"}},required:["meetingId"]},handler:async(e,t)=>e.deleteSummary(t.meetingId),readOnly:!1},{name:"appoiint_meeting_transcript",description:"Get the stored transcript for a meeting: timestamped segments plus the speaker list.",inputSchema:{type:"object",properties:{meetingId:{type:"string",description:"Meeting/booking ID"},limit:{type:"number",description:"Max segments to return (default 50)"}},required:["meetingId"]},handler:async(e,t)=>e.meetingTranscript(t.meetingId,t.limit),readOnly:!0},{name:"appoiint_meeting_action_items",description:"List the action items extracted from a meeting.",inputSchema:{type:"object",properties:{meetingId:{type:"string",description:"Meeting/booking ID"}},required:["meetingId"]},handler:async(e,t)=>e.meetingActionItems(t.meetingId),readOnly:!0},{name:"appoiint_meeting_highlights",description:"List highlighted moments in a meeting transcript.",inputSchema:{type:"object",properties:{meetingId:{type:"string",description:"Meeting/booking ID"}},required:["meetingId"]},handler:async(e,t)=>e.meetingHighlights(t.meetingId),readOnly:!0},{name:"appoiint_meeting_notes_status",description:"Meeting-notes overview for a meeting: segment/speaker/highlight/comment counts and duration. Cheap way to check whether a meeting has anything worth summarizing.",inputSchema:{type:"object",properties:{meetingId:{type:"string",description:"Meeting/booking ID"}},required:["meetingId"]},handler:async(e,t)=>e.meetingNotesStatus(t.meetingId),readOnly:!0},{name:"appoiint_search_meeting",description:"Full-text search across a meeting's transcript segments, highlights and comments.",inputSchema:{type:"object",properties:{meetingId:{type:"string",description:"Meeting/booking ID"},query:{type:"string",description:"Search term"}},required:["meetingId","query"]},handler:async(e,t)=>e.meetingSearch(t.meetingId,t.query),readOnly:!0},{name:"appoiint_export_meeting_notes",description:"Export a meeting's notes as md (default), txt or json — ready to paste into a doc or email.",inputSchema:{type:"object",properties:{meetingId:{type:"string",description:"Meeting/booking ID"},format:{type:"string",description:"md | txt | json (default md)"}},required:["meetingId"]},handler:async(e,t)=>{const i=(t.format||"md").toLowerCase();if(!["md","txt","json"].includes(i))throw new Error("format must be one of: md, txt, json");return e.meetingExport(t.meetingId,i)},readOnly:!0},{name:"appoiint_list_events",description:"List bookable event types.",inputSchema:{type:"object",properties:{}},handler:async e=>e.events(),readOnly:!0},{name:"appoiint_create_event",description:"Create a bookable event type for the authenticated host.",inputSchema:{type:"object",properties:{name:{type:"string",description:"Event type name/title"},duration:{type:"number",description:"Duration in minutes (default: 30)"},slug:{type:"string",description:"URL slug (default: derived from name)"},description:{type:"string",description:"Short description"},timezone:{type:"string",description:"Availability timezone (default: Europe/London)"},price:{type:"number",description:"Price (default: 0 = free)"},currency:{type:"string",description:"Currency (default: USD)"}},required:["name"]},handler:async(e,t,i)=>{const n=t.slug||String(t.name).toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,""),a=Number(t.price)||0;return e.createEvent({userId:i.config.userId,name:t.name,slug:n,duration:t.duration||30,description:t.description||"",availability:{type:"inherit",timezone:t.timezone||"Europe/London"},pricing:{enabled:a>0,amount:a,currency:t.currency||"USD"},meetingSettings:{location:{type:"video"},autoCreateMeeting:!0}})},readOnly:!1},{name:"appoiint_delete_event",description:"Delete an event type. SAFETY: refuses to hard-delete when bookings are attached (would orphan meetings) unless force=true; use soft=true to deactivate while preserving bookings.",inputSchema:{type:"object",properties:{eventTypeId:{type:"string",description:"Event type id (from appoiint_list_events)"},soft:{type:"boolean",description:"Deactivate instead of hard delete (preserves bookings)"},force:{type:"boolean",description:"Hard delete even if bookings are attached"}},required:["eventTypeId"]},handler:async(e,t)=>{const i=t.eventTypeId;let n=null;try{const t=await e.events();n=(Array.isArray(t)?t:t.eventTypes||t.data&&(Array.isArray(t.data)?t.data:t.data.eventTypes)||[]).find(e=>e.id===i||e._id===i||e.eventTypeId===i||e.slug===i)}catch{}const a=n&&Number.isFinite(n.bookingCount)?n.bookingCount:null,r=n&&(n.name||n.slug)||i;if(!t.soft&&!t.force&&a>0)throw new Error(`Refusing to delete "${r}" — it has ${a} booking(s) attached and they would be orphaned. Re-run with soft:true (deactivate, keep bookings) or force:true (hard delete anyway).`);const o=await e.deleteEvent(i,!!t.soft);return{deleted:!t.soft,deactivated:!!t.soft,eventType:r,bookingsAffected:a||0,result:o}},readOnly:!1},{name:"appoiint_check_availability",description:"Check available time slots for a host on a given date.",inputSchema:{type:"object",properties:{email:{type:"string",description:"Host email address"},date:{type:"string",description:"Date (YYYY-MM-DD)"},timezone:{type:"string",description:"Timezone (default: UTC)"}},required:["email","date"]},handler:async(e,t)=>e.slots(t.email,t.date,t.timezone||"UTC"),readOnly:!0},{name:"appoiint_create_booking",description:"Create a booking on behalf of a customer.",inputSchema:{type:"object",properties:{eventTypeId:{type:"string",description:"Event type ID"},scheduledAt:{type:"string",description:"ISO 8601 date-time (e.g. 2026-06-15T14:00:00Z)"},duration:{type:"number",description:"Duration in minutes (default: 30)"},customerName:{type:"string",description:"Customer name"},customerEmail:{type:"string",description:"Customer email"}},required:["scheduledAt","customerEmail"]},handler:async(e,t)=>e.book({eventTypeId:t.eventTypeId,scheduledAt:t.scheduledAt,duration:t.duration||30,customer:{name:t.customerName||"Guest",email:t.customerEmail},location:{type:"video"}}),readOnly:!1},{name:"appoiint_tts",description:"Synthesize text-to-speech with CxO agent voice.",inputSchema:{type:"object",properties:{text:{type:"string",description:"Text to synthesize"},voice:{type:"string",description:"Voice: cfo, cto, cmo, cco, cso, clo, heart, michael, nova, echo (default: heart)"}},required:["text"]},handler:async(e,t)=>e.synthesize({text:t.text,voice:t.voice||"heart",agentRole:t.voice}),readOnly:!0},{name:"appoiint_voices",description:"List available TTS voices and CxO agent voice mappings.",inputSchema:{type:"object",properties:{}},handler:async e=>e.voices(),readOnly:!0},{name:"appoiint_helpdesk_tickets",description:"List helpdesk tickets. Filter by status.",inputSchema:{type:"object",properties:{status:{type:"string",description:"Filter: open, resolved, closed"}}},handler:async(e,t,i)=>{const{rawApi:n}=require("./api");return n(i.config)("GET","/saas/api/v1/sdk/helpdeskryClient/tickets"+(t.status?`?status=${t.status}`:""))},readOnly:!0},{name:"appoiint_helpdesk_ticket_show",description:"Get ticket detail with messages.",inputSchema:{type:"object",properties:{ticketId:{type:"string"}},required:["ticketId"]},handler:async(e,t,i)=>{const{rawApi:n}=require("./api");return n(i.config)("GET",`/saas/api/v1/sdk/helpdeskryClient/tickets/${t.ticketId}`)},readOnly:!0},{name:"appoiint_helpdesk_ticket_create",description:"Create a helpdesk ticket.",inputSchema:{type:"object",properties:{subject:{type:"string"},priority:{type:"string"},category:{type:"string"}},required:["subject"]},handler:async(e,t,i)=>{const{rawApi:n}=require("./api");return n(i.config)("POST","/saas/api/v1/sdk/helpdeskryClient/tickets",t)},readOnly:!1},{name:"appoiint_helpdesk_ticket_reply",description:"Reply to a helpdesk ticket.",inputSchema:{type:"object",properties:{ticketId:{type:"string"},message:{type:"string"}},required:["ticketId","message"]},handler:async(e,t,i)=>{const{rawApi:n}=require("./api");return n(i.config)("POST",`/saas/api/v1/sdk/helpdeskryClient/tickets/${t.ticketId}/messages`,{content:t.message})},readOnly:!1},{name:"appoiint_crm_leads",description:"List CRM leads. Filter by status.",inputSchema:{type:"object",properties:{status:{type:"string"}}},handler:async(e,t,i)=>{const{rawApi:n}=require("./api"),a=t.status?`?status=${t.status}`:"";return n(i.config)("GET",`/saas/api/v1/sdk/crm/leads${a}`)},readOnly:!0},{name:"appoiint_crm_lead_show",description:"Get lead detail.",inputSchema:{type:"object",properties:{leadId:{type:"string"}},required:["leadId"]},handler:async(e,t,i)=>{const{rawApi:n}=require("./api");return n(i.config)("GET",`/saas/api/v1/sdk/crm/leads/${t.leadId}`)},readOnly:!0},{name:"appoiint_crm_lead_create",description:"Create a CRM lead.",inputSchema:{type:"object",properties:{name:{type:"string"},email:{type:"string"},source:{type:"string"}},required:["name"]},handler:async(e,t,i)=>{const{rawApi:n}=require("./api");return n(i.config)("POST","/saas/api/v1/sdk/crm/leads",t)},readOnly:!1},{name:"appoiint_crm_opportunities",description:"List CRM opportunities. Filter by stage.",inputSchema:{type:"object",properties:{stage:{type:"string"}}},handler:async(e,t,i)=>{const{rawApi:n}=require("./api"),a=t.stage?`?stage=${t.stage}`:"";return n(i.config)("GET",`/saas/api/v1/sdk/crm/opportunities${a}`)},readOnly:!0},{name:"appoiint_crm_pipelines",description:"List CRM pipelines with stages.",inputSchema:{type:"object",properties:{}},handler:async(e,t,i)=>{const{rawApi:n}=require("./api");return n(i.config)("GET","/saas/api/v1/sdk/crm/pipelines")},readOnly:!0},{name:"appoiint_channel_send",description:"Send a 1:1 message to another agent. Use for ops requests, data queries, or notifications.",inputSchema:{type:"object",properties:{to:{type:"string",description:"Recipient agent ID"},message:{type:"string",description:"Message text"},action:{type:"string",description:"Intent/action (e.g. deploy, query, notify)"},payload:{type:"object",description:"Structured data payload"}},required:["to"]},handler:async(e,t)=>{const i={to:t.to,type:t.action?"request":"notify"};return t.message&&(i.payload={message:t.message}),t.action&&(i.action=t.action,i.requestId=`req_${Date.now()}`),t.payload&&(i.payload={...i.payload,...t.payload}),e.channelSend(i)},readOnly:!1},{name:"appoiint_channel_inbox",description:"Get unread messages from other agents.",inputSchema:{type:"object",properties:{}},handler:async e=>e.channelInbox(),readOnly:!0},{name:"appoiint_channel_history",description:"Get message history with a specific agent.",inputSchema:{type:"object",properties:{agentId:{type:"string",description:"Agent ID to get history with"},limit:{type:"number",description:"Max messages (default 50)"}},required:["agentId"]},handler:async(e,t)=>e.channelHistory(t.agentId,t.limit),readOnly:!0},{name:"appoiint_channel_agents",description:"List agents you have communicated with.",inputSchema:{type:"object",properties:{}},handler:async e=>e.channelAgents(),readOnly:!0},{name:"appoiint_wall",description:"All your message threads (DMs + channels) with unread badges, newest first. Start here to see what needs attention.",inputSchema:{type:"object",properties:{}},handler:async e=>e.wall(),readOnly:!0},{name:"appoiint_notifications",description:"Unread digest across all DM and channel threads. Reading a thread history marks it read.",inputSchema:{type:"object",properties:{}},handler:async e=>e.notificationsDigest(),readOnly:!0},{name:"appoiint_channel_create",description:"Create a named channel for multi-agent group messaging. You auto-join as creator.",inputSchema:{type:"object",properties:{name:{type:"string",description:'Channel name (slugified, e.g. "dev-ops")'},topic:{type:"string",description:"Optional channel topic"}},required:["name"]},handler:async(e,t)=>e.groupCreate(t.name,t.topic),readOnly:!1},{name:"appoiint_channel_join",description:"Join an existing named channel.",inputSchema:{type:"object",properties:{name:{type:"string",description:"Channel name/slug"}},required:["name"]},handler:async(e,t)=>e.groupJoin(t.name),readOnly:!1},{name:"appoiint_channel_leave",description:"Leave a named channel.",inputSchema:{type:"object",properties:{name:{type:"string",description:"Channel name/slug"}},required:["name"]},handler:async(e,t)=>e.groupLeave(t.name),readOnly:!1},{name:"appoiint_channel_list",description:"List named channels with membership and your unread counts.",inputSchema:{type:"object",properties:{}},handler:async e=>e.groupList(),readOnly:!0},{name:"appoiint_channel_post",description:"Post a message to a named channel (must be a member).",inputSchema:{type:"object",properties:{name:{type:"string",description:"Channel name/slug"},message:{type:"string",description:"Message text"},payload:{type:"object",description:"Optional structured payload"}},required:["name","message"]},handler:async(e,t)=>e.groupSend(t.name,{text:t.message,payload:t.payload||{message:t.message}}),readOnly:!1},{name:"appoiint_channel_messages",description:"Read a named channel history (reading marks it read).",inputSchema:{type:"object",properties:{name:{type:"string",description:"Channel name/slug"},limit:{type:"number",description:"Max messages (default 50)"}},required:["name"]},handler:async(e,t)=>e.groupHistory(t.name,t.limit),readOnly:!0},{name:"appoiint_admin_tenants",description:"List all tenants with plan, status, usage. Superadmin only.",inputSchema:{type:"object",properties:{}},handler:async e=>e.adminTenants(),readOnly:!0},{name:"appoiint_admin_tenant_view",description:"Full tenant detail: plan, stats, usage, created date.",inputSchema:{type:"object",properties:{slug:{type:"string",description:"Tenant slug"}},required:["slug"]},handler:async(e,t)=>e.adminTenantView(t.slug),readOnly:!0},{name:"appoiint_platform_stats",description:"Platform overview: tenants, users, rooms, bookings, meetings.",inputSchema:{type:"object",properties:{}},handler:async e=>e.platformStats(),readOnly:!0},{name:"appoiint_funnel",description:"Signup funnel with conversion rates.",inputSchema:{type:"object",properties:{}},handler:async e=>e.funnel(),readOnly:!0},{name:"appoiint_revenue",description:"Revenue breakdown: MRR, ARR, fees, payouts.",inputSchema:{type:"object",properties:{}},handler:async e=>e.revenue(),readOnly:!0},{name:"appoiint_audit",description:"Activity audit trail.",inputSchema:{type:"object",properties:{limit:{type:"number",description:"Max entries"},action:{type:"string",description:"Filter by action type"}}},handler:async(e,t)=>{const i=new URLSearchParams;return t.limit&&i.set("limit",t.limit),t.action&&i.set("action",t.action),e.audit(i.toString())},readOnly:!0},{name:"appoiint_list_bookings",description:"List bookings for a host (defaults to the authenticated user).",inputSchema:{type:"object",properties:{hostId:{type:"string",description:"Host user id (default: authenticated user)"}}},handler:async(e,t,i)=>e.bookingsByHost(t.hostId||i.config.userId),readOnly:!0},{name:"appoiint_get_booking",description:"Get a single booking by id.",inputSchema:{type:"object",properties:{bookingId:{type:"string",description:"Booking id"}},required:["bookingId"]},handler:async(e,t)=>e.getBooking(t.bookingId),readOnly:!0},{name:"appoiint_cancel_booking",description:"Cancel a booking.",inputSchema:{type:"object",properties:{bookingId:{type:"string",description:"Booking id"},reason:{type:"string",description:"Cancellation reason"}},required:["bookingId"]},handler:async(e,t)=>e.cancelBooking(t.bookingId,{reason:t.reason||"Cancelled via MCP"}),readOnly:!1},{name:"appoiint_reschedule_booking",description:"Reschedule a booking to a new date/time.",inputSchema:{type:"object",properties:{bookingId:{type:"string",description:"Booking id"},newDate:{type:"string",description:"ISO 8601 date-time (e.g. 2026-10-01T14:00:00Z)"},reason:{type:"string",description:"Reschedule reason"}},required:["bookingId","newDate"]},handler:async(e,t)=>{const i={newDate:t.newDate,newScheduledAt:t.newDate};return t.reason&&(i.reason=t.reason),e.rescheduleBooking(t.bookingId,i)},readOnly:!1},{name:"appoiint_get_availability",description:"Get weekly availability (working hours) for a user (defaults to the authenticated user).",inputSchema:{type:"object",properties:{userId:{type:"string",description:"User id (default: authenticated user)"}}},handler:async(e,t,i)=>e.availabilityGet(t.userId||i.config.userId),readOnly:!0},{name:"appoiint_set_availability",description:"Set weekly availability: one time window applied to the given days. Mirrors PUT /availability/:userId (body: userId, organizationId, workingHours, timezone).",inputSchema:{type:"object",properties:{from:{type:"string",description:"Start time HH:MM (default 09:00)"},to:{type:"string",description:"End time HH:MM (default 17:00)"},days:{type:"array",items:{type:"number"},description:"Days of week 0=Sun..6=Sat (default Mon-Fri [1,2,3,4,5])"},timezone:{type:"string",description:"IANA timezone (default Europe/London)"},organizationId:{type:"string",description:"Org id (auto-resolved from current availability or event types if omitted)"},userId:{type:"string",description:"Target user (default: authenticated user)"}}},handler:async(e,t,i)=>{const n=t.userId||i.config.userId;if(!n)throw new Error("No userId available — pass userId explicitly.");const a=t.from||"09:00",r=t.to||"17:00";if(!/^\d{2}:\d{2}$/.test(a)||!/^\d{2}:\d{2}$/.test(r))throw new Error("Times must be HH:MM (e.g. 09:00)");const o=Array.isArray(t.days)&&t.days.length?t.days:[1,2,3,4,5],s=[0,1,2,3,4,5,6].map(e=>({dayOfWeek:e,isAvailable:o.includes(e),timeSlots:o.includes(e)?[{startTime:a,endTime:r}]:[]})),p=t.organizationId||await _resolveOrgId(e,n);if(!p)throw new Error("Could not resolve organizationId — pass organizationId explicitly.");const d=t.timezone||"Europe/London";return e.availabilityUpdate(n,{userId:n,organizationId:p,workingHours:s,timezone:d})},readOnly:!1},{name:"appoiint_list_domains",description:"List custom meeting-page domains for this tenant.",inputSchema:{type:"object",properties:{}},handler:async e=>_domainArray(await e.domains()),readOnly:!0},{name:"appoiint_register_domain",description:"Register a custom domain for the tenant meetings page. Returns the CNAME target to publish, then verify with appoiint_verify_domain.",inputSchema:{type:"object",properties:{hostname:{type:"string",description:"e.g. meetings.yourdomain.com"}},required:["hostname"]},handler:async(e,t,i)=>{if(!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(t.hostname))throw new Error("Invalid hostname");if(!i.config.userId)throw new Error("No userId on this API key — cannot register a tenant domain.");const n=_unwrap(await e.domainRegister({hostname:t.hostname,domainType:"custom",tenantId:i.config.userId,tenantType:"user",baseDomain:"appoiint.ai"})),a=n.dnsVerification&&n.dnsVerification.expectedValue;return{registered:!0,hostname:t.hostname,domainId:_idOf(n),cnameTarget:a||null,next:a?`Publish CNAME ${t.hostname} -> ${a}, then call appoiint_verify_domain.`:"SDK returned no CNAME target — inspect with appoiint_domain_status."}},readOnly:!1},{name:"appoiint_verify_domain",description:"Verify DNS for a custom domain and auto-activate it once verified (custom domains ride the wildcard cert, so activation gates on DNS only).",inputSchema:{type:"object",properties:{domain:{type:"string",description:"Hostname or domain id"}},required:["domain"]},handler:async(e,t)=>{const i=await _findDomain(e,t.domain);if(!i)throw new Error(`Domain not found: ${t.domain}`);const n=await e.domainVerify(_idOf(i)),a=_unwrap(n),r=a.verified||a.dnsVerification&&a.dnsVerification.verified||n.success&&"active"===a.status;let o=null;if(r&&"active"!==a.status)try{o=_unwrap(await e.domainActivate(_idOf(i)))}catch(e){o={error:e.message}}return{hostname:i.hostname,verified:!!r,status:o&&o.status||a.status||null,activate:o}},readOnly:!1},{name:"appoiint_domain_status",description:"DNS + SSL status for a custom domain.",inputSchema:{type:"object",properties:{domain:{type:"string",description:"Hostname or domain id"}},required:["domain"]},handler:async(e,t)=>{const i=await _findDomain(e,t.domain);if(!i)throw new Error(`Domain not found: ${t.domain}`);const n=_unwrap(await e.domainDnsStatus(_idOf(i)));return{hostname:i.hostname,domainId:_idOf(i),status:i.status,dns:n}},readOnly:!0},{name:"appoiint_remove_domain",description:"Delete a custom domain. IRREVERSIBLE — requires confirm=true.",inputSchema:{type:"object",properties:{domain:{type:"string",description:"Hostname or domain id"},confirm:{type:"boolean",description:"Must be true to delete"}},required:["domain"]},handler:async(e,t)=>{if(!0!==t.confirm)throw new Error("Refusing to delete without confirm=true (this cannot be undone).");const i=await _findDomain(e,t.domain);if(!i)throw new Error(`Domain not found: ${t.domain}`);const n=await e.domainDelete(_idOf(i));return{removed:!0,hostname:i.hostname,result:n}},readOnly:!1},{name:"appoiint_page",description:"Show the tenant meetings-page profile (name, title, company, website, slug — drives the vCard + OG tags).",inputSchema:{type:"object",properties:{}},handler:async e=>e.profileGet(),readOnly:!0},{name:"appoiint_page_set",description:"Update the tenant meetings-page profile. Verified-writable fields only: firstName, lastName, company, title (stored as position), phone (mobile_no), website (powers the vCard VISIT CTA).",inputSchema:{type:"object",properties:{firstName:{type:"string"},lastName:{type:"string"},company:{type:"string"},title:{type:"string",description:"Job title (stored as position)"},phone:{type:"string"},website:{type:"string",description:"https URL — powers the vCard VISIT CTA"}}},handler:async(e,t)=>{const i={};if(void 0!==t.firstName&&(i.firstName=t.firstName),void 0!==t.lastName&&(i.lastName=t.lastName),void 0!==t.company&&(i.company=t.company),void 0!==t.title&&(i.position=t.title),void 0!==t.phone&&(i.mobile_no=t.phone),void 0!==t.website&&(i.website=t.website),!Object.keys(i).length)throw new Error("No fields to update.");return e.profileUpdate(i)},readOnly:!1},{name:"appoiint_update_event",description:"Update an existing event type (PUT /eventtypes/:id). Pass only the fields to change.",inputSchema:{type:"object",properties:{eventTypeId:{type:"string",description:"Event type id"},updates:{type:"object",description:"Fields to update (name, duration, description, pricing, ...)"}},required:["eventTypeId","updates"]},handler:async(e,t)=>e.updateEvent(t.eventTypeId,t.updates),readOnly:!1},{name:"appoiint_resolve_domain",description:"Resolve which tenant a hostname routes to (GET /domains/resolve).",inputSchema:{type:"object",properties:{hostname:{type:"string"}},required:["hostname"]},handler:async(e,t)=>e.domainResolve(t.hostname),readOnly:!0},{name:"appoiint_stt_status",description:"Speech-to-text provider status.",inputSchema:{type:"object",properties:{}},handler:async e=>e.sttStatus(),readOnly:!0},{name:"appoiint_costs",description:"Usage costs summary for this account.",inputSchema:{type:"object",properties:{}},handler:async e=>e.costs(),readOnly:!0},{name:"appoiint_costs_room",description:"Usage costs for a specific room.",inputSchema:{type:"object",properties:{roomId:{type:"string"}},required:["roomId"]},handler:async(e,t)=>e.costsByRoom(t.roomId),readOnly:!0},{name:"appoiint_costs_pricing",description:"Current usage pricing table.",inputSchema:{type:"object",properties:{}},handler:async e=>e.costsPricing(),readOnly:!0},{name:"appoiint_channel_ack",description:"Acknowledge an agent-channel message by id.",inputSchema:{type:"object",properties:{messageId:{type:"string"}},required:["messageId"]},handler:async(e,t)=>e.channelAck(t.messageId),readOnly:!1},{name:"appoiint_signups",description:"SUPERADMIN: signup counts over the last N days.",inputSchema:{type:"object",properties:{days:{type:"number",description:"Window in days"}}},handler:async(e,t)=>e.signups(t.days),readOnly:!0},{name:"appoiint_audit_export",description:"SUPERADMIN: export the audit trail (json or csv).",inputSchema:{type:"object",properties:{format:{type:"string",description:"json or csv (default json)"}}},handler:async(e,t)=>e.auditExport(t.format||"json"),readOnly:!0},{name:"appoiint_admin_notifications",description:"SUPERADMIN: platform notifications feed.",inputSchema:{type:"object",properties:{since:{type:"string",description:"ISO timestamp — only newer items"}}},handler:async(e,t)=>e.notifications(t.since),readOnly:!0},{name:"appoiint_tickets",description:"SUPERADMIN: list support tickets.",inputSchema:{type:"object",properties:{status:{type:"string",description:"Filter: open, closed, ..."}}},handler:async(e,t)=>e.tickets(t.status),readOnly:!0},{name:"appoiint_ticket_show",description:"SUPERADMIN: show a support ticket.",inputSchema:{type:"object",properties:{ticketId:{type:"string"}},required:["ticketId"]},handler:async(e,t)=>e.ticketShow(t.ticketId),readOnly:!0},{name:"appoiint_ticket_reply",description:"SUPERADMIN: reply to a support ticket.",inputSchema:{type:"object",properties:{ticketId:{type:"string"},message:{type:"string"}},required:["ticketId","message"]},handler:async(e,t)=>e.ticketReply(t.ticketId,t.message),readOnly:!1},{name:"appoiint_ticket_close",description:"SUPERADMIN: close a support ticket.",inputSchema:{type:"object",properties:{ticketId:{type:"string"}},required:["ticketId"]},handler:async(e,t)=>e.ticketClose(t.ticketId),readOnly:!1},{name:"appoiint_admin_users",description:"SUPERADMIN: list platform users.",inputSchema:{type:"object",properties:{}},handler:async e=>e.adminUsers(),readOnly:!0},{name:"appoiint_admin_user_create",description:"SUPERADMIN: create a platform user.",inputSchema:{type:"object",properties:{email:{type:"string"},name:{type:"string"},organizationName:{type:"string"},planId:{type:"string",description:"e.g. appoiint-enterprise"},skipPayment:{type:"boolean"}},required:["email","name"]},handler:async(e,t)=>e.adminUserCreate(t),readOnly:!1},{name:"appoiint_admin_user_reset",description:"SUPERADMIN: reset a user password.",inputSchema:{type:"object",properties:{email:{type:"string"},newPassword:{type:"string",description:"Omit to auto-generate"}},required:["email"]},handler:async(e,t)=>e.adminUserReset(t.email,t.newPassword?{newPassword:t.newPassword}:{}),readOnly:!1},{name:"appoiint_admin_user_delete",description:"SUPERADMIN: delete a platform user. IRREVERSIBLE — requires confirm=true.",inputSchema:{type:"object",properties:{email:{type:"string"},confirm:{type:"boolean",description:"Must be true to delete"}},required:["email"]},handler:async(e,t)=>{if(!0!==t.confirm)throw new Error("Refusing to delete without confirm=true (this cannot be undone).");return e.adminUserDelete(t.email)},readOnly:!1},{name:"appoiint_admin_tenant_create",description:"SUPERADMIN: create a tenant.",inputSchema:{type:"object",properties:{data:{type:"object",description:"Tenant payload (slug, name, plan, ...)"}},required:["data"]},handler:async(e,t)=>e.adminTenantCreate(t.data),readOnly:!1},{name:"appoiint_admin_tenant_set",description:"SUPERADMIN: tenant lifecycle action — plan | verify | suspend | activate.",inputSchema:{type:"object",properties:{slug:{type:"string",description:"Tenant slug"},action:{type:"string",description:"plan | verify | suspend | activate"},plan:{type:"string",description:"Plan id (required when action=plan)"}},required:["slug","action"]},handler:async(e,t)=>{switch(t.action){case"plan":if(!t.plan)throw new Error("action=plan requires the plan parameter");return e.adminTenantPlan(t.slug,t.plan);case"verify":return e.adminTenantVerify(t.slug);case"suspend":return e.adminTenantSuspend(t.slug);case"activate":return e.adminTenantActivate(t.slug);default:throw new Error(`Unknown action: ${t.action} (use plan | verify | suspend | activate)`)}},readOnly:!1},{name:"appoiint_admin_tenant_delete",description:"SUPERADMIN: delete a tenant. IRREVERSIBLE — requires confirm=true.",inputSchema:{type:"object",properties:{slug:{type:"string"},confirm:{type:"boolean",description:"Must be true to delete"}},required:["slug"]},handler:async(e,t)=>{if(!0!==t.confirm)throw new Error("Refusing to delete without confirm=true (this cannot be undone).");return e.adminTenantDelete(t.slug)},readOnly:!1},{name:"appoiint_admin_billing",description:"SUPERADMIN: platform billing/revenue summary.",inputSchema:{type:"object",properties:{}},handler:async e=>e.adminBilling(),readOnly:!0},{name:"appoiint_admin_discount",description:"SUPERADMIN: apply a discount.",inputSchema:{type:"object",properties:{data:{type:"object",description:"Discount payload"}},required:["data"]},handler:async(e,t)=>e.adminDiscount(t.data),readOnly:!1}];function _notesFrom(e,t){let i=Array.isArray(e.notes)?e.notes:null;if(!i&&e.roomId&&t&&t.meetingSessions){const n=t.meetingSessions.get(e.roomId);n&&(i=n.getTranscripts(!1))}return Array.isArray(i)?i.map(e=>"string"==typeof e?{speaker:"Unknown",text:e,timestamp:Date.now()}:{speaker:e.speaker||e.peer_name||"Unknown",text:e.text||"",timestamp:e.timestamp||Date.now()}).filter(e=>e.text):[]}function _unwrap(e){let t=e;for(;t&&"object"==typeof t&&t.data&&!Array.isArray(t);)t=t.data;return t||{}}function _domainArray(e){const t=_unwrap(e);return Array.isArray(t)?t:t.domains||t.tenantDomains||[]}function _idOf(e){return e.tenantDomainId||e._id||e.id||null}async function _findDomain(e,t){return _domainArray(await e.domains()).find(e=>e.hostname===t||_idOf(e)===t)||null}async function _resolveOrgId(e,t){try{const i=_unwrap(await e.availabilityGet(t)),n=i.settings&&i.settings.organizationId||i.organizationId;if(n)return n}catch{}try{const t=await e.events(),i=(Array.isArray(t)?t:t.eventTypes||t.data&&(Array.isArray(t.data)?t.data:t.data.eventTypes)||[]).find(e=>e.organization&&e.organization.id);if(i)return i.organization.id}catch{}return null}module.exports={TOOLS:TOOLS};
|
|
1
|
+
const TOOLS=[{name:"appoiint_status",description:"Get Appoiint platform status and version.",inputSchema:{type:"object",properties:{}},handler:async e=>e.status(),readOnly:!0},{name:"appoiint_create_room",description:"Create a video meeting room. Returns roomId and join URL.",inputSchema:{type:"object",properties:{title:{type:"string",description:'Meeting title (e.g. "Team Standup")'}},required:["title"]},handler:async(e,t)=>{const i=require("./config").loadConfig([]);return e.createRoom({title:t.title,hostId:i.userId})},readOnly:!1},{name:"appoiint_list_rooms",description:"List active meeting rooms.",inputSchema:{type:"object",properties:{}},handler:async e=>e.listRooms(),readOnly:!0},{name:"appoiint_room_status",description:"Get room status and participants.",inputSchema:{type:"object",properties:{roomId:{type:"string",description:"Room ID"}},required:["roomId"]},handler:async(e,t)=>e.roomStatus(t.roomId),readOnly:!0},{name:"appoiint_end_room",description:"End a meeting room.",inputSchema:{type:"object",properties:{roomId:{type:"string",description:"Room ID"}},required:["roomId"]},handler:async(e,t)=>e.endRoom(t.roomId),readOnly:!1},{name:"appoiint_invite",description:"Invite a human or agent to a meeting room.",inputSchema:{type:"object",properties:{roomId:{type:"string",description:"Room ID"},email:{type:"string",description:"Invitee email"},name:{type:"string",description:"Invitee name"},type:{type:"string",description:"human or agent (default: human)"}},required:["roomId","email"]},handler:async(e,t)=>e.invite(t.roomId,{email:t.email,name:t.name||"Guest",type:t.type||"human"}),readOnly:!1},{name:"appoiint_transcript",description:"Get meeting transcript.",inputSchema:{type:"object",properties:{roomId:{type:"string",description:"Room ID"},format:{type:"string",description:"json, text, or srt (default: json)"}},required:["roomId"]},handler:async(e,t)=>e.transcript(t.roomId,t.format||"json"),readOnly:!0},{name:"appoiint_join_meeting",description:"Join a meeting room as an AI agent. Creates persistent Socket.IO connection. You will hear transcripts and can send chat messages. Call appoiint_get_transcripts to read what was said, appoiint_send_chat to respond.",inputSchema:{type:"object",properties:{roomId:{type:"string",description:"Room ID to join"},agentName:{type:"string",description:"Your display name in the meeting (default: Claude Code)"},agentRole:{type:"string",description:"Your role badge (default: assistant)"}},required:["roomId"]},handler:async(e,t,i)=>{const{MeetingSession:n}=require("./meetingSession"),a=new n({baseUrl:i.config.baseUrl,roomId:t.roomId,agentName:t.agentName||"Claude Code",agentRole:t.agentRole||"assistant"}),r=await a.join();return i.meetingSessions.set(t.roomId,a),r},readOnly:!1},{name:"appoiint_get_transcripts",description:"Get speech transcripts from the meeting. Returns what participants have said since last read. Buffer is cleared after reading.",inputSchema:{type:"object",properties:{roomId:{type:"string",description:"Room ID"}},required:["roomId"]},handler:async(e,t,i)=>{const n=i.meetingSessions.get(t.roomId);if(!n)return{error:"Not in this meeting. Call appoiint_join_meeting first."};const a=n.getTranscripts();return{roomId:t.roomId,count:a.length,transcripts:a}},readOnly:!0},{name:"appoiint_send_chat",description:"Send a chat message to all meeting participants. Your message appears in the AI chat panel with your role badge.",inputSchema:{type:"object",properties:{roomId:{type:"string",description:"Room ID"},message:{type:"string",description:"Message to send to meeting participants"}},required:["roomId","message"]},handler:async(e,t,i)=>{const n=i.meetingSessions.get(t.roomId);return n?n.sendChat(t.message):{error:"Not in this meeting. Call appoiint_join_meeting first."}},readOnly:!1},{name:"appoiint_meeting_status",description:"Check meeting connection status: connected, peer count, buffered transcript count.",inputSchema:{type:"object",properties:{roomId:{type:"string",description:"Room ID"}},required:["roomId"]},handler:async(e,t,i)=>{const n=i.meetingSessions.get(t.roomId);return n?n.status():{error:"Not in this meeting.",connected:!1}},readOnly:!0},{name:"appoiint_leave_meeting",description:"Leave a meeting room and disconnect.",inputSchema:{type:"object",properties:{roomId:{type:"string",description:"Room ID"}},required:["roomId"]},handler:async(e,t,i)=>{const n=i.meetingSessions.get(t.roomId);if(!n)return{error:"Not in this meeting."};const a=n.leave();return i.meetingSessions.delete(t.roomId),a},readOnly:!1},{name:"appoiint_speak",description:"Speak in a meeting room with an agent voice: synthesizes text via TTS and broadcasts the audio to all participants in one call. Requires an active appoiint_join_meeting session. Text is trimmed to 500 chars server-side; audio over 500KB is dropped by the relay.",inputSchema:{type:"object",properties:{roomId:{type:"string",description:"Room ID (must have joined via appoiint_join_meeting)"},text:{type:"string",description:"Text to speak (max 500 chars)"},voice:{type:"string",description:"Voice: cfo, cto, cmo, cco, cso, clo, heart, michael, nova, echo (default: heart)"}},required:["roomId","text"]},handler:async(e,t,i)=>{const n=i.meetingSessions.get(t.roomId);if(!n)return{error:"Not in this meeting. Call appoiint_join_meeting first."};const a=await e.synthesize({text:t.text,voice:t.voice||"heart",agentRole:t.voice});if(!a||!a.audio)return{error:"TTS synthesis failed",detail:a};const r=n.sendAudio({audio:a.audio,format:a.format});return{spoken:!0,roomId:t.roomId,voice:a.voice,provider:a.provider,bytes:r.bytes}},readOnly:!1},{name:"appoiint_summary_status",description:"Check whether the AI meeting-summary service is available, and which model backs it. Call this before summarizing if you want to fail fast.",inputSchema:{type:"object",properties:{}},handler:async e=>e.summaryStatus(),readOnly:!0},{name:"appoiint_summarize_notes",description:"Summarize a meeting transcript into an overview, key topics and action items. Pass either roomId (uses the live transcript buffer from appoiint_join_meeting, without clearing it) or an explicit notes array. Returns overview, keyTopics[{topic,summary,timestamp,speakers}] and actionItems[{task,assignee,deadline,priority,context}]. Nothing is saved — use appoiint_summarize_meeting to persist against a booking.",inputSchema:{type:"object",properties:{roomId:{type:"string",description:"Room ID of an active appoiint_join_meeting session — its transcripts are summarized"},notes:{type:"array",description:"Transcript entries: [{speaker, text, timestamp}] or plain strings",items:{type:"object"}},metadata:{type:"object",description:"Optional meeting context: {title, participants, duration}"}}},handler:async(e,t,i)=>{const n=_notesFrom(t,i);return n.length?e.summarizeNotes({notes:n,metadata:t.metadata||{}}):{error:"No transcript to summarize. Pass notes[], or a roomId with buffered transcripts."}},readOnly:!0},{name:"appoiint_ask_transcript",description:'Ask a natural-language question about a meeting transcript ("what did we decide about pricing?", "who owns the migration?"). Pass either roomId (live transcript buffer) or an explicit notes array, plus the question.',inputSchema:{type:"object",properties:{question:{type:"string",description:"Question to answer from the transcript"},roomId:{type:"string",description:"Room ID of an active appoiint_join_meeting session"},notes:{type:"array",description:"Transcript entries: [{speaker, text, timestamp}] or plain strings",items:{type:"object"}},metadata:{type:"object",description:"Optional meeting context"}},required:["question"]},handler:async(e,t,i)=>{const n=_notesFrom(t,i);return n.length?e.askTranscript({notes:n,question:t.question,metadata:t.metadata||{}}):{error:"No transcript to ask about. Pass notes[], or a roomId with buffered transcripts."}},readOnly:!0},{name:"appoiint_summarize_meeting",description:"Generate an AI summary from a meeting's stored notes and SAVE it against that meeting. Use the booking/meeting id. Fails if the meeting has no transcript entries yet.",inputSchema:{type:"object",properties:{meetingId:{type:"string",description:"Meeting/booking ID"}},required:["meetingId"]},handler:async(e,t)=>e.summarizeMeeting(t.meetingId),readOnly:!1},{name:"appoiint_get_summary",description:"Get the saved AI summary for a meeting (404 if one has not been generated — call appoiint_summarize_meeting first).",inputSchema:{type:"object",properties:{meetingId:{type:"string",description:"Meeting/booking ID"}},required:["meetingId"]},handler:async(e,t)=>e.getSummary(t.meetingId),readOnly:!0},{name:"appoiint_delete_summary",description:"Delete the saved AI summary for a meeting. The transcript itself is kept.",inputSchema:{type:"object",properties:{meetingId:{type:"string",description:"Meeting/booking ID"}},required:["meetingId"]},handler:async(e,t)=>e.deleteSummary(t.meetingId),readOnly:!1},{name:"appoiint_meeting_transcript",description:"Get the stored transcript for a meeting: timestamped segments plus the speaker list.",inputSchema:{type:"object",properties:{meetingId:{type:"string",description:"Meeting/booking ID"},limit:{type:"number",description:"Max segments to return (default 50)"}},required:["meetingId"]},handler:async(e,t)=>e.meetingTranscript(t.meetingId,t.limit),readOnly:!0},{name:"appoiint_meeting_action_items",description:"List the action items extracted from a meeting.",inputSchema:{type:"object",properties:{meetingId:{type:"string",description:"Meeting/booking ID"}},required:["meetingId"]},handler:async(e,t)=>e.meetingActionItems(t.meetingId),readOnly:!0},{name:"appoiint_meeting_highlights",description:"List highlighted moments in a meeting transcript.",inputSchema:{type:"object",properties:{meetingId:{type:"string",description:"Meeting/booking ID"}},required:["meetingId"]},handler:async(e,t)=>e.meetingHighlights(t.meetingId),readOnly:!0},{name:"appoiint_meeting_notes_status",description:"Meeting-notes overview for a meeting: segment/speaker/highlight/comment counts and duration. Cheap way to check whether a meeting has anything worth summarizing.",inputSchema:{type:"object",properties:{meetingId:{type:"string",description:"Meeting/booking ID"}},required:["meetingId"]},handler:async(e,t)=>e.meetingNotesStatus(t.meetingId),readOnly:!0},{name:"appoiint_search_meeting",description:"Full-text search across a meeting's transcript segments, highlights and comments.",inputSchema:{type:"object",properties:{meetingId:{type:"string",description:"Meeting/booking ID"},query:{type:"string",description:"Search term"}},required:["meetingId","query"]},handler:async(e,t)=>e.meetingSearch(t.meetingId,t.query),readOnly:!0},{name:"appoiint_export_meeting_notes",description:"Export a meeting's notes as md (default), txt or json — ready to paste into a doc or email.",inputSchema:{type:"object",properties:{meetingId:{type:"string",description:"Meeting/booking ID"},format:{type:"string",description:"md | txt | json (default md)"}},required:["meetingId"]},handler:async(e,t)=>{const i=(t.format||"md").toLowerCase();if(!["md","txt","json"].includes(i))throw new Error("format must be one of: md, txt, json");return e.meetingExport(t.meetingId,i)},readOnly:!0},{name:"appoiint_list_events",description:"List bookable event types.",inputSchema:{type:"object",properties:{}},handler:async e=>e.events(),readOnly:!0},{name:"appoiint_create_event",description:"Create a bookable event type for the authenticated host.",inputSchema:{type:"object",properties:{name:{type:"string",description:"Event type name/title"},duration:{type:"number",description:"Duration in minutes (default: 30)"},slug:{type:"string",description:"URL slug (default: derived from name)"},description:{type:"string",description:"Short description"},timezone:{type:"string",description:"Availability timezone (default: Europe/London)"},price:{type:"number",description:"Price (default: 0 = free)"},currency:{type:"string",description:"Currency (default: USD)"}},required:["name"]},handler:async(e,t,i)=>{const n=t.slug||String(t.name).toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,""),a=Number(t.price)||0;return e.createEvent({userId:i.config.userId,name:t.name,slug:n,duration:t.duration||30,description:t.description||"",availability:{type:"inherit",timezone:t.timezone||"Europe/London"},pricing:{enabled:a>0,amount:a,currency:t.currency||"USD"},meetingSettings:{location:{type:"video"},autoCreateMeeting:!0}})},readOnly:!1},{name:"appoiint_delete_event",description:"Delete an event type. SAFETY: refuses to hard-delete when bookings are attached (would orphan meetings) unless force=true; use soft=true to deactivate while preserving bookings.",inputSchema:{type:"object",properties:{eventTypeId:{type:"string",description:"Event type id (from appoiint_list_events)"},soft:{type:"boolean",description:"Deactivate instead of hard delete (preserves bookings)"},force:{type:"boolean",description:"Hard delete even if bookings are attached"}},required:["eventTypeId"]},handler:async(e,t)=>{const i=t.eventTypeId;let n=null;try{const t=await e.events();n=(Array.isArray(t)?t:t.eventTypes||t.data&&(Array.isArray(t.data)?t.data:t.data.eventTypes)||[]).find(e=>e.id===i||e._id===i||e.eventTypeId===i||e.slug===i)}catch{}const a=n&&Number.isFinite(n.bookingCount)?n.bookingCount:null,r=n&&(n.name||n.slug)||i;if(!t.soft&&!t.force&&a>0)throw new Error(`Refusing to delete "${r}" — it has ${a} booking(s) attached and they would be orphaned. Re-run with soft:true (deactivate, keep bookings) or force:true (hard delete anyway).`);const o=await e.deleteEvent(i,!!t.soft);return{deleted:!t.soft,deactivated:!!t.soft,eventType:r,bookingsAffected:a||0,result:o}},readOnly:!1},{name:"appoiint_check_availability",description:"Check available time slots for a host on a given date.",inputSchema:{type:"object",properties:{email:{type:"string",description:"Host email address"},date:{type:"string",description:"Date (YYYY-MM-DD)"},timezone:{type:"string",description:"Timezone (default: UTC)"}},required:["email","date"]},handler:async(e,t)=>e.slots(t.email,t.date,t.timezone||"UTC"),readOnly:!0},{name:"appoiint_create_booking",description:"Create a booking on behalf of a customer.",inputSchema:{type:"object",properties:{eventTypeId:{type:"string",description:"Event type ID"},scheduledAt:{type:"string",description:"ISO 8601 date-time (e.g. 2026-06-15T14:00:00Z)"},duration:{type:"number",description:"Duration in minutes (default: 30)"},customerName:{type:"string",description:"Customer name"},customerEmail:{type:"string",description:"Customer email"}},required:["scheduledAt","customerEmail"]},handler:async(e,t)=>e.book({eventTypeId:t.eventTypeId,scheduledAt:t.scheduledAt,duration:t.duration||30,customer:{name:t.customerName||"Guest",email:t.customerEmail},location:{type:"video"}}),readOnly:!1},{name:"appoiint_tts",description:"Synthesize text-to-speech with CxO agent voice.",inputSchema:{type:"object",properties:{text:{type:"string",description:"Text to synthesize"},voice:{type:"string",description:"Voice: cfo, cto, cmo, cco, cso, clo, heart, michael, nova, echo (default: heart)"}},required:["text"]},handler:async(e,t)=>e.synthesize({text:t.text,voice:t.voice||"heart",agentRole:t.voice}),readOnly:!0},{name:"appoiint_voices",description:"List available TTS voices and CxO agent voice mappings.",inputSchema:{type:"object",properties:{}},handler:async e=>e.voices(),readOnly:!0},{name:"appoiint_helpdesk_tickets",description:"List helpdesk tickets. Filter by status.",inputSchema:{type:"object",properties:{status:{type:"string",description:"Filter: open, resolved, closed"}}},handler:async(e,t,i)=>{const{rawApi:n}=require("./api");return n(i.config)("GET","/saas/api/v1/sdk/helpdeskryClient/tickets"+(t.status?`?status=${t.status}`:""))},readOnly:!0},{name:"appoiint_helpdesk_ticket_show",description:"Get ticket detail with messages.",inputSchema:{type:"object",properties:{ticketId:{type:"string"}},required:["ticketId"]},handler:async(e,t,i)=>{const{rawApi:n}=require("./api");return n(i.config)("GET",`/saas/api/v1/sdk/helpdeskryClient/tickets/${t.ticketId}`)},readOnly:!0},{name:"appoiint_helpdesk_ticket_create",description:"Create a helpdesk ticket.",inputSchema:{type:"object",properties:{subject:{type:"string"},priority:{type:"string"},category:{type:"string"}},required:["subject"]},handler:async(e,t,i)=>{const{rawApi:n}=require("./api");return n(i.config)("POST","/saas/api/v1/sdk/helpdeskryClient/tickets",t)},readOnly:!1},{name:"appoiint_helpdesk_ticket_reply",description:"Reply to a helpdesk ticket.",inputSchema:{type:"object",properties:{ticketId:{type:"string"},message:{type:"string"}},required:["ticketId","message"]},handler:async(e,t,i)=>{const{rawApi:n}=require("./api");return n(i.config)("POST",`/saas/api/v1/sdk/helpdeskryClient/tickets/${t.ticketId}/messages`,{content:t.message})},readOnly:!1},{name:"appoiint_crm_leads",description:"List CRM leads. Filter by status.",inputSchema:{type:"object",properties:{status:{type:"string"}}},handler:async(e,t,i)=>{const{rawApi:n}=require("./api"),a=t.status?`?status=${t.status}`:"";return n(i.config)("GET",`/saas/api/v1/sdk/crm/leads${a}`)},readOnly:!0},{name:"appoiint_crm_lead_show",description:"Get lead detail.",inputSchema:{type:"object",properties:{leadId:{type:"string"}},required:["leadId"]},handler:async(e,t,i)=>{const{rawApi:n}=require("./api");return n(i.config)("GET",`/saas/api/v1/sdk/crm/leads/${t.leadId}`)},readOnly:!0},{name:"appoiint_crm_lead_create",description:"Create a CRM lead.",inputSchema:{type:"object",properties:{name:{type:"string"},email:{type:"string"},source:{type:"string"}},required:["name"]},handler:async(e,t,i)=>{const{rawApi:n}=require("./api");return n(i.config)("POST","/saas/api/v1/sdk/crm/leads",t)},readOnly:!1},{name:"appoiint_crm_opportunities",description:"List CRM opportunities. Filter by stage.",inputSchema:{type:"object",properties:{stage:{type:"string"}}},handler:async(e,t,i)=>{const{rawApi:n}=require("./api"),a=t.stage?`?stage=${t.stage}`:"";return n(i.config)("GET",`/saas/api/v1/sdk/crm/opportunities${a}`)},readOnly:!0},{name:"appoiint_crm_pipelines",description:"List CRM pipelines with stages.",inputSchema:{type:"object",properties:{}},handler:async(e,t,i)=>{const{rawApi:n}=require("./api");return n(i.config)("GET","/saas/api/v1/sdk/crm/pipelines")},readOnly:!0},{name:"appoiint_channel_send",description:"Send a 1:1 message to another agent. Use for ops requests, data queries, or notifications.",inputSchema:{type:"object",properties:{to:{type:"string",description:"Recipient agent ID"},message:{type:"string",description:"Message text"},action:{type:"string",description:"Intent/action (e.g. deploy, query, notify)"},payload:{type:"object",description:"Structured data payload"}},required:["to"]},handler:async(e,t)=>{const i={to:t.to,type:t.action?"request":"notify"};return t.message&&(i.payload={message:t.message}),t.action&&(i.action=t.action,i.requestId=`req_${Date.now()}`),t.payload&&(i.payload={...i.payload,...t.payload}),e.channelSend(i)},readOnly:!1},{name:"appoiint_channel_inbox",description:"Get unread messages from other agents.",inputSchema:{type:"object",properties:{}},handler:async e=>e.channelInbox(),readOnly:!0},{name:"appoiint_channel_history",description:"Get message history with a specific agent.",inputSchema:{type:"object",properties:{agentId:{type:"string",description:"Agent ID to get history with"},limit:{type:"number",description:"Max messages (default 50)"}},required:["agentId"]},handler:async(e,t)=>e.channelHistory(t.agentId,t.limit),readOnly:!0},{name:"appoiint_channel_agents",description:"List agents you have communicated with.",inputSchema:{type:"object",properties:{}},handler:async e=>e.channelAgents(),readOnly:!0},{name:"appoiint_wall",description:"All your message threads (DMs + channels) with unread badges, newest first. Start here to see what needs attention.",inputSchema:{type:"object",properties:{}},handler:async e=>e.wall(),readOnly:!0},{name:"appoiint_notifications",description:"Unread digest across all DM and channel threads. Reading a thread history marks it read.",inputSchema:{type:"object",properties:{}},handler:async e=>e.notificationsDigest(),readOnly:!0},{name:"appoiint_channel_create",description:"Create a named channel for multi-agent group messaging. You auto-join as creator.",inputSchema:{type:"object",properties:{name:{type:"string",description:'Channel name (slugified, e.g. "dev-ops")'},topic:{type:"string",description:"Optional channel topic"}},required:["name"]},handler:async(e,t)=>e.groupCreate(t.name,t.topic),readOnly:!1},{name:"appoiint_channel_join",description:"Join an existing named channel.",inputSchema:{type:"object",properties:{name:{type:"string",description:"Channel name/slug"}},required:["name"]},handler:async(e,t)=>e.groupJoin(t.name),readOnly:!1},{name:"appoiint_channel_leave",description:"Leave a named channel.",inputSchema:{type:"object",properties:{name:{type:"string",description:"Channel name/slug"}},required:["name"]},handler:async(e,t)=>e.groupLeave(t.name),readOnly:!1},{name:"appoiint_channel_list",description:"List named channels with membership and your unread counts.",inputSchema:{type:"object",properties:{}},handler:async e=>e.groupList(),readOnly:!0},{name:"appoiint_channel_post",description:"Post a message to a named channel (must be a member).",inputSchema:{type:"object",properties:{name:{type:"string",description:"Channel name/slug"},message:{type:"string",description:"Message text"},payload:{type:"object",description:"Optional structured payload"}},required:["name","message"]},handler:async(e,t)=>e.groupSend(t.name,{text:t.message,payload:t.payload||{message:t.message}}),readOnly:!1},{name:"appoiint_channel_messages",description:"Read a named channel history (reading marks it read).",inputSchema:{type:"object",properties:{name:{type:"string",description:"Channel name/slug"},limit:{type:"number",description:"Max messages (default 50)"}},required:["name"]},handler:async(e,t)=>e.groupHistory(t.name,t.limit),readOnly:!0},{name:"appoiint_admin_tenants",description:"List all tenants with plan, status, usage. Superadmin only.",inputSchema:{type:"object",properties:{}},handler:async e=>e.adminTenants(),readOnly:!0},{name:"appoiint_admin_tenant_view",description:"Full tenant detail: plan, stats, usage, created date.",inputSchema:{type:"object",properties:{slug:{type:"string",description:"Tenant slug"}},required:["slug"]},handler:async(e,t)=>e.adminTenantView(t.slug),readOnly:!0},{name:"appoiint_platform_stats",description:"Platform overview: tenants, users, rooms, bookings, meetings.",inputSchema:{type:"object",properties:{}},handler:async e=>e.platformStats(),readOnly:!0},{name:"appoiint_funnel",description:"Signup funnel with conversion rates.",inputSchema:{type:"object",properties:{}},handler:async e=>e.funnel(),readOnly:!0},{name:"appoiint_revenue",description:"Revenue breakdown: MRR, ARR, fees, payouts.",inputSchema:{type:"object",properties:{}},handler:async e=>e.revenue(),readOnly:!0},{name:"appoiint_audit",description:"Activity audit trail.",inputSchema:{type:"object",properties:{limit:{type:"number",description:"Max entries"},action:{type:"string",description:"Filter by action type"}}},handler:async(e,t)=>{const i=new URLSearchParams;return t.limit&&i.set("limit",t.limit),t.action&&i.set("action",t.action),e.audit(i.toString())},readOnly:!0},{name:"appoiint_list_bookings",description:"List bookings for a host (defaults to the authenticated user).",inputSchema:{type:"object",properties:{hostId:{type:"string",description:"Host user id (default: authenticated user)"}}},handler:async(e,t,i)=>e.bookingsByHost(t.hostId||i.config.userId),readOnly:!0},{name:"appoiint_get_booking",description:"Get a single booking by id.",inputSchema:{type:"object",properties:{bookingId:{type:"string",description:"Booking id"}},required:["bookingId"]},handler:async(e,t)=>e.getBooking(t.bookingId),readOnly:!0},{name:"appoiint_cancel_booking",description:"Cancel a booking.",inputSchema:{type:"object",properties:{bookingId:{type:"string",description:"Booking id"},reason:{type:"string",description:"Cancellation reason"}},required:["bookingId"]},handler:async(e,t)=>e.cancelBooking(t.bookingId,{reason:t.reason||"Cancelled via MCP"}),readOnly:!1},{name:"appoiint_reschedule_booking",description:"Reschedule a booking to a new date/time.",inputSchema:{type:"object",properties:{bookingId:{type:"string",description:"Booking id"},newDate:{type:"string",description:"ISO 8601 date-time (e.g. 2026-10-01T14:00:00Z)"},reason:{type:"string",description:"Reschedule reason"}},required:["bookingId","newDate"]},handler:async(e,t)=>{const i={newDate:t.newDate,newScheduledAt:t.newDate};return t.reason&&(i.reason=t.reason),e.rescheduleBooking(t.bookingId,i)},readOnly:!1},{name:"appoiint_get_availability",description:"Get weekly availability (working hours) for a user (defaults to the authenticated user).",inputSchema:{type:"object",properties:{userId:{type:"string",description:"User id (default: authenticated user)"}}},handler:async(e,t,i)=>e.availabilityGet(t.userId||i.config.userId),readOnly:!0},{name:"appoiint_set_availability",description:"Set weekly availability: one time window applied to the given days. Mirrors PUT /availability/:userId (body: userId, organizationId, workingHours, timezone).",inputSchema:{type:"object",properties:{from:{type:"string",description:"Start time HH:MM (default 09:00)"},to:{type:"string",description:"End time HH:MM (default 17:00)"},days:{type:"array",items:{type:"number"},description:"Days of week 0=Sun..6=Sat (default Mon-Fri [1,2,3,4,5])"},timezone:{type:"string",description:"IANA timezone (default Europe/London)"},organizationId:{type:"string",description:"Org id (auto-resolved from current availability or event types if omitted)"},userId:{type:"string",description:"Target user (default: authenticated user)"}}},handler:async(e,t,i)=>{const n=t.userId||i.config.userId;if(!n)throw new Error("No userId available — pass userId explicitly.");const a=t.from||"09:00",r=t.to||"17:00";if(!/^\d{2}:\d{2}$/.test(a)||!/^\d{2}:\d{2}$/.test(r))throw new Error("Times must be HH:MM (e.g. 09:00)");const o=Array.isArray(t.days)&&t.days.length?t.days:[1,2,3,4,5],s=[0,1,2,3,4,5,6].map(e=>({dayOfWeek:e,isAvailable:o.includes(e),timeSlots:o.includes(e)?[{startTime:a,endTime:r}]:[]})),p=t.organizationId||await _resolveOrgId(e,n);if(!p)throw new Error("Could not resolve organizationId — pass organizationId explicitly.");const d=t.timezone||"Europe/London";return e.availabilityUpdate(n,{userId:n,organizationId:p,workingHours:s,timezone:d})},readOnly:!1},{name:"appoiint_list_domains",description:"List custom meeting-page domains for this tenant.",inputSchema:{type:"object",properties:{}},handler:async e=>_domainArray(await e.domains()),readOnly:!0},{name:"appoiint_register_domain",description:"Register a custom domain for the tenant meetings page. Returns the CNAME target to publish, then verify with appoiint_verify_domain.",inputSchema:{type:"object",properties:{hostname:{type:"string",description:"e.g. meetings.yourdomain.com"}},required:["hostname"]},handler:async(e,t,i)=>{if(!/^[a-zA-Z0-9][a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(t.hostname))throw new Error("Invalid hostname");if(!i.config.userId)throw new Error("No userId on this API key — cannot register a tenant domain.");const n=_unwrap(await e.domainRegister({hostname:t.hostname,domainType:"custom",tenantId:i.config.userId,tenantType:"user",baseDomain:"appoiint.ai"})),a=n.dnsVerification&&n.dnsVerification.expectedValue;return{registered:!0,hostname:t.hostname,domainId:_idOf(n),cnameTarget:a||null,next:a?`Publish CNAME ${t.hostname} -> ${a}, then call appoiint_verify_domain.`:"SDK returned no CNAME target — inspect with appoiint_domain_status."}},readOnly:!1},{name:"appoiint_verify_domain",description:"Verify DNS for a custom domain and auto-activate it once verified (custom domains ride the wildcard cert, so activation gates on DNS only).",inputSchema:{type:"object",properties:{domain:{type:"string",description:"Hostname or domain id"}},required:["domain"]},handler:async(e,t)=>{const i=await _findDomain(e,t.domain);if(!i)throw new Error(`Domain not found: ${t.domain}`);const n=await e.domainVerify(_idOf(i)),a=_unwrap(n),r=a.verified||a.dnsVerification&&a.dnsVerification.verified||n.success&&"active"===a.status;let o=null;if(r&&"active"!==a.status)try{o=_unwrap(await e.domainActivate(_idOf(i)))}catch(e){o={error:e.message}}return{hostname:i.hostname,verified:!!r,status:o&&o.status||a.status||null,activate:o}},readOnly:!1},{name:"appoiint_domain_status",description:"DNS + SSL status for a custom domain.",inputSchema:{type:"object",properties:{domain:{type:"string",description:"Hostname or domain id"}},required:["domain"]},handler:async(e,t)=>{const i=await _findDomain(e,t.domain);if(!i)throw new Error(`Domain not found: ${t.domain}`);const n=_unwrap(await e.domainDnsStatus(_idOf(i)));return{hostname:i.hostname,domainId:_idOf(i),status:i.status,dns:n}},readOnly:!0},{name:"appoiint_remove_domain",description:"Delete a custom domain. IRREVERSIBLE — requires confirm=true.",inputSchema:{type:"object",properties:{domain:{type:"string",description:"Hostname or domain id"},confirm:{type:"boolean",description:"Must be true to delete"}},required:["domain"]},handler:async(e,t)=>{if(!0!==t.confirm)throw new Error("Refusing to delete without confirm=true (this cannot be undone).");const i=await _findDomain(e,t.domain);if(!i)throw new Error(`Domain not found: ${t.domain}`);const n=await e.domainDelete(_idOf(i));return{removed:!0,hostname:i.hostname,result:n}},readOnly:!1},{name:"appoiint_page",description:"Show the tenant meetings-page profile (name, title, company, website, slug — drives the vCard + OG tags).",inputSchema:{type:"object",properties:{}},handler:async e=>e.profileGet(),readOnly:!0},{name:"appoiint_page_set",description:"Update the tenant meetings-page profile. Verified-writable fields only: firstName, lastName, company, title (stored as position), phone (mobile_no), website (powers the vCard VISIT CTA).",inputSchema:{type:"object",properties:{firstName:{type:"string"},lastName:{type:"string"},company:{type:"string"},title:{type:"string",description:"Job title (stored as position)"},phone:{type:"string"},website:{type:"string",description:"https URL — powers the vCard VISIT CTA"}}},handler:async(e,t)=>{const i={};if(void 0!==t.firstName&&(i.firstName=t.firstName),void 0!==t.lastName&&(i.lastName=t.lastName),void 0!==t.company&&(i.company=t.company),void 0!==t.title&&(i.position=t.title),void 0!==t.phone&&(i.mobile_no=t.phone),void 0!==t.website&&(i.website=t.website),!Object.keys(i).length)throw new Error("No fields to update.");return e.profileUpdate(i)},readOnly:!1},{name:"appoiint_update_event",description:"Update an existing event type (PUT /eventtypes/:id). Pass only the fields to change.",inputSchema:{type:"object",properties:{eventTypeId:{type:"string",description:"Event type id"},updates:{type:"object",description:"Fields to update (name, duration, description, pricing, ...)"}},required:["eventTypeId","updates"]},handler:async(e,t)=>e.updateEvent(t.eventTypeId,t.updates),readOnly:!1},{name:"appoiint_resolve_domain",description:"Resolve which tenant a hostname routes to (GET /domains/resolve).",inputSchema:{type:"object",properties:{hostname:{type:"string"}},required:["hostname"]},handler:async(e,t)=>e.domainResolve(t.hostname),readOnly:!0},{name:"appoiint_stt_status",description:"Speech-to-text provider status.",inputSchema:{type:"object",properties:{}},handler:async e=>e.sttStatus(),readOnly:!0},{name:"appoiint_costs",description:"Usage costs summary for this account.",inputSchema:{type:"object",properties:{}},handler:async e=>e.costs(),readOnly:!0},{name:"appoiint_costs_room",description:"Usage costs for a specific room.",inputSchema:{type:"object",properties:{roomId:{type:"string"}},required:["roomId"]},handler:async(e,t)=>e.costsByRoom(t.roomId),readOnly:!0},{name:"appoiint_costs_pricing",description:"Current usage pricing table.",inputSchema:{type:"object",properties:{}},handler:async e=>e.costsPricing(),readOnly:!0},{name:"appoiint_channel_ack",description:"Acknowledge an agent-channel message by id.",inputSchema:{type:"object",properties:{messageId:{type:"string"}},required:["messageId"]},handler:async(e,t)=>e.channelAck(t.messageId),readOnly:!1},{name:"appoiint_signups",description:"SUPERADMIN: signup counts over the last N days.",inputSchema:{type:"object",properties:{days:{type:"number",description:"Window in days"}}},handler:async(e,t)=>e.signups(t.days),readOnly:!0},{name:"appoiint_audit_export",description:"SUPERADMIN: export the audit trail (json or csv).",inputSchema:{type:"object",properties:{format:{type:"string",description:"json or csv (default json)"}}},handler:async(e,t)=>e.auditExport(t.format||"json"),readOnly:!0},{name:"appoiint_admin_notifications",description:"SUPERADMIN: platform notifications feed.",inputSchema:{type:"object",properties:{since:{type:"string",description:"ISO timestamp — only newer items"}}},handler:async(e,t)=>e.notifications(t.since),readOnly:!0},{name:"appoiint_tickets",description:"SUPERADMIN: list support tickets.",inputSchema:{type:"object",properties:{status:{type:"string",description:"Filter: open, closed, ..."}}},handler:async(e,t)=>e.tickets(t.status),readOnly:!0},{name:"appoiint_ticket_show",description:"SUPERADMIN: show a support ticket.",inputSchema:{type:"object",properties:{ticketId:{type:"string"}},required:["ticketId"]},handler:async(e,t)=>e.ticketShow(t.ticketId),readOnly:!0},{name:"appoiint_ticket_reply",description:"SUPERADMIN: reply to a support ticket.",inputSchema:{type:"object",properties:{ticketId:{type:"string"},message:{type:"string"}},required:["ticketId","message"]},handler:async(e,t)=>e.ticketReply(t.ticketId,t.message),readOnly:!1},{name:"appoiint_ticket_close",description:"SUPERADMIN: close a support ticket.",inputSchema:{type:"object",properties:{ticketId:{type:"string"}},required:["ticketId"]},handler:async(e,t)=>e.ticketClose(t.ticketId),readOnly:!1},{name:"appoiint_admin_users",description:"SUPERADMIN: list platform users.",inputSchema:{type:"object",properties:{}},handler:async e=>e.adminUsers(),readOnly:!0},{name:"appoiint_admin_user_create",description:"SUPERADMIN: create a platform user.",inputSchema:{type:"object",properties:{email:{type:"string"},name:{type:"string"},organizationName:{type:"string"},planId:{type:"string",description:"e.g. appoiint-enterprise"},skipPayment:{type:"boolean"}},required:["email","name"]},handler:async(e,t)=>e.adminUserCreate(t),readOnly:!1},{name:"appoiint_admin_user_reset",description:"SUPERADMIN: reset a user password.",inputSchema:{type:"object",properties:{email:{type:"string"},newPassword:{type:"string",description:"Omit to auto-generate"}},required:["email"]},handler:async(e,t)=>e.adminUserReset(t.email,t.newPassword?{newPassword:t.newPassword}:{}),readOnly:!1},{name:"appoiint_admin_user_delete",description:"SUPERADMIN: delete a platform user. IRREVERSIBLE — requires confirm=true.",inputSchema:{type:"object",properties:{email:{type:"string"},confirm:{type:"boolean",description:"Must be true to delete"}},required:["email"]},handler:async(e,t)=>{if(!0!==t.confirm)throw new Error("Refusing to delete without confirm=true (this cannot be undone).");return e.adminUserDelete(t.email)},readOnly:!1},{name:"appoiint_admin_tenant_create",description:"SUPERADMIN: create a tenant.",inputSchema:{type:"object",properties:{data:{type:"object",description:"Tenant payload (slug, name, plan, ...)"}},required:["data"]},handler:async(e,t)=>e.adminTenantCreate(t.data),readOnly:!1},{name:"appoiint_admin_tenant_set",description:"SUPERADMIN: tenant lifecycle action — plan | verify | suspend | activate.",inputSchema:{type:"object",properties:{slug:{type:"string",description:"Tenant slug"},action:{type:"string",description:"plan | verify | suspend | activate"},plan:{type:"string",description:"Plan id (required when action=plan)"}},required:["slug","action"]},handler:async(e,t)=>{switch(t.action){case"plan":if(!t.plan)throw new Error("action=plan requires the plan parameter");return e.adminTenantPlan(t.slug,t.plan);case"verify":return e.adminTenantVerify(t.slug);case"suspend":return e.adminTenantSuspend(t.slug);case"activate":return e.adminTenantActivate(t.slug);default:throw new Error(`Unknown action: ${t.action} (use plan | verify | suspend | activate)`)}},readOnly:!1},{name:"appoiint_admin_tenant_delete",description:"SUPERADMIN: delete a tenant. IRREVERSIBLE — requires confirm=true.",inputSchema:{type:"object",properties:{slug:{type:"string"},confirm:{type:"boolean",description:"Must be true to delete"}},required:["slug"]},handler:async(e,t)=>{if(!0!==t.confirm)throw new Error("Refusing to delete without confirm=true (this cannot be undone).");return e.adminTenantDelete(t.slug)},readOnly:!1},{name:"appoiint_admin_billing",description:"SUPERADMIN: platform billing/revenue summary.",inputSchema:{type:"object",properties:{}},handler:async e=>e.adminBilling(),readOnly:!0},{name:"appoiint_admin_ice",description:"Diagnose WebRTC STUN/TURN for this deployment. mode=relay (default) allocates on the advertised TURN hosts and relays datagrams between their relay ports — the probing network only carries the 3478 control channel, so a drop is a fact about the TURN servers (relay port range firewall, coturn peer policy). mode=reachability checks binding + authenticated allocate per ICE URL, control-gated (INCONCLUSIVE is not a fault). Only probes servers advertised by /stt/config.",inputSchema:{type:"object",properties:{mode:{type:"string",enum:["relay","reachability"],description:"Default relay"},transport:{type:"string",enum:["tcp","udp"],description:"Control-channel transport for relay mode (default tcp)"},hosts:{type:"array",items:{type:"string"},description:"Extra hosts advertised in /stt/config (e.g. a coturn box listed only as STUN) to include as TURN relay targets"},timeoutMs:{type:"number",description:"Per-step timeout (default 6000)"}}},handler:async(e,t)=>{const i=require("./config").loadConfig([]);return require("./lib/turnProbe").iceCheck({baseUrl:i.baseUrl,...t})},readOnly:!0},{name:"appoiint_admin_discount",description:"SUPERADMIN: apply a discount.",inputSchema:{type:"object",properties:{data:{type:"object",description:"Discount payload"}},required:["data"]},handler:async(e,t)=>e.adminDiscount(t.data),readOnly:!1}];function _notesFrom(e,t){let i=Array.isArray(e.notes)?e.notes:null;if(!i&&e.roomId&&t&&t.meetingSessions){const n=t.meetingSessions.get(e.roomId);n&&(i=n.getTranscripts(!1))}return Array.isArray(i)?i.map(e=>"string"==typeof e?{speaker:"Unknown",text:e,timestamp:Date.now()}:{speaker:e.speaker||e.peer_name||"Unknown",text:e.text||"",timestamp:e.timestamp||Date.now()}).filter(e=>e.text):[]}function _unwrap(e){let t=e;for(;t&&"object"==typeof t&&t.data&&!Array.isArray(t);)t=t.data;return t||{}}function _domainArray(e){const t=_unwrap(e);return Array.isArray(t)?t:t.domains||t.tenantDomains||[]}function _idOf(e){return e.tenantDomainId||e._id||e.id||null}async function _findDomain(e,t){return _domainArray(await e.domains()).find(e=>e.hostname===t||_idOf(e)===t)||null}async function _resolveOrgId(e,t){try{const i=_unwrap(await e.availabilityGet(t)),n=i.settings&&i.settings.organizationId||i.organizationId;if(n)return n}catch{}try{const t=await e.events(),i=(Array.isArray(t)?t:t.eventTypes||t.data&&(Array.isArray(t.data)?t.data:t.data.eventTypes)||[]).find(e=>e.organization&&e.organization.id);if(i)return i.organization.id}catch{}return null}module.exports={TOOLS:TOOLS};
|