@getdial/cli 0.33.6 → 0.34.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
@@ -18,7 +18,7 @@ Or use the bootstrap script, which installs the CLI and walks you through sign-u
18
18
  curl -fsSL https://getdial.ai/install | bash
19
19
  ```
20
20
 
21
- Requires **Node.js 22+**.
21
+ Requires **Node.js 22.19+**.
22
22
 
23
23
  ## Quick start
24
24
 
package/dist/cli.js CHANGED
@@ -147,6 +147,9 @@ number
147
147
  return n;
148
148
  })
149
149
  .option("--clear-max-call-duration", "remove the per-number call duration cap")
150
+ .option("--first-name <text>", 'iMessage display first name shown beside this number\'s messages (iMessage numbers only); pass "" to clear')
151
+ .option("--last-name <text>", 'iMessage display last name (iMessage numbers only); pass "" to clear')
152
+ .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")
150
153
  .option("--json", "machine-readable output")
151
154
  .action(async (numberArg, opts) => {
152
155
  let maxCallDurationSeconds;
@@ -163,6 +166,9 @@ number
163
166
  inboundLanguage: opts.inboundLanguage,
164
167
  nickname: opts.nickname,
165
168
  maxCallDurationSeconds,
169
+ firstName: opts.firstName,
170
+ lastName: opts.lastName,
171
+ avatar: opts.avatar,
166
172
  json: !!opts.json,
167
173
  }));
168
174
  });
@@ -14,6 +14,9 @@ export async function runNumberSet(opts) {
14
14
  ...(opts.maxCallDurationSeconds !== undefined
15
15
  ? { maxCallDurationSeconds: opts.maxCallDurationSeconds }
16
16
  : {}),
17
+ ...(opts.firstName !== undefined ? { firstName: opts.firstName } : {}),
18
+ ...(opts.lastName !== undefined ? { lastName: opts.lastName } : {}),
19
+ ...(opts.avatar !== undefined ? { avatar: opts.avatar } : {}),
17
20
  });
18
21
  if (opts.json) {
19
22
  console.log(JSON.stringify({ ok: true, number: n }));
@@ -26,6 +29,11 @@ export async function runNumberSet(opts) {
26
29
  console.log(` inbound instruction: ${n.inboundInstruction ?? ""}`);
27
30
  console.log(` inbound voice gender: ${n.inboundVoiceGender ?? ""}`);
28
31
  console.log(` inbound language: ${n.inboundLanguage ?? ""}`);
32
+ const hasIdentity = n.firstName != null || n.lastName != null || n.avatarUrl != null;
33
+ if (hasIdentity) {
34
+ console.log(` display name: ${[n.firstName, n.lastName].filter(Boolean).join(" ")}`);
35
+ console.log(` avatar: ${n.avatarUrl ?? ""}`);
36
+ }
29
37
  }
30
38
  return 0;
31
39
  }
package/dist/lib/api.js CHANGED
@@ -88,12 +88,22 @@ async function apiRequest(method, path, body, apiKey, extraHeaders) {
88
88
  }
89
89
  /** POST a multipart/form-data body (file uploads). fetch sets the boundary header itself. */
90
90
  export async function apiPostMultipart(path, form, apiKey) {
91
+ return sendMultipart("POST", path, form, apiKey);
92
+ }
93
+ /** PATCH a multipart/form-data body (e.g. a number's avatar upload). fetch sets the boundary header itself. */
94
+ export async function apiPatchMultipart(path, form, apiKey) {
95
+ return sendMultipart("PATCH", path, form, apiKey);
96
+ }
97
+ async function sendMultipart(method, path, form, apiKey) {
91
98
  const url = `${baseUrl()}${path}`;
99
+ // No content-type here on purpose: undici's fetch sets the multipart boundary.
100
+ // The user-agent must still be set (server request logs key off it — the
101
+ // POST path once regressed by dropping it, see the git history).
92
102
  const headers = applyRefParamsHeader({ "user-agent": USER_AGENT });
93
103
  if (apiKey)
94
104
  headers.authorization = `Bearer ${apiKey}`;
95
105
  try {
96
- const res = await undiciFetch(url, { method: "POST", headers, body: form });
106
+ const res = await undiciFetch(url, { method, headers, body: form });
97
107
  return toResult(res.status, await res.text());
98
108
  }
99
109
  catch (err) {
@@ -1,6 +1,34 @@
1
- import { apiGet, apiPost, apiPatch } from "../api.js";
1
+ import { readFileSync } from "node:fs";
2
+ import { basename, extname } from "node:path";
3
+ import { apiGet, apiPost, apiPatch, apiPatchMultipart, ApiFormData, } from "../api.js";
2
4
  import { maybeAuth } from "./auth.js";
3
5
  import { DialError } from "./errors.js";
6
+ // Image types the avatar upload accepts, keyed by file extension.
7
+ const AVATAR_EXT_CONTENT_TYPE = {
8
+ jpg: "image/jpeg",
9
+ jpeg: "image/jpeg",
10
+ png: "image/png",
11
+ gif: "image/gif",
12
+ webp: "image/webp",
13
+ };
14
+ function isHttpUrl(value) {
15
+ return /^https?:\/\//i.test(value);
16
+ }
17
+ /** Read a local avatar image and resolve its MIME type from the extension. */
18
+ function readAvatarFile(path) {
19
+ const ext = extname(path).slice(1).toLowerCase();
20
+ const contentType = AVATAR_EXT_CONTENT_TYPE[ext];
21
+ if (!contentType) {
22
+ const supported = Object.keys(AVATAR_EXT_CONTENT_TYPE).join(", ");
23
+ throw new DialError("unsupported_avatar", `unsupported avatar file extension ".${ext}" (${path}). Supported: ${supported}`);
24
+ }
25
+ try {
26
+ return { data: readFileSync(path), contentType, name: basename(path) };
27
+ }
28
+ catch (err) {
29
+ throw new DialError("avatar_read_failed", `could not read avatar file ${path}: ${err instanceof Error ? err.message : String(err)}`);
30
+ }
31
+ }
4
32
  export async function listNumbers() {
5
33
  const auth = maybeAuth();
6
34
  const res = await apiGet("/api/v1/numbers", auth?.apiKey);
@@ -41,8 +69,18 @@ export async function setNumberProperties(opts) {
41
69
  body.nickname = opts.nickname;
42
70
  if (opts.maxCallDurationSeconds !== undefined)
43
71
  body.maxCallDurationSeconds = opts.maxCallDurationSeconds;
44
- if (Object.keys(body).length === 0) {
45
- throw new DialError("bad_request", "Provide at least one property to update (inboundInstruction, inboundVoiceGender, inboundLanguage, nickname, or maxCallDurationSeconds).");
72
+ if (opts.firstName !== undefined)
73
+ body.firstName = opts.firstName;
74
+ if (opts.lastName !== undefined)
75
+ body.lastName = opts.lastName;
76
+ // A URL avatar goes in the JSON body; a local file forces multipart (below).
77
+ // Read + validate the file up front, before any API round-trip, so a bad
78
+ // path or unsupported type fails fast.
79
+ const avatarFile = opts.avatar !== undefined && !isHttpUrl(opts.avatar) ? readAvatarFile(opts.avatar) : null;
80
+ if (opts.avatar !== undefined && !avatarFile)
81
+ body.avatarUrl = opts.avatar;
82
+ if (Object.keys(body).length === 0 && !avatarFile) {
83
+ throw new DialError("bad_request", "Provide at least one property to update (inboundInstruction, inboundVoiceGender, inboundLanguage, nickname, maxCallDurationSeconds, firstName, lastName, or avatar).");
46
84
  }
47
85
  const auth = maybeAuth();
48
86
  // The REST API keys numbers by id; the CLI/tool takes the E.164 number for ergonomics,
@@ -55,7 +93,20 @@ export async function setNumberProperties(opts) {
55
93
  const known = list.data.numbers.map((n) => n.number).join(", ") || "(none)";
56
94
  throw new DialError("number_not_found", `No phone number ${opts.number} on your account. Yours: ${known}.`);
57
95
  }
58
- const res = await apiPatch(`/api/v1/numbers/${match.id}`, body, auth?.apiKey);
96
+ const path = `/api/v1/numbers/${match.id}`;
97
+ // A local avatar file forces a multipart PATCH: every scalar field goes in as a
98
+ // text part alongside the uploaded `avatar` file. Otherwise a plain JSON PATCH.
99
+ let res;
100
+ if (avatarFile) {
101
+ const form = new ApiFormData();
102
+ for (const [field, value] of Object.entries(body))
103
+ form.set(field, String(value));
104
+ form.append("avatar", new Blob([new Uint8Array(avatarFile.data)], { type: avatarFile.contentType }), avatarFile.name);
105
+ res = await apiPatchMultipart(path, form, auth?.apiKey);
106
+ }
107
+ else {
108
+ res = await apiPatch(path, body, auth?.apiKey);
109
+ }
59
110
  if (!res.ok)
60
111
  throw new DialError("update_failed", res.error, res.status);
61
112
  return res.data.number;
@@ -38,6 +38,21 @@ 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
+ firstName: z
42
+ .string()
43
+ .nullable()
44
+ .optional()
45
+ .describe("iMessage display first name; null on numbers without iMessage"),
46
+ lastName: z
47
+ .string()
48
+ .nullable()
49
+ .optional()
50
+ .describe("iMessage display last name; null on numbers without iMessage"),
51
+ avatarUrl: z
52
+ .string()
53
+ .nullable()
54
+ .optional()
55
+ .describe("URL of the number's iMessage avatar photo; null when unset or not an iMessage number"),
41
56
  })
42
57
  .passthrough();
43
58
  export const messageSchema = z
@@ -29,12 +29,27 @@ const inputSchema = {
29
29
  .nullable()
30
30
  .optional()
31
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."),
32
+ firstName: z
33
+ .string()
34
+ .max(30)
35
+ .optional()
36
+ .describe("iMessage display first name shown beside this number's messages in recipients' Messages apps. iMessage numbers only. Pass an empty string to clear it."),
37
+ lastName: z
38
+ .string()
39
+ .max(30)
40
+ .optional()
41
+ .describe("iMessage display last name. iMessage numbers only. Pass an empty string to clear it."),
42
+ avatarUrl: z
43
+ .string()
44
+ .url()
45
+ .optional()
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."),
32
47
  };
33
48
  export const setNumberPropertiesTool = {
34
49
  name: "set_number_properties",
35
50
  config: {
36
51
  title: "Set Number Properties",
37
- 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.",
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.",
38
53
  inputSchema,
39
54
  outputSchema: { number: phoneNumberSchema },
40
55
  annotations: { openWorldHint: true },
@@ -53,6 +68,9 @@ export const setNumberPropertiesTool = {
53
68
  ...(args.maxCallDurationSeconds !== undefined
54
69
  ? { maxCallDurationSeconds: args.maxCallDurationSeconds }
55
70
  : {}),
71
+ ...(args.firstName !== undefined ? { firstName: args.firstName } : {}),
72
+ ...(args.lastName !== undefined ? { lastName: args.lastName } : {}),
73
+ ...(args.avatarUrl !== undefined ? { avatar: args.avatarUrl } : {}),
56
74
  }),
57
75
  }),
58
76
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getdial/cli",
3
- "version": "0.33.6",
3
+ "version": "0.34.0",
4
4
  "description": "Dial CLI — install, sign up, and run the local listen service.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -17,7 +17,7 @@
17
17
  "preferGlobal": true,
18
18
  "type": "module",
19
19
  "engines": {
20
- "node": ">=22"
20
+ "node": ">=22.19.0"
21
21
  },
22
22
  "files": [
23
23
  "dist",
@@ -40,7 +40,7 @@
40
40
  "pino": "^10.3.1",
41
41
  "pino-pretty": "^13.1.3",
42
42
  "pubnub": "^11.0.1",
43
- "undici": "^8.3.0",
43
+ "undici": "^8.7.0",
44
44
  "zod": "^4.4.3"
45
45
  },
46
46
  "devDependencies": {
package/skills.tar.gz CHANGED
Binary file