@getdial/cli 0.13.1 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +13 -1
  2. package/dist/cli.js +5 -0
  3. package/dist/commands/call/get.js +28 -36
  4. package/dist/commands/call/list.js +20 -37
  5. package/dist/commands/call/send.js +52 -65
  6. package/dist/commands/doctor.js +2 -68
  7. package/dist/commands/mcp.js +17 -0
  8. package/dist/commands/message/list.js +20 -37
  9. package/dist/commands/message/send.js +22 -35
  10. package/dist/commands/number/list.js +21 -29
  11. package/dist/commands/number/purchase.js +23 -32
  12. package/dist/commands/number/set.js +19 -40
  13. package/dist/commands/onboard.js +36 -67
  14. package/dist/commands/signup.js +23 -24
  15. package/dist/commands/wait-for.js +33 -67
  16. package/dist/lib/cli-error.js +24 -0
  17. package/dist/lib/ops/account.js +136 -0
  18. package/dist/lib/ops/auth.js +18 -0
  19. package/dist/lib/ops/calls.js +33 -0
  20. package/dist/lib/ops/errors.js +23 -0
  21. package/dist/lib/ops/events.js +63 -0
  22. package/dist/lib/ops/listen.js +51 -0
  23. package/dist/lib/ops/local-targets.js +35 -0
  24. package/dist/lib/ops/messages.js +26 -0
  25. package/dist/lib/ops/numbers.js +39 -0
  26. package/dist/lib/supervisor/systemd.js +7 -23
  27. package/dist/mcp/register.js +27 -0
  28. package/dist/mcp/result.js +6 -0
  29. package/dist/mcp/server.js +20 -0
  30. package/dist/mcp/tool.js +1 -0
  31. package/dist/mcp/tools/add-command-target.js +22 -0
  32. package/dist/mcp/tools/add-url-target.js +26 -0
  33. package/dist/mcp/tools/get-account-status.js +13 -0
  34. package/dist/mcp/tools/get-call.js +16 -0
  35. package/dist/mcp/tools/index.js +41 -0
  36. package/dist/mcp/tools/list-calls.js +22 -0
  37. package/dist/mcp/tools/list-local-targets.js +12 -0
  38. package/dist/mcp/tools/list-messages.js +22 -0
  39. package/dist/mcp/tools/list-numbers.js +12 -0
  40. package/dist/mcp/tools/listen-install.js +13 -0
  41. package/dist/mcp/tools/listen-status.js +12 -0
  42. package/dist/mcp/tools/listen-uninstall.js +12 -0
  43. package/dist/mcp/tools/onboard.js +34 -0
  44. package/dist/mcp/tools/place-call.js +33 -0
  45. package/dist/mcp/tools/purchase-number.js +22 -0
  46. package/dist/mcp/tools/remove-local-target.js +16 -0
  47. package/dist/mcp/tools/send-message.js +22 -0
  48. package/dist/mcp/tools/set-number-properties.js +20 -0
  49. package/dist/mcp/tools/sign-up.js +18 -0
  50. package/dist/mcp/tools/wait-for-event.js +25 -0
  51. package/package.json +2 -1
  52. package/skills.tar.gz +0 -0
@@ -0,0 +1,63 @@
1
+ import { paths } from "../paths.js";
2
+ import { supervisorStatus } from "../supervisor/index.js";
3
+ import { parseFieldArg, parseRegexArg } from "../event-filter.js";
4
+ import { currentSize, findLatestMatch, tailUntilMatch } from "../log-tail.js";
5
+ import { apiPost } from "../api.js";
6
+ import { requireAuth } from "./auth.js";
7
+ import { DialError } from "./errors.js";
8
+ const PER_POLL_SECONDS = 30;
9
+ /**
10
+ * Wait for a matching account event. Tails the local listen log when the daemon is
11
+ * running, otherwise long-polls the REST API. Console-free: returns what to print and
12
+ * whether it timed out; throws DialError for auth/API failures.
13
+ */
14
+ export async function waitForEvent(opts) {
15
+ const spec = {
16
+ eventType: opts.eventType,
17
+ fields: opts.fields.map(parseFieldArg),
18
+ regexes: opts.regexes.map(parseRegexArg),
19
+ };
20
+ const status = supervisorStatus();
21
+ if (status.installed && status.running)
22
+ return waitFromLog(spec, opts);
23
+ return waitFromApi(spec, opts);
24
+ }
25
+ async function waitFromLog(spec, opts) {
26
+ const file = paths().listenLog;
27
+ const startOffset = currentSize(file);
28
+ const hit = await tailUntilMatch(file, spec, startOffset, opts.timeoutSeconds * 1000);
29
+ if (hit)
30
+ return { source: "log", timedOut: false, event: hit.obj, line: hit.line };
31
+ const fallback = findLatestMatch(file, spec);
32
+ if (fallback)
33
+ return { source: "log", timedOut: true, event: fallback.obj, line: fallback.line };
34
+ return { source: null, timedOut: true, event: null, line: null };
35
+ }
36
+ async function waitFromApi(spec, opts) {
37
+ const auth = requireAuth();
38
+ const filters = {};
39
+ for (const f of spec.fields)
40
+ filters[f.name] = f.value;
41
+ const regexFilters = {};
42
+ for (const r of spec.regexes)
43
+ regexFilters[r.name] = { pattern: r.regex.source, flags: r.regex.flags };
44
+ const deadline = Date.now() + opts.timeoutSeconds * 1000;
45
+ while (Date.now() < deadline) {
46
+ const remainingSec = Math.max(1, Math.ceil((deadline - Date.now()) / 1000));
47
+ const timeout = Math.min(PER_POLL_SECONDS, remainingSec);
48
+ const res = await apiPost("/api/v1/events/wait", {
49
+ eventType: spec.eventType,
50
+ filters: Object.keys(filters).length > 0 ? filters : undefined,
51
+ regexFilters: Object.keys(regexFilters).length > 0 ? regexFilters : undefined,
52
+ timeout,
53
+ }, auth.apiKey);
54
+ if (res.ok && res.data?.event) {
55
+ return { source: "api", timedOut: false, event: res.data.event, line: JSON.stringify(res.data.event) };
56
+ }
57
+ if (res.ok === false && res.status === 408)
58
+ continue;
59
+ if (res.ok === false)
60
+ throw new DialError("api_fallback_failed", res.error, res.status);
61
+ }
62
+ return { source: "api", timedOut: true, event: null, line: null };
63
+ }
@@ -0,0 +1,51 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { installSupervised, uninstallSupervised, supervisorStatus, supervisorAvailability, lastEventAtFromLog, } from "../supervisor/index.js";
3
+ import { paths } from "../paths.js";
4
+ import { requireAuth } from "./auth.js";
5
+ import { DialError } from "./errors.js";
6
+ function resolveDialPath() {
7
+ return process.env.DIAL_BIN_OVERRIDE ?? process.argv[1] ?? "dial";
8
+ }
9
+ export function listenInstall() {
10
+ requireAuth();
11
+ const supervisor = supervisorAvailability();
12
+ if (!supervisor.available) {
13
+ throw new DialError("supervisor_unavailable", supervisor.reason);
14
+ }
15
+ try {
16
+ return installSupervised(resolveDialPath());
17
+ }
18
+ catch (err) {
19
+ throw new DialError("install_failed", err instanceof Error ? err.message : String(err));
20
+ }
21
+ }
22
+ export function listenUninstall() {
23
+ try {
24
+ uninstallSupervised();
25
+ return { ok: true };
26
+ }
27
+ catch (err) {
28
+ throw new DialError("uninstall_failed", err instanceof Error ? err.message : String(err));
29
+ }
30
+ }
31
+ export function listenStatus() {
32
+ const s = supervisorStatus();
33
+ const lastEventAt = lastEventAtFromLog(paths().listenLog);
34
+ let lastEvents = [];
35
+ try {
36
+ const raw = readFileSync(paths().listenLog, "utf8");
37
+ const lines = raw.trim().split("\n").filter(Boolean);
38
+ lastEvents = lines.slice(-5).map((l) => {
39
+ try {
40
+ return JSON.parse(l);
41
+ }
42
+ catch {
43
+ return l;
44
+ }
45
+ });
46
+ }
47
+ catch {
48
+ // no log yet — leave empty
49
+ }
50
+ return { installed: s.installed, running: s.running, pid: s.pid, unitPath: s.unitPath, lastEventAt, lastEvents };
51
+ }
@@ -0,0 +1,35 @@
1
+ import { addTarget, removeTarget, listTargets, targetId, LocalTargetError, DEFAULT_SIGNATURE_HEADER, } from "../local-targets.js";
2
+ import { DialError } from "./errors.js";
3
+ function wrap(fn) {
4
+ try {
5
+ return fn();
6
+ }
7
+ catch (err) {
8
+ if (err instanceof LocalTargetError)
9
+ throw new DialError(err.code, err.message);
10
+ throw err;
11
+ }
12
+ }
13
+ export function addUrlTarget(opts) {
14
+ const { added } = wrap(() => addTarget({
15
+ kind: "url",
16
+ url: opts.url,
17
+ secret: opts.secret,
18
+ signatureHeader: opts.signatureHeader ?? (opts.secret ? DEFAULT_SIGNATURE_HEADER : undefined),
19
+ bearer: opts.bearer,
20
+ timeoutSeconds: opts.timeoutSeconds,
21
+ }));
22
+ return { added, url: opts.url };
23
+ }
24
+ export function addCommandTarget(opts) {
25
+ const args = opts.args ?? [];
26
+ const { added } = wrap(() => addTarget({ kind: "cmd", path: opts.path, args, timeoutSeconds: opts.timeoutSeconds }));
27
+ return { added, path: opts.path, args };
28
+ }
29
+ export function removeLocalTarget(id) {
30
+ const { removed } = removeTarget(id);
31
+ return { removed, id };
32
+ }
33
+ export function listLocalTargets() {
34
+ return listTargets().map((t) => ({ kind: t.kind, id: targetId(t), target: t }));
35
+ }
@@ -0,0 +1,26 @@
1
+ import { apiGet, apiPost } from "../api.js";
2
+ import { requireAuth, requireFromNumberId } from "./auth.js";
3
+ import { DialError } from "./errors.js";
4
+ export async function sendMessage(opts) {
5
+ const auth = requireAuth();
6
+ const fromNumberId = requireFromNumberId(auth, opts.fromNumberId);
7
+ const res = await apiPost("/api/v1/messages", { to: opts.to, body: opts.body, channel: "sms", fromNumberId }, auth.apiKey);
8
+ if (!res.ok)
9
+ throw new DialError("send_failed", res.error, res.status);
10
+ return res.data.message;
11
+ }
12
+ export async function listMessages(opts) {
13
+ const auth = requireAuth();
14
+ const params = new URLSearchParams();
15
+ if (opts.numberId)
16
+ params.set("numberId", opts.numberId);
17
+ if (opts.direction)
18
+ params.set("direction", opts.direction);
19
+ if (opts.since)
20
+ params.set("since", opts.since);
21
+ const qs = params.toString();
22
+ const res = await apiGet(qs ? `/api/v1/messages?${qs}` : "/api/v1/messages", auth.apiKey);
23
+ if (!res.ok)
24
+ throw new DialError("list_failed", res.error, res.status);
25
+ return res.data.messages ?? [];
26
+ }
@@ -0,0 +1,39 @@
1
+ import { apiGet, apiPost, apiPatch } from "../api.js";
2
+ import { requireAuth } from "./auth.js";
3
+ import { DialError } from "./errors.js";
4
+ export async function listNumbers() {
5
+ const auth = requireAuth();
6
+ const res = await apiGet("/api/v1/numbers", auth.apiKey);
7
+ if (!res.ok)
8
+ throw new DialError("list_failed", res.error, res.status);
9
+ return { numbers: res.data.numbers ?? [], defaultNumberId: auth.phoneNumberId ?? null };
10
+ }
11
+ export async function purchaseNumber(opts) {
12
+ const auth = requireAuth();
13
+ const body = { inboundInstruction: opts.inboundInstruction };
14
+ if (opts.country)
15
+ body.country = opts.country;
16
+ if (opts.areaCode)
17
+ body.areaCode = opts.areaCode;
18
+ const res = await apiPost("/api/v1/numbers", body, auth.apiKey);
19
+ if (!res.ok)
20
+ throw new DialError("purchase_failed", res.error, res.status);
21
+ return res.data.number;
22
+ }
23
+ export async function setNumberProperties(opts) {
24
+ const auth = requireAuth();
25
+ // The REST API keys numbers by id; the CLI/tool takes the E.164 number for ergonomics,
26
+ // so resolve it to its id first.
27
+ const list = await apiGet("/api/v1/numbers", auth.apiKey);
28
+ if (!list.ok)
29
+ throw new DialError("list_failed", list.error, list.status);
30
+ const match = list.data.numbers.find((n) => n.number === opts.number);
31
+ if (!match) {
32
+ const known = list.data.numbers.map((n) => n.number).join(", ") || "(none)";
33
+ throw new DialError("number_not_found", `No phone number ${opts.number} on your account. Yours: ${known}.`);
34
+ }
35
+ const res = await apiPatch(`/api/v1/numbers/${match.id}`, { inboundInstruction: opts.inboundInstruction }, auth.apiKey);
36
+ if (!res.ok)
37
+ throw new DialError("update_failed", res.error, res.status);
38
+ return res.data.number;
39
+ }
@@ -8,20 +8,6 @@ function isSystemctlNotLoaded(err) {
8
8
  const text = stderr ? stderr.toString() : "";
9
9
  return /not loaded|does not exist|Unit .* not found/i.test(text);
10
10
  }
11
- /**
12
- * True when the error means systemd simply isn't usable here — no systemctl on
13
- * PATH, or no user session bus (sandbox / container / CI / non-systemd distro).
14
- * These are expected on many machines, so they should be logged at debug, not
15
- * dumped as a scary warning with a full stack trace.
16
- */
17
- function isSystemdUnavailable(err) {
18
- const e = err;
19
- if (e.code === "ENOENT")
20
- return true; // systemctl / loginctl not installed
21
- const stderr = e.stderr ? e.stderr.toString() : "";
22
- const text = stderr || e.message || "";
23
- return /Failed to connect to bus|No medium found|XDG_RUNTIME_DIR|not been booted with systemd|Connection refused|Permission denied/i.test(text);
24
- }
25
11
  function isEnoent(err) {
26
12
  return err.code === "ENOENT";
27
13
  }
@@ -88,11 +74,11 @@ export function systemctlStatus() {
88
74
  return { running: active, pid: active && pid > 0 ? pid : null };
89
75
  }
90
76
  catch (err) {
91
- // Expected on machines without a systemd user session stay quiet there.
92
- if (isSystemdUnavailable(err))
93
- logger.debug({ err }, "systemd not available; treating listen service as not running");
94
- else
95
- logger.warn({ err }, "systemctl show failed");
77
+ // Best-effort status probe. Any failure systemctl missing, no user session
78
+ // bus (sandbox/container/CI), unit unknown — just means "not running" for our
79
+ // purposes. Log at debug so we never dump a stack trace to a user's stderr;
80
+ // run with DIAL_LOG_LEVEL=debug to see it.
81
+ logger.debug({ err }, "systemctl show failed; treating listen service as not running");
96
82
  return { running: false, pid: null };
97
83
  }
98
84
  }
@@ -102,10 +88,8 @@ export function lingerEnabled(user) {
102
88
  return /Linger=yes/.test(out);
103
89
  }
104
90
  catch (err) {
105
- if (isSystemdUnavailable(err))
106
- logger.debug({ err, user }, "loginctl unavailable; assuming linger disabled");
107
- else
108
- logger.warn({ err, user }, "loginctl show-user failed; assuming linger disabled");
91
+ // Best-effort; debug only (see systemctlStatus). Assume linger disabled.
92
+ logger.debug({ err, user }, "loginctl show-user failed; assuming linger disabled");
109
93
  return false;
110
94
  }
111
95
  }
@@ -0,0 +1,27 @@
1
+ import { ZodError } from "zod";
2
+ import { tools } from "./tools/index.js";
3
+ import { errorResult } from "./result.js";
4
+ import { isDialError } from "../lib/ops/errors.js";
5
+ import { logger } from "../lib/log.js";
6
+ /**
7
+ * Register every tool onto the MCP server. DialError/ZodError surface their message to
8
+ * the model as a tool error; unexpected errors are logged (to stderr — safe for stdio)
9
+ * and surfaced generically.
10
+ */
11
+ export function registerTools(server) {
12
+ for (const tool of tools) {
13
+ server.registerTool(tool.name, tool.config, async (args) => {
14
+ try {
15
+ return await tool.run(args ?? {});
16
+ }
17
+ catch (err) {
18
+ if (isDialError(err))
19
+ return errorResult(err.message);
20
+ if (err instanceof ZodError)
21
+ return errorResult(`Invalid input: ${err.message}`);
22
+ logger.error({ err, tool: tool.name }, "mcp tool error");
23
+ return errorResult("Internal error running tool.");
24
+ }
25
+ });
26
+ }
27
+ }
@@ -0,0 +1,6 @@
1
+ export function jsonResult(data) {
2
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
3
+ }
4
+ export function errorResult(message) {
5
+ return { content: [{ type: "text", text: message }], isError: true };
6
+ }
@@ -0,0 +1,20 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { VERSION } from "../lib/version.js";
3
+ import { registerTools } from "./register.js";
4
+ // Server identity shown by MCP clients. Mirrors the remote Dial MCP server: name/title +
5
+ // the scalable Dial favicon (served same-origin, https). SVG with sizes:["any"] covers
6
+ // every render size.
7
+ const SERVER_INFO = {
8
+ name: "dial",
9
+ title: "Dial",
10
+ version: VERSION,
11
+ websiteUrl: "https://getdial.ai",
12
+ icons: [
13
+ { src: "https://getdial.ai/favicon.svg", mimeType: "image/svg+xml", sizes: ["any"] },
14
+ ],
15
+ };
16
+ export function buildServer() {
17
+ const server = new McpServer(SERVER_INFO);
18
+ registerTools(server);
19
+ return server;
20
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,22 @@
1
+ import { z } from "zod";
2
+ import { jsonResult } from "../result.js";
3
+ import { addCommandTarget } from "../../lib/ops/local-targets.js";
4
+ const inputSchema = {
5
+ path: z.string().min(1).describe("Absolute path to an executable the daemon spawns once per event"),
6
+ args: z.array(z.string()).optional().describe("Extra args; the event JSON is appended as the final positional arg"),
7
+ timeoutSeconds: z.number().int().positive().optional().describe("Per-attempt timeout (default 5)"),
8
+ };
9
+ export const addCommandTargetTool = {
10
+ name: "add_command_target",
11
+ config: {
12
+ title: "Add Command Fan-out Target",
13
+ description: "Register an executable the local listen daemon runs once per event (event JSON as the final arg).",
14
+ inputSchema,
15
+ annotations: {},
16
+ },
17
+ run: async (args) => jsonResult(addCommandTarget({
18
+ path: args.path,
19
+ args: args.args,
20
+ timeoutSeconds: args.timeoutSeconds,
21
+ })),
22
+ };
@@ -0,0 +1,26 @@
1
+ import { z } from "zod";
2
+ import { jsonResult } from "../result.js";
3
+ import { addUrlTarget } from "../../lib/ops/local-targets.js";
4
+ const inputSchema = {
5
+ url: z.string().min(1).describe("Loopback HTTP endpoint the listen daemon POSTs each event JSON to"),
6
+ secret: z.string().optional().describe("HMAC-SHA256 key; the daemon signs each request body and sends the hex digest"),
7
+ signatureHeader: z.string().optional().describe("Header for the HMAC signature (default X-Dial-Signature; only with secret)"),
8
+ bearer: z.string().optional().describe("Static bearer token, sent as Authorization: Bearer <token>"),
9
+ timeoutSeconds: z.number().int().positive().optional().describe("Per-attempt timeout (default 5)"),
10
+ };
11
+ export const addUrlTargetTool = {
12
+ name: "add_url_target",
13
+ config: {
14
+ title: "Add URL Fan-out Target",
15
+ description: "Register a loopback HTTP endpoint the local listen daemon delivers each event to.",
16
+ inputSchema,
17
+ annotations: {},
18
+ },
19
+ run: async (args) => jsonResult(addUrlTarget({
20
+ url: args.url,
21
+ secret: args.secret,
22
+ signatureHeader: args.signatureHeader,
23
+ bearer: args.bearer,
24
+ timeoutSeconds: args.timeoutSeconds,
25
+ })),
26
+ };
@@ -0,0 +1,13 @@
1
+ import { jsonResult } from "../result.js";
2
+ import { accountStatus } from "../../lib/ops/account.js";
3
+ export const getAccountStatusTool = {
4
+ name: "get_account_status",
5
+ config: {
6
+ title: "Get Account Status",
7
+ description: "Report local Dial setup state: CLI version, backend reachability, sign-in/key validity, " +
8
+ "any pending OTP, the listen daemon state, and the recommended next step.",
9
+ inputSchema: {},
10
+ annotations: { readOnlyHint: true, openWorldHint: true },
11
+ },
12
+ run: async () => jsonResult(await accountStatus()),
13
+ };
@@ -0,0 +1,16 @@
1
+ import { z } from "zod";
2
+ import { jsonResult } from "../result.js";
3
+ import { getCall } from "../../lib/ops/calls.js";
4
+ const inputSchema = {
5
+ callId: z.string().min(1).describe("The call id to fetch"),
6
+ };
7
+ export const getCallTool = {
8
+ name: "get_call",
9
+ config: {
10
+ title: "Get Call",
11
+ description: "Fetch a single call by id — status, duration, and transcript when available.",
12
+ inputSchema,
13
+ annotations: { readOnlyHint: true, openWorldHint: true },
14
+ },
15
+ run: async (args) => jsonResult(await getCall(args.callId)),
16
+ };
@@ -0,0 +1,41 @@
1
+ import { listNumbersTool } from "./list-numbers.js";
2
+ import { purchaseNumberTool } from "./purchase-number.js";
3
+ import { setNumberPropertiesTool } from "./set-number-properties.js";
4
+ import { sendMessageTool } from "./send-message.js";
5
+ import { listMessagesTool } from "./list-messages.js";
6
+ import { placeCallTool } from "./place-call.js";
7
+ import { listCallsTool } from "./list-calls.js";
8
+ import { getCallTool } from "./get-call.js";
9
+ import { getAccountStatusTool } from "./get-account-status.js";
10
+ import { signUpTool } from "./sign-up.js";
11
+ import { onboardTool } from "./onboard.js";
12
+ import { waitForEventTool } from "./wait-for-event.js";
13
+ import { addUrlTargetTool } from "./add-url-target.js";
14
+ import { addCommandTargetTool } from "./add-command-target.js";
15
+ import { removeLocalTargetTool } from "./remove-local-target.js";
16
+ import { listLocalTargetsTool } from "./list-local-targets.js";
17
+ import { listenInstallTool } from "./listen-install.js";
18
+ import { listenUninstallTool } from "./listen-uninstall.js";
19
+ import { listenStatusTool } from "./listen-status.js";
20
+ /** Every tool registered on the local stdio MCP server. */
21
+ export const tools = [
22
+ listNumbersTool,
23
+ purchaseNumberTool,
24
+ setNumberPropertiesTool,
25
+ sendMessageTool,
26
+ listMessagesTool,
27
+ placeCallTool,
28
+ listCallsTool,
29
+ getCallTool,
30
+ getAccountStatusTool,
31
+ signUpTool,
32
+ onboardTool,
33
+ waitForEventTool,
34
+ addUrlTargetTool,
35
+ addCommandTargetTool,
36
+ removeLocalTargetTool,
37
+ listLocalTargetsTool,
38
+ listenInstallTool,
39
+ listenUninstallTool,
40
+ listenStatusTool,
41
+ ];
@@ -0,0 +1,22 @@
1
+ import { z } from "zod";
2
+ import { jsonResult } from "../result.js";
3
+ import { listCalls } from "../../lib/ops/calls.js";
4
+ const inputSchema = {
5
+ numberId: z.string().optional().describe("Filter to a single phone number id"),
6
+ direction: z.enum(["inbound", "outbound"]).optional().describe("Filter by direction"),
7
+ since: z.string().optional().describe("Only calls created after this ISO-8601 timestamp"),
8
+ };
9
+ export const listCallsTool = {
10
+ name: "list_calls",
11
+ config: {
12
+ title: "List Calls",
13
+ description: "List recent calls on your account, newest first.",
14
+ inputSchema,
15
+ annotations: { readOnlyHint: true, openWorldHint: true },
16
+ },
17
+ run: async (args) => jsonResult(await listCalls({
18
+ numberId: args.numberId,
19
+ direction: args.direction,
20
+ since: args.since,
21
+ })),
22
+ };
@@ -0,0 +1,12 @@
1
+ import { jsonResult } from "../result.js";
2
+ import { listLocalTargets } from "../../lib/ops/local-targets.js";
3
+ export const listLocalTargetsTool = {
4
+ name: "list_local_targets",
5
+ config: {
6
+ title: "List Fan-out Targets",
7
+ description: "List the local fan-out targets the listen daemon currently delivers events to.",
8
+ inputSchema: {},
9
+ annotations: { readOnlyHint: true },
10
+ },
11
+ run: async () => jsonResult(listLocalTargets()),
12
+ };
@@ -0,0 +1,22 @@
1
+ import { z } from "zod";
2
+ import { jsonResult } from "../result.js";
3
+ import { listMessages } from "../../lib/ops/messages.js";
4
+ const inputSchema = {
5
+ numberId: z.string().optional().describe("Filter to a single phone number id"),
6
+ direction: z.enum(["inbound", "outbound"]).optional().describe("Filter by direction"),
7
+ since: z.string().optional().describe("Only messages created after this ISO-8601 timestamp"),
8
+ };
9
+ export const listMessagesTool = {
10
+ name: "list_messages",
11
+ config: {
12
+ title: "List Messages",
13
+ description: "List recent messages on your account, newest first.",
14
+ inputSchema,
15
+ annotations: { readOnlyHint: true, openWorldHint: true },
16
+ },
17
+ run: async (args) => jsonResult(await listMessages({
18
+ numberId: args.numberId,
19
+ direction: args.direction,
20
+ since: args.since,
21
+ })),
22
+ };
@@ -0,0 +1,12 @@
1
+ import { jsonResult } from "../result.js";
2
+ import { listNumbers } from "../../lib/ops/numbers.js";
3
+ export const listNumbersTool = {
4
+ name: "list_numbers",
5
+ config: {
6
+ title: "List Phone Numbers",
7
+ description: "List the phone numbers on your Dial account, with the default number id.",
8
+ inputSchema: {},
9
+ annotations: { readOnlyHint: true, openWorldHint: true },
10
+ },
11
+ run: async () => jsonResult(await listNumbers()),
12
+ };
@@ -0,0 +1,13 @@
1
+ import { jsonResult } from "../result.js";
2
+ import { listenInstall } from "../../lib/ops/listen.js";
3
+ export const listenInstallTool = {
4
+ name: "listen_install",
5
+ config: {
6
+ title: "Install Listen Daemon",
7
+ description: "Install the background event daemon (launchd on macOS, systemd --user on Linux) so inbound " +
8
+ "SMS and call-ended events are delivered to this machine in real time.",
9
+ inputSchema: {},
10
+ annotations: {},
11
+ },
12
+ run: async () => jsonResult(listenInstall()),
13
+ };
@@ -0,0 +1,12 @@
1
+ import { jsonResult } from "../result.js";
2
+ import { listenStatus } from "../../lib/ops/listen.js";
3
+ export const listenStatusTool = {
4
+ name: "listen_status",
5
+ config: {
6
+ title: "Listen Daemon Status",
7
+ description: "Report the background event daemon's state (installed/running/pid) and the last few events.",
8
+ inputSchema: {},
9
+ annotations: { readOnlyHint: true },
10
+ },
11
+ run: async () => jsonResult(listenStatus()),
12
+ };
@@ -0,0 +1,12 @@
1
+ import { jsonResult } from "../result.js";
2
+ import { listenUninstall } from "../../lib/ops/listen.js";
3
+ export const listenUninstallTool = {
4
+ name: "listen_uninstall",
5
+ config: {
6
+ title: "Uninstall Listen Daemon",
7
+ description: "Stop and remove the background event daemon from this machine.",
8
+ inputSchema: {},
9
+ annotations: { destructiveHint: true },
10
+ },
11
+ run: async () => jsonResult(listenUninstall()),
12
+ };
@@ -0,0 +1,34 @@
1
+ import { z } from "zod";
2
+ import { jsonResult } from "../result.js";
3
+ import { onboard } from "../../lib/ops/account.js";
4
+ const inputSchema = {
5
+ code: z.string().min(1).describe("6-digit OTP from the sign-up email"),
6
+ verificationId: z.string().optional().describe("Explicit verification id (defaults to the local pending signup)"),
7
+ inboundInstruction: z.string().optional().describe("System prompt for inbound calls to a newly provisioned number (new accounts)"),
8
+ agents: z.array(z.string()).optional().describe("Agent names to install the Dial skill into (e.g. claude-code, cursor)"),
9
+ };
10
+ export const onboardTool = {
11
+ name: "onboard",
12
+ config: {
13
+ title: "Onboard",
14
+ description: "Verify the sign-up OTP and finish onboarding: saves the API key locally and optionally installs " +
15
+ "the Dial skill into named agents. Returns the account summary (the raw API key is never returned).",
16
+ inputSchema,
17
+ annotations: { openWorldHint: true },
18
+ },
19
+ run: async (args) => {
20
+ const r = await onboard({
21
+ code: args.code,
22
+ verificationId: args.verificationId,
23
+ inboundInstruction: args.inboundInstruction,
24
+ agents: args.agents,
25
+ });
26
+ // Never surface the raw API key to the model; it's saved to disk for the CLI to read.
27
+ const { apiKey: _omit, ...safe } = r;
28
+ void _omit;
29
+ return jsonResult({
30
+ ...safe,
31
+ listenAvailable: r.supervisor.available,
32
+ });
33
+ },
34
+ };
@@ -0,0 +1,33 @@
1
+ import { z } from "zod";
2
+ import { jsonResult } from "../result.js";
3
+ import { placeCall } from "../../lib/ops/calls.js";
4
+ const inputSchema = {
5
+ to: z.string().min(7).describe("Destination phone number, E.164 (e.g. +14155550123)"),
6
+ outboundInstruction: z.string().min(1).describe("System prompt for the AI voice agent on this call"),
7
+ language: z.string().default("en-US").describe("BCP-47 language tag for the call"),
8
+ fromNumberId: z.string().optional().describe("Number id to call from; defaults to your primary number"),
9
+ };
10
+ export const placeCallTool = {
11
+ name: "place_call",
12
+ config: {
13
+ title: "Place AI Voice Call",
14
+ description: "Place an outbound voice call handled by an AI agent. The call runs asynchronously — " +
15
+ "use wait_for_event to block until it ends, then get_call for the transcript.",
16
+ inputSchema,
17
+ annotations: { openWorldHint: true },
18
+ },
19
+ run: async (args) => {
20
+ const call = await placeCall({
21
+ to: args.to,
22
+ outboundInstruction: args.outboundInstruction,
23
+ language: args.language ?? "en-US",
24
+ fromNumberId: args.fromNumberId,
25
+ });
26
+ return jsonResult({
27
+ call,
28
+ hint: `The call is now in progress. To be notified when it ends, call wait_for_event with ` +
29
+ `eventType "call.ended" and field "callId=${call.id}". Then call get_call with callId ` +
30
+ `"${call.id}" for the final status, duration, and transcript.`,
31
+ });
32
+ },
33
+ };