@getdial/cli 0.33.7 → 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/dist/cli.js +6 -0
- package/dist/commands/number/set.js +8 -0
- package/dist/lib/api.js +11 -1
- package/dist/lib/ops/numbers.js +55 -4
- package/dist/mcp/schemas.js +15 -0
- package/dist/mcp/tools/set-number-properties.js +19 -1
- package/package.json +1 -1
- package/skills.tar.gz +0 -0
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
|
|
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) {
|
package/dist/lib/ops/numbers.js
CHANGED
|
@@ -1,6 +1,34 @@
|
|
|
1
|
-
import {
|
|
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 (
|
|
45
|
-
|
|
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
|
|
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;
|
package/dist/mcp/schemas.js
CHANGED
|
@@ -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
|
|
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
package/skills.tar.gz
CHANGED
|
Binary file
|