@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 +45 -2
- package/dist/commands/call/send.js +1 -0
- package/dist/commands/message/send.js +1 -0
- package/dist/commands/typing/start.js +20 -0
- package/dist/commands/typing/stop.js +20 -0
- package/dist/lib/ops/auth.js +25 -0
- package/dist/lib/ops/calls.js +3 -3
- package/dist/lib/ops/messages.js +5 -4
- package/dist/lib/ops/typing.js +16 -0
- package/dist/mcp/tools/index.js +4 -0
- package/dist/mcp/tools/place-call.js +6 -0
- package/dist/mcp/tools/send-message.js +6 -0
- package/dist/mcp/tools/start-typing.js +29 -0
- package/dist/mcp/tools/stop-typing.js +27 -0
- package/package.json +1 -1
- package/skills.tar.gz +0 -0
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
|
|
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
|
|
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
|
});
|
|
@@ -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
|
+
}
|
package/dist/lib/ops/auth.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/lib/ops/calls.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { apiGet, apiPost } from "../api.js";
|
|
2
|
-
import { requireAuth,
|
|
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
|
|
6
|
+
const from = resolveFromSelector(auth, opts);
|
|
7
7
|
const res = await apiPost("/api/v1/calls", {
|
|
8
8
|
to: opts.to,
|
|
9
|
-
|
|
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).
|
package/dist/lib/ops/messages.js
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/mcp/tools/index.js
CHANGED
|
@@ -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
package/skills.tar.gz
CHANGED
|
Binary file
|