@getdial/cli 0.28.0 → 0.30.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/README.md CHANGED
@@ -54,6 +54,7 @@ dial wait-for message.received --field to=+14155550123
54
54
  | `dial number set <number>` | Update a number's inbound instruction. |
55
55
  | `dial message` | Send an SMS. |
56
56
  | `dial message list` | List recent messages. |
57
+ | `dial message reply` | Reply or react to a message. |
57
58
  | `dial call` | Place an outbound AI voice call. |
58
59
  | `dial call list` | List recent calls. |
59
60
  | `dial call get <id>` | Fetch a single call — status, duration, transcript. |
package/dist/cli.js CHANGED
@@ -14,7 +14,10 @@ import { runNumberList } from "./commands/number/list.js";
14
14
  import { runNumberPurchase } from "./commands/number/purchase.js";
15
15
  import { runNumberSet } from "./commands/number/set.js";
16
16
  import { runMessageSend } from "./commands/message/send.js";
17
+ import { runMessageReply } from "./commands/message/reply.js";
17
18
  import { runMessageList } from "./commands/message/list.js";
19
+ import { runTypingStart } from "./commands/typing/start.js";
20
+ import { runTypingStop } from "./commands/typing/stop.js";
18
21
  import { runCallSend } from "./commands/call/send.js";
19
22
  import { runCallList } from "./commands/call/list.js";
20
23
  import { runCallGet } from "./commands/call/get.js";
@@ -150,7 +153,8 @@ const message = program
150
153
  .description("Send an SMS, optionally with media (MMS). POST /api/v1/messages.")
151
154
  .option("--to <e164>", "destination phone number, E.164 (e.g. +14155551234)")
152
155
  .option("--body <text>", "message body")
153
- .option("--from-number-id <id>", "phoneNumberId to send from (defaults to onboard's number)")
156
+ .option("--from-number <ref>", "number to send from: id, owned E.164, or nickname (defaults to onboard's number; exclusive with --from-number-id)")
157
+ .option("--from-number-id <id>", "phoneNumberId to send from (defaults to onboard's number; exclusive with --from-number)")
154
158
  .option("--media <path-or-url>", "media attachment: local file path (uploaded) or public http(s) URL (repeatable, max 10)", (v, prev = []) => [...prev, v], [])
155
159
  .option("--force-audio-file", "send an audio attachment as a regular file attachment instead of an iMessage voice message")
156
160
  .option("--json", "machine-readable output")
@@ -166,12 +170,31 @@ const message = program
166
170
  process.exit(await runMessageSend({
167
171
  to: opts.to,
168
172
  body: opts.body,
173
+ fromNumber: opts.fromNumber,
169
174
  fromNumberId: opts.fromNumberId,
170
175
  media: opts.media,
171
176
  forceAudioFile: !!opts.forceAudioFile,
172
177
  json: !!opts.json,
173
178
  }));
174
179
  });
180
+ message
181
+ .command("reply <messageId>")
182
+ .description("Reply or react to a message. POST /api/v1/messages/:id/reply.")
183
+ .option("--body <text>", "reply text (threads under the target on iMessage numbers)")
184
+ .option("--react <reaction>", "reaction: love|like|dislike|laugh|emphasize|question, or a single emoji")
185
+ .option("--json", "machine-readable output")
186
+ .action(async (messageId, opts) => {
187
+ if ((opts.body === undefined) === (opts.react === undefined)) {
188
+ console.error("error: provide exactly one of --body or --react. Use `dial message reply --help` for usage.");
189
+ process.exit(2);
190
+ }
191
+ process.exit(await runMessageReply({
192
+ messageId,
193
+ body: opts.body,
194
+ react: opts.react,
195
+ json: !!opts.json,
196
+ }));
197
+ });
175
198
  message
176
199
  .command("list")
177
200
  .description("List recent messages on your account. GET /api/v1/messages.")
@@ -185,6 +208,43 @@ message
185
208
  since: opts.since,
186
209
  json: !!opts.json,
187
210
  })));
211
+ const typing = program
212
+ .command("typing")
213
+ .description("Show or clear a typing indicator. iMessage numbers display it; SMS numbers ignore it. POST /api/v1/typing.");
214
+ typing
215
+ .command("start")
216
+ .description("Show a typing indicator to a recipient, as if composing a message from your number.")
217
+ .option("--to-number <e164>", "recipient phone number, E.164 (e.g. +14155551234)")
218
+ .option("--from-number <ref>", "number the indicator appears from: id, owned E.164, or nickname (defaults to onboard's number)")
219
+ .option("--json", "machine-readable output")
220
+ .action(async (opts) => {
221
+ if (!opts.toNumber) {
222
+ console.error("error: --to-number is required. Use `dial typing start --help` for usage.");
223
+ process.exit(2);
224
+ }
225
+ process.exit(await runTypingStart({
226
+ toNumber: opts.toNumber,
227
+ fromNumber: opts.fromNumber,
228
+ json: !!opts.json,
229
+ }));
230
+ });
231
+ typing
232
+ .command("stop")
233
+ .description("Clear a typing indicator previously shown with `typing start`.")
234
+ .option("--to-number <e164>", "recipient phone number, E.164 (e.g. +14155551234)")
235
+ .option("--from-number <ref>", "number the indicator appears from: id, owned E.164, or nickname (defaults to onboard's number)")
236
+ .option("--json", "machine-readable output")
237
+ .action(async (opts) => {
238
+ if (!opts.toNumber) {
239
+ console.error("error: --to-number is required. Use `dial typing stop --help` for usage.");
240
+ process.exit(2);
241
+ }
242
+ process.exit(await runTypingStop({
243
+ toNumber: opts.toNumber,
244
+ fromNumber: opts.fromNumber,
245
+ json: !!opts.json,
246
+ }));
247
+ });
188
248
  const call = program
189
249
  .command("call")
190
250
  .description("Place an outbound voice call. POST /api/v1/calls.")
@@ -194,7 +254,8 @@ const call = program
194
254
  .option("--voice-gender <male|female>", "voice gender for the agent (default: female; pass male to override)")
195
255
  .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")
196
256
  .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")
197
- .option("--from-number-id <id>", "phoneNumberId to call from (defaults to onboard's number)")
257
+ .option("--from-number <ref>", "number to call from: id, owned E.164, or nickname (defaults to onboard's number; exclusive with --from-number-id)")
258
+ .option("--from-number-id <id>", "phoneNumberId to call from (defaults to onboard's number; exclusive with --from-number)")
198
259
  .option("--max-call-duration <seconds>", "maximum call duration cap (seconds); call is terminated when this limit is reached", (v) => {
199
260
  const n = parseInt(v, 10);
200
261
  if (!Number.isInteger(n) || n <= 0 || String(n) !== v.trim()) {
@@ -216,6 +277,7 @@ const call = program
216
277
  voiceGender: opts.voiceGender,
217
278
  transferTo: opts.transferTo,
218
279
  idempotencyKey: opts.idempotencyKey,
280
+ fromNumber: opts.fromNumber,
219
281
  fromNumberId: opts.fromNumberId,
220
282
  maxCallDurationSeconds: opts.maxCallDuration,
221
283
  json: !!opts.json,
@@ -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
  });
@@ -0,0 +1,29 @@
1
+ import { replyToMessage } from "../../lib/ops/messages.js";
2
+ import { isDialError } from "../../lib/ops/errors.js";
3
+ import { printDialError } from "../../lib/cli-error.js";
4
+ export async function runMessageReply(opts) {
5
+ try {
6
+ const m = await replyToMessage({ messageId: opts.messageId, body: opts.body, reaction: opts.react });
7
+ if (opts.json) {
8
+ console.log(JSON.stringify({ ok: true, message: m }));
9
+ }
10
+ else {
11
+ console.log(`sent.`);
12
+ console.log(` channel: ${m.channel}`);
13
+ console.log(` from: ${m.from}`);
14
+ console.log(` to: ${m.to}`);
15
+ console.log(` body: ${m.body}`);
16
+ if (m.reaction)
17
+ console.log(` reaction: ${m.reaction}`);
18
+ if (m.replyToId)
19
+ console.log(` replyTo: ${m.replyToId}`);
20
+ console.log(` status: ${m.status}`);
21
+ }
22
+ return 0;
23
+ }
24
+ catch (e) {
25
+ if (isDialError(e))
26
+ return printDialError(opts.json, e);
27
+ throw e;
28
+ }
29
+ }
@@ -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,
@@ -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) {
@@ -104,3 +105,17 @@ export async function listMessages(opts) {
104
105
  throw new DialError("list_failed", res.error, res.status);
105
106
  return res.data.messages ?? [];
106
107
  }
108
+ export async function replyToMessage(opts) {
109
+ const auth = requireAuth();
110
+ // No `to`/`fromNumberId`: the server derives both from the target message —
111
+ // the reply stays in the conversation the target is part of.
112
+ const payload = {};
113
+ if (opts.body !== undefined)
114
+ payload.body = opts.body;
115
+ if (opts.reaction !== undefined)
116
+ payload.reaction = opts.reaction;
117
+ const res = await apiPost(`/api/v1/messages/${encodeURIComponent(opts.messageId)}/reply`, payload, auth.apiKey);
118
+ if (!res.ok)
119
+ throw new DialError("reply_failed", res.error, res.status);
120
+ return res.data.message;
121
+ }
@@ -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
+ }
@@ -38,6 +38,8 @@ export const messageSchema = z
38
38
  direction: z.string().optional(),
39
39
  status: statusSchema,
40
40
  statusError: z.string().nullish().describe("Failure reason when status is undelivered/failed"),
41
+ replyToId: z.string().nullish().describe("Id of the message this one replies or reacts to; null for ordinary messages"),
42
+ reaction: z.string().nullish().describe("The reaction this message carries (a reaction name or an emoji); null otherwise"),
41
43
  createdAt: z.string().optional(),
42
44
  })
43
45
  .passthrough();
@@ -2,6 +2,9 @@ import { listNumbersTool } from "./list-numbers.js";
2
2
  import { purchaseNumberTool } from "./purchase-number.js";
3
3
  import { setNumberPropertiesTool } from "./set-number-properties.js";
4
4
  import { sendMessageTool } from "./send-message.js";
5
+ import { replyToMessageTool } from "./reply-to-message.js";
6
+ import { startTypingTool } from "./start-typing.js";
7
+ import { stopTypingTool } from "./stop-typing.js";
5
8
  import { listMessagesTool } from "./list-messages.js";
6
9
  import { placeCallTool } from "./place-call.js";
7
10
  import { listCallsTool } from "./list-calls.js";
@@ -23,6 +26,9 @@ export const tools = [
23
26
  purchaseNumberTool,
24
27
  setNumberPropertiesTool,
25
28
  sendMessageTool,
29
+ replyToMessageTool,
30
+ startTypingTool,
31
+ stopTypingTool,
26
32
  listMessagesTool,
27
33
  placeCallTool,
28
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
  });
@@ -0,0 +1,31 @@
1
+ import { z } from "zod";
2
+ import { jsonResult } from "../result.js";
3
+ import { replyToMessage } from "../../lib/ops/messages.js";
4
+ import { messageSchema } from "../schemas.js";
5
+ const inputSchema = {
6
+ messageId: z
7
+ .string()
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"),
10
+ reaction: z
11
+ .string()
12
+ .optional()
13
+ .describe("Reaction to send instead of a body: love, like, dislike, laugh, emphasize, question, or a single emoji"),
14
+ };
15
+ export const replyToMessageTool = {
16
+ name: "reply_to_message",
17
+ config: {
18
+ title: "Reply to a message",
19
+ description: "Reply in-thread or react to an existing message. The reply goes out from the Dial number the target message belongs to, to the other party — no from/to needed. Provide exactly one of body or reaction. On iMessage numbers replies thread and reactions are native; recipients that can only receive SMS get an emoji reaction as a regular text, and named reactions are rejected.",
20
+ inputSchema,
21
+ outputSchema: { message: messageSchema },
22
+ annotations: { openWorldHint: true },
23
+ },
24
+ run: async (args) => jsonResult({
25
+ message: await replyToMessage({
26
+ messageId: args.messageId,
27
+ body: args.body,
28
+ reaction: args.reaction,
29
+ }),
30
+ }),
31
+ };
@@ -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,
@@ -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.28.0",
3
+ "version": "0.30.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