@getdial/cli 0.38.0 → 0.40.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
@@ -47,6 +47,22 @@ function parsePositiveInteger(value) {
47
47
  }
48
48
  return parsed;
49
49
  }
50
+ /**
51
+ * `--calling <on|off>` → boolean.
52
+ *
53
+ * One flag taking an explicit value, rather than a `--calling`/`--no-calling`
54
+ * pair: it reads the same in a script and in a skill, and it cannot be confused
55
+ * with "leave unchanged" (which is what omitting the flag means). Anything other
56
+ * than the two spellings is an error, never quietly read as off.
57
+ */
58
+ function parseCalling(value) {
59
+ const normalized = value.trim().toLowerCase();
60
+ if (normalized === "on")
61
+ return true;
62
+ if (normalized === "off")
63
+ return false;
64
+ throw new InvalidArgumentError(`must be "on" or "off", got: ${value}`);
65
+ }
50
66
  program
51
67
  .name("dial")
52
68
  .description("Dial CLI — set up your account and run the listen service.")
@@ -166,6 +182,7 @@ number
166
182
  .option("--area-code <code>", "preferred US area code (only US numbers can be provisioned; ignored with --include-imessage)")
167
183
  .option("--include-imessage", "provision an iMessage number (pay-as-you-go only; provisioned asynchronously — poll `dial number list` until ready)")
168
184
  .option("--whatsapp", "also connect WhatsApp to the new line (beta, enabled per account). Requires --include-imessage: WhatsApp is a channel on an iMessage line")
185
+ .option("--calling <on|off>", 'whether calling is switched on for the new number (default: on). "off" provisions a messaging-only line with no window in which it answers a call', parseCalling)
169
186
  .option("--json", "machine-readable output")
170
187
  .action(async (opts) => process.exit(await runNumberPurchase({
171
188
  inboundInstruction: opts.inboundInstruction,
@@ -175,6 +192,7 @@ number
175
192
  areaCode: opts.areaCode,
176
193
  includeImessage: !!opts.includeImessage,
177
194
  whatsapp: !!opts.whatsapp,
195
+ callingEnabled: opts.calling,
178
196
  json: !!opts.json,
179
197
  })));
180
198
  number
@@ -201,6 +219,9 @@ number
201
219
  .option("--first-name <text>", 'iMessage display first name shown beside this number\'s messages (iMessage numbers only); pass "" to clear')
202
220
  .option("--last-name <text>", 'iMessage display last name (iMessage numbers only); pass "" to clear')
203
221
  .option("--avatar <path-or-url>", "iMessage avatar photo (iMessage numbers only): a local image file (jpeg/png/gif/webp, max 5 MB) to upload, or a public image URL to fetch. Replaces the current photo; photos can't be removed")
222
+ .option("--whatsapp-name <text>", "WhatsApp display name shown to recipients (WhatsApp-ready numbers only): 1-25 chars, no reserved marks. The call blocks until WhatsApp applies it")
223
+ .option("--whatsapp-avatar <path-or-url>", "WhatsApp avatar photo (WhatsApp-ready numbers only): a local image file or public URL. Square jpeg/png between 192x192 and 640x640 (not resized)")
224
+ .option("--calling <on|off>", "switch calling on or off for this number, both directions: off means inbound calls aren't connected and none can be placed from it (messaging is unaffected)", parseCalling)
204
225
  .option("--json", "machine-readable output")
205
226
  .action(async (numberArg, opts) => {
206
227
  let maxCallDurationSeconds;
@@ -220,6 +241,9 @@ number
220
241
  firstName: opts.firstName,
221
242
  lastName: opts.lastName,
222
243
  avatar: opts.avatar,
244
+ whatsappName: opts.whatsappName,
245
+ whatsappAvatar: opts.whatsappAvatar,
246
+ callingEnabled: opts.calling,
223
247
  json: !!opts.json,
224
248
  }));
225
249
  });
@@ -15,7 +15,11 @@ export async function runNumberList(opts) {
15
15
  for (const n of numbers) {
16
16
  const tag = n.id === defaultNumberId ? " (default)" : "";
17
17
  const nickname = n.nickname ? ` "${n.nickname}"` : "";
18
- console.log(`${n.number} id=${n.id} ${n.country}${nickname}${tag}`);
18
+ // Marked only when off. `--json` carries callingEnabled either way, but
19
+ // this line is hand-built, so without this the switch would be invisible
20
+ // in the CLI's default output.
21
+ const calling = n.callingEnabled === false ? " calling:off" : "";
22
+ console.log(`${n.number} id=${n.id} ${n.country}${nickname}${calling}${tag}`);
19
23
  }
20
24
  return 0;
21
25
  }
@@ -17,6 +17,7 @@ export async function runNumberPurchase(opts) {
17
17
  inboundLanguage: opts.inboundLanguage,
18
18
  areaCode: opts.areaCode,
19
19
  includeImessage: opts.includeImessage,
20
+ callingEnabled: opts.callingEnabled,
20
21
  whatsapp: opts.whatsapp,
21
22
  });
22
23
  if (opts.json) {
@@ -27,6 +28,10 @@ export async function runNumberPurchase(opts) {
27
28
  console.log(` number: ${n.number}`);
28
29
  console.log(` id: ${n.id}`);
29
30
  console.log(` country: ${n.country}`);
31
+ // Only worth a line when it isn't the default — a messaging-only line is
32
+ // a surprising thing to discover later.
33
+ if (n.callingEnabled === false)
34
+ console.log(` calling: off (messaging only)`);
30
35
  // iMessage numbers provision asynchronously: the number is returned right
31
36
  // away in setupStatus "provisioning". Tell the user to poll before using it.
32
37
  if (opts.includeImessage) {
@@ -17,6 +17,9 @@ export async function runNumberSet(opts) {
17
17
  ...(opts.firstName !== undefined ? { firstName: opts.firstName } : {}),
18
18
  ...(opts.lastName !== undefined ? { lastName: opts.lastName } : {}),
19
19
  ...(opts.avatar !== undefined ? { avatar: opts.avatar } : {}),
20
+ ...(opts.callingEnabled !== undefined ? { callingEnabled: opts.callingEnabled } : {}),
21
+ ...(opts.whatsappName !== undefined ? { whatsappName: opts.whatsappName } : {}),
22
+ ...(opts.whatsappAvatar !== undefined ? { whatsappAvatar: opts.whatsappAvatar } : {}),
20
23
  });
21
24
  if (opts.json) {
22
25
  console.log(JSON.stringify({ ok: true, number: n }));
@@ -29,11 +32,18 @@ export async function runNumberSet(opts) {
29
32
  console.log(` inbound instruction: ${n.inboundInstruction ?? ""}`);
30
33
  console.log(` inbound voice gender: ${n.inboundVoiceGender ?? ""}`);
31
34
  console.log(` inbound language: ${n.inboundLanguage ?? ""}`);
35
+ // Always printed, not only when it changed: "is calling on?" is the
36
+ // question someone runs this command to settle.
37
+ console.log(` calling: ${n.callingEnabled === false ? "off" : "on"}`);
32
38
  const hasIdentity = n.firstName != null || n.lastName != null || n.avatarUrl != null;
33
39
  if (hasIdentity) {
34
40
  console.log(` display name: ${[n.firstName, n.lastName].filter(Boolean).join(" ")}`);
35
41
  console.log(` avatar: ${n.avatarUrl ?? ""}`);
36
42
  }
43
+ if (n.whatsappName != null || n.whatsappAvatarUrl != null) {
44
+ console.log(` whatsapp name: ${n.whatsappName ?? ""}`);
45
+ console.log(` whatsapp avatar: ${n.whatsappAvatarUrl ?? ""}`);
46
+ }
37
47
  }
38
48
  return 0;
39
49
  }
@@ -46,6 +46,10 @@ export async function purchaseNumber(opts) {
46
46
  body.inboundVoiceGender = opts.inboundVoiceGender;
47
47
  if (opts.inboundLanguage)
48
48
  body.inboundLanguage = opts.inboundLanguage;
49
+ // Only sent when asked for, so the server's default (on) stays the one place
50
+ // that decides what an unspecified number does.
51
+ if (opts.callingEnabled !== undefined)
52
+ body.callingEnabled = opts.callingEnabled;
49
53
  // iMessage numbers ignore areaCode, so only send it for standard numbers.
50
54
  if (opts.includeImessage)
51
55
  body.capabilities = ["sms", "call", "imessage"];
@@ -117,14 +121,23 @@ export async function setNumberProperties(opts) {
117
121
  body.firstName = opts.firstName;
118
122
  if (opts.lastName !== undefined)
119
123
  body.lastName = opts.lastName;
124
+ if (opts.callingEnabled !== undefined)
125
+ body.callingEnabled = opts.callingEnabled;
126
+ if (opts.whatsappName !== undefined)
127
+ body.whatsappName = opts.whatsappName;
120
128
  // A URL avatar goes in the JSON body; a local file forces multipart (below).
121
129
  // Read + validate the file up front, before any API round-trip, so a bad
122
130
  // path or unsupported type fails fast.
123
131
  const avatarFile = opts.avatar !== undefined && !isHttpUrl(opts.avatar) ? readAvatarFile(opts.avatar) : null;
124
132
  if (opts.avatar !== undefined && !avatarFile)
125
133
  body.avatarUrl = opts.avatar;
126
- if (Object.keys(body).length === 0 && !avatarFile) {
127
- throw new DialError("bad_request", "Provide at least one property to update (inboundInstruction, inboundVoiceGender, inboundLanguage, nickname, maxCallDurationSeconds, firstName, lastName, or avatar).");
134
+ const whatsappAvatarFile = opts.whatsappAvatar !== undefined && !isHttpUrl(opts.whatsappAvatar)
135
+ ? readAvatarFile(opts.whatsappAvatar)
136
+ : null;
137
+ if (opts.whatsappAvatar !== undefined && !whatsappAvatarFile)
138
+ body.whatsappAvatarUrl = opts.whatsappAvatar;
139
+ if (Object.keys(body).length === 0 && !avatarFile && !whatsappAvatarFile) {
140
+ throw new DialError("bad_request", "Provide at least one property to update (inboundInstruction, inboundVoiceGender, inboundLanguage, nickname, maxCallDurationSeconds, calling, firstName, lastName, avatar, whatsappName, or whatsappAvatar).");
128
141
  }
129
142
  const auth = maybeAuth();
130
143
  // The REST API keys numbers by id; the CLI/tool takes the E.164 number for ergonomics,
@@ -141,11 +154,18 @@ export async function setNumberProperties(opts) {
141
154
  // A local avatar file forces a multipart PATCH: every scalar field goes in as a
142
155
  // text part alongside the uploaded `avatar` file. Otherwise a plain JSON PATCH.
143
156
  let res;
144
- if (avatarFile) {
157
+ if (avatarFile || whatsappAvatarFile) {
145
158
  const form = new ApiFormData();
146
159
  for (const [field, value] of Object.entries(body))
147
160
  form.set(field, String(value));
148
- form.append("avatar", new Blob([new Uint8Array(avatarFile.data)], { type: avatarFile.contentType }), avatarFile.name);
161
+ if (avatarFile) {
162
+ form.append("avatar", new Blob([new Uint8Array(avatarFile.data)], { type: avatarFile.contentType }), avatarFile.name);
163
+ }
164
+ if (whatsappAvatarFile) {
165
+ form.append("whatsappAvatar", new Blob([new Uint8Array(whatsappAvatarFile.data)], {
166
+ type: whatsappAvatarFile.contentType,
167
+ }), whatsappAvatarFile.name);
168
+ }
149
169
  res = await apiPatchMultipart(path, form, auth?.apiKey);
150
170
  }
151
171
  else {
@@ -38,6 +38,10 @@ export const phoneNumberSchema = z
38
38
  .nullable()
39
39
  .optional()
40
40
  .describe("BCP-47 language tag inbound calls are pinned to; null → detected from the caller's country prefix per call"),
41
+ callingEnabled: z
42
+ .boolean()
43
+ .optional()
44
+ .describe("Whether calling is switched on for this number, both directions. false → inbound calls are not connected and place_call from it fails with calling_disabled; messaging is unaffected"),
41
45
  firstName: z
42
46
  .string()
43
47
  .nullable()
@@ -53,6 +57,16 @@ export const phoneNumberSchema = z
53
57
  .nullable()
54
58
  .optional()
55
59
  .describe("URL of the number's iMessage avatar photo; null when unset or not an iMessage number"),
60
+ whatsappName: z
61
+ .string()
62
+ .nullable()
63
+ .optional()
64
+ .describe("WhatsApp display name; null on numbers without a WhatsApp track"),
65
+ whatsappAvatarUrl: z
66
+ .string()
67
+ .nullable()
68
+ .optional()
69
+ .describe("URL of the number's WhatsApp avatar; null when unset"),
56
70
  })
57
71
  .passthrough();
58
72
  /**
@@ -25,6 +25,10 @@ const inputSchema = {
25
25
  .boolean()
26
26
  .optional()
27
27
  .describe('Provision an iMessage number (pay-as-you-go only; provisioned asynchronously — poll List Numbers until setupStatus is "ready")'),
28
+ callingEnabled: z
29
+ .boolean()
30
+ .optional()
31
+ .describe("Whether calling is switched on for the new number; omitted → true. Pass false for a messaging-only line: inbound calls are never connected and place_call from it fails, with no window in which it answers a call. Changeable later with set_number_properties."),
28
32
  };
29
33
  export const purchaseNumberTool = {
30
34
  name: "purchase_number",
@@ -43,6 +47,7 @@ export const purchaseNumberTool = {
43
47
  inboundLanguage: args.inboundLanguage,
44
48
  areaCode: args.areaCode,
45
49
  includeImessage: args.includeImessage,
50
+ callingEnabled: args.callingEnabled,
46
51
  }),
47
52
  }),
48
53
  };
@@ -44,12 +44,25 @@ const inputSchema = {
44
44
  .url()
45
45
  .optional()
46
46
  .describe("Public image URL to set as the number's iMessage avatar photo (the server downloads it). jpeg/png/gif/webp, max 5 MB. iMessage numbers only. The photo can be replaced but not removed."),
47
+ whatsappName: z
48
+ .string()
49
+ .optional()
50
+ .describe("WhatsApp display name shown to recipients. 1-25 chars, no reserved verification marks. WhatsApp-ready numbers only; the call blocks until WhatsApp applies it."),
51
+ whatsappAvatarUrl: z
52
+ .string()
53
+ .url()
54
+ .optional()
55
+ .describe("Public image URL to set as the number's WhatsApp avatar (the server downloads it). Square jpeg or png between 192x192 and 640x640 (not resized). WhatsApp-ready numbers only."),
56
+ callingEnabled: z
57
+ .boolean()
58
+ .optional()
59
+ .describe("Switch calling on or off for this number, in both directions. false stops inbound calls from being connected (the caller is never answered) and makes place_call from this number fail with calling_disabled (409); messaging on the number is unaffected. Takes effect on the next call — a call already in progress is not ended. Omit to leave it unchanged."),
47
60
  };
48
61
  export const setNumberPropertiesTool = {
49
62
  name: "set_number_properties",
50
63
  config: {
51
64
  title: "Set Number Properties",
52
- description: "Update a phone number's properties: its inbound instruction (the system prompt for inbound calls), inbound voice gender, inbound language, nickname, and — for iMessage numbers — its display identity (firstName, lastName, avatarUrl shown beside its messages). Provide at least one.",
65
+ description: "Update a phone number's properties: its inbound instruction (the system prompt for inbound calls), inbound voice gender, inbound language, nickname, whether calling is switched on at all (callingEnabled), and — for iMessage numbers — its display identity (firstName, lastName, avatarUrl), and for WhatsApp-ready numbers its WhatsApp identity (whatsappName, whatsappAvatarUrl). Provide at least one.",
53
66
  inputSchema,
54
67
  outputSchema: { number: phoneNumberSchema },
55
68
  annotations: { openWorldHint: true },
@@ -69,6 +82,13 @@ export const setNumberPropertiesTool = {
69
82
  ? { maxCallDurationSeconds: args.maxCallDurationSeconds }
70
83
  : {}),
71
84
  ...(args.firstName !== undefined ? { firstName: args.firstName } : {}),
85
+ ...(args.whatsappName !== undefined ? { whatsappName: args.whatsappName } : {}),
86
+ ...(args.callingEnabled !== undefined
87
+ ? { callingEnabled: args.callingEnabled }
88
+ : {}),
89
+ ...(args.whatsappAvatarUrl !== undefined
90
+ ? { whatsappAvatar: args.whatsappAvatarUrl }
91
+ : {}),
72
92
  ...(args.lastName !== undefined ? { lastName: args.lastName } : {}),
73
93
  ...(args.avatarUrl !== undefined ? { avatar: args.avatarUrl } : {}),
74
94
  }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getdial/cli",
3
- "version": "0.38.0",
3
+ "version": "0.40.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