@getdial/cli 0.33.1 → 0.33.3

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 (45) hide show
  1. package/dist/cli.js +1 -3
  2. package/dist/commands/call/list.js +5 -1
  3. package/dist/commands/listen/index.js +17 -3
  4. package/dist/commands/listen/install.js +7 -2
  5. package/dist/commands/listen/status.js +11 -7
  6. package/dist/commands/message/list.js +5 -1
  7. package/dist/commands/message/reply.js +5 -1
  8. package/dist/commands/number/set.js +6 -2
  9. package/dist/commands/onboard.js +17 -5
  10. package/dist/commands/signup.js +7 -1
  11. package/dist/commands/typing/start.js +5 -1
  12. package/dist/commands/typing/stop.js +5 -1
  13. package/dist/commands/uninstall.js +3 -1
  14. package/dist/commands/wait-for.js +2 -2
  15. package/dist/lib/api.js +24 -5
  16. package/dist/lib/cli-error.js +6 -1
  17. package/dist/lib/fanout.js +5 -3
  18. package/dist/lib/log-tail.js +5 -2
  19. package/dist/lib/log.js +2 -2
  20. package/dist/lib/ops/account.js +20 -7
  21. package/dist/lib/ops/calls.js +3 -1
  22. package/dist/lib/ops/events.js +6 -1
  23. package/dist/lib/ops/listen.js +8 -1
  24. package/dist/lib/ops/uninstall.js +9 -3
  25. package/dist/lib/pubnub.js +47 -9
  26. package/dist/lib/ref-params.js +37 -0
  27. package/dist/lib/skill-install.js +1 -1
  28. package/dist/lib/supervisor/index.js +6 -3
  29. package/dist/lib/supervisor/launchd.js +13 -4
  30. package/dist/lib/supervisor/systemd.js +3 -1
  31. package/dist/lib/versioned-file.js +1 -1
  32. package/dist/mcp/schemas.js +22 -5
  33. package/dist/mcp/server.js +1 -3
  34. package/dist/mcp/tools/add-command-target.js +14 -3
  35. package/dist/mcp/tools/add-url-target.js +22 -5
  36. package/dist/mcp/tools/get-account-status.js +3 -1
  37. package/dist/mcp/tools/onboard.js +26 -7
  38. package/dist/mcp/tools/place-call.js +34 -8
  39. package/dist/mcp/tools/purchase-number.js +21 -5
  40. package/dist/mcp/tools/reply-to-message.js +4 -1
  41. package/dist/mcp/tools/send-message.js +8 -2
  42. package/dist/mcp/tools/set-number-properties.js +34 -8
  43. package/dist/mcp/tools/wait-for-event.js +12 -3
  44. package/package.json +5 -1
  45. package/skills.tar.gz +0 -0
@@ -19,7 +19,9 @@ export function startWorker(apiKey, accountId) {
19
19
  let stopped = false;
20
20
  let consecutiveFailures = 0;
21
21
  let resolveStopped;
22
- const whenStopped = new Promise((r) => { resolveStopped = r; });
22
+ const whenStopped = new Promise((r) => {
23
+ resolveStopped = r;
24
+ });
23
25
  function logLine(obj) {
24
26
  rotateIfLarge(logFile, MAX_LOG_BYTES);
25
27
  appendJsonl(logFile, obj);
@@ -34,14 +36,24 @@ export function startWorker(apiKey, accountId) {
34
36
  }
35
37
  catch (err) {
36
38
  consecutiveFailures += 1;
37
- logLine({ ts: new Date().toISOString(), lifecycle: "token_refresh", ok: false, error: err instanceof Error ? err.message : String(err), consecutive_failures: consecutiveFailures });
39
+ logLine({
40
+ ts: new Date().toISOString(),
41
+ lifecycle: "token_refresh",
42
+ ok: false,
43
+ error: err instanceof Error ? err.message : String(err),
44
+ consecutive_failures: consecutiveFailures,
45
+ });
38
46
  if (consecutiveFailures >= REFRESH_FAILURES_BEFORE_EXIT) {
39
- logLine({ ts: new Date().toISOString(), lifecycle: "shutdown", reason: "refresh_failures_exceeded" });
47
+ logLine({
48
+ ts: new Date().toISOString(),
49
+ lifecycle: "shutdown",
50
+ reason: "refresh_failures_exceeded",
51
+ });
40
52
  await stop();
41
53
  process.exitCode = 1;
42
54
  return;
43
55
  }
44
- const backoff = Math.min(60, Math.pow(2, consecutiveFailures)) * 1000;
56
+ const backoff = Math.min(60, 2 ** consecutiveFailures) * 1000;
45
57
  refreshTimer = setTimeout(() => refresh(creds), backoff);
46
58
  }
47
59
  }
@@ -59,13 +71,25 @@ export function startWorker(apiKey, accountId) {
59
71
  pn?.unsubscribeAll();
60
72
  }
61
73
  catch (err) {
62
- logLine({ ts: new Date().toISOString(), lifecycle: "shutdown_error", phase: "unsubscribeAll", error: err instanceof Error ? err.message : String(err), stack: err instanceof Error ? err.stack : null });
74
+ logLine({
75
+ ts: new Date().toISOString(),
76
+ lifecycle: "shutdown_error",
77
+ phase: "unsubscribeAll",
78
+ error: err instanceof Error ? err.message : String(err),
79
+ stack: err instanceof Error ? err.stack : null,
80
+ });
63
81
  }
64
82
  try {
65
83
  pn?.destroy?.();
66
84
  }
67
85
  catch (err) {
68
- logLine({ ts: new Date().toISOString(), lifecycle: "shutdown_error", phase: "destroy", error: err instanceof Error ? err.message : String(err), stack: err instanceof Error ? err.stack : null });
86
+ logLine({
87
+ ts: new Date().toISOString(),
88
+ lifecycle: "shutdown_error",
89
+ phase: "destroy",
90
+ error: err instanceof Error ? err.message : String(err),
91
+ stack: err instanceof Error ? err.stack : null,
92
+ });
69
93
  }
70
94
  resolveStopped();
71
95
  return whenStopped;
@@ -76,7 +100,12 @@ export function startWorker(apiKey, accountId) {
76
100
  creds = await fetchSubscribeCreds(apiKey);
77
101
  }
78
102
  catch (err) {
79
- logLine({ ts: new Date().toISOString(), lifecycle: "startup", ok: false, error: err instanceof Error ? err.message : String(err) });
103
+ logLine({
104
+ ts: new Date().toISOString(),
105
+ lifecycle: "startup",
106
+ ok: false,
107
+ error: err instanceof Error ? err.message : String(err),
108
+ });
80
109
  process.exitCode = 1;
81
110
  resolveStopped();
82
111
  return;
@@ -94,11 +123,20 @@ export function startWorker(apiKey, accountId) {
94
123
  message: (ev) => {
95
124
  logLine({ ts: new Date().toISOString(), ...ev.message });
96
125
  void fanout(ev.message, logLine).catch((err) => {
97
- logLine({ ts: new Date().toISOString(), lifecycle: "fanout_error", error: err instanceof Error ? err.message : String(err) });
126
+ logLine({
127
+ ts: new Date().toISOString(),
128
+ lifecycle: "fanout_error",
129
+ error: err instanceof Error ? err.message : String(err),
130
+ });
98
131
  });
99
132
  },
100
133
  status: (s) => {
101
- logLine({ ts: new Date().toISOString(), lifecycle: "status", category: s.category, operation: s.operation ?? null });
134
+ logLine({
135
+ ts: new Date().toISOString(),
136
+ lifecycle: "status",
137
+ category: s.category,
138
+ operation: s.operation ?? null,
139
+ });
102
140
  },
103
141
  });
104
142
  pn.subscribe({ channels: [creds.channel] });
@@ -0,0 +1,37 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { paths } from "./paths.js";
4
+ import { logger } from "./log.js";
5
+ // Reads the attribution ref params the install script persisted to
6
+ // ${dataDir}/ref-params.txt and returns them base64-encoded for the
7
+ // X-Dial-Ref-Params header. The CLI forwards the file verbatim — no parsing, no
8
+ // allowlist here; the server decodes + validates. Cached per process (the file is
9
+ // write-once and stable for the CLI's lifetime).
10
+ let cache;
11
+ function compute() {
12
+ const file = join(paths().dataDir, "ref-params.txt");
13
+ try {
14
+ const text = readFileSync(file, "utf8");
15
+ if (!text.trim())
16
+ return null;
17
+ return Buffer.from(text, "utf8").toString("base64");
18
+ }
19
+ catch (err) {
20
+ // No file is the normal case (the user never went through an attributed
21
+ // install) — not an error worth logging. Anything else is unexpected.
22
+ if (err?.code === "ENOENT")
23
+ return null;
24
+ logger.warn({ err }, "failed to read ref-params.txt");
25
+ return null;
26
+ }
27
+ }
28
+ /** Base64 of ref-params.txt for the X-Dial-Ref-Params header, or null if absent. */
29
+ export function refParamsHeader() {
30
+ if (!cache)
31
+ cache = { value: compute() };
32
+ return cache.value;
33
+ }
34
+ /** Test-only: clear the per-process cache. */
35
+ export function resetRefParamsCache() {
36
+ cache = undefined;
37
+ }
@@ -1,5 +1,5 @@
1
1
  import { execFileSync } from "node:child_process";
2
- import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
2
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync, } from "node:fs";
3
3
  import { homedir, tmpdir } from "node:os";
4
4
  import { dirname, join, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
@@ -3,8 +3,8 @@ import { userInfo } from "node:os";
3
3
  import { dirname, join } from "node:path";
4
4
  import { paths } from "../paths.js";
5
5
  import { logger } from "../log.js";
6
- import { LAUNCHD_LABEL, launchctlBootoutSilent, launchctlLoad, launchctlStatus, launchctlUnload, launchdPlistPath, renderLaunchdPlist, writeLaunchdPlist } from "./launchd.js";
7
- import { lingerEnabled, renderSystemdUnit, systemctlDisable, systemctlEnableAndStart, systemctlStatus, systemdUnitPath, writeSystemdUnit } from "./systemd.js";
6
+ import { LAUNCHD_LABEL, launchctlBootoutSilent, launchctlLoad, launchctlStatus, launchctlUnload, launchdPlistPath, renderLaunchdPlist, writeLaunchdPlist, } from "./launchd.js";
7
+ import { lingerEnabled, renderSystemdUnit, systemctlDisable, systemctlEnableAndStart, systemctlStatus, systemdUnitPath, writeSystemdUnit, } from "./systemd.js";
8
8
  export function currentPlatform() {
9
9
  if (process.platform === "darwin")
10
10
  return "darwin";
@@ -30,7 +30,10 @@ export function supervisorAvailability() {
30
30
  return { available: false, reason: "XDG_RUNTIME_DIR is not set (no systemd user session)" };
31
31
  }
32
32
  if (!existsSync(`${runtimeDir}/systemd/private`)) {
33
- return { available: false, reason: "systemd user bus socket not found (sandbox or container without systemd --user)" };
33
+ return {
34
+ available: false,
35
+ reason: "systemd user bus socket not found (sandbox or container without systemd --user)",
36
+ };
34
37
  }
35
38
  return { available: true };
36
39
  }
@@ -35,7 +35,9 @@ export function renderLaunchdPlist(params) {
35
35
  // so we must prepend the directory of the currently running node (e.g. nvm's bin dir)
36
36
  // so the shebang can resolve. Falls back to /usr/local/bin which is where Homebrew puts node.
37
37
  const nodeDir = dirname(process.execPath);
38
- const programArguments = params.programArgs.map((arg) => ` <string>${arg}</string>`).join("\n");
38
+ const programArguments = params.programArgs
39
+ .map((arg) => ` <string>${arg}</string>`)
40
+ .join("\n");
39
41
  return `<?xml version="1.0" encoding="UTF-8"?>
40
42
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
41
43
  <plist version="1.0">
@@ -130,13 +132,20 @@ export function launchctlUnload(plistPath) {
130
132
  }
131
133
  export function launchctlStatus() {
132
134
  try {
133
- const out = execFileSync("launchctl", ["list"], { stdio: ["ignore", "pipe", "ignore"] }).toString();
134
- const line = out.split("\n").find((l) => l.endsWith(`\t${LAUNCHD_LABEL}`) || l.endsWith(` ${LAUNCHD_LABEL}`));
135
+ const out = execFileSync("launchctl", ["list"], {
136
+ stdio: ["ignore", "pipe", "ignore"],
137
+ }).toString();
138
+ const line = out
139
+ .split("\n")
140
+ .find((l) => l.endsWith(`\t${LAUNCHD_LABEL}`) || l.endsWith(` ${LAUNCHD_LABEL}`));
135
141
  if (!line)
136
142
  return { running: false, pid: null };
137
143
  const cols = line.split(/\s+/);
138
144
  const pid = parseInt(cols[0], 10);
139
- return { running: Number.isFinite(pid) && pid > 0, pid: Number.isFinite(pid) && pid > 0 ? pid : null };
145
+ return {
146
+ running: Number.isFinite(pid) && pid > 0,
147
+ pid: Number.isFinite(pid) && pid > 0 ? pid : null,
148
+ };
140
149
  }
141
150
  catch (err) {
142
151
  logger.warn({ err: redactBuffers(err) }, "launchctl list failed");
@@ -84,7 +84,9 @@ export function systemctlStatus() {
84
84
  }
85
85
  export function lingerEnabled(user) {
86
86
  try {
87
- const out = execFileSync("loginctl", ["show-user", user, "--property=Linger"], { stdio: ["ignore", "pipe", "ignore"] }).toString();
87
+ const out = execFileSync("loginctl", ["show-user", user, "--property=Linger"], {
88
+ stdio: ["ignore", "pipe", "ignore"],
89
+ }).toString();
88
90
  return /Linger=yes/.test(out);
89
91
  }
90
92
  catch (err) {
@@ -1,4 +1,4 @@
1
- import { chmodSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
1
+ import { chmodSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { logger } from "./log.js";
4
4
  const CHMOD_UNSUPPORTED_CODES = new Set(["ENOTSUP", "EOPNOTSUPP", "EPERM"]);
@@ -9,7 +9,10 @@ import { z } from "zod";
9
9
  const callStatusObjectSchema = z
10
10
  .object({
11
11
  state: z.string().nullish().describe('Lifecycle state, e.g. "Terminated", "Registered"'),
12
- terminationType: z.string().nullish().describe('How it ended, e.g. "completed", "no-answer" (null until the call ends)'),
12
+ terminationType: z
13
+ .string()
14
+ .nullish()
15
+ .describe('How it ended, e.g. "completed", "no-answer" (null until the call ends)'),
13
16
  label: z.string().nullish().describe('Human-readable status, e.g. "Completed"'),
14
17
  cancelRequested: z.boolean().nullish(),
15
18
  cancelPending: z.boolean().nullish(),
@@ -25,8 +28,16 @@ export const phoneNumberSchema = z
25
28
  nickname: z.string().nullable().optional().describe("Human-readable label for the number"),
26
29
  country: z.string().optional(),
27
30
  inboundInstruction: z.string().nullable().optional(),
28
- inboundVoiceGender: z.string().nullable().optional().describe('Voice gender for inbound calls ("male"/"female"); null → female (the default)'),
29
- inboundLanguage: z.string().nullable().optional().describe("BCP-47 language tag inbound calls are pinned to; null → detected from the caller's country prefix per call"),
31
+ inboundVoiceGender: z
32
+ .string()
33
+ .nullable()
34
+ .optional()
35
+ .describe('Voice gender for inbound calls ("male"/"female"); null → female (the default)'),
36
+ inboundLanguage: z
37
+ .string()
38
+ .nullable()
39
+ .optional()
40
+ .describe("BCP-47 language tag inbound calls are pinned to; null → detected from the caller's country prefix per call"),
30
41
  })
31
42
  .passthrough();
32
43
  export const messageSchema = z
@@ -39,8 +50,14 @@ export const messageSchema = z
39
50
  direction: z.string().optional(),
40
51
  status: statusSchema,
41
52
  statusError: z.string().nullish().describe("Failure reason when status is undelivered/failed"),
42
- replyToId: z.string().nullish().describe("Id of the message this one replies or reacts to; null for ordinary messages"),
43
- reaction: z.string().nullish().describe("The reaction this message carries (a reaction name or an emoji); null otherwise"),
53
+ replyToId: z
54
+ .string()
55
+ .nullish()
56
+ .describe("Id of the message this one replies or reacts to; null for ordinary messages"),
57
+ reaction: z
58
+ .string()
59
+ .nullish()
60
+ .describe("The reaction this message carries (a reaction name or an emoji); null otherwise"),
44
61
  createdAt: z.string().optional(),
45
62
  })
46
63
  .passthrough();
@@ -9,9 +9,7 @@ const SERVER_INFO = {
9
9
  title: "Dial",
10
10
  version: VERSION,
11
11
  websiteUrl: "https://getdial.ai",
12
- icons: [
13
- { src: "https://getdial.ai/favicon.svg", mimeType: "image/svg+xml", sizes: ["any"] },
14
- ],
12
+ icons: [{ src: "https://getdial.ai/favicon.svg", mimeType: "image/svg+xml", sizes: ["any"] }],
15
13
  };
16
14
  export function buildServer() {
17
15
  const server = new McpServer(SERVER_INFO);
@@ -2,9 +2,20 @@ import { z } from "zod";
2
2
  import { jsonResult } from "../result.js";
3
3
  import { addCommandTarget } from "../../lib/ops/local-targets.js";
4
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)"),
5
+ path: z
6
+ .string()
7
+ .min(1)
8
+ .describe("Absolute path to an executable the daemon spawns once per event"),
9
+ args: z
10
+ .array(z.string())
11
+ .optional()
12
+ .describe("Extra args; the event JSON is appended as the final positional arg"),
13
+ timeoutSeconds: z
14
+ .number()
15
+ .int()
16
+ .positive()
17
+ .optional()
18
+ .describe("Per-attempt timeout (default 5)"),
8
19
  };
9
20
  export const addCommandTargetTool = {
10
21
  name: "add_command_target",
@@ -2,11 +2,28 @@ import { z } from "zod";
2
2
  import { jsonResult } from "../result.js";
3
3
  import { addUrlTarget } from "../../lib/ops/local-targets.js";
4
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)"),
5
+ url: z
6
+ .string()
7
+ .min(1)
8
+ .describe("Loopback HTTP endpoint the listen daemon POSTs each event JSON to"),
9
+ secret: z
10
+ .string()
11
+ .optional()
12
+ .describe("HMAC-SHA256 key; the daemon signs each request body and sends the hex digest"),
13
+ signatureHeader: z
14
+ .string()
15
+ .optional()
16
+ .describe("Header for the HMAC signature (default X-Dial-Signature; only with secret)"),
17
+ bearer: z
18
+ .string()
19
+ .optional()
20
+ .describe("Static bearer token, sent as Authorization: Bearer <token>"),
21
+ timeoutSeconds: z
22
+ .number()
23
+ .int()
24
+ .positive()
25
+ .optional()
26
+ .describe("Per-attempt timeout (default 5)"),
10
27
  };
11
28
  export const addUrlTargetTool = {
12
29
  name: "add_url_target",
@@ -14,7 +14,9 @@ export const getAccountStatusTool = {
14
14
  auth: z.object({}).passthrough().describe("Sign-in and API-key state"),
15
15
  pendingOtp: z.object({}).passthrough().describe("Any pending sign-up OTP"),
16
16
  listen: z.object({}).passthrough().describe("Listen daemon state"),
17
- nextStep: z.string().describe("Recommended next step (signup, onboard, install_listen, ready, …)"),
17
+ nextStep: z
18
+ .string()
19
+ .describe("Recommended next step (signup, onboard, install_listen, ready, …)"),
18
20
  },
19
21
  annotations: { readOnlyHint: true, openWorldHint: true },
20
22
  },
@@ -2,13 +2,26 @@ import { z } from "zod";
2
2
  import { jsonResult } from "../result.js";
3
3
  import { onboard } from "../../lib/ops/account.js";
4
4
  import { readAuth, authFilePath } from "../../lib/state.js";
5
- import { installSkill, isSupportedAgent, SUPPORTED_AGENTS } from "../../lib/skill-install.js";
5
+ import { installSkill, isSupportedAgent, SUPPORTED_AGENTS, } from "../../lib/skill-install.js";
6
6
  import { supervisorAvailability } from "../../lib/supervisor/index.js";
7
7
  const inputSchema = {
8
- code: z.string().min(1).optional().describe("6-digit OTP from the sign-up email. Omit if the account is already signed in — the tool will just install the requested --agent skills and skip verification."),
9
- verificationId: z.string().optional().describe("Explicit verification id (defaults to the local pending signup)"),
10
- inboundInstruction: z.string().optional().describe("System prompt for inbound calls to a newly provisioned number (new accounts)"),
11
- agents: z.array(z.string()).optional().describe("Agent names to install the Dial skill into (e.g. claude-code, cursor)"),
8
+ code: z
9
+ .string()
10
+ .min(1)
11
+ .optional()
12
+ .describe("6-digit OTP from the sign-up email. Omit if the account is already signed in — the tool will just install the requested --agent skills and skip verification."),
13
+ verificationId: z
14
+ .string()
15
+ .optional()
16
+ .describe("Explicit verification id (defaults to the local pending signup)"),
17
+ inboundInstruction: z
18
+ .string()
19
+ .optional()
20
+ .describe("System prompt for inbound calls to a newly provisioned number (new accounts)"),
21
+ agents: z
22
+ .array(z.string())
23
+ .optional()
24
+ .describe("Agent names to install the Dial skill into (e.g. claude-code, cursor)"),
12
25
  };
13
26
  export const onboardTool = {
14
27
  name: "onboard",
@@ -40,14 +53,20 @@ export const onboardTool = {
40
53
  const skills = [];
41
54
  for (const requested of args.agents ?? []) {
42
55
  if (!isSupportedAgent(requested)) {
43
- skills.push({ agent: requested, error: `unknown agent "${requested}". Supported: ${SUPPORTED_AGENTS.join(", ")}.` });
56
+ skills.push({
57
+ agent: requested,
58
+ error: `unknown agent "${requested}". Supported: ${SUPPORTED_AGENTS.join(", ")}.`,
59
+ });
44
60
  continue;
45
61
  }
46
62
  try {
47
63
  skills.push(installSkill(requested));
48
64
  }
49
65
  catch (err) {
50
- skills.push({ agent: requested, error: err instanceof Error ? err.message : String(err) });
66
+ skills.push({
67
+ agent: requested,
68
+ error: err instanceof Error ? err.message : String(err),
69
+ });
51
70
  }
52
71
  }
53
72
  const supervisor = supervisorAvailability();
@@ -4,18 +4,41 @@ import { placeCall } from "../../lib/ops/calls.js";
4
4
  import { callSchema } from "../schemas.js";
5
5
  const inputSchema = {
6
6
  to: z.string().min(7).describe("Destination phone number, E.164 (e.g. +14155550123)"),
7
- outboundInstruction: z.string().min(1).describe("System prompt for the AI voice agent on this call"),
8
- language: z.string().optional().describe("BCP-47 language tag for the call. Omit to auto-detect from the destination number's country (alongside en-US)."),
9
- voiceGender: z.enum(["male", "female"]).optional().describe("Voice gender for the agent; the default is female"),
10
- transferTo: z.string().optional().describe("Forward-to number, E.164: the agent waits for a real human (riding out hold/IVR) then cold-transfers the call here. Must differ from `to` and the from number."),
11
- idempotencyKey: z.string().optional().describe("Unique key (e.g. a UUID) making the placement idempotent: retrying with the same key returns the already-placed call instead of dialing again"),
7
+ outboundInstruction: z
8
+ .string()
9
+ .min(1)
10
+ .describe("System prompt for the AI voice agent on this call"),
11
+ language: z
12
+ .string()
13
+ .optional()
14
+ .describe("BCP-47 language tag for the call. Omit to auto-detect from the destination number's country (alongside en-US)."),
15
+ voiceGender: z
16
+ .enum(["male", "female"])
17
+ .optional()
18
+ .describe("Voice gender for the agent; the default is female"),
19
+ transferTo: z
20
+ .string()
21
+ .optional()
22
+ .describe("Forward-to number, E.164: the agent waits for a real human (riding out hold/IVR) then cold-transfers the call here. Must differ from `to` and the from number."),
23
+ idempotencyKey: z
24
+ .string()
25
+ .optional()
26
+ .describe("Unique key (e.g. a UUID) making the placement idempotent: retrying with the same key returns the already-placed call instead of dialing again"),
12
27
  fromNumber: z
13
28
  .string()
14
29
  .min(1)
15
30
  .optional()
16
31
  .describe("Number to call from: a phone number id, one of your numbers in E.164, or a nickname. Exclusive with fromNumberId; omit both to use your primary number"),
17
- fromNumberId: z.string().optional().describe("Number id to call from; defaults to your primary number"),
18
- maxCallDurationSeconds: z.number().int().positive().optional().describe("Maximum call duration cap (seconds); the call is terminated when this limit is reached"),
32
+ fromNumberId: z
33
+ .string()
34
+ .optional()
35
+ .describe("Number id to call from; defaults to your primary number"),
36
+ maxCallDurationSeconds: z
37
+ .number()
38
+ .int()
39
+ .positive()
40
+ .optional()
41
+ .describe("Maximum call duration cap (seconds); the call is terminated when this limit is reached"),
19
42
  };
20
43
  export const placeCallTool = {
21
44
  name: "place_call",
@@ -24,7 +47,10 @@ export const placeCallTool = {
24
47
  description: "Place an outbound voice call handled by an AI agent. The call runs asynchronously — " +
25
48
  "use wait_for_event to block until it ends, then get_call for the transcript.",
26
49
  inputSchema,
27
- outputSchema: { call: callSchema, hint: z.string().describe("Next-step guidance for tracking the call") },
50
+ outputSchema: {
51
+ call: callSchema,
52
+ hint: z.string().describe("Next-step guidance for tracking the call"),
53
+ },
28
54
  annotations: { openWorldHint: true },
29
55
  },
30
56
  run: async (args) => {
@@ -4,11 +4,27 @@ import { purchaseNumber } from "../../lib/ops/numbers.js";
4
4
  import { phoneNumberSchema } from "../schemas.js";
5
5
  const inputSchema = {
6
6
  inboundInstruction: z.string().min(1).describe("System prompt for inbound calls to this number"),
7
- explicitProgrammaticConsent: z.string().min(1).max(2000).describe("Required attestation (max 2000 chars) that the account holder consented to provisioning this number programmatically; stored on the number"),
8
- inboundVoiceGender: z.enum(["male", "female"]).optional().describe("Voice gender for inbound calls to this number; the default is female"),
9
- inboundLanguage: z.string().optional().describe("BCP-47 language tag pinning inbound calls to this number to one language (e.g. es-ES); omitted → the language is detected from the caller's country prefix on each call (plus en-US)"),
10
- areaCode: z.string().optional().describe("Preferred US area code; omitted → any available US number. Only US numbers can be provisioned at this time. Ignored for iMessage numbers"),
11
- includeImessage: z.boolean().optional().describe('Provision an iMessage number (pay-as-you-go only; provisioned asynchronously poll List Numbers until setupStatus is "ready")'),
7
+ explicitProgrammaticConsent: z
8
+ .string()
9
+ .min(1)
10
+ .max(2000)
11
+ .describe("Required attestation (max 2000 chars) that the account holder consented to provisioning this number programmatically; stored on the number"),
12
+ inboundVoiceGender: z
13
+ .enum(["male", "female"])
14
+ .optional()
15
+ .describe("Voice gender for inbound calls to this number; the default is female"),
16
+ inboundLanguage: z
17
+ .string()
18
+ .optional()
19
+ .describe("BCP-47 language tag pinning inbound calls to this number to one language (e.g. es-ES); omitted → the language is detected from the caller's country prefix on each call (plus en-US)"),
20
+ areaCode: z
21
+ .string()
22
+ .optional()
23
+ .describe("Preferred US area code; omitted → any available US number. Only US numbers can be provisioned at this time. Ignored for iMessage numbers"),
24
+ includeImessage: z
25
+ .boolean()
26
+ .optional()
27
+ .describe('Provision an iMessage number (pay-as-you-go only; provisioned asynchronously — poll List Numbers until setupStatus is "ready")'),
12
28
  };
13
29
  export const purchaseNumberTool = {
14
30
  name: "purchase_number",
@@ -6,7 +6,10 @@ const inputSchema = {
6
6
  messageId: z
7
7
  .string()
8
8
  .describe("Id of the message to reply or react to (from list_messages or a message.received event)"),
9
- body: z.string().optional().describe("Reply text; on an iMessage number it threads under the target message"),
9
+ body: z
10
+ .string()
11
+ .optional()
12
+ .describe("Reply text; on an iMessage number it threads under the target message"),
10
13
  reaction: z
11
14
  .string()
12
15
  .optional()
@@ -4,13 +4,19 @@ import { sendMessage, MAX_MEDIA_ITEMS } from "../../lib/ops/messages.js";
4
4
  import { messageSchema } from "../schemas.js";
5
5
  const inputSchema = {
6
6
  to: z.string().min(7).describe("Destination phone number, E.164 (e.g. +14155550123)"),
7
- body: z.string().optional().describe("Message body; optional when mediaUrls is given (media-only send)"),
7
+ body: z
8
+ .string()
9
+ .optional()
10
+ .describe("Message body; optional when mediaUrls is given (media-only send)"),
8
11
  fromNumber: z
9
12
  .string()
10
13
  .min(1)
11
14
  .optional()
12
15
  .describe("Number to send from: a phone number id, one of your numbers in E.164, or a nickname. Exclusive with fromNumberId; omit both to use your primary number"),
13
- fromNumberId: z.string().optional().describe("Number id to send from; defaults to your primary number"),
16
+ fromNumberId: z
17
+ .string()
18
+ .optional()
19
+ .describe("Number id to send from; defaults to your primary number"),
14
20
  mediaUrls: z
15
21
  .array(z.string().url())
16
22
  .max(MAX_MEDIA_ITEMS)
@@ -4,11 +4,31 @@ import { setNumberProperties } from "../../lib/ops/numbers.js";
4
4
  import { phoneNumberSchema } from "../schemas.js";
5
5
  const inputSchema = {
6
6
  number: z.string().min(7).describe("The E.164 phone number to update (e.g. +14155550123)"),
7
- inboundInstruction: z.string().min(1).optional().describe("New system prompt for inbound calls to this number"),
8
- inboundVoiceGender: z.enum(["male", "female"]).optional().describe("Voice gender for inbound calls to this number; the default is female"),
9
- inboundLanguage: z.string().optional().describe("BCP-47 language tag pinning inbound calls to this number to one language (e.g. es-ES). Pass an empty string to clear it (reverts to detecting the language from the caller's country prefix per call)."),
10
- nickname: z.string().max(100).optional().describe('Human-readable label for the number, e.g. "Support line". Pass an empty string to clear it.'),
11
- maxCallDurationSeconds: z.number().int().positive().nullable().optional().describe("Call duration cap for this number, in seconds, applied as a hard ceiling to both inbound and outbound calls (the smallest of the per-number, account, and per-call caps wins). Pass null to clear the cap; omit to leave it unchanged."),
7
+ inboundInstruction: z
8
+ .string()
9
+ .min(1)
10
+ .optional()
11
+ .describe("New system prompt for inbound calls to this number"),
12
+ inboundVoiceGender: z
13
+ .enum(["male", "female"])
14
+ .optional()
15
+ .describe("Voice gender for inbound calls to this number; the default is female"),
16
+ inboundLanguage: z
17
+ .string()
18
+ .optional()
19
+ .describe("BCP-47 language tag pinning inbound calls to this number to one language (e.g. es-ES). Pass an empty string to clear it (reverts to detecting the language from the caller's country prefix per call)."),
20
+ nickname: z
21
+ .string()
22
+ .max(100)
23
+ .optional()
24
+ .describe('Human-readable label for the number, e.g. "Support line". Pass an empty string to clear it.'),
25
+ maxCallDurationSeconds: z
26
+ .number()
27
+ .int()
28
+ .positive()
29
+ .nullable()
30
+ .optional()
31
+ .describe("Call duration cap for this number, in seconds, applied as a hard ceiling to both inbound and outbound calls (the smallest of the per-number, account, and per-call caps wins). Pass null to clear the cap; omit to leave it unchanged."),
12
32
  };
13
33
  export const setNumberPropertiesTool = {
14
34
  name: "set_number_properties",
@@ -23,10 +43,16 @@ export const setNumberPropertiesTool = {
23
43
  number: await setNumberProperties({
24
44
  number: args.number,
25
45
  inboundInstruction: args.inboundInstruction,
26
- ...(args.inboundVoiceGender !== undefined ? { inboundVoiceGender: args.inboundVoiceGender } : {}),
27
- ...(args.inboundLanguage !== undefined ? { inboundLanguage: args.inboundLanguage } : {}),
46
+ ...(args.inboundVoiceGender !== undefined
47
+ ? { inboundVoiceGender: args.inboundVoiceGender }
48
+ : {}),
49
+ ...(args.inboundLanguage !== undefined
50
+ ? { inboundLanguage: args.inboundLanguage }
51
+ : {}),
28
52
  ...(args.nickname !== undefined ? { nickname: args.nickname } : {}),
29
- ...(args.maxCallDurationSeconds !== undefined ? { maxCallDurationSeconds: args.maxCallDurationSeconds } : {}),
53
+ ...(args.maxCallDurationSeconds !== undefined
54
+ ? { maxCallDurationSeconds: args.maxCallDurationSeconds }
55
+ : {}),
30
56
  }),
31
57
  }),
32
58
  };
@@ -3,9 +3,18 @@ import { jsonResult } from "../result.js";
3
3
  import { waitForEvent } from "../../lib/ops/events.js";
4
4
  import { eventSchema } from "../schemas.js";
5
5
  const inputSchema = {
6
- eventType: z.string().min(1).describe('Event type to wait for (e.g. "call.ended", "message.received")'),
7
- field: z.array(z.string()).optional().describe('Exact-match filters, each "name=value" (e.g. "callId=abc")'),
8
- regex: z.array(z.string()).optional().describe('Regex filters, each "name=pattern" (/re/flags or a bare regex)'),
6
+ eventType: z
7
+ .string()
8
+ .min(1)
9
+ .describe('Event type to wait for (e.g. "call.ended", "message.received")'),
10
+ field: z
11
+ .array(z.string())
12
+ .optional()
13
+ .describe('Exact-match filters, each "name=value" (e.g. "callId=abc")'),
14
+ regex: z
15
+ .array(z.string())
16
+ .optional()
17
+ .describe('Regex filters, each "name=pattern" (/re/flags or a bare regex)'),
9
18
  timeoutSeconds: z.number().default(30).describe("How long to wait before giving up"),
10
19
  };
11
20
  export const waitForEventTool = {