@getdial/cli 0.29.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/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";
@@ -151,7 +153,8 @@ const message = program
151
153
  .description("Send an SMS, optionally with media (MMS). POST /api/v1/messages.")
152
154
  .option("--to <e164>", "destination phone number, E.164 (e.g. +14155551234)")
153
155
  .option("--body <text>", "message body")
154
- .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)")
155
158
  .option("--media <path-or-url>", "media attachment: local file path (uploaded) or public http(s) URL (repeatable, max 10)", (v, prev = []) => [...prev, v], [])
156
159
  .option("--force-audio-file", "send an audio attachment as a regular file attachment instead of an iMessage voice message")
157
160
  .option("--json", "machine-readable output")
@@ -167,6 +170,7 @@ const message = program
167
170
  process.exit(await runMessageSend({
168
171
  to: opts.to,
169
172
  body: opts.body,
173
+ fromNumber: opts.fromNumber,
170
174
  fromNumberId: opts.fromNumberId,
171
175
  media: opts.media,
172
176
  forceAudioFile: !!opts.forceAudioFile,
@@ -204,6 +208,43 @@ message
204
208
  since: opts.since,
205
209
  json: !!opts.json,
206
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
+ });
207
248
  const call = program
208
249
  .command("call")
209
250
  .description("Place an outbound voice call. POST /api/v1/calls.")
@@ -213,7 +254,8 @@ const call = program
213
254
  .option("--voice-gender <male|female>", "voice gender for the agent (default: female; pass male to override)")
214
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")
215
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")
216
- .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)")
217
259
  .option("--max-call-duration <seconds>", "maximum call duration cap (seconds); call is terminated when this limit is reached", (v) => {
218
260
  const n = parseInt(v, 10);
219
261
  if (!Number.isInteger(n) || n <= 0 || String(n) !== v.trim()) {
@@ -235,6 +277,7 @@ const call = program
235
277
  voiceGender: opts.voiceGender,
236
278
  transferTo: opts.transferTo,
237
279
  idempotencyKey: opts.idempotencyKey,
280
+ fromNumber: opts.fromNumber,
238
281
  fromNumberId: opts.fromNumberId,
239
282
  maxCallDurationSeconds: opts.maxCallDuration,
240
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
  });
@@ -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) {
@@ -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
+ }
@@ -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
  });
@@ -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.29.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