@getdial/cli 0.37.2 → 0.39.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 +37 -3
- package/dist/commands/group/list.js +28 -0
- package/dist/commands/message/list.js +5 -1
- package/dist/commands/message/send.js +21 -1
- package/dist/commands/number/purchase.js +8 -0
- package/dist/commands/number/set.js +6 -0
- package/dist/commands/number/whatsapp.js +26 -0
- package/dist/commands/typing/start.js +14 -0
- package/dist/commands/typing/stop.js +6 -0
- package/dist/lib/ops/groups.js +16 -0
- package/dist/lib/ops/messages.js +24 -6
- package/dist/lib/ops/numbers.js +62 -4
- package/dist/lib/ops/typing.js +7 -1
- package/dist/mcp/schemas.js +35 -2
- package/dist/mcp/tools/index.js +2 -0
- package/dist/mcp/tools/list-groups.js +20 -0
- package/dist/mcp/tools/list-messages.js +8 -1
- package/dist/mcp/tools/send-message.js +20 -2
- package/dist/mcp/tools/set-number-properties.js +14 -1
- package/dist/mcp/tools/start-typing.js +5 -0
- package/dist/mcp/tools/stop-typing.js +5 -0
- package/dist/mcp/tools/tool-names.js +54 -0
- package/package.json +1 -1
- package/skills.tar.gz +0 -0
package/dist/cli.js
CHANGED
|
@@ -16,6 +16,8 @@ import { runWaitFor } from "./commands/wait-for.js";
|
|
|
16
16
|
import { runNumberList } from "./commands/number/list.js";
|
|
17
17
|
import { runNumberPurchase } from "./commands/number/purchase.js";
|
|
18
18
|
import { runNumberSet } from "./commands/number/set.js";
|
|
19
|
+
import { runNumberWhatsapp } from "./commands/number/whatsapp.js";
|
|
20
|
+
import { runGroupList } from "./commands/group/list.js";
|
|
19
21
|
import { runMessageSend } from "./commands/message/send.js";
|
|
20
22
|
import { runMessageReply } from "./commands/message/reply.js";
|
|
21
23
|
import { runMessageList } from "./commands/message/list.js";
|
|
@@ -163,6 +165,7 @@ number
|
|
|
163
165
|
.option("--inbound-language <bcp47>", "language tag inbound calls are pinned to (default: detect from the caller's country prefix, alongside en-US)")
|
|
164
166
|
.option("--area-code <code>", "preferred US area code (only US numbers can be provisioned; ignored with --include-imessage)")
|
|
165
167
|
.option("--include-imessage", "provision an iMessage number (pay-as-you-go only; provisioned asynchronously — poll `dial number list` until ready)")
|
|
168
|
+
.option("--whatsapp", "also connect WhatsApp to the new line (beta, enabled per account). Requires --include-imessage: WhatsApp is a channel on an iMessage line")
|
|
166
169
|
.option("--json", "machine-readable output")
|
|
167
170
|
.action(async (opts) => process.exit(await runNumberPurchase({
|
|
168
171
|
inboundInstruction: opts.inboundInstruction,
|
|
@@ -171,8 +174,14 @@ number
|
|
|
171
174
|
inboundLanguage: opts.inboundLanguage,
|
|
172
175
|
areaCode: opts.areaCode,
|
|
173
176
|
includeImessage: !!opts.includeImessage,
|
|
177
|
+
whatsapp: !!opts.whatsapp,
|
|
174
178
|
json: !!opts.json,
|
|
175
179
|
})));
|
|
180
|
+
number
|
|
181
|
+
.command("whatsapp <number>")
|
|
182
|
+
.description("Connect WhatsApp to a number you already hold (beta, enabled per account). POST /api/v1/numbers/<id>/whatsapp.")
|
|
183
|
+
.option("--json", "machine-readable output")
|
|
184
|
+
.action(async (number, opts) => process.exit(await runNumberWhatsapp({ number, json: !!opts.json })));
|
|
176
185
|
number
|
|
177
186
|
.command("set <number>")
|
|
178
187
|
.description("Update a number's properties (at least one flag). PATCH /api/v1/numbers/<id>.")
|
|
@@ -192,6 +201,8 @@ number
|
|
|
192
201
|
.option("--first-name <text>", 'iMessage display first name shown beside this number\'s messages (iMessage numbers only); pass "" to clear')
|
|
193
202
|
.option("--last-name <text>", 'iMessage display last name (iMessage numbers only); pass "" to clear')
|
|
194
203
|
.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")
|
|
204
|
+
.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")
|
|
205
|
+
.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)")
|
|
195
206
|
.option("--json", "machine-readable output")
|
|
196
207
|
.action(async (numberArg, opts) => {
|
|
197
208
|
let maxCallDurationSeconds;
|
|
@@ -211,13 +222,17 @@ number
|
|
|
211
222
|
firstName: opts.firstName,
|
|
212
223
|
lastName: opts.lastName,
|
|
213
224
|
avatar: opts.avatar,
|
|
225
|
+
whatsappName: opts.whatsappName,
|
|
226
|
+
whatsappAvatar: opts.whatsappAvatar,
|
|
214
227
|
json: !!opts.json,
|
|
215
228
|
}));
|
|
216
229
|
});
|
|
217
230
|
const message = program
|
|
218
231
|
.command("message")
|
|
219
|
-
.description("Send
|
|
232
|
+
.description("Send a message to a number or a group, optionally with media (MMS). POST /api/v1/messages.")
|
|
220
233
|
.option("--to <e164>", "destination phone number, E.164 (e.g. +14155551234)")
|
|
234
|
+
.option("--group <id>", "send into a group conversation instead (see `dial group list`); the sending line comes from the group. Exclusive with --to")
|
|
235
|
+
.option("--channel <imessage|whatsapp>", "which channel to send on, for a line carrying both; omit to use the number's own default")
|
|
221
236
|
.option("--body <text>", "message body")
|
|
222
237
|
.option("--from-number <ref>", "number to send from: id, owned E.164, or nickname (defaults to onboard's number; exclusive with --from-number-id)")
|
|
223
238
|
.option("--from-number-id <id>", "phoneNumberId to send from (defaults to onboard's number; exclusive with --from-number)")
|
|
@@ -225,8 +240,11 @@ const message = program
|
|
|
225
240
|
.option("--force-audio-file", "send an audio attachment as a regular file attachment instead of an iMessage voice message")
|
|
226
241
|
.option("--json", "machine-readable output")
|
|
227
242
|
.action(async (opts) => {
|
|
228
|
-
|
|
229
|
-
|
|
243
|
+
// A destination is still required — it just has two forms now. The exactly-one
|
|
244
|
+
// rule itself lives in runMessageSend, so the MCP tool and the verb enforce it
|
|
245
|
+
// from one place; this only keeps the familiar message for the common mistake.
|
|
246
|
+
if (!opts.to && !opts.group) {
|
|
247
|
+
console.error("error: --to or --group is required to send a message. Use `dial message list` to list, `dial group list` for your groups, or `dial message --help` for usage.");
|
|
230
248
|
process.exit(2);
|
|
231
249
|
}
|
|
232
250
|
if (!opts.body && (opts.media ?? []).length === 0) {
|
|
@@ -235,6 +253,8 @@ const message = program
|
|
|
235
253
|
}
|
|
236
254
|
process.exit(await runMessageSend({
|
|
237
255
|
to: opts.to,
|
|
256
|
+
group: opts.group,
|
|
257
|
+
channel: opts.channel,
|
|
238
258
|
body: opts.body,
|
|
239
259
|
fromNumber: opts.fromNumber,
|
|
240
260
|
fromNumberId: opts.fromNumberId,
|
|
@@ -265,15 +285,25 @@ message
|
|
|
265
285
|
.command("list")
|
|
266
286
|
.description("List recent messages on your account. GET /api/v1/messages.")
|
|
267
287
|
.option("--number-id <id>", "filter to a single phone number")
|
|
288
|
+
.option("--group <id>", "filter to one group conversation (see `dial group list`)")
|
|
268
289
|
.option("--direction <dir>", "inbound or outbound")
|
|
269
290
|
.option("--since <iso8601>", "only messages created after this timestamp")
|
|
270
291
|
.option("--json", "machine-readable output")
|
|
271
292
|
.action(async (opts) => process.exit(await runMessageList({
|
|
272
293
|
numberId: opts.numberId,
|
|
294
|
+
group: opts.group,
|
|
273
295
|
direction: opts.direction,
|
|
274
296
|
since: opts.since,
|
|
275
297
|
json: !!opts.json,
|
|
276
298
|
})));
|
|
299
|
+
const group = program
|
|
300
|
+
.command("group")
|
|
301
|
+
.description("Group conversations your lines are in (WhatsApp).");
|
|
302
|
+
group
|
|
303
|
+
.command("list")
|
|
304
|
+
.description("List the group conversations your lines are in. GET /api/v1/groups.")
|
|
305
|
+
.option("--json", "machine-readable output")
|
|
306
|
+
.action(async (opts) => process.exit(await runGroupList({ json: !!opts.json })));
|
|
277
307
|
const typing = program
|
|
278
308
|
.command("typing")
|
|
279
309
|
.description("Show or clear a typing indicator. iMessage numbers display it; SMS numbers ignore it. POST /api/v1/typing.");
|
|
@@ -282,6 +312,7 @@ typing
|
|
|
282
312
|
.description("Show a typing indicator to a recipient, as if composing a message from your number.")
|
|
283
313
|
.option("--to-number <e164>", "recipient phone number, E.164 (e.g. +14155551234)")
|
|
284
314
|
.option("--from-number <ref>", "number the indicator appears from: id, owned E.164, or nickname (defaults to onboard's number)")
|
|
315
|
+
.option("--channel <imessage|whatsapp>", "which channel to show it on, for a line carrying both; omit to use the number's own default. Typing inside a group isn't supported")
|
|
285
316
|
.option("--json", "machine-readable output")
|
|
286
317
|
.action(async (opts) => {
|
|
287
318
|
if (!opts.toNumber) {
|
|
@@ -291,6 +322,7 @@ typing
|
|
|
291
322
|
process.exit(await runTypingStart({
|
|
292
323
|
toNumber: opts.toNumber,
|
|
293
324
|
fromNumber: opts.fromNumber,
|
|
325
|
+
channel: opts.channel,
|
|
294
326
|
json: !!opts.json,
|
|
295
327
|
}));
|
|
296
328
|
});
|
|
@@ -299,6 +331,7 @@ typing
|
|
|
299
331
|
.description("Clear a typing indicator previously shown with `typing start`.")
|
|
300
332
|
.option("--to-number <e164>", "recipient phone number, E.164 (e.g. +14155551234)")
|
|
301
333
|
.option("--from-number <ref>", "number the indicator appears from: id, owned E.164, or nickname (defaults to onboard's number)")
|
|
334
|
+
.option("--channel <imessage|whatsapp>", "which channel to clear it on; pass the same channel `typing start` was given")
|
|
302
335
|
.option("--json", "machine-readable output")
|
|
303
336
|
.action(async (opts) => {
|
|
304
337
|
if (!opts.toNumber) {
|
|
@@ -308,6 +341,7 @@ typing
|
|
|
308
341
|
process.exit(await runTypingStop({
|
|
309
342
|
toNumber: opts.toNumber,
|
|
310
343
|
fromNumber: opts.fromNumber,
|
|
344
|
+
channel: opts.channel,
|
|
311
345
|
json: !!opts.json,
|
|
312
346
|
}));
|
|
313
347
|
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { listGroups } from "../../lib/ops/groups.js";
|
|
2
|
+
import { isDialError } from "../../lib/ops/errors.js";
|
|
3
|
+
import { printDialError } from "../../lib/cli-error.js";
|
|
4
|
+
export async function runGroupList(opts) {
|
|
5
|
+
try {
|
|
6
|
+
const groups = await listGroups();
|
|
7
|
+
if (opts.json) {
|
|
8
|
+
console.log(JSON.stringify({ ok: true, groups }));
|
|
9
|
+
return 0;
|
|
10
|
+
}
|
|
11
|
+
if (groups.length === 0) {
|
|
12
|
+
console.log("no groups. a group appears here once one of your lines is added to it.");
|
|
13
|
+
return 0;
|
|
14
|
+
}
|
|
15
|
+
for (const g of groups) {
|
|
16
|
+
// A name Dial could not read is rendered as a dash — never the literal "null",
|
|
17
|
+
// and never the group id standing in for a name, which would read as one.
|
|
18
|
+
const name = g.name ?? "—";
|
|
19
|
+
console.log(`${g.id} ${name}`);
|
|
20
|
+
}
|
|
21
|
+
return 0;
|
|
22
|
+
}
|
|
23
|
+
catch (e) {
|
|
24
|
+
if (isDialError(e))
|
|
25
|
+
return printDialError(opts.json, e);
|
|
26
|
+
throw e;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -5,6 +5,7 @@ export async function runMessageList(opts) {
|
|
|
5
5
|
try {
|
|
6
6
|
const messages = await listMessages({
|
|
7
7
|
numberId: opts.numberId,
|
|
8
|
+
groupId: opts.group,
|
|
8
9
|
direction: opts.direction,
|
|
9
10
|
since: opts.since,
|
|
10
11
|
});
|
|
@@ -18,7 +19,10 @@ export async function runMessageList(opts) {
|
|
|
18
19
|
}
|
|
19
20
|
for (const m of messages) {
|
|
20
21
|
const mediaTag = m.media && m.media.length > 0 ? ` [${m.media.length} media]` : "";
|
|
21
|
-
|
|
22
|
+
// A group message has no `to`: the destination is the group. Naming it keeps the
|
|
23
|
+
// column meaningful instead of printing an empty slot.
|
|
24
|
+
const destination = m.to ?? `group ${m.groupId}`;
|
|
25
|
+
console.log(`${m.createdAt} ${(m.direction ?? "").padEnd(8)} ${m.from} -> ${destination} ${m.body}${mediaTag}`);
|
|
22
26
|
}
|
|
23
27
|
return 0;
|
|
24
28
|
}
|
|
@@ -1,13 +1,28 @@
|
|
|
1
1
|
import { sendMessage } from "../../lib/ops/messages.js";
|
|
2
2
|
import { isDialError } from "../../lib/ops/errors.js";
|
|
3
3
|
import { printDialError } from "../../lib/cli-error.js";
|
|
4
|
+
/** The channels the API accepts. Checked locally so a typo never becomes a 400. */
|
|
5
|
+
export const CHANNELS = ["imessage", "whatsapp"];
|
|
4
6
|
export async function runMessageSend(opts) {
|
|
7
|
+
// Both destination checks happen BEFORE any HTTP call: a caller who gave two
|
|
8
|
+
// destinations, or none, gets told what to fix rather than a server rejection
|
|
9
|
+
// they have to map back to their own flags.
|
|
10
|
+
if ((opts.to === undefined) === (opts.group === undefined)) {
|
|
11
|
+
console.error("error: provide exactly one of --to and --group.");
|
|
12
|
+
return 2;
|
|
13
|
+
}
|
|
14
|
+
if (opts.channel !== undefined && !CHANNELS.includes(opts.channel)) {
|
|
15
|
+
console.error(`error: --channel must be one of ${CHANNELS.join(", ")}.`);
|
|
16
|
+
return 2;
|
|
17
|
+
}
|
|
5
18
|
try {
|
|
6
19
|
const m = await sendMessage({
|
|
7
20
|
to: opts.to,
|
|
21
|
+
groupId: opts.group,
|
|
8
22
|
body: opts.body,
|
|
9
23
|
fromNumber: opts.fromNumber,
|
|
10
24
|
fromNumberId: opts.fromNumberId,
|
|
25
|
+
channel: opts.channel,
|
|
11
26
|
media: opts.media,
|
|
12
27
|
forceAudioFile: opts.forceAudioFile,
|
|
13
28
|
});
|
|
@@ -18,7 +33,12 @@ export async function runMessageSend(opts) {
|
|
|
18
33
|
console.log(`sent.`);
|
|
19
34
|
console.log(` channel: ${m.channel}`);
|
|
20
35
|
console.log(` from: ${m.from}`);
|
|
21
|
-
|
|
36
|
+
// A group message has no `to` — the destination is the group — so print the
|
|
37
|
+
// group instead of an empty line that reads like a bug.
|
|
38
|
+
if (m.groupId)
|
|
39
|
+
console.log(` group: ${m.groupId}`);
|
|
40
|
+
else
|
|
41
|
+
console.log(` to: ${m.to}`);
|
|
22
42
|
console.log(` body: ${m.body}`);
|
|
23
43
|
for (const item of m.media ?? []) {
|
|
24
44
|
console.log(` media: ${item.url} (${item.contentType})`);
|
|
@@ -2,6 +2,13 @@ import { purchaseNumber } from "../../lib/ops/numbers.js";
|
|
|
2
2
|
import { isDialError } from "../../lib/ops/errors.js";
|
|
3
3
|
import { printDialError } from "../../lib/cli-error.js";
|
|
4
4
|
export async function runNumberPurchase(opts) {
|
|
5
|
+
// WhatsApp rides on an iMessage line, so there is no standard-number combination to
|
|
6
|
+
// ask for. Refused here, before spending anything, and naming the requirement rather
|
|
7
|
+
// than relaying a 400 the caller has to map back to their flags.
|
|
8
|
+
if (opts.whatsapp && !opts.includeImessage) {
|
|
9
|
+
console.error("error: --whatsapp requires --include-imessage (WhatsApp is a channel on an iMessage line).");
|
|
10
|
+
return 2;
|
|
11
|
+
}
|
|
5
12
|
try {
|
|
6
13
|
const n = await purchaseNumber({
|
|
7
14
|
inboundInstruction: opts.inboundInstruction,
|
|
@@ -10,6 +17,7 @@ export async function runNumberPurchase(opts) {
|
|
|
10
17
|
inboundLanguage: opts.inboundLanguage,
|
|
11
18
|
areaCode: opts.areaCode,
|
|
12
19
|
includeImessage: opts.includeImessage,
|
|
20
|
+
whatsapp: opts.whatsapp,
|
|
13
21
|
});
|
|
14
22
|
if (opts.json) {
|
|
15
23
|
console.log(JSON.stringify({ ok: true, number: n }));
|
|
@@ -17,6 +17,8 @@ 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.whatsappName !== undefined ? { whatsappName: opts.whatsappName } : {}),
|
|
21
|
+
...(opts.whatsappAvatar !== undefined ? { whatsappAvatar: opts.whatsappAvatar } : {}),
|
|
20
22
|
});
|
|
21
23
|
if (opts.json) {
|
|
22
24
|
console.log(JSON.stringify({ ok: true, number: n }));
|
|
@@ -34,6 +36,10 @@ export async function runNumberSet(opts) {
|
|
|
34
36
|
console.log(` display name: ${[n.firstName, n.lastName].filter(Boolean).join(" ")}`);
|
|
35
37
|
console.log(` avatar: ${n.avatarUrl ?? ""}`);
|
|
36
38
|
}
|
|
39
|
+
if (n.whatsappName != null || n.whatsappAvatarUrl != null) {
|
|
40
|
+
console.log(` whatsapp name: ${n.whatsappName ?? ""}`);
|
|
41
|
+
console.log(` whatsapp avatar: ${n.whatsappAvatarUrl ?? ""}`);
|
|
42
|
+
}
|
|
37
43
|
}
|
|
38
44
|
return 0;
|
|
39
45
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { addWhatsappToNumber, resolveNumberId } from "../../lib/ops/numbers.js";
|
|
2
|
+
import { isDialError } from "../../lib/ops/errors.js";
|
|
3
|
+
import { printDialError } from "../../lib/cli-error.js";
|
|
4
|
+
export async function runNumberWhatsapp(opts) {
|
|
5
|
+
try {
|
|
6
|
+
const id = await resolveNumberId(opts.number);
|
|
7
|
+
const n = await addWhatsappToNumber(id);
|
|
8
|
+
if (opts.json) {
|
|
9
|
+
console.log(JSON.stringify({ ok: true, number: n }));
|
|
10
|
+
return 0;
|
|
11
|
+
}
|
|
12
|
+
console.log(`connecting WhatsApp.`);
|
|
13
|
+
console.log(` number: ${n.number}`);
|
|
14
|
+
console.log(` id: ${n.id}`);
|
|
15
|
+
// The track's own status, not the number's: they are independent, and it is this
|
|
16
|
+
// one the caller is waiting on.
|
|
17
|
+
console.log(` whatsapp: ${n.whatsapp?.status ?? "provisioning"}`);
|
|
18
|
+
console.log(`\nsetup runs in the background. poll \`dial number list\` until whatsapp is ready.`);
|
|
19
|
+
return 0;
|
|
20
|
+
}
|
|
21
|
+
catch (e) {
|
|
22
|
+
if (isDialError(e))
|
|
23
|
+
return printDialError(opts.json, e);
|
|
24
|
+
throw e;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -1,12 +1,26 @@
|
|
|
1
1
|
import { setTyping } from "../../lib/ops/typing.js";
|
|
2
2
|
import { isDialError } from "../../lib/ops/errors.js";
|
|
3
3
|
import { printDialError } from "../../lib/cli-error.js";
|
|
4
|
+
/** The channels the API accepts. Checked locally so a typo never becomes a 400. */
|
|
5
|
+
export const TYPING_CHANNELS = ["imessage", "whatsapp"];
|
|
6
|
+
/**
|
|
7
|
+
* Validate `--channel` before any request. Shared by start and stop so the two
|
|
8
|
+
* cannot drift into accepting different words for the same thing.
|
|
9
|
+
*/
|
|
10
|
+
export function invalidChannel(channel) {
|
|
11
|
+
return (channel !== undefined && !TYPING_CHANNELS.includes(channel));
|
|
12
|
+
}
|
|
4
13
|
export async function runTypingStart(opts) {
|
|
14
|
+
if (invalidChannel(opts.channel)) {
|
|
15
|
+
console.error(`error: --channel must be one of ${TYPING_CHANNELS.join(", ")}.`);
|
|
16
|
+
return 2;
|
|
17
|
+
}
|
|
5
18
|
try {
|
|
6
19
|
const result = await setTyping({
|
|
7
20
|
toNumber: opts.toNumber,
|
|
8
21
|
value: true,
|
|
9
22
|
fromNumber: opts.fromNumber,
|
|
23
|
+
channel: opts.channel,
|
|
10
24
|
});
|
|
11
25
|
if (opts.json) {
|
|
12
26
|
console.log(JSON.stringify(result));
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { setTyping } from "../../lib/ops/typing.js";
|
|
2
2
|
import { isDialError } from "../../lib/ops/errors.js";
|
|
3
3
|
import { printDialError } from "../../lib/cli-error.js";
|
|
4
|
+
import { invalidChannel, TYPING_CHANNELS } from "./start.js";
|
|
4
5
|
export async function runTypingStop(opts) {
|
|
6
|
+
if (invalidChannel(opts.channel)) {
|
|
7
|
+
console.error(`error: --channel must be one of ${TYPING_CHANNELS.join(", ")}.`);
|
|
8
|
+
return 2;
|
|
9
|
+
}
|
|
5
10
|
try {
|
|
6
11
|
const result = await setTyping({
|
|
7
12
|
toNumber: opts.toNumber,
|
|
8
13
|
value: false,
|
|
9
14
|
fromNumber: opts.fromNumber,
|
|
15
|
+
channel: opts.channel,
|
|
10
16
|
});
|
|
11
17
|
if (opts.json) {
|
|
12
18
|
console.log(JSON.stringify(result));
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { apiGet } from "../api.js";
|
|
2
|
+
import { maybeAuth } from "./auth.js";
|
|
3
|
+
import { DialError } from "./errors.js";
|
|
4
|
+
/**
|
|
5
|
+
* The group conversations the account's lines are in.
|
|
6
|
+
*
|
|
7
|
+
* Shared by `dial group list` and the local MCP `list_groups` tool, so both speak to
|
|
8
|
+
* the API through one place and inherit the saved key the same way.
|
|
9
|
+
*/
|
|
10
|
+
export async function listGroups() {
|
|
11
|
+
const auth = maybeAuth();
|
|
12
|
+
const res = await apiGet("/api/v1/groups", auth?.apiKey);
|
|
13
|
+
if (!res.ok)
|
|
14
|
+
throw new DialError("list_failed", res.error, res.status);
|
|
15
|
+
return res.data.groups ?? [];
|
|
16
|
+
}
|
package/dist/lib/ops/messages.js
CHANGED
|
@@ -46,20 +46,31 @@ function readMediaFile(path) {
|
|
|
46
46
|
}
|
|
47
47
|
export async function sendMessage(opts) {
|
|
48
48
|
const auth = maybeAuth();
|
|
49
|
-
|
|
49
|
+
// A group already belongs to one of the account's lines, so a group send needs no
|
|
50
|
+
// from-number — and must not inherit the SAVED DEFAULT one, because the server
|
|
51
|
+
// refuses a from-number that disagrees with the group. Inheriting it would turn
|
|
52
|
+
// the onboarding convenience into a failed send. An explicitly passed one is still
|
|
53
|
+
// forwarded, and still checked server-side.
|
|
54
|
+
const explicitFrom = opts.fromNumber !== undefined || opts.fromNumberId !== undefined;
|
|
55
|
+
const from = opts.groupId && !explicitFrom ? {} : resolveFromSelector(auth, opts);
|
|
50
56
|
const media = opts.media ?? [];
|
|
51
57
|
if (media.length > MAX_MEDIA_ITEMS) {
|
|
52
58
|
throw new DialError("too_much_media", `at most ${MAX_MEDIA_ITEMS} media items are allowed per message (got ${media.length})`);
|
|
53
59
|
}
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
60
|
+
// `channel` is sent only when the caller named one. Omitted, the server uses the
|
|
61
|
+
// from-number's own default (a standard number sends SMS; an iMessage number sends
|
|
62
|
+
// iMessage with RCS/SMS fallback) — and the send schema is strict, so an empty or
|
|
63
|
+
// stale field would be a 400 rather than a no-op.
|
|
64
|
+
// Each destination likewise appears only when given: the server enforces the
|
|
65
|
+
// to/groupId XOR, and a key present-but-empty reads as a second destination.
|
|
57
66
|
// URLs-only goes as plain JSON; any local file switches to multipart.
|
|
58
67
|
const hasFiles = media.some((m) => !isHttpUrl(m));
|
|
59
68
|
let res;
|
|
60
69
|
if (!hasFiles) {
|
|
61
70
|
res = await apiPost("/api/v1/messages", {
|
|
62
|
-
to: opts.to,
|
|
71
|
+
...(opts.to !== undefined ? { to: opts.to } : {}),
|
|
72
|
+
...(opts.groupId !== undefined ? { groupId: opts.groupId } : {}),
|
|
73
|
+
...(opts.channel !== undefined ? { channel: opts.channel } : {}),
|
|
63
74
|
...(opts.body ? { body: opts.body } : {}),
|
|
64
75
|
...from,
|
|
65
76
|
...(media.length ? { mediaUrls: media } : {}),
|
|
@@ -68,7 +79,12 @@ export async function sendMessage(opts) {
|
|
|
68
79
|
}
|
|
69
80
|
else {
|
|
70
81
|
const form = new ApiFormData();
|
|
71
|
-
|
|
82
|
+
if (opts.to !== undefined)
|
|
83
|
+
form.set("to", opts.to);
|
|
84
|
+
if (opts.groupId !== undefined)
|
|
85
|
+
form.set("groupId", opts.groupId);
|
|
86
|
+
if (opts.channel !== undefined)
|
|
87
|
+
form.set("channel", opts.channel);
|
|
72
88
|
if (opts.body)
|
|
73
89
|
form.set("body", opts.body);
|
|
74
90
|
for (const [field, value] of Object.entries(from))
|
|
@@ -95,6 +111,8 @@ export async function listMessages(opts) {
|
|
|
95
111
|
const params = new URLSearchParams();
|
|
96
112
|
if (opts.numberId)
|
|
97
113
|
params.set("numberId", opts.numberId);
|
|
114
|
+
if (opts.groupId)
|
|
115
|
+
params.set("groupId", opts.groupId);
|
|
98
116
|
if (opts.direction)
|
|
99
117
|
params.set("direction", opts.direction);
|
|
100
118
|
if (opts.since)
|
package/dist/lib/ops/numbers.js
CHANGED
|
@@ -51,11 +51,55 @@ export async function purchaseNumber(opts) {
|
|
|
51
51
|
body.capabilities = ["sms", "call", "imessage"];
|
|
52
52
|
else if (opts.areaCode)
|
|
53
53
|
body.areaCode = opts.areaCode;
|
|
54
|
+
if (opts.whatsapp)
|
|
55
|
+
body.whatsapp = true;
|
|
54
56
|
const res = await apiPost("/api/v1/numbers", body, auth?.apiKey);
|
|
55
57
|
if (!res.ok)
|
|
56
58
|
throw new DialError("purchase_failed", res.error, res.status);
|
|
57
59
|
return res.data.number;
|
|
58
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Resolve a number reference — an id, an owned E.164, or a nickname — to its id.
|
|
63
|
+
*
|
|
64
|
+
* The REST API keys numbers by id while every CLI verb takes whichever form the caller
|
|
65
|
+
* has to hand, so one lookup serves them all. An id is returned as given when it
|
|
66
|
+
* matches a number the account owns, which also keeps a copy-pasted id from a previous
|
|
67
|
+
* `dial number list` working.
|
|
68
|
+
*/
|
|
69
|
+
export async function resolveNumberId(ref) {
|
|
70
|
+
const auth = maybeAuth();
|
|
71
|
+
const list = await apiGet("/api/v1/numbers", auth?.apiKey);
|
|
72
|
+
if (!list.ok)
|
|
73
|
+
throw new DialError("list_failed", list.error, list.status);
|
|
74
|
+
const match = list.data.numbers.find((n) => n.id === ref || n.number === ref || n.nickname === ref);
|
|
75
|
+
if (!match) {
|
|
76
|
+
const known = list.data.numbers.map((n) => n.number).join(", ") || "(none)";
|
|
77
|
+
throw new DialError("number_not_found", `No phone number ${ref} on your account. Yours: ${known}.`);
|
|
78
|
+
}
|
|
79
|
+
return match.id;
|
|
80
|
+
}
|
|
81
|
+
/** Copy for the 404 both WhatsApp provisioning paths answer without beta access. */
|
|
82
|
+
export const WHATSAPP_NOT_ENABLED = "WhatsApp is in beta and isn't enabled for this account. " +
|
|
83
|
+
"See https://docs.getdial.ai/documentation/capabilities/whatsapp to request access.";
|
|
84
|
+
/**
|
|
85
|
+
* Connect WhatsApp to a number the account already holds
|
|
86
|
+
* (POST /api/v1/numbers/{id}/whatsapp).
|
|
87
|
+
*
|
|
88
|
+
* A 404 means one of two things — no such number, or no beta access — and the API
|
|
89
|
+
* cannot distinguish them by design: to an account without access the endpoint does
|
|
90
|
+
* not exist. Reported as the access message, because "not found" on a number the
|
|
91
|
+
* caller just listed reads as a typo and sends them looking in the wrong place.
|
|
92
|
+
*/
|
|
93
|
+
export async function addWhatsappToNumber(numberId) {
|
|
94
|
+
const auth = maybeAuth();
|
|
95
|
+
const res = await apiPost(`/api/v1/numbers/${encodeURIComponent(numberId)}/whatsapp`, {}, auth?.apiKey);
|
|
96
|
+
if (!res.ok) {
|
|
97
|
+
if (res.status === 404)
|
|
98
|
+
throw new DialError("whatsapp_unavailable", WHATSAPP_NOT_ENABLED, 404);
|
|
99
|
+
throw new DialError("whatsapp_failed", res.error, res.status);
|
|
100
|
+
}
|
|
101
|
+
return res.data.number;
|
|
102
|
+
}
|
|
59
103
|
export async function setNumberProperties(opts) {
|
|
60
104
|
const body = {};
|
|
61
105
|
if (opts.inboundInstruction !== undefined)
|
|
@@ -73,14 +117,21 @@ export async function setNumberProperties(opts) {
|
|
|
73
117
|
body.firstName = opts.firstName;
|
|
74
118
|
if (opts.lastName !== undefined)
|
|
75
119
|
body.lastName = opts.lastName;
|
|
120
|
+
if (opts.whatsappName !== undefined)
|
|
121
|
+
body.whatsappName = opts.whatsappName;
|
|
76
122
|
// A URL avatar goes in the JSON body; a local file forces multipart (below).
|
|
77
123
|
// Read + validate the file up front, before any API round-trip, so a bad
|
|
78
124
|
// path or unsupported type fails fast.
|
|
79
125
|
const avatarFile = opts.avatar !== undefined && !isHttpUrl(opts.avatar) ? readAvatarFile(opts.avatar) : null;
|
|
80
126
|
if (opts.avatar !== undefined && !avatarFile)
|
|
81
127
|
body.avatarUrl = opts.avatar;
|
|
82
|
-
|
|
83
|
-
|
|
128
|
+
const whatsappAvatarFile = opts.whatsappAvatar !== undefined && !isHttpUrl(opts.whatsappAvatar)
|
|
129
|
+
? readAvatarFile(opts.whatsappAvatar)
|
|
130
|
+
: null;
|
|
131
|
+
if (opts.whatsappAvatar !== undefined && !whatsappAvatarFile)
|
|
132
|
+
body.whatsappAvatarUrl = opts.whatsappAvatar;
|
|
133
|
+
if (Object.keys(body).length === 0 && !avatarFile && !whatsappAvatarFile) {
|
|
134
|
+
throw new DialError("bad_request", "Provide at least one property to update (inboundInstruction, inboundVoiceGender, inboundLanguage, nickname, maxCallDurationSeconds, firstName, lastName, avatar, whatsappName, or whatsappAvatar).");
|
|
84
135
|
}
|
|
85
136
|
const auth = maybeAuth();
|
|
86
137
|
// The REST API keys numbers by id; the CLI/tool takes the E.164 number for ergonomics,
|
|
@@ -97,11 +148,18 @@ export async function setNumberProperties(opts) {
|
|
|
97
148
|
// A local avatar file forces a multipart PATCH: every scalar field goes in as a
|
|
98
149
|
// text part alongside the uploaded `avatar` file. Otherwise a plain JSON PATCH.
|
|
99
150
|
let res;
|
|
100
|
-
if (avatarFile) {
|
|
151
|
+
if (avatarFile || whatsappAvatarFile) {
|
|
101
152
|
const form = new ApiFormData();
|
|
102
153
|
for (const [field, value] of Object.entries(body))
|
|
103
154
|
form.set(field, String(value));
|
|
104
|
-
|
|
155
|
+
if (avatarFile) {
|
|
156
|
+
form.append("avatar", new Blob([new Uint8Array(avatarFile.data)], { type: avatarFile.contentType }), avatarFile.name);
|
|
157
|
+
}
|
|
158
|
+
if (whatsappAvatarFile) {
|
|
159
|
+
form.append("whatsappAvatar", new Blob([new Uint8Array(whatsappAvatarFile.data)], {
|
|
160
|
+
type: whatsappAvatarFile.contentType,
|
|
161
|
+
}), whatsappAvatarFile.name);
|
|
162
|
+
}
|
|
105
163
|
res = await apiPatchMultipart(path, form, auth?.apiKey);
|
|
106
164
|
}
|
|
107
165
|
else {
|
package/dist/lib/ops/typing.js
CHANGED
|
@@ -9,7 +9,13 @@ import { DialError } from "./errors.js";
|
|
|
9
9
|
export async function setTyping(opts) {
|
|
10
10
|
const auth = maybeAuth();
|
|
11
11
|
const fromNumber = requireFromNumber(auth, opts.fromNumber);
|
|
12
|
-
const res = await apiPost("/api/v1/typing", {
|
|
12
|
+
const res = await apiPost("/api/v1/typing", {
|
|
13
|
+
toNumber: opts.toNumber,
|
|
14
|
+
value: opts.value,
|
|
15
|
+
fromNumber,
|
|
16
|
+
// Only when named: the typing schema is strict, so an empty field is a 400.
|
|
17
|
+
...(opts.channel !== undefined ? { channel: opts.channel } : {}),
|
|
18
|
+
}, auth?.apiKey);
|
|
13
19
|
if (!res.ok)
|
|
14
20
|
throw new DialError("typing_failed", res.error, res.status);
|
|
15
21
|
return res.data;
|
package/dist/mcp/schemas.js
CHANGED
|
@@ -53,13 +53,46 @@ export const phoneNumberSchema = z
|
|
|
53
53
|
.nullable()
|
|
54
54
|
.optional()
|
|
55
55
|
.describe("URL of the number's iMessage avatar photo; null when unset or not an iMessage number"),
|
|
56
|
+
whatsappName: z
|
|
57
|
+
.string()
|
|
58
|
+
.nullable()
|
|
59
|
+
.optional()
|
|
60
|
+
.describe("WhatsApp display name; null on numbers without a WhatsApp track"),
|
|
61
|
+
whatsappAvatarUrl: z
|
|
62
|
+
.string()
|
|
63
|
+
.nullable()
|
|
64
|
+
.optional()
|
|
65
|
+
.describe("URL of the number's WhatsApp avatar; null when unset"),
|
|
56
66
|
})
|
|
57
67
|
.passthrough();
|
|
68
|
+
/**
|
|
69
|
+
* A group conversation. Mirrors the hosted server's `groupSchema` — `createdAt` is a
|
|
70
|
+
* string, never a z.date(), because a Date is unrepresentable in JSON Schema and one
|
|
71
|
+
* bad schema fails the whole `tools/list`.
|
|
72
|
+
*/
|
|
73
|
+
export const groupSchema = z.object({
|
|
74
|
+
id: z.string().describe("Group id — pass as groupId to send_message or list_messages"),
|
|
75
|
+
name: z
|
|
76
|
+
.string()
|
|
77
|
+
.nullable()
|
|
78
|
+
.describe("The group's current name, or null when no line could report it in time"),
|
|
79
|
+
createdAt: z
|
|
80
|
+
.string()
|
|
81
|
+
.optional()
|
|
82
|
+
.describe("ISO-8601: when Dial first learned of this group (a join, or its first message)"),
|
|
83
|
+
});
|
|
58
84
|
export const messageSchema = z
|
|
59
85
|
.object({
|
|
60
86
|
id: z.string(),
|
|
61
|
-
from: z.string(),
|
|
62
|
-
to: z
|
|
87
|
+
from: z.string().describe("Sender, E.164. On a group message, the participant who sent it"),
|
|
88
|
+
to: z
|
|
89
|
+
.string()
|
|
90
|
+
.nullable()
|
|
91
|
+
.describe("Recipient, E.164 — null on a group message, whose destination is groupId"),
|
|
92
|
+
groupId: z
|
|
93
|
+
.string()
|
|
94
|
+
.nullish()
|
|
95
|
+
.describe("The group this message belongs to, or null for a one-to-one conversation"),
|
|
63
96
|
body: z.string(),
|
|
64
97
|
channel: z.string().optional(),
|
|
65
98
|
direction: z.string().optional(),
|
package/dist/mcp/tools/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { replyToMessageTool } from "./reply-to-message.js";
|
|
|
6
6
|
import { startTypingTool } from "./start-typing.js";
|
|
7
7
|
import { stopTypingTool } from "./stop-typing.js";
|
|
8
8
|
import { listMessagesTool } from "./list-messages.js";
|
|
9
|
+
import { listGroupsTool } from "./list-groups.js";
|
|
9
10
|
import { placeCallTool } from "./place-call.js";
|
|
10
11
|
import { listCallsTool } from "./list-calls.js";
|
|
11
12
|
import { getCallTool } from "./get-call.js";
|
|
@@ -31,6 +32,7 @@ export const tools = [
|
|
|
31
32
|
startTypingTool,
|
|
32
33
|
stopTypingTool,
|
|
33
34
|
listMessagesTool,
|
|
35
|
+
listGroupsTool,
|
|
34
36
|
placeCallTool,
|
|
35
37
|
listCallsTool,
|
|
36
38
|
getCallTool,
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { jsonResult } from "../result.js";
|
|
3
|
+
import { listGroups } from "../../lib/ops/groups.js";
|
|
4
|
+
import { groupSchema } from "../schemas.js";
|
|
5
|
+
export const listGroupsTool = {
|
|
6
|
+
name: "list_groups",
|
|
7
|
+
config: {
|
|
8
|
+
title: "List Groups",
|
|
9
|
+
description: "List the group conversations your lines are in. A group is a conversation that isn't a phone " +
|
|
10
|
+
"number, so it has an id of its own: pass it as groupId to send_message, or to list_messages to " +
|
|
11
|
+
"read that conversation. Groups exist on WhatsApp lines. " +
|
|
12
|
+
"A group's name can be null — it is read live from the line holding the conversation, so a line " +
|
|
13
|
+
"that can't answer in time yields a null name rather than hiding the group. " +
|
|
14
|
+
"There is no join event: list again to see a group your line was just added to.",
|
|
15
|
+
inputSchema: {},
|
|
16
|
+
outputSchema: { groups: z.array(groupSchema) },
|
|
17
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
18
|
+
},
|
|
19
|
+
run: async () => jsonResult({ groups: await listGroups() }),
|
|
20
|
+
};
|
|
@@ -4,6 +4,10 @@ import { listMessages } from "../../lib/ops/messages.js";
|
|
|
4
4
|
import { messageSchema } from "../schemas.js";
|
|
5
5
|
const inputSchema = {
|
|
6
6
|
numberId: z.string().optional().describe("Filter to a single phone number id"),
|
|
7
|
+
groupId: z
|
|
8
|
+
.string()
|
|
9
|
+
.optional()
|
|
10
|
+
.describe("Filter to one group conversation (see list_groups). Combines with the other filters"),
|
|
7
11
|
direction: z.enum(["inbound", "outbound"]).optional().describe("Filter by direction"),
|
|
8
12
|
since: z.string().optional().describe("Only messages created after this ISO-8601 timestamp"),
|
|
9
13
|
};
|
|
@@ -11,7 +15,9 @@ export const listMessagesTool = {
|
|
|
11
15
|
name: "list_messages",
|
|
12
16
|
config: {
|
|
13
17
|
title: "List Messages",
|
|
14
|
-
description: "List recent messages on your account, newest first."
|
|
18
|
+
description: "List recent messages on your account, newest first. Pass groupId to read one group " +
|
|
19
|
+
"conversation. On a group message `to` is null and the destination is groupId; which of your " +
|
|
20
|
+
"numbers the conversation is on is phoneNumberId.",
|
|
15
21
|
inputSchema,
|
|
16
22
|
outputSchema: { messages: z.array(messageSchema) },
|
|
17
23
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
@@ -19,6 +25,7 @@ export const listMessagesTool = {
|
|
|
19
25
|
run: async (args) => jsonResult({
|
|
20
26
|
messages: await listMessages({
|
|
21
27
|
numberId: args.numberId,
|
|
28
|
+
groupId: args.groupId,
|
|
22
29
|
direction: args.direction,
|
|
23
30
|
since: args.since,
|
|
24
31
|
}),
|
|
@@ -3,7 +3,20 @@ import { jsonResult } from "../result.js";
|
|
|
3
3
|
import { sendMessage, MAX_MEDIA_ITEMS } from "../../lib/ops/messages.js";
|
|
4
4
|
import { messageSchema } from "../schemas.js";
|
|
5
5
|
const inputSchema = {
|
|
6
|
-
to: z
|
|
6
|
+
to: z
|
|
7
|
+
.string()
|
|
8
|
+
.min(7)
|
|
9
|
+
.optional()
|
|
10
|
+
.describe("Destination phone number, E.164 (e.g. +14155550123). Provide exactly one of to or groupId"),
|
|
11
|
+
groupId: z
|
|
12
|
+
.string()
|
|
13
|
+
.min(1)
|
|
14
|
+
.optional()
|
|
15
|
+
.describe("A group conversation to send into (see list_groups), instead of a to number. The sending line comes from the group, so no from-number is needed. Provide exactly one of to or groupId"),
|
|
16
|
+
channel: z
|
|
17
|
+
.enum(["imessage", "whatsapp"])
|
|
18
|
+
.optional()
|
|
19
|
+
.describe("Which channel to send on, for a line that carries more than one. Omit to use the number's own default — a standard number sends SMS, an iMessage number sends iMessage. 'whatsapp' needs a line whose WhatsApp channel is ready; 'imessage' is refused on a number without an iMessage rail"),
|
|
7
20
|
body: z
|
|
8
21
|
.string()
|
|
9
22
|
.optional()
|
|
@@ -31,7 +44,10 @@ export const sendMessageTool = {
|
|
|
31
44
|
name: "send_message",
|
|
32
45
|
config: {
|
|
33
46
|
title: "Send message",
|
|
34
|
-
description: "Send a message from one of your Dial numbers
|
|
47
|
+
description: "Send a message from one of your Dial numbers — to a phone number, or into a group conversation — " +
|
|
48
|
+
"optionally with media attachments (MMS). On an iMessage number, a single audio attachment is " +
|
|
49
|
+
"delivered as a voice message unless forceAudioFile is true. " +
|
|
50
|
+
"Address it with exactly one of to or groupId. WhatsApp sends are text-only.",
|
|
35
51
|
inputSchema,
|
|
36
52
|
outputSchema: { message: messageSchema },
|
|
37
53
|
annotations: { openWorldHint: true },
|
|
@@ -39,6 +55,8 @@ export const sendMessageTool = {
|
|
|
39
55
|
run: async (args) => jsonResult({
|
|
40
56
|
message: await sendMessage({
|
|
41
57
|
to: args.to,
|
|
58
|
+
groupId: args.groupId,
|
|
59
|
+
channel: args.channel,
|
|
42
60
|
body: args.body,
|
|
43
61
|
fromNumber: args.fromNumber,
|
|
44
62
|
fromNumberId: args.fromNumberId,
|
|
@@ -44,12 +44,21 @@ 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."),
|
|
47
56
|
};
|
|
48
57
|
export const setNumberPropertiesTool = {
|
|
49
58
|
name: "set_number_properties",
|
|
50
59
|
config: {
|
|
51
60
|
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
|
|
61
|
+
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), and for WhatsApp-ready numbers its WhatsApp identity (whatsappName, whatsappAvatarUrl). Provide at least one.",
|
|
53
62
|
inputSchema,
|
|
54
63
|
outputSchema: { number: phoneNumberSchema },
|
|
55
64
|
annotations: { openWorldHint: true },
|
|
@@ -69,6 +78,10 @@ export const setNumberPropertiesTool = {
|
|
|
69
78
|
? { maxCallDurationSeconds: args.maxCallDurationSeconds }
|
|
70
79
|
: {}),
|
|
71
80
|
...(args.firstName !== undefined ? { firstName: args.firstName } : {}),
|
|
81
|
+
...(args.whatsappName !== undefined ? { whatsappName: args.whatsappName } : {}),
|
|
82
|
+
...(args.whatsappAvatarUrl !== undefined
|
|
83
|
+
? { whatsappAvatar: args.whatsappAvatarUrl }
|
|
84
|
+
: {}),
|
|
72
85
|
...(args.lastName !== undefined ? { lastName: args.lastName } : {}),
|
|
73
86
|
...(args.avatarUrl !== undefined ? { avatar: args.avatarUrl } : {}),
|
|
74
87
|
}),
|
|
@@ -7,6 +7,10 @@ const inputSchema = {
|
|
|
7
7
|
.string()
|
|
8
8
|
.min(1)
|
|
9
9
|
.describe("Number the indicator appears from: a phone number id, one of your numbers in E.164, or a nickname"),
|
|
10
|
+
channel: z
|
|
11
|
+
.enum(["imessage", "whatsapp"])
|
|
12
|
+
.optional()
|
|
13
|
+
.describe("Which channel to show it on, for a line that carries more than one. Omit to use the number's own default. Typing inside a group is not supported"),
|
|
10
14
|
};
|
|
11
15
|
export const startTypingTool = {
|
|
12
16
|
name: "start_typing",
|
|
@@ -24,6 +28,7 @@ export const startTypingTool = {
|
|
|
24
28
|
run: async (args) => jsonResult(await setTyping({
|
|
25
29
|
toNumber: args.toNumber,
|
|
26
30
|
fromNumber: args.fromNumber,
|
|
31
|
+
channel: args.channel,
|
|
27
32
|
value: true,
|
|
28
33
|
})),
|
|
29
34
|
};
|
|
@@ -7,6 +7,10 @@ const inputSchema = {
|
|
|
7
7
|
.string()
|
|
8
8
|
.min(1)
|
|
9
9
|
.describe("Number the indicator appears from: a phone number id, one of your numbers in E.164, or a nickname"),
|
|
10
|
+
channel: z
|
|
11
|
+
.enum(["imessage", "whatsapp"])
|
|
12
|
+
.optional()
|
|
13
|
+
.describe("Which channel to clear it on, for a line that carries more than one. Omit to use the number's own default. Pass the same channel start_typing was given"),
|
|
10
14
|
};
|
|
11
15
|
export const stopTypingTool = {
|
|
12
16
|
name: "stop_typing",
|
|
@@ -22,6 +26,7 @@ export const stopTypingTool = {
|
|
|
22
26
|
run: async (args) => jsonResult(await setTyping({
|
|
23
27
|
toNumber: args.toNumber,
|
|
24
28
|
fromNumber: args.fromNumber,
|
|
29
|
+
channel: args.channel,
|
|
25
30
|
value: false,
|
|
26
31
|
})),
|
|
27
32
|
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tool names this server is expected to expose, split into the half it shares with
|
|
3
|
+
* the hosted server and the half only a local install can offer.
|
|
4
|
+
*
|
|
5
|
+
* There are THREE copies of the MCP tool surface, and nothing but a check keeps them in
|
|
6
|
+
* step:
|
|
7
|
+
*
|
|
8
|
+
* 1. the hosted Remote server (`frontend/src/lib/mcp/tools/tool-names.ts` — the twin
|
|
9
|
+
* of this file, whose contents must equal OPERATIONAL_TOOL_NAMES exactly, in the
|
|
10
|
+
* same order),
|
|
11
|
+
* 2. this registry,
|
|
12
|
+
* 3. the published Remote/Local matrix in `dial-docs`
|
|
13
|
+
* (`fern/docs/pages/integrations/tools/mcp.mdx`).
|
|
14
|
+
*
|
|
15
|
+
* A tool added to one and forgotten in another is the drift AGENTS.md forbids, and it is
|
|
16
|
+
* invisible in review because each file reads correctly on its own. Asserting the
|
|
17
|
+
* registry against these lists turns that into a failing build instead.
|
|
18
|
+
*
|
|
19
|
+
* If the two files ever disagree, the fix is the SERVER that is missing a tool — never
|
|
20
|
+
* an edited list.
|
|
21
|
+
*/
|
|
22
|
+
export const OPERATIONAL_TOOL_NAMES = [
|
|
23
|
+
"get_account_status",
|
|
24
|
+
"list_numbers",
|
|
25
|
+
"purchase_number",
|
|
26
|
+
"set_number_properties",
|
|
27
|
+
"send_message",
|
|
28
|
+
"reply_to_message",
|
|
29
|
+
"start_typing",
|
|
30
|
+
"stop_typing",
|
|
31
|
+
"list_messages",
|
|
32
|
+
"list_groups",
|
|
33
|
+
"place_call",
|
|
34
|
+
"list_calls",
|
|
35
|
+
"get_call",
|
|
36
|
+
"wait_for_event",
|
|
37
|
+
];
|
|
38
|
+
/**
|
|
39
|
+
* Verbs only the local server has: they touch this machine (onboarding, the listen
|
|
40
|
+
* daemon, local event fan-out), which is why the Local server is a strict superset of
|
|
41
|
+
* the Remote one rather than a different surface.
|
|
42
|
+
*/
|
|
43
|
+
export const LOCAL_ONLY_TOOL_NAMES = [
|
|
44
|
+
"auth_login",
|
|
45
|
+
"auth_verify_otp",
|
|
46
|
+
"auth_register_number",
|
|
47
|
+
"listen_install",
|
|
48
|
+
"listen_uninstall",
|
|
49
|
+
"listen_status",
|
|
50
|
+
"add_url_target",
|
|
51
|
+
"add_command_target",
|
|
52
|
+
"remove_local_target",
|
|
53
|
+
"list_local_targets",
|
|
54
|
+
];
|
package/package.json
CHANGED
package/skills.tar.gz
CHANGED
|
Binary file
|