@getdial/cli 0.29.0 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -16,6 +16,8 @@ import { runNumberSet } from "./commands/number/set.js";
16
16
  import { runMessageSend } from "./commands/message/send.js";
17
17
  import { runMessageReply } from "./commands/message/reply.js";
18
18
  import { runMessageList } from "./commands/message/list.js";
19
+ import { runTypingStart } from "./commands/typing/start.js";
20
+ import { runTypingStop } from "./commands/typing/stop.js";
19
21
  import { runCallSend } from "./commands/call/send.js";
20
22
  import { runCallList } from "./commands/call/list.js";
21
23
  import { runCallGet } from "./commands/call/get.js";
@@ -102,6 +104,7 @@ number
102
104
  .requiredOption("--inbound-instruction <text>", "system prompt for inbound calls to this number")
103
105
  .requiredOption("--explicit-programmatic-consent <text>", "required attestation that the account holder consented to provisioning this number programmatically (stored on the number)")
104
106
  .option("--inbound-voice-gender <male|female>", "voice gender for inbound calls (default: female; pass male to override)")
107
+ .option("--inbound-language <bcp47>", "language tag inbound calls are pinned to (default: detect from the caller's country prefix, alongside en-US)")
105
108
  .option("--area-code <code>", "preferred US area code (only US numbers can be provisioned; ignored with --include-imessage)")
106
109
  .option("--include-imessage", "provision an iMessage number (pay-as-you-go only; provisioned asynchronously — poll `dial number list` until ready)")
107
110
  .option("--json", "machine-readable output")
@@ -109,6 +112,7 @@ number
109
112
  inboundInstruction: opts.inboundInstruction,
110
113
  explicitProgrammaticConsent: opts.explicitProgrammaticConsent,
111
114
  inboundVoiceGender: opts.inboundVoiceGender,
115
+ inboundLanguage: opts.inboundLanguage,
112
116
  areaCode: opts.areaCode,
113
117
  includeImessage: !!opts.includeImessage,
114
118
  json: !!opts.json,
@@ -118,6 +122,7 @@ number
118
122
  .description("Update a number's properties (at least one flag). PATCH /api/v1/numbers/<id>.")
119
123
  .option("--inbound-instruction <text>", "new system prompt for inbound calls to this number")
120
124
  .option("--inbound-voice-gender <male|female>", 'voice gender for inbound calls; pass "" to clear (reverts to the default, female)')
125
+ .option("--inbound-language <bcp47>", 'language tag inbound calls are pinned to; pass "" to clear (reverts to detecting from the caller\'s country prefix)')
121
126
  .option("--nickname <text>", 'human-readable label for the number, e.g. "Support line"; pass "" to clear')
122
127
  .option("--max-call-duration <seconds>", "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)", (v) => {
123
128
  const n = parseInt(v, 10);
@@ -141,6 +146,7 @@ number
141
146
  number: numberArg,
142
147
  inboundInstruction: opts.inboundInstruction,
143
148
  inboundVoiceGender: opts.inboundVoiceGender,
149
+ inboundLanguage: opts.inboundLanguage,
144
150
  nickname: opts.nickname,
145
151
  maxCallDurationSeconds,
146
152
  json: !!opts.json,
@@ -151,7 +157,8 @@ const message = program
151
157
  .description("Send an SMS, optionally with media (MMS). POST /api/v1/messages.")
152
158
  .option("--to <e164>", "destination phone number, E.164 (e.g. +14155551234)")
153
159
  .option("--body <text>", "message body")
154
- .option("--from-number-id <id>", "phoneNumberId to send from (defaults to onboard's number)")
160
+ .option("--from-number <ref>", "number to send from: id, owned E.164, or nickname (defaults to onboard's number; exclusive with --from-number-id)")
161
+ .option("--from-number-id <id>", "phoneNumberId to send from (defaults to onboard's number; exclusive with --from-number)")
155
162
  .option("--media <path-or-url>", "media attachment: local file path (uploaded) or public http(s) URL (repeatable, max 10)", (v, prev = []) => [...prev, v], [])
156
163
  .option("--force-audio-file", "send an audio attachment as a regular file attachment instead of an iMessage voice message")
157
164
  .option("--json", "machine-readable output")
@@ -167,6 +174,7 @@ const message = program
167
174
  process.exit(await runMessageSend({
168
175
  to: opts.to,
169
176
  body: opts.body,
177
+ fromNumber: opts.fromNumber,
170
178
  fromNumberId: opts.fromNumberId,
171
179
  media: opts.media,
172
180
  forceAudioFile: !!opts.forceAudioFile,
@@ -204,6 +212,43 @@ message
204
212
  since: opts.since,
205
213
  json: !!opts.json,
206
214
  })));
215
+ const typing = program
216
+ .command("typing")
217
+ .description("Show or clear a typing indicator. iMessage numbers display it; SMS numbers ignore it. POST /api/v1/typing.");
218
+ typing
219
+ .command("start")
220
+ .description("Show a typing indicator to a recipient, as if composing a message from your number.")
221
+ .option("--to-number <e164>", "recipient phone number, E.164 (e.g. +14155551234)")
222
+ .option("--from-number <ref>", "number the indicator appears from: id, owned E.164, or nickname (defaults to onboard's number)")
223
+ .option("--json", "machine-readable output")
224
+ .action(async (opts) => {
225
+ if (!opts.toNumber) {
226
+ console.error("error: --to-number is required. Use `dial typing start --help` for usage.");
227
+ process.exit(2);
228
+ }
229
+ process.exit(await runTypingStart({
230
+ toNumber: opts.toNumber,
231
+ fromNumber: opts.fromNumber,
232
+ json: !!opts.json,
233
+ }));
234
+ });
235
+ typing
236
+ .command("stop")
237
+ .description("Clear a typing indicator previously shown with `typing start`.")
238
+ .option("--to-number <e164>", "recipient phone number, E.164 (e.g. +14155551234)")
239
+ .option("--from-number <ref>", "number the indicator appears from: id, owned E.164, or nickname (defaults to onboard's number)")
240
+ .option("--json", "machine-readable output")
241
+ .action(async (opts) => {
242
+ if (!opts.toNumber) {
243
+ console.error("error: --to-number is required. Use `dial typing stop --help` for usage.");
244
+ process.exit(2);
245
+ }
246
+ process.exit(await runTypingStop({
247
+ toNumber: opts.toNumber,
248
+ fromNumber: opts.fromNumber,
249
+ json: !!opts.json,
250
+ }));
251
+ });
207
252
  const call = program
208
253
  .command("call")
209
254
  .description("Place an outbound voice call. POST /api/v1/calls.")
@@ -213,7 +258,8 @@ const call = program
213
258
  .option("--voice-gender <male|female>", "voice gender for the agent (default: female; pass male to override)")
214
259
  .option("--transfer-to <e164>", "forward-to number, E.164: the agent waits for a real human (riding out hold/IVR) then cold-transfers the call here")
215
260
  .option("--idempotency-key <key>", "unique key (e.g. a UUID) making the placement idempotent: re-running with the same key returns the already-placed call instead of dialing again")
216
- .option("--from-number-id <id>", "phoneNumberId to call from (defaults to onboard's number)")
261
+ .option("--from-number <ref>", "number to call from: id, owned E.164, or nickname (defaults to onboard's number; exclusive with --from-number-id)")
262
+ .option("--from-number-id <id>", "phoneNumberId to call from (defaults to onboard's number; exclusive with --from-number)")
217
263
  .option("--max-call-duration <seconds>", "maximum call duration cap (seconds); call is terminated when this limit is reached", (v) => {
218
264
  const n = parseInt(v, 10);
219
265
  if (!Number.isInteger(n) || n <= 0 || String(n) !== v.trim()) {
@@ -235,6 +281,7 @@ const call = program
235
281
  voiceGender: opts.voiceGender,
236
282
  transferTo: opts.transferTo,
237
283
  idempotencyKey: opts.idempotencyKey,
284
+ fromNumber: opts.fromNumber,
238
285
  fromNumberId: opts.fromNumberId,
239
286
  maxCallDurationSeconds: opts.maxCallDuration,
240
287
  json: !!opts.json,
@@ -19,6 +19,9 @@ export async function runBilling(opts) {
19
19
  else {
20
20
  console.log(`plan: pay-as-you-go`);
21
21
  }
22
+ if (billing.numbersReleaseAt) {
23
+ console.log(`at risk: balance is negative — all numbers release ${billing.numbersReleaseAt} for non-payment (top up to keep them)`);
24
+ }
22
25
  if (billing.numbers.length > 0) {
23
26
  console.log(`numbers:`);
24
27
  for (const n of billing.numbers) {
@@ -10,6 +10,7 @@ export async function runCallSend(opts) {
10
10
  voiceGender: opts.voiceGender,
11
11
  transferTo: opts.transferTo,
12
12
  idempotencyKey: opts.idempotencyKey,
13
+ fromNumber: opts.fromNumber,
13
14
  fromNumberId: opts.fromNumberId,
14
15
  maxCallDurationSeconds: opts.maxCallDurationSeconds,
15
16
  });
@@ -6,6 +6,7 @@ export async function runMessageSend(opts) {
6
6
  const m = await sendMessage({
7
7
  to: opts.to,
8
8
  body: opts.body,
9
+ fromNumber: opts.fromNumber,
9
10
  fromNumberId: opts.fromNumberId,
10
11
  media: opts.media,
11
12
  forceAudioFile: opts.forceAudioFile,
@@ -7,6 +7,7 @@ export async function runNumberPurchase(opts) {
7
7
  inboundInstruction: opts.inboundInstruction,
8
8
  explicitProgrammaticConsent: opts.explicitProgrammaticConsent,
9
9
  inboundVoiceGender: opts.inboundVoiceGender,
10
+ inboundLanguage: opts.inboundLanguage,
10
11
  areaCode: opts.areaCode,
11
12
  includeImessage: opts.includeImessage,
12
13
  });
@@ -7,6 +7,7 @@ export async function runNumberSet(opts) {
7
7
  number: opts.number,
8
8
  inboundInstruction: opts.inboundInstruction,
9
9
  ...(opts.inboundVoiceGender !== undefined ? { inboundVoiceGender: opts.inboundVoiceGender } : {}),
10
+ ...(opts.inboundLanguage !== undefined ? { inboundLanguage: opts.inboundLanguage } : {}),
10
11
  ...(opts.nickname !== undefined ? { nickname: opts.nickname } : {}),
11
12
  ...(opts.maxCallDurationSeconds !== undefined ? { maxCallDurationSeconds: opts.maxCallDurationSeconds } : {}),
12
13
  });
@@ -20,6 +21,7 @@ export async function runNumberSet(opts) {
20
21
  console.log(` nickname: ${n.nickname ?? ""}`);
21
22
  console.log(` inbound instruction: ${n.inboundInstruction ?? ""}`);
22
23
  console.log(` inbound voice gender: ${n.inboundVoiceGender ?? ""}`);
24
+ console.log(` inbound language: ${n.inboundLanguage ?? ""}`);
23
25
  }
24
26
  return 0;
25
27
  }
@@ -0,0 +1,20 @@
1
+ import { setTyping } from "../../lib/ops/typing.js";
2
+ import { isDialError } from "../../lib/ops/errors.js";
3
+ import { printDialError } from "../../lib/cli-error.js";
4
+ export async function runTypingStart(opts) {
5
+ try {
6
+ const result = await setTyping({ toNumber: opts.toNumber, value: true, fromNumber: opts.fromNumber });
7
+ if (opts.json) {
8
+ console.log(JSON.stringify(result));
9
+ }
10
+ else {
11
+ console.log(`typing indicator shown to ${opts.toNumber} (iMessage numbers only — SMS numbers ignore it).`);
12
+ }
13
+ return 0;
14
+ }
15
+ catch (e) {
16
+ if (isDialError(e))
17
+ return printDialError(opts.json, e);
18
+ throw e;
19
+ }
20
+ }
@@ -0,0 +1,20 @@
1
+ import { setTyping } from "../../lib/ops/typing.js";
2
+ import { isDialError } from "../../lib/ops/errors.js";
3
+ import { printDialError } from "../../lib/cli-error.js";
4
+ export async function runTypingStop(opts) {
5
+ try {
6
+ const result = await setTyping({ toNumber: opts.toNumber, value: false, fromNumber: opts.fromNumber });
7
+ if (opts.json) {
8
+ console.log(JSON.stringify(result));
9
+ }
10
+ else {
11
+ console.log(`typing indicator cleared for ${opts.toNumber}.`);
12
+ }
13
+ return 0;
14
+ }
15
+ catch (e) {
16
+ if (isDialError(e))
17
+ return printDialError(opts.json, e);
18
+ throw e;
19
+ }
20
+ }
@@ -16,3 +16,28 @@ export function requireFromNumberId(auth, override) {
16
16
  }
17
17
  return id;
18
18
  }
19
+ /**
20
+ * Resolve a flexible from-number ref (id, owned E.164, or nickname): explicit
21
+ * override, else the saved default number id (an id is a valid ref), else throw.
22
+ */
23
+ export function requireFromNumber(auth, override) {
24
+ const ref = override ?? auth.phoneNumberId;
25
+ if (!ref) {
26
+ throw new DialError("no_from_number", "No default phoneNumberId in auth. Pass --from-number <id|E.164|nickname>.");
27
+ }
28
+ return ref;
29
+ }
30
+ /**
31
+ * Pick the from-number selector field for send/call requests. `--from-number`
32
+ * (flexible ref) and `--from-number-id` (id only) are mutually exclusive —
33
+ * both given fails fast here, before any request; neither falls back to the
34
+ * saved default id via the legacy field.
35
+ */
36
+ export function resolveFromSelector(auth, opts) {
37
+ if (opts.fromNumber && opts.fromNumberId) {
38
+ throw new DialError("from_number_conflict", "Provide only one of --from-number and --from-number-id.");
39
+ }
40
+ if (opts.fromNumber)
41
+ return { fromNumber: opts.fromNumber };
42
+ return { fromNumberId: requireFromNumberId(auth, opts.fromNumberId) };
43
+ }
@@ -1,12 +1,12 @@
1
1
  import { apiGet, apiPost } from "../api.js";
2
- import { requireAuth, requireFromNumberId } from "./auth.js";
2
+ import { requireAuth, resolveFromSelector } from "./auth.js";
3
3
  import { DialError } from "./errors.js";
4
4
  export async function placeCall(opts) {
5
5
  const auth = requireAuth();
6
- const fromNumberId = requireFromNumberId(auth, opts.fromNumberId);
6
+ const from = resolveFromSelector(auth, opts);
7
7
  const res = await apiPost("/api/v1/calls", {
8
8
  to: opts.to,
9
- fromNumberId,
9
+ ...from,
10
10
  outboundInstruction: opts.outboundInstruction,
11
11
  ...(opts.language && { language: opts.language }),
12
12
  // Omitted → the server uses the default voice gender (female).
@@ -1,7 +1,7 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { basename, extname } from "node:path";
3
3
  import { apiGet, apiPost, apiPostMultipart, ApiFormData } from "../api.js";
4
- import { requireAuth, requireFromNumberId } from "./auth.js";
4
+ import { requireAuth, resolveFromSelector } from "./auth.js";
5
5
  import { DialError } from "./errors.js";
6
6
  export const MAX_MEDIA_ITEMS = 10;
7
7
  // File extensions the API accepts for uploads, mapped to their MIME type
@@ -46,7 +46,7 @@ function readMediaFile(path) {
46
46
  }
47
47
  export async function sendMessage(opts) {
48
48
  const auth = requireAuth();
49
- const fromNumberId = requireFromNumberId(auth, opts.fromNumberId);
49
+ const from = resolveFromSelector(auth, opts);
50
50
  const media = opts.media ?? [];
51
51
  if (media.length > MAX_MEDIA_ITEMS) {
52
52
  throw new DialError("too_much_media", `at most ${MAX_MEDIA_ITEMS} media items are allowed per message (got ${media.length})`);
@@ -61,7 +61,7 @@ export async function sendMessage(opts) {
61
61
  res = await apiPost("/api/v1/messages", {
62
62
  to: opts.to,
63
63
  ...(opts.body ? { body: opts.body } : {}),
64
- fromNumberId,
64
+ ...from,
65
65
  ...(media.length ? { mediaUrls: media } : {}),
66
66
  ...(opts.forceAudioFile ? { forceAudioFile: true } : {}),
67
67
  }, auth.apiKey);
@@ -71,7 +71,8 @@ export async function sendMessage(opts) {
71
71
  form.set("to", opts.to);
72
72
  if (opts.body)
73
73
  form.set("body", opts.body);
74
- form.set("fromNumberId", fromNumberId);
74
+ for (const [field, value] of Object.entries(from))
75
+ form.set(field, value);
75
76
  if (opts.forceAudioFile)
76
77
  form.set("forceAudioFile", "true");
77
78
  for (const item of media) {
@@ -16,6 +16,8 @@ export async function purchaseNumber(opts) {
16
16
  };
17
17
  if (opts.inboundVoiceGender)
18
18
  body.inboundVoiceGender = opts.inboundVoiceGender;
19
+ if (opts.inboundLanguage)
20
+ body.inboundLanguage = opts.inboundLanguage;
19
21
  // iMessage numbers ignore areaCode, so only send it for standard numbers.
20
22
  if (opts.includeImessage)
21
23
  body.capabilities = ["sms", "call", "imessage"];
@@ -33,12 +35,14 @@ export async function setNumberProperties(opts) {
33
35
  // Empty string clears the override → send null (the enum API rejects "").
34
36
  if (opts.inboundVoiceGender !== undefined)
35
37
  body.inboundVoiceGender = opts.inboundVoiceGender || null;
38
+ if (opts.inboundLanguage !== undefined)
39
+ body.inboundLanguage = opts.inboundLanguage || null;
36
40
  if (opts.nickname !== undefined)
37
41
  body.nickname = opts.nickname;
38
42
  if (opts.maxCallDurationSeconds !== undefined)
39
43
  body.maxCallDurationSeconds = opts.maxCallDurationSeconds;
40
44
  if (Object.keys(body).length === 0) {
41
- throw new DialError("bad_request", "Provide at least one property to update (inboundInstruction, inboundVoiceGender, nickname, or maxCallDurationSeconds).");
45
+ throw new DialError("bad_request", "Provide at least one property to update (inboundInstruction, inboundVoiceGender, inboundLanguage, nickname, or maxCallDurationSeconds).");
42
46
  }
43
47
  const auth = requireAuth();
44
48
  // The REST API keys numbers by id; the CLI/tool takes the E.164 number for ergonomics,
@@ -0,0 +1,16 @@
1
+ import { apiPost } from "../api.js";
2
+ import { requireAuth, requireFromNumber } from "./auth.js";
3
+ import { DialError } from "./errors.js";
4
+ /**
5
+ * Show or clear a typing indicator (POST /api/v1/typing). iMessage numbers
6
+ * display it; standard numbers have no typing concept and the server silently
7
+ * no-ops, so calling this unconditionally is safe.
8
+ */
9
+ export async function setTyping(opts) {
10
+ const auth = requireAuth();
11
+ const fromNumber = requireFromNumber(auth, opts.fromNumber);
12
+ const res = await apiPost("/api/v1/typing", { toNumber: opts.toNumber, value: opts.value, fromNumber }, auth.apiKey);
13
+ if (!res.ok)
14
+ throw new DialError("typing_failed", res.error, res.status);
15
+ return res.data;
16
+ }
@@ -26,6 +26,7 @@ export const phoneNumberSchema = z
26
26
  country: z.string().optional(),
27
27
  inboundInstruction: z.string().nullable().optional(),
28
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"),
29
30
  })
30
31
  .passthrough();
31
32
  export const messageSchema = z
@@ -3,6 +3,8 @@ import { purchaseNumberTool } from "./purchase-number.js";
3
3
  import { setNumberPropertiesTool } from "./set-number-properties.js";
4
4
  import { sendMessageTool } from "./send-message.js";
5
5
  import { replyToMessageTool } from "./reply-to-message.js";
6
+ import { startTypingTool } from "./start-typing.js";
7
+ import { stopTypingTool } from "./stop-typing.js";
6
8
  import { listMessagesTool } from "./list-messages.js";
7
9
  import { placeCallTool } from "./place-call.js";
8
10
  import { listCallsTool } from "./list-calls.js";
@@ -25,6 +27,8 @@ export const tools = [
25
27
  setNumberPropertiesTool,
26
28
  sendMessageTool,
27
29
  replyToMessageTool,
30
+ startTypingTool,
31
+ stopTypingTool,
28
32
  listMessagesTool,
29
33
  placeCallTool,
30
34
  listCallsTool,
@@ -9,6 +9,11 @@ const inputSchema = {
9
9
  voiceGender: z.enum(["male", "female"]).optional().describe("Voice gender for the agent; the default is female"),
10
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
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"),
12
+ fromNumber: z
13
+ .string()
14
+ .min(1)
15
+ .optional()
16
+ .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"),
12
17
  fromNumberId: z.string().optional().describe("Number id to call from; defaults to your primary number"),
13
18
  maxCallDurationSeconds: z.number().int().positive().optional().describe("Maximum call duration cap (seconds); the call is terminated when this limit is reached"),
14
19
  };
@@ -30,6 +35,7 @@ export const placeCallTool = {
30
35
  voiceGender: args.voiceGender,
31
36
  transferTo: args.transferTo,
32
37
  idempotencyKey: args.idempotencyKey,
38
+ fromNumber: args.fromNumber,
33
39
  fromNumberId: args.fromNumberId,
34
40
  maxCallDurationSeconds: args.maxCallDurationSeconds,
35
41
  });
@@ -6,6 +6,7 @@ const inputSchema = {
6
6
  inboundInstruction: z.string().min(1).describe("System prompt for inbound calls to this number"),
7
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
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)"),
9
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"),
10
11
  includeImessage: z.boolean().optional().describe('Provision an iMessage number (pay-as-you-go only; provisioned asynchronously — poll List Numbers until setupStatus is "ready")'),
11
12
  };
@@ -23,6 +24,7 @@ export const purchaseNumberTool = {
23
24
  inboundInstruction: args.inboundInstruction,
24
25
  explicitProgrammaticConsent: args.explicitProgrammaticConsent,
25
26
  inboundVoiceGender: args.inboundVoiceGender,
27
+ inboundLanguage: args.inboundLanguage,
26
28
  areaCode: args.areaCode,
27
29
  includeImessage: args.includeImessage,
28
30
  }),
@@ -5,6 +5,11 @@ 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
7
  body: z.string().optional().describe("Message body; optional when mediaUrls is given (media-only send)"),
8
+ fromNumber: z
9
+ .string()
10
+ .min(1)
11
+ .optional()
12
+ .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"),
8
13
  fromNumberId: z.string().optional().describe("Number id to send from; defaults to your primary number"),
9
14
  mediaUrls: z
10
15
  .array(z.string().url())
@@ -29,6 +34,7 @@ export const sendMessageTool = {
29
34
  message: await sendMessage({
30
35
  to: args.to,
31
36
  body: args.body,
37
+ fromNumber: args.fromNumber,
32
38
  fromNumberId: args.fromNumberId,
33
39
  media: args.mediaUrls,
34
40
  forceAudioFile: args.forceAudioFile,
@@ -6,6 +6,7 @@ const inputSchema = {
6
6
  number: z.string().min(7).describe("The E.164 phone number to update (e.g. +14155550123)"),
7
7
  inboundInstruction: z.string().min(1).optional().describe("New system prompt for inbound calls to this number"),
8
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)."),
9
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.'),
10
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."),
11
12
  };
@@ -13,7 +14,7 @@ export const setNumberPropertiesTool = {
13
14
  name: "set_number_properties",
14
15
  config: {
15
16
  title: "Set Number Properties",
16
- description: "Update a phone number's properties: its inbound instruction (the system prompt for inbound calls) and/or its nickname. Provide at least one.",
17
+ description: "Update a phone number's properties: its inbound instruction (the system prompt for inbound calls), inbound voice gender, inbound language, and/or its nickname. Provide at least one.",
17
18
  inputSchema,
18
19
  outputSchema: { number: phoneNumberSchema },
19
20
  annotations: { openWorldHint: true },
@@ -23,6 +24,7 @@ export const setNumberPropertiesTool = {
23
24
  number: args.number,
24
25
  inboundInstruction: args.inboundInstruction,
25
26
  ...(args.inboundVoiceGender !== undefined ? { inboundVoiceGender: args.inboundVoiceGender } : {}),
27
+ ...(args.inboundLanguage !== undefined ? { inboundLanguage: args.inboundLanguage } : {}),
26
28
  ...(args.nickname !== undefined ? { nickname: args.nickname } : {}),
27
29
  ...(args.maxCallDurationSeconds !== undefined ? { maxCallDurationSeconds: args.maxCallDurationSeconds } : {}),
28
30
  }),
@@ -0,0 +1,29 @@
1
+ import { z } from "zod";
2
+ import { jsonResult } from "../result.js";
3
+ import { setTyping } from "../../lib/ops/typing.js";
4
+ const inputSchema = {
5
+ toNumber: z.string().min(7).describe("Recipient phone number, E.164 (e.g. +14155550123)"),
6
+ fromNumber: z
7
+ .string()
8
+ .min(1)
9
+ .describe("Number the indicator appears from: a phone number id, one of your numbers in E.164, or a nickname"),
10
+ };
11
+ export const startTypingTool = {
12
+ name: "start_typing",
13
+ config: {
14
+ title: "Start typing indicator",
15
+ description: "Show a typing indicator to the recipient, as if someone were composing a message from your number. " +
16
+ "iMessage numbers display it; standard (SMS) numbers have no typing concept and silently ignore it, " +
17
+ "so this is safe to call unconditionally before a send. Fire-and-forget and free. " +
18
+ "Delivering a message or reaction clears the indicator natively on the recipient's device — " +
19
+ "call start_typing again after a send to keep composing, and stop_typing when you stop without sending.",
20
+ inputSchema,
21
+ outputSchema: { ok: z.boolean() },
22
+ annotations: { openWorldHint: true },
23
+ },
24
+ run: async (args) => jsonResult(await setTyping({
25
+ toNumber: args.toNumber,
26
+ fromNumber: args.fromNumber,
27
+ value: true,
28
+ })),
29
+ };
@@ -0,0 +1,27 @@
1
+ import { z } from "zod";
2
+ import { jsonResult } from "../result.js";
3
+ import { setTyping } from "../../lib/ops/typing.js";
4
+ const inputSchema = {
5
+ toNumber: z.string().min(7).describe("Recipient phone number, E.164 (e.g. +14155550123)"),
6
+ fromNumber: z
7
+ .string()
8
+ .min(1)
9
+ .describe("Number the indicator appears from: a phone number id, one of your numbers in E.164, or a nickname"),
10
+ };
11
+ export const stopTypingTool = {
12
+ name: "stop_typing",
13
+ config: {
14
+ title: "Stop typing indicator",
15
+ description: "Clear a typing indicator previously shown with start_typing. Delivering a message or reaction " +
16
+ "already clears it natively on the recipient's device — call this when you stop composing " +
17
+ "without sending. Standard (SMS) numbers silently ignore it.",
18
+ inputSchema,
19
+ outputSchema: { ok: z.boolean() },
20
+ annotations: { openWorldHint: true },
21
+ },
22
+ run: async (args) => jsonResult(await setTyping({
23
+ toNumber: args.toNumber,
24
+ fromNumber: args.fromNumber,
25
+ value: false,
26
+ })),
27
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getdial/cli",
3
- "version": "0.29.0",
3
+ "version": "0.31.0",
4
4
  "description": "Dial CLI — install, sign up, and run the local listen service.",
5
5
  "license": "MIT",
6
6
  "repository": {
package/skills.tar.gz CHANGED
Binary file