@getdial/cli 0.33.0 → 0.33.2

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/doctor.js +11 -0
  4. package/dist/commands/listen/index.js +17 -3
  5. package/dist/commands/listen/install.js +7 -2
  6. package/dist/commands/listen/status.js +11 -7
  7. package/dist/commands/message/list.js +5 -1
  8. package/dist/commands/message/reply.js +5 -1
  9. package/dist/commands/number/set.js +6 -2
  10. package/dist/commands/onboard.js +17 -5
  11. package/dist/commands/signup.js +7 -1
  12. package/dist/commands/typing/start.js +5 -1
  13. package/dist/commands/typing/stop.js +5 -1
  14. package/dist/commands/uninstall.js +3 -1
  15. package/dist/commands/wait-for.js +2 -2
  16. package/dist/lib/api.js +22 -3
  17. package/dist/lib/cli-error.js +6 -1
  18. package/dist/lib/fanout.js +5 -3
  19. package/dist/lib/log-tail.js +5 -2
  20. package/dist/lib/log.js +2 -2
  21. package/dist/lib/ops/account.js +46 -7
  22. package/dist/lib/ops/calls.js +3 -1
  23. package/dist/lib/ops/events.js +6 -1
  24. package/dist/lib/ops/listen.js +8 -1
  25. package/dist/lib/ops/uninstall.js +9 -3
  26. package/dist/lib/pubnub.js +47 -9
  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
package/dist/cli.js CHANGED
@@ -99,9 +99,7 @@ if (!sandbox) {
99
99
  .option("--json", "machine-readable output")
100
100
  .action(async (opts) => process.exit(await runListenStatus({ json: !!opts.json })));
101
101
  }
102
- const number = program
103
- .command("number")
104
- .description("Manage your Dial phone numbers.");
102
+ const number = program.command("number").description("Manage your Dial phone numbers.");
105
103
  number
106
104
  .command("list")
107
105
  .description("List the numbers on your account. GET /api/v1/numbers.")
@@ -3,7 +3,11 @@ import { isDialError } from "../../lib/ops/errors.js";
3
3
  import { printDialError } from "../../lib/cli-error.js";
4
4
  export async function runCallList(opts) {
5
5
  try {
6
- const calls = await listCalls({ numberId: opts.numberId, direction: opts.direction, since: opts.since });
6
+ const calls = await listCalls({
7
+ numberId: opts.numberId,
8
+ direction: opts.direction,
9
+ since: opts.since,
10
+ });
7
11
  if (opts.json) {
8
12
  console.log(JSON.stringify({ ok: true, calls }));
9
13
  return 0;
@@ -3,6 +3,17 @@ function humanRender(r) {
3
3
  const lines = [];
4
4
  lines.push(`dial ${r.cli.version} (node ${r.cli.node})`);
5
5
  lines.push(`backend: ${r.backend.url} ${r.backend.reachable ? `reachable${r.backend.latencyMs != null ? ` (${r.backend.latencyMs}ms)` : ""}` : "UNREACHABLE"}`);
6
+ if (r.sandbox) {
7
+ // Sandbox: no local key — the gateway injects credentials. Don't say "not
8
+ // signed in" (misleading) or surface pending-otp/listen (all disabled here).
9
+ lines.push(`mode: sandbox (credentials injected by the gateway)`);
10
+ lines.push(`auth: ${r.auth.keyValid ? "credential connected via the gateway" : "no valid Dial credential connected in the vault"}`);
11
+ lines.push("");
12
+ lines.push(r.nextStep === "ready"
13
+ ? "next: ready"
14
+ : "next: connect the Dial credential in your gateway/vault (this agent has none)");
15
+ return lines.join("\n");
16
+ }
6
17
  if (r.auth.signedIn) {
7
18
  lines.push(`auth: signed in as ${r.auth.email} (account ${r.auth.accountId}, key sk_live_***${r.auth.apiKeyFingerprint})${r.auth.keyValid === false ? " [key rejected by backend]" : ""}`);
8
19
  }
@@ -11,17 +11,31 @@ function isSupervised() {
11
11
  export async function runListen() {
12
12
  const auth = readAuth();
13
13
  if (!auth) {
14
- appendJsonl(paths().listenLog, { ts: new Date().toISOString(), lifecycle: "startup", ok: false, error: "no saved auth" });
14
+ appendJsonl(paths().listenLog, {
15
+ ts: new Date().toISOString(),
16
+ lifecycle: "startup",
17
+ ok: false,
18
+ error: "no saved auth",
19
+ });
15
20
  if (isSupervised()) {
16
21
  await delay(30_000);
17
22
  }
18
23
  return 1;
19
24
  }
20
- appendJsonl(paths().listenLog, { ts: new Date().toISOString(), lifecycle: "startup", ok: true, accountId: auth.accountId });
25
+ appendJsonl(paths().listenLog, {
26
+ ts: new Date().toISOString(),
27
+ lifecycle: "startup",
28
+ ok: true,
29
+ accountId: auth.accountId,
30
+ });
21
31
  recordListenVersion();
22
32
  const ctrl = startWorker(auth.apiKey, auth.accountId);
23
33
  const onSignal = async (sig) => {
24
- appendJsonl(paths().listenLog, { ts: new Date().toISOString(), lifecycle: "shutdown", signal: sig });
34
+ appendJsonl(paths().listenLog, {
35
+ ts: new Date().toISOString(),
36
+ lifecycle: "shutdown",
37
+ signal: sig,
38
+ });
25
39
  await ctrl.stop();
26
40
  };
27
41
  process.on("SIGTERM", onSignal);
@@ -1,5 +1,5 @@
1
1
  import { readAuth } from "../../lib/state.js";
2
- import { installSupervised, resolveListenCommand, supervisorAvailability } from "../../lib/supervisor/index.js";
2
+ import { installSupervised, resolveListenCommand, supervisorAvailability, } from "../../lib/supervisor/index.js";
3
3
  export async function runListenInstall(opts) {
4
4
  const auth = readAuth();
5
5
  if (!auth) {
@@ -20,7 +20,12 @@ export async function runListenInstall(opts) {
20
20
  try {
21
21
  const result = installSupervised(resolveListenCommand());
22
22
  if (opts.json)
23
- console.log(JSON.stringify({ ok: true, changed: result.changed, unit_path: result.unitPath, warnings: result.warnings }));
23
+ console.log(JSON.stringify({
24
+ ok: true,
25
+ changed: result.changed,
26
+ unit_path: result.unitPath,
27
+ warnings: result.warnings,
28
+ }));
24
29
  else {
25
30
  console.log(`listen service installed${result.changed ? "" : " (no change)"}.`);
26
31
  console.log(` unit: ${result.unitPath}`);
@@ -8,14 +8,18 @@ export async function runListenStatus(opts) {
8
8
  try {
9
9
  const raw = readFileSync(paths().listenLog, "utf8");
10
10
  const lines = raw.trim().split("\n").filter(Boolean);
11
- lastEvents = lines.slice(-5).map((l) => { try {
12
- return JSON.parse(l);
13
- }
14
- catch {
15
- return l;
16
- } });
11
+ lastEvents = lines.slice(-5).map((l) => {
12
+ try {
13
+ return JSON.parse(l);
14
+ }
15
+ catch {
16
+ return l;
17
+ }
18
+ });
19
+ }
20
+ catch {
21
+ /* ignore */
17
22
  }
18
- catch { /* ignore */ }
19
23
  const out = {
20
24
  installed: s.installed,
21
25
  running: s.running,
@@ -3,7 +3,11 @@ import { isDialError } from "../../lib/ops/errors.js";
3
3
  import { printDialError } from "../../lib/cli-error.js";
4
4
  export async function runMessageList(opts) {
5
5
  try {
6
- const messages = await listMessages({ numberId: opts.numberId, direction: opts.direction, since: opts.since });
6
+ const messages = await listMessages({
7
+ numberId: opts.numberId,
8
+ direction: opts.direction,
9
+ since: opts.since,
10
+ });
7
11
  if (opts.json) {
8
12
  console.log(JSON.stringify({ ok: true, messages }));
9
13
  return 0;
@@ -3,7 +3,11 @@ import { isDialError } from "../../lib/ops/errors.js";
3
3
  import { printDialError } from "../../lib/cli-error.js";
4
4
  export async function runMessageReply(opts) {
5
5
  try {
6
- const m = await replyToMessage({ messageId: opts.messageId, body: opts.body, reaction: opts.react });
6
+ const m = await replyToMessage({
7
+ messageId: opts.messageId,
8
+ body: opts.body,
9
+ reaction: opts.react,
10
+ });
7
11
  if (opts.json) {
8
12
  console.log(JSON.stringify({ ok: true, message: m }));
9
13
  }
@@ -6,10 +6,14 @@ export async function runNumberSet(opts) {
6
6
  const n = await setNumberProperties({
7
7
  number: opts.number,
8
8
  inboundInstruction: opts.inboundInstruction,
9
- ...(opts.inboundVoiceGender !== undefined ? { inboundVoiceGender: opts.inboundVoiceGender } : {}),
9
+ ...(opts.inboundVoiceGender !== undefined
10
+ ? { inboundVoiceGender: opts.inboundVoiceGender }
11
+ : {}),
10
12
  ...(opts.inboundLanguage !== undefined ? { inboundLanguage: opts.inboundLanguage } : {}),
11
13
  ...(opts.nickname !== undefined ? { nickname: opts.nickname } : {}),
12
- ...(opts.maxCallDurationSeconds !== undefined ? { maxCallDurationSeconds: opts.maxCallDurationSeconds } : {}),
14
+ ...(opts.maxCallDurationSeconds !== undefined
15
+ ? { maxCallDurationSeconds: opts.maxCallDurationSeconds }
16
+ : {}),
13
17
  });
14
18
  if (opts.json) {
15
19
  console.log(JSON.stringify({ ok: true, number: n }));
@@ -1,7 +1,7 @@
1
1
  import { onboard } from "../lib/ops/account.js";
2
2
  import { isDialError } from "../lib/ops/errors.js";
3
3
  import { readAuth, authFilePath } from "../lib/state.js";
4
- import { installSkill, isSupportedAgent, SUPPORTED_AGENTS } from "../lib/skill-install.js";
4
+ import { installSkill, isSupportedAgent, SUPPORTED_AGENTS, } from "../lib/skill-install.js";
5
5
  import { supervisorAvailability } from "../lib/supervisor/index.js";
6
6
  function maskApiKey(key) {
7
7
  return key.length >= 4 ? `sk_live_***${key.slice(-4)}` : "sk_live_***";
@@ -13,7 +13,10 @@ function installAgentSkills(agents) {
13
13
  const skills = [];
14
14
  for (const requested of agents) {
15
15
  if (!isSupportedAgent(requested)) {
16
- skills.push({ agent: requested, error: `unknown agent "${requested}". Supported: ${SUPPORTED_AGENTS.join(", ")}.` });
16
+ skills.push({
17
+ agent: requested,
18
+ error: `unknown agent "${requested}". Supported: ${SUPPORTED_AGENTS.join(", ")}.`,
19
+ });
17
20
  continue;
18
21
  }
19
22
  try {
@@ -30,7 +33,7 @@ const LISTEN_RATIONALE = "The background notification listening service runs on
30
33
  "Without it the agent is send-only: it can place outbound calls and send messages, but it has no way to know when something arrives at your Dial number. " +
31
34
  "It can always be installed later with `dial listen install`.";
32
35
  const PRESENTATION_HINT = "When you ask the user, describe it as \"the background notification listening service.\" Don't expose technical details (launchd, plists, daemons, systemd units) — that's not how to talk about it to a non-technical user.";
33
- const FINALIZATION_HINT = "To finalize onboarding, ask the user this question before continuing with any other task. If the user's original request was something else (e.g. \"call X\", \"send Y\"), briefly pause that task, ask the question, get an answer, then resume.";
36
+ const FINALIZATION_HINT = 'To finalize onboarding, ask the user this question before continuing with any other task. If the user\'s original request was something else (e.g. "call X", "send Y"), briefly pause that task, ask the question, get an answer, then resume.';
34
37
  export async function runOnboard(opts) {
35
38
  // Skill-install-only branch: when --code isn't supplied, we can't (and shouldn't)
36
39
  // re-verify — but if the machine is already signed in, the useful thing to do
@@ -59,9 +62,18 @@ export async function runOnboard(opts) {
59
62
  accountId: auth.accountId,
60
63
  phoneNumber: auth.phoneNumber ?? null,
61
64
  phoneNumberId: auth.phoneNumberId ?? null,
62
- listen: { installed: false, autoInstalled: false, canInstall: supervisor.available, unavailableReason: supervisor.available ? null : supervisor.reason },
65
+ listen: {
66
+ installed: false,
67
+ autoInstalled: false,
68
+ canInstall: supervisor.available,
69
+ unavailableReason: supervisor.available ? null : supervisor.reason,
70
+ },
63
71
  skills,
64
- agentHint: { action: "skip", kind: "already_signed_in", note: "Account is already signed in; verification was skipped and only the requested --agent skills were installed." },
72
+ agentHint: {
73
+ action: "skip",
74
+ kind: "already_signed_in",
75
+ note: "Account is already signed in; verification was skipped and only the requested --agent skills were installed.",
76
+ },
65
77
  }));
66
78
  }
67
79
  else {
@@ -18,7 +18,13 @@ export async function runSignup(email, opts) {
18
18
  if (e.code === "pending_exists") {
19
19
  const d = e.data ?? {};
20
20
  if (opts.json) {
21
- console.log(JSON.stringify({ ok: false, code: "pending_exists", verificationId: d.verificationId, email: d.email, ageSeconds: d.ageSeconds }));
21
+ console.log(JSON.stringify({
22
+ ok: false,
23
+ code: "pending_exists",
24
+ verificationId: d.verificationId,
25
+ email: d.email,
26
+ ageSeconds: d.ageSeconds,
27
+ }));
22
28
  }
23
29
  else {
24
30
  console.error(e.message);
@@ -3,7 +3,11 @@ import { isDialError } from "../../lib/ops/errors.js";
3
3
  import { printDialError } from "../../lib/cli-error.js";
4
4
  export async function runTypingStart(opts) {
5
5
  try {
6
- const result = await setTyping({ toNumber: opts.toNumber, value: true, fromNumber: opts.fromNumber });
6
+ const result = await setTyping({
7
+ toNumber: opts.toNumber,
8
+ value: true,
9
+ fromNumber: opts.fromNumber,
10
+ });
7
11
  if (opts.json) {
8
12
  console.log(JSON.stringify(result));
9
13
  }
@@ -3,7 +3,11 @@ import { isDialError } from "../../lib/ops/errors.js";
3
3
  import { printDialError } from "../../lib/cli-error.js";
4
4
  export async function runTypingStop(opts) {
5
5
  try {
6
- const result = await setTyping({ toNumber: opts.toNumber, value: false, fromNumber: opts.fromNumber });
6
+ const result = await setTyping({
7
+ toNumber: opts.toNumber,
8
+ value: false,
9
+ fromNumber: opts.fromNumber,
10
+ });
7
11
  if (opts.json) {
8
12
  console.log(JSON.stringify(result));
9
13
  }
@@ -16,7 +16,9 @@ export async function runUninstall(opts) {
16
16
  ? `agent skills: removed from ${removedSkills.map((s) => s.agent).join(", ")}`
17
17
  : "agent skills: none installed");
18
18
  const removedDirs = report.dirs.filter((d) => d.removed);
19
- console.log(removedDirs.length ? `state dirs: removed ${removedDirs.map((d) => d.path).join(", ")}` : "state dirs: none present");
19
+ console.log(removedDirs.length
20
+ ? `state dirs: removed ${removedDirs.map((d) => d.path).join(", ")}`
21
+ : "state dirs: none present");
20
22
  for (const e of report.errors) {
21
23
  console.error(`error in ${e.step}: ${e.message}`);
22
24
  }
@@ -19,7 +19,7 @@ export async function runWaitFor(opts) {
19
19
  }
20
20
  // Hit: print the raw line and succeed.
21
21
  if (!r.timedOut && r.line != null) {
22
- process.stdout.write(r.line + "\n");
22
+ process.stdout.write(`${r.line}\n`);
23
23
  return 0;
24
24
  }
25
25
  // Timed out tailing the log, but there was an earlier matching entry.
@@ -29,7 +29,7 @@ export async function runWaitFor(opts) {
29
29
  }
30
30
  else {
31
31
  console.error(`timed out after ${opts.timeoutSeconds}s; latest matching entry in log:`);
32
- process.stdout.write(r.line + "\n");
32
+ process.stdout.write(`${r.line}\n`);
33
33
  }
34
34
  return 1;
35
35
  }
package/dist/lib/api.js CHANGED
@@ -1,6 +1,19 @@
1
- import { request, fetch as undiciFetch, FormData as UndiciFormData } from "undici";
1
+ import { request, fetch as undiciFetch, FormData as UndiciFormData, setGlobalDispatcher, EnvHttpProxyAgent, } from "undici";
2
2
  import { logger } from "./log.js";
3
3
  import { VERSION } from "./version.js";
4
+ // Route this package's undici requests through HTTP(S)_PROXY when one is set.
5
+ // The `undici` npm package keeps its OWN global dispatcher, which — unlike
6
+ // Node's built-in fetch (wired by NODE_USE_ENV_PROXY) — ignores the proxy env
7
+ // vars. In a proxied environment (e.g. the NanoClaw agent sandbox, where the
8
+ // OneCLI gateway sits on HTTPS_PROXY and injects the Authorization header for
9
+ // api.getdial.ai) our `request()`/`fetch()` calls would otherwise bypass the
10
+ // gateway entirely and go out UNAUTHENTICATED → 401. Opt in explicitly.
11
+ if (process.env.HTTPS_PROXY ||
12
+ process.env.https_proxy ||
13
+ process.env.HTTP_PROXY ||
14
+ process.env.http_proxy) {
15
+ setGlobalDispatcher(new EnvHttpProxyAgent());
16
+ }
4
17
  // The bundled undici only multipart-encodes its own FormData class (realm
5
18
  // check) — Node's global FormData would be coerced to a text/plain string.
6
19
  export { UndiciFormData as ApiFormData };
@@ -25,7 +38,9 @@ function toResult(statusCode, text) {
25
38
  try {
26
39
  parsed = text ? JSON.parse(text) : null;
27
40
  }
28
- catch { /* keep raw */ }
41
+ catch {
42
+ /* keep raw */
43
+ }
29
44
  if (statusCode >= 200 && statusCode < 300) {
30
45
  return { ok: true, status: statusCode, data: parsed };
31
46
  }
@@ -42,7 +57,11 @@ function toResult(statusCode, text) {
42
57
  }
43
58
  async function apiRequest(method, path, body, apiKey, extraHeaders) {
44
59
  const url = `${baseUrl()}${path}`;
45
- const headers = { "content-type": "application/json", "user-agent": USER_AGENT, ...(extraHeaders ?? {}) };
60
+ const headers = {
61
+ "content-type": "application/json",
62
+ "user-agent": USER_AGENT,
63
+ ...(extraHeaders ?? {}),
64
+ };
46
65
  if (apiKey)
47
66
  headers.authorization = `Bearer ${apiKey}`;
48
67
  try {
@@ -16,7 +16,12 @@ const EXIT_1_CODES = new Set([
16
16
  */
17
17
  export function printDialError(json, e) {
18
18
  if (json) {
19
- console.log(JSON.stringify({ ok: false, code: e.code, message: e.message, ...(e.status ? { status: e.status } : {}) }));
19
+ console.log(JSON.stringify({
20
+ ok: false,
21
+ code: e.code,
22
+ message: e.message,
23
+ ...(e.status ? { status: e.status } : {}),
24
+ }));
20
25
  }
21
26
  else {
22
27
  console.error(e.message);
@@ -7,12 +7,12 @@ function clipCapture(buf) {
7
7
  const s = typeof buf === "string" ? buf : buf.toString("utf8");
8
8
  if (s.length <= MAX_CAPTURE_BYTES)
9
9
  return s;
10
- return s.slice(0, MAX_CAPTURE_BYTES) + `…[truncated, total ${s.length} chars]`;
10
+ return `${s.slice(0, MAX_CAPTURE_BYTES)}…[truncated, total ${s.length} chars]`;
11
11
  }
12
12
  async function attemptUrl(target, body) {
13
13
  const headers = { "Content-Type": "application/json" };
14
14
  if (target.bearer)
15
- headers["Authorization"] = `Bearer ${target.bearer}`;
15
+ headers.Authorization = `Bearer ${target.bearer}`;
16
16
  if (target.secret) {
17
17
  const sig = createHmac("sha256", target.secret).update(body).digest("hex");
18
18
  headers[target.signatureHeader ?? DEFAULT_SIGNATURE_HEADER] = sig;
@@ -65,7 +65,9 @@ async function attemptCmd(target, eventJson) {
65
65
  try {
66
66
  child.kill("SIGKILL");
67
67
  }
68
- catch { /* already exited */ }
68
+ catch {
69
+ /* already exited */
70
+ }
69
71
  resolve({
70
72
  ok: false,
71
73
  timedOut: true,
@@ -43,7 +43,7 @@ export function findLatestMatch(file, spec) {
43
43
  return { line: lines[i], obj };
44
44
  }
45
45
  catch {
46
- continue;
46
+ // skip unparsable lines
47
47
  }
48
48
  }
49
49
  return null;
@@ -68,7 +68,10 @@ function readRange(file, offset, length) {
68
68
  }
69
69
  }
70
70
  function parseLines(chunk) {
71
- return chunk.split("\n").filter(Boolean).map((line) => {
71
+ return chunk
72
+ .split("\n")
73
+ .filter(Boolean)
74
+ .map((line) => {
72
75
  try {
73
76
  return { line, obj: JSON.parse(line) };
74
77
  }
package/dist/lib/log.js CHANGED
@@ -3,7 +3,7 @@ import { dirname } from "node:path";
3
3
  import pino from "pino";
4
4
  export function appendJsonl(file, obj) {
5
5
  mkdirSync(dirname(file), { recursive: true });
6
- appendFileSync(file, JSON.stringify(obj) + "\n");
6
+ appendFileSync(file, `${JSON.stringify(obj)}\n`);
7
7
  }
8
8
  export const logger = pino({
9
9
  level: process.env.DIAL_LOG_LEVEL ?? "warn",
@@ -30,5 +30,5 @@ export function rotateIfLarge(file, maxBytes) {
30
30
  const raw = readFileSync(file, "utf8");
31
31
  const lines = raw.split("\n").filter(Boolean);
32
32
  const keep = lines.slice(Math.floor(lines.length / 2));
33
- writeFileSync(file, keep.join("\n") + "\n");
33
+ writeFileSync(file, `${keep.join("\n")}\n`);
34
34
  }
@@ -1,14 +1,39 @@
1
- import { readAuth, readPendingSignup, writePendingSignup, clearPendingSignup, writeAuth, authFilePath } from "../state.js";
1
+ import { readAuth, readPendingSignup, writePendingSignup, clearPendingSignup, writeAuth, authFilePath, } from "../state.js";
2
2
  import { apiGet, apiPost, baseUrl, pingBackend } from "../api.js";
3
- import { supervisorStatus, lastEventAtFromLog, supervisorAvailability } from "../supervisor/index.js";
3
+ import { supervisorStatus, lastEventAtFromLog, supervisorAvailability, } from "../supervisor/index.js";
4
4
  import { paths } from "../paths.js";
5
5
  import { VERSION } from "../version.js";
6
- import { installSkill, isSupportedAgent, SUPPORTED_AGENTS } from "../skill-install.js";
6
+ import { installSkill, isSupportedAgent, SUPPORTED_AGENTS, } from "../skill-install.js";
7
+ import { isSandbox } from "../sandbox.js";
7
8
  import { DialError } from "./errors.js";
8
9
  const OTP_EXPIRY_MS = 10 * 60 * 1000;
9
10
  const PENDING_FRESH_MS = 10 * 60 * 1000;
10
11
  export async function accountStatus() {
11
12
  const ping = await pingBackend();
13
+ if (isSandbox()) {
14
+ // No local auth file in a sandbox — the gateway injects the credential.
15
+ // Probe keyless (the proxy adds the Authorization header) to report whether
16
+ // the credential is actually connected in the vault. Never suggest
17
+ // signup/onboard/listen here — those are disabled in a sandbox.
18
+ const probe = await apiGet("/api/v1/account");
19
+ const connected = probe.ok;
20
+ return {
21
+ cli: { version: VERSION, node: process.versions.node },
22
+ backend: { url: baseUrl(), reachable: ping.reachable, latencyMs: ping.latencyMs },
23
+ auth: {
24
+ signedIn: connected,
25
+ email: null,
26
+ accountId: null,
27
+ apiKeyPresent: connected,
28
+ apiKeyFingerprint: null,
29
+ keyValid: connected,
30
+ },
31
+ pendingOtp: { verificationId: null, ageSeconds: null, expired: null },
32
+ listen: { installed: false, running: false, lastEventAt: null },
33
+ sandbox: true,
34
+ nextStep: connected ? "ready" : "connect_credential",
35
+ };
36
+ }
12
37
  const auth = readAuth();
13
38
  const pending = readPendingSignup();
14
39
  let keyValid = null;
@@ -21,7 +46,11 @@ export async function accountStatus() {
21
46
  let listenState = { installed: false, running: false, lastEventAt: null };
22
47
  try {
23
48
  const s = supervisorStatus();
24
- listenState = { installed: s.installed, running: s.running, lastEventAt: lastEventAtFromLog(paths().listenLog) };
49
+ listenState = {
50
+ installed: s.installed,
51
+ running: s.running,
52
+ lastEventAt: lastEventAtFromLog(paths().listenLog),
53
+ };
25
54
  }
26
55
  catch {
27
56
  // unsupported platform — leave defaults
@@ -61,6 +90,7 @@ export async function accountStatus() {
61
90
  expired: pendingExpired,
62
91
  },
63
92
  listen: listenState,
93
+ sandbox: false,
64
94
  nextStep,
65
95
  };
66
96
  }
@@ -74,10 +104,16 @@ export async function signup(opts) {
74
104
  throw new DialError("pending_exists", `A pending OTP for ${existing.email} is still fresh (${ageSeconds}s old). Use \`dial onboard --code <code>\` or re-run with --force to start a new one.`, undefined, { verificationId: existing.verificationId, email: existing.email, ageSeconds });
75
105
  }
76
106
  }
77
- const res = await apiPost("/api/v1/auth/signup", { email: opts.email });
107
+ const res = await apiPost("/api/v1/auth/signup", {
108
+ email: opts.email,
109
+ });
78
110
  if (!res.ok)
79
111
  throw new DialError("signup_failed", res.error, res.status);
80
- writePendingSignup({ verificationId: res.data.verificationId, email: opts.email, createdAt: new Date().toISOString() });
112
+ writePendingSignup({
113
+ verificationId: res.data.verificationId,
114
+ email: opts.email,
115
+ createdAt: new Date().toISOString(),
116
+ });
81
117
  return { verificationId: res.data.verificationId, email: opts.email };
82
118
  }
83
119
  export async function onboard(opts) {
@@ -113,7 +149,10 @@ export async function onboard(opts) {
113
149
  const skills = [];
114
150
  for (const requested of opts.agents ?? []) {
115
151
  if (!isSupportedAgent(requested)) {
116
- skills.push({ agent: requested, error: `unknown agent "${requested}". Supported: ${SUPPORTED_AGENTS.join(", ")}.` });
152
+ skills.push({
153
+ agent: requested,
154
+ error: `unknown agent "${requested}". Supported: ${SUPPORTED_AGENTS.join(", ")}.`,
155
+ });
117
156
  continue;
118
157
  }
119
158
  try {
@@ -12,7 +12,9 @@ export async function placeCall(opts) {
12
12
  // Omitted → the server uses the default voice gender (female).
13
13
  ...(opts.voiceGender ? { voiceGender: opts.voiceGender } : {}),
14
14
  ...(opts.transferTo ? { transferTo: opts.transferTo } : {}),
15
- ...(opts.maxCallDurationSeconds !== undefined ? { maxCallDurationSeconds: opts.maxCallDurationSeconds } : {}),
15
+ ...(opts.maxCallDurationSeconds !== undefined
16
+ ? { maxCallDurationSeconds: opts.maxCallDurationSeconds }
17
+ : {}),
16
18
  }, auth?.apiKey, opts.idempotencyKey ? { "idempotency-key": opts.idempotencyKey } : undefined);
17
19
  if (!res.ok)
18
20
  throw new DialError("call_failed", res.error, res.status);
@@ -52,7 +52,12 @@ async function waitFromApi(spec, opts) {
52
52
  timeout,
53
53
  }, auth?.apiKey);
54
54
  if (res.ok && res.data?.event) {
55
- return { source: "api", timedOut: false, event: res.data.event, line: JSON.stringify(res.data.event) };
55
+ return {
56
+ source: "api",
57
+ timedOut: false,
58
+ event: res.data.event,
59
+ line: JSON.stringify(res.data.event),
60
+ };
56
61
  }
57
62
  if (res.ok === false && res.status === 408)
58
63
  continue;
@@ -44,5 +44,12 @@ export function listenStatus() {
44
44
  catch {
45
45
  // no log yet — leave empty
46
46
  }
47
- return { installed: s.installed, running: s.running, pid: s.pid, unitPath: s.unitPath, lastEventAt, lastEvents };
47
+ return {
48
+ installed: s.installed,
49
+ running: s.running,
50
+ pid: s.pid,
51
+ unitPath: s.unitPath,
52
+ lastEventAt,
53
+ lastEvents,
54
+ };
48
55
  }
@@ -1,7 +1,7 @@
1
1
  import { existsSync, rmSync } from "node:fs";
2
2
  import { paths } from "../paths.js";
3
3
  import { SUPPORTED_AGENTS, uninstallSkill } from "../skill-install.js";
4
- import { supervisorAvailability, uninstallSupervised } from "../supervisor/index.js";
4
+ import { supervisorAvailability, uninstallSupervised, } from "../supervisor/index.js";
5
5
  export const UNINSTALL_HINT = "npm uninstall -g @getdial/cli";
6
6
  /**
7
7
  * Full local teardown, best-effort: every step runs even if an earlier one
@@ -33,7 +33,10 @@ export function uninstallEverything(deps = {}) {
33
33
  skills.push(uninstallSkill(agent, { home: deps.home, cwd: deps.cwd }));
34
34
  }
35
35
  catch (err) {
36
- errors.push({ step: `skill:${agent}`, message: err instanceof Error ? err.message : String(err) });
36
+ errors.push({
37
+ step: `skill:${agent}`,
38
+ message: err instanceof Error ? err.message : String(err),
39
+ });
37
40
  }
38
41
  }
39
42
  const p = paths();
@@ -47,7 +50,10 @@ export function uninstallEverything(deps = {}) {
47
50
  }
48
51
  catch (err) {
49
52
  dirs.push({ path: dir, removed: false });
50
- errors.push({ step: `dir:${dir}`, message: err instanceof Error ? err.message : String(err) });
53
+ errors.push({
54
+ step: `dir:${dir}`,
55
+ message: err instanceof Error ? err.message : String(err),
56
+ });
51
57
  }
52
58
  }
53
59
  return { ok: errors.length === 0, daemon, skills, dirs, hint: UNINSTALL_HINT, errors };
@@ -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] });
@@ -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 = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getdial/cli",
3
- "version": "0.33.0",
3
+ "version": "0.33.2",
4
4
  "description": "Dial CLI — install, sign up, and run the local listen service.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -27,6 +27,9 @@
27
27
  "scripts": {
28
28
  "build": "tsc -p .",
29
29
  "test": "node --import tsx --test",
30
+ "lint": "biome lint src",
31
+ "format": "biome format --write src",
32
+ "format:check": "biome format src",
30
33
  "clean": "rm -rf dist skills.tar.gz",
31
34
  "build:skill": "tar -czf skills.tar.gz skills",
32
35
  "prepack": "npm run build && npm run build:skill"
@@ -41,6 +44,7 @@
41
44
  "zod": "^4.4.3"
42
45
  },
43
46
  "devDependencies": {
47
+ "@biomejs/biome": "2.5.4",
44
48
  "@types/node": "^22.10.0",
45
49
  "tsx": "^4.22.3",
46
50
  "typescript": "^5.6.0"
package/skills.tar.gz CHANGED
Binary file