@getdial/cli 0.35.0 → 0.37.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 +48 -6
- package/dist/commands/auth/login.js +46 -0
- package/dist/commands/auth/register-number.js +30 -0
- package/dist/commands/auth/verify-otp.js +264 -0
- package/dist/commands/onboard.js +29 -208
- package/dist/commands/signup.js +23 -37
- package/dist/lib/api.js +3 -6
- package/dist/lib/ops/account.js +106 -17
- package/dist/lib/state.js +6 -0
- package/dist/lib/user-agent.js +46 -0
- package/dist/mcp/tools/{sign-up.js → auth-login.js} +6 -5
- package/dist/mcp/tools/auth-register-number.js +34 -0
- package/dist/mcp/tools/{onboard.js → auth-verify-otp.js} +36 -13
- package/dist/mcp/tools/index.js +6 -4
- package/package.json +1 -1
- package/skills.tar.gz +0 -0
package/dist/cli.js
CHANGED
|
@@ -5,6 +5,9 @@ import { runDoctor } from "./commands/doctor.js";
|
|
|
5
5
|
import { runBilling } from "./commands/billing.js";
|
|
6
6
|
import { runSignup } from "./commands/signup.js";
|
|
7
7
|
import { runOnboard } from "./commands/onboard.js";
|
|
8
|
+
import { runAuthLogin } from "./commands/auth/login.js";
|
|
9
|
+
import { runAuthVerifyOtp } from "./commands/auth/verify-otp.js";
|
|
10
|
+
import { runAuthRegisterNumber } from "./commands/auth/register-number.js";
|
|
8
11
|
import { runListen } from "./commands/listen/index.js";
|
|
9
12
|
import { runListenInstall } from "./commands/listen/install.js";
|
|
10
13
|
import { runListenUninstall } from "./commands/listen/uninstall.js";
|
|
@@ -62,21 +65,60 @@ program
|
|
|
62
65
|
.description("Show account billing: balance, plan, per-number mode, recent activity. GET /api/v1/billing.")
|
|
63
66
|
.option("--json", "machine-readable output")
|
|
64
67
|
.action(async (opts) => process.exit(await runBilling({ json: !!opts.json })));
|
|
68
|
+
if (!sandbox) {
|
|
69
|
+
const auth = program
|
|
70
|
+
.command("auth")
|
|
71
|
+
.description("Create an account or sign in: email OTP, then phone verification.");
|
|
72
|
+
auth
|
|
73
|
+
.command("login <email>")
|
|
74
|
+
.description("Request an email OTP for the given address.")
|
|
75
|
+
.option("--force", "overwrite any pending signup")
|
|
76
|
+
.option("--json", "machine-readable output")
|
|
77
|
+
.action(async (email, opts) => process.exit(await runAuthLogin(email, { force: !!opts.force, json: !!opts.json })));
|
|
78
|
+
auth
|
|
79
|
+
.command("register-number <phone>")
|
|
80
|
+
.description("Text a verification code to the phone number that will own the account.")
|
|
81
|
+
.option("--registration-id <id>", "explicit registration id (falls back to local pending signup)")
|
|
82
|
+
.option("--json", "machine-readable output")
|
|
83
|
+
.action(async (phone, opts) => process.exit(await runAuthRegisterNumber(phone, {
|
|
84
|
+
registrationId: opts.registrationId,
|
|
85
|
+
json: !!opts.json,
|
|
86
|
+
})));
|
|
87
|
+
auth
|
|
88
|
+
.command("verify-otp")
|
|
89
|
+
.description("Verify a one-time code — the emailed one, or with --number the texted one.")
|
|
90
|
+
.option("--code <code>", "the 6-digit OTP (omit if already signed in — the command just installs the --agent skill and skips verification)")
|
|
91
|
+
.option("--number", "verify the SMS code for a registered phone number instead of the email code")
|
|
92
|
+
.option("--verification-id <id>", "explicit verification id for the email step (falls back to local pending signup)")
|
|
93
|
+
.option("--registration-id <id>", "explicit registration id for --number (falls back to local pending signup)")
|
|
94
|
+
.option("--agent <name>", "install the Dial skill into the named agent's config dir. One of: claude-code, cursor, codex, opencode, pi, openclaw, nanoclaw, hermes. Repeatable.", (v, prev = []) => [...prev, v], [])
|
|
95
|
+
.option("--json", "machine-readable output")
|
|
96
|
+
.action(async (opts) => process.exit(await runAuthVerifyOtp({
|
|
97
|
+
verificationId: opts.verificationId,
|
|
98
|
+
registrationId: opts.registrationId,
|
|
99
|
+
code: opts.code,
|
|
100
|
+
number: !!opts.number,
|
|
101
|
+
agents: opts.agent,
|
|
102
|
+
json: !!opts.json,
|
|
103
|
+
})));
|
|
104
|
+
}
|
|
105
|
+
// DEPRECATED, kept registered so a stale skill gets a pointer rather than
|
|
106
|
+
// commander's "unknown command". Both exit non-zero without doing anything.
|
|
65
107
|
if (!sandbox)
|
|
66
108
|
program
|
|
67
109
|
.command("signup <email>")
|
|
68
|
-
.description("
|
|
110
|
+
.description("DEPRECATED — use `dial auth login <email>`.")
|
|
69
111
|
.option("--force", "overwrite any pending signup")
|
|
70
112
|
.option("--json", "machine-readable output")
|
|
71
113
|
.action(async (email, opts) => process.exit(await runSignup(email, { force: !!opts.force, json: !!opts.json })));
|
|
72
114
|
if (!sandbox)
|
|
73
115
|
program
|
|
74
116
|
.command("onboard")
|
|
75
|
-
.description("
|
|
76
|
-
.option("--verification-id <id>", "
|
|
77
|
-
.option("--code <code>", "
|
|
78
|
-
.option("--inbound-instruction <text>", "
|
|
79
|
-
.option("--agent <name>", "
|
|
117
|
+
.description("DEPRECATED — use `dial auth verify-otp --code <code>`.")
|
|
118
|
+
.option("--verification-id <id>", "no longer used")
|
|
119
|
+
.option("--code <code>", "no longer used")
|
|
120
|
+
.option("--inbound-instruction <text>", "no longer used")
|
|
121
|
+
.option("--agent <name>", "no longer used", (v, prev = []) => [...prev, v], [])
|
|
80
122
|
.option("--json", "machine-readable output")
|
|
81
123
|
.action(async (opts) => process.exit(await runOnboard({
|
|
82
124
|
verificationId: opts.verificationId,
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { signup } from "../../lib/ops/account.js";
|
|
2
|
+
import { isDialError } from "../../lib/ops/errors.js";
|
|
3
|
+
export async function runAuthLogin(email, opts) {
|
|
4
|
+
try {
|
|
5
|
+
const { verificationId } = await signup({ email, force: opts.force });
|
|
6
|
+
if (opts.json) {
|
|
7
|
+
console.log(JSON.stringify({
|
|
8
|
+
ok: true,
|
|
9
|
+
verificationId,
|
|
10
|
+
email,
|
|
11
|
+
// Every step names the next one, so an agent never has to guess the flow.
|
|
12
|
+
nextCommand: "dial auth verify-otp --code <code>",
|
|
13
|
+
}));
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
console.log(`OTP sent to ${email}.`);
|
|
17
|
+
console.log(`Run \`dial auth verify-otp --code <code>\` once you have it (verificationId is stored locally).`);
|
|
18
|
+
}
|
|
19
|
+
return 0;
|
|
20
|
+
}
|
|
21
|
+
catch (e) {
|
|
22
|
+
if (!isDialError(e))
|
|
23
|
+
throw e;
|
|
24
|
+
if (e.code === "pending_exists") {
|
|
25
|
+
const d = e.data ?? {};
|
|
26
|
+
if (opts.json) {
|
|
27
|
+
console.log(JSON.stringify({
|
|
28
|
+
ok: false,
|
|
29
|
+
code: "pending_exists",
|
|
30
|
+
verificationId: d.verificationId,
|
|
31
|
+
email: d.email,
|
|
32
|
+
ageSeconds: d.ageSeconds,
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
console.error(e.message);
|
|
37
|
+
}
|
|
38
|
+
return 3;
|
|
39
|
+
}
|
|
40
|
+
if (opts.json)
|
|
41
|
+
console.log(JSON.stringify({ ok: false, code: e.code, status: e.status, error: e.message }));
|
|
42
|
+
else
|
|
43
|
+
console.error(`login failed: ${e.message}`);
|
|
44
|
+
return 2;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { registerNumber } from "../../lib/ops/account.js";
|
|
2
|
+
import { isDialError } from "../../lib/ops/errors.js";
|
|
3
|
+
const NEXT_COMMAND = "dial auth verify-otp --number --code <code>";
|
|
4
|
+
export async function runAuthRegisterNumber(phone, opts) {
|
|
5
|
+
try {
|
|
6
|
+
const { registrationId, phoneNumber } = await registerNumber({
|
|
7
|
+
registrationId: opts.registrationId,
|
|
8
|
+
phoneNumber: phone,
|
|
9
|
+
});
|
|
10
|
+
if (opts.json) {
|
|
11
|
+
console.log(JSON.stringify({ ok: true, registrationId, phoneNumber, nextCommand: NEXT_COMMAND }));
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
console.log(`code sent to ${phoneNumber}.`);
|
|
15
|
+
console.log(`Ask the user to read it back, then run \`${NEXT_COMMAND}\`.`);
|
|
16
|
+
}
|
|
17
|
+
return 0;
|
|
18
|
+
}
|
|
19
|
+
catch (e) {
|
|
20
|
+
if (!isDialError(e))
|
|
21
|
+
throw e;
|
|
22
|
+
if (opts.json)
|
|
23
|
+
console.log(JSON.stringify({ ok: false, code: e.code, status: e.status, error: e.message }));
|
|
24
|
+
else
|
|
25
|
+
console.error(`register-number failed: ${e.message}`);
|
|
26
|
+
// A missing registration is the caller's sequencing mistake (1); anything else
|
|
27
|
+
// is a request failure (2).
|
|
28
|
+
return e.code === "no_pending_registration" ? 1 : 2;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { onboard, verifyNumber, installAgentSkills, } from "../../lib/ops/account.js";
|
|
2
|
+
import { isDialError } from "../../lib/ops/errors.js";
|
|
3
|
+
import { readAuth, readPendingSignup, authFilePath } from "../../lib/state.js";
|
|
4
|
+
import { supervisorAvailability } from "../../lib/supervisor/index.js";
|
|
5
|
+
import { baseUrl } from "../../lib/api.js";
|
|
6
|
+
import { dashboardHint, dashboardUrl } from "../../lib/dashboard.js";
|
|
7
|
+
function maskApiKey(key) {
|
|
8
|
+
return key.length >= 4 ? `sk_live_***${key.slice(-4)}` : "sk_live_***";
|
|
9
|
+
}
|
|
10
|
+
const LISTEN_QUESTION = "Install the background notification listening service?";
|
|
11
|
+
const LISTEN_RATIONALE = "The background notification listening service runs on this machine and listens for notifications from Dial — SMS arrivals (2FA codes, replies) and call-ended notifications — so the agent on this machine learns about them in real time. " +
|
|
12
|
+
"Without it the agent is send-only: it can place outbound calls and send messages, but it has no way to know when something arrives at your Dial number. " +
|
|
13
|
+
"It can always be installed later with `dial listen install`.";
|
|
14
|
+
const PRESENTATION_HINT = "When you ask the user, describe it as \"the background notification listening service.\" Don't expose technical details (launchd, plists, daemons, systemd units) — that's not how to talk about it to a non-technical user.";
|
|
15
|
+
const FINALIZATION_HINT = 'To finalize onboarding, ask the user this question before continuing with any other task. If the user\'s original request was something else (e.g. "call X", "send Y"), briefly pause that task, ask the question, get an answer, then resume.';
|
|
16
|
+
const REGISTER_NUMBER_COMMAND = "dial auth register-number <phone>";
|
|
17
|
+
function printSkills(skills) {
|
|
18
|
+
for (const r of skills) {
|
|
19
|
+
if ("error" in r)
|
|
20
|
+
console.log(` skill (${r.agent}): failed — ${r.error}`);
|
|
21
|
+
else if (r.written)
|
|
22
|
+
console.log(` skill (${r.agent}): installed → ${r.path}`);
|
|
23
|
+
else if (r.unchanged)
|
|
24
|
+
console.log(` skill (${r.agent}): already up to date → ${r.path}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** The account exists: report the key, the skills, and the listen-service offer. */
|
|
28
|
+
function reportOnboarded(result, json) {
|
|
29
|
+
const { apiKey, accountId, phoneNumber, phoneNumberId, apiKeyPath, skills, supervisor } = result;
|
|
30
|
+
const masked = maskApiKey(apiKey);
|
|
31
|
+
if (json) {
|
|
32
|
+
console.log(JSON.stringify({
|
|
33
|
+
ok: true,
|
|
34
|
+
apiKeyFingerprint: apiKey.slice(-4),
|
|
35
|
+
apiKeyMasked: masked,
|
|
36
|
+
apiKeyPath,
|
|
37
|
+
accountId,
|
|
38
|
+
email: result.email,
|
|
39
|
+
phoneNumber,
|
|
40
|
+
phoneNumberId,
|
|
41
|
+
dashboardUrl: result.dashboardUrl,
|
|
42
|
+
listen: {
|
|
43
|
+
installed: false,
|
|
44
|
+
autoInstalled: false,
|
|
45
|
+
canInstall: supervisor.available,
|
|
46
|
+
unavailableReason: supervisor.available ? null : supervisor.reason,
|
|
47
|
+
},
|
|
48
|
+
skills,
|
|
49
|
+
agentHint: supervisor.available
|
|
50
|
+
? {
|
|
51
|
+
action: "ask_user",
|
|
52
|
+
kind: "onboarding_finalization",
|
|
53
|
+
question: LISTEN_QUESTION,
|
|
54
|
+
rationale: LISTEN_RATIONALE,
|
|
55
|
+
presentation: PRESENTATION_HINT,
|
|
56
|
+
finalizationNote: FINALIZATION_HINT,
|
|
57
|
+
useStructuredToolIfAvailable: true,
|
|
58
|
+
onYes: "dial listen install",
|
|
59
|
+
onNo: null,
|
|
60
|
+
}
|
|
61
|
+
: {
|
|
62
|
+
action: "skip",
|
|
63
|
+
kind: "listen_install_unavailable",
|
|
64
|
+
reason: supervisor.reason,
|
|
65
|
+
note: "No user-level service supervisor on this machine (sandbox/container/CI). Skip the install offer and continue with the original task. Inbound events are still reachable via `dial wait-for` (long-poll); only the always-on background listener and `dial local-target` fan-out are unavailable here.",
|
|
66
|
+
},
|
|
67
|
+
}));
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
console.log("onboarded.");
|
|
71
|
+
console.log(` api key: ${masked} (saved to ${apiKeyPath})`);
|
|
72
|
+
if (phoneNumber)
|
|
73
|
+
console.log(` phone number: ${phoneNumber}`);
|
|
74
|
+
printSkills(skills);
|
|
75
|
+
// Part of the summary, deliberately above the finalization block so that block
|
|
76
|
+
// stays the last thing an agent reads.
|
|
77
|
+
console.log(dashboardHint(result.dashboardUrl, result.email));
|
|
78
|
+
console.log(``);
|
|
79
|
+
if (!supervisor.available) {
|
|
80
|
+
console.log(`listen service: not available on this machine (${supervisor.reason}).`);
|
|
81
|
+
console.log(`Skipping the install offer. Inbound events still work via \`dial wait-for\` (long-poll);`);
|
|
82
|
+
console.log(`only the always-on background listener and \`dial local-target\` fan-out are unavailable here.`);
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
console.log(`Onboarding finalization (required):`);
|
|
86
|
+
console.log(``);
|
|
87
|
+
console.log(FINALIZATION_HINT);
|
|
88
|
+
console.log(``);
|
|
89
|
+
console.log(`Ask the user:`);
|
|
90
|
+
console.log(``);
|
|
91
|
+
console.log(` "${LISTEN_QUESTION}"`);
|
|
92
|
+
console.log(``);
|
|
93
|
+
console.log(`Background: ${LISTEN_RATIONALE}`);
|
|
94
|
+
console.log(``);
|
|
95
|
+
console.log(`Phrasing rules: ${PRESENTATION_HINT}`);
|
|
96
|
+
console.log(``);
|
|
97
|
+
console.log(`Use your structured ask-user / clarification tool if you have one;`);
|
|
98
|
+
console.log(`otherwise ask in your regular reply and wait for the answer.`);
|
|
99
|
+
console.log(``);
|
|
100
|
+
console.log(`After the user answers:`);
|
|
101
|
+
console.log(` yes → run \`dial listen install\`, then resume the original task.`);
|
|
102
|
+
console.log(` no → resume the original task. They can install later with \`dial listen install\`.`);
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
/** The email is verified but the account still needs a phone number. */
|
|
106
|
+
function reportPendingPhone(result, json) {
|
|
107
|
+
if (json) {
|
|
108
|
+
console.log(JSON.stringify({
|
|
109
|
+
ok: true,
|
|
110
|
+
pendingPhone: true,
|
|
111
|
+
registrationId: result.registrationId,
|
|
112
|
+
email: result.email,
|
|
113
|
+
skills: result.skills,
|
|
114
|
+
nextCommand: REGISTER_NUMBER_COMMAND,
|
|
115
|
+
agentHint: {
|
|
116
|
+
action: "ask_user",
|
|
117
|
+
kind: "phone_number_required",
|
|
118
|
+
question: "What phone number should this Dial account be registered to?",
|
|
119
|
+
rationale: "Creating a Dial account requires a verified phone number as well as a verified email. Dial texts a 6-digit code to the number, and the user reads it back. The number is linked to the account permanently — a number can register only one Dial account — so it should be one the user keeps. It must be able to receive SMS, and a Dial number cannot be used.",
|
|
120
|
+
presentation: "Ask for the number in international form including the country code. Don't invent one, and don't reuse a Dial number.",
|
|
121
|
+
useStructuredToolIfAvailable: true,
|
|
122
|
+
onAnswer: REGISTER_NUMBER_COMMAND,
|
|
123
|
+
},
|
|
124
|
+
}));
|
|
125
|
+
return 0;
|
|
126
|
+
}
|
|
127
|
+
console.log("email verified — a phone number is still required.");
|
|
128
|
+
printSkills(result.skills);
|
|
129
|
+
console.log(``);
|
|
130
|
+
console.log(`Creating an account needs a verified phone number as well as a verified email.`);
|
|
131
|
+
console.log(`No API key has been issued yet.`);
|
|
132
|
+
console.log(``);
|
|
133
|
+
console.log(`Ask the user for a phone number that can receive SMS, then run:`);
|
|
134
|
+
console.log(``);
|
|
135
|
+
console.log(` ${REGISTER_NUMBER_COMMAND}`);
|
|
136
|
+
console.log(``);
|
|
137
|
+
console.log(`That number is linked to the account permanently — a number can register only one`);
|
|
138
|
+
console.log(`Dial account — so use one the user keeps. A Dial number can't be used here.`);
|
|
139
|
+
console.log(``);
|
|
140
|
+
console.log(`Dial texts a 6-digit code to it; then run:`);
|
|
141
|
+
console.log(``);
|
|
142
|
+
console.log(` dial auth verify-otp --number --code <code>`);
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
145
|
+
/** No --code on a machine that's already signed in: just install the skills. */
|
|
146
|
+
function reportAlreadySignedIn(opts) {
|
|
147
|
+
const auth = readAuth();
|
|
148
|
+
if (!auth) {
|
|
149
|
+
const message = "Not signed in. Run `dial auth login <email>` first, then re-run with --code from your inbox.";
|
|
150
|
+
if (opts.json)
|
|
151
|
+
console.log(JSON.stringify({ ok: false, code: "not_signed_in", error: message }));
|
|
152
|
+
else
|
|
153
|
+
console.error(message);
|
|
154
|
+
return 1;
|
|
155
|
+
}
|
|
156
|
+
const skills = installAgentSkills(opts.agents);
|
|
157
|
+
const supervisor = supervisorAvailability();
|
|
158
|
+
if (opts.json) {
|
|
159
|
+
console.log(JSON.stringify({
|
|
160
|
+
ok: true,
|
|
161
|
+
alreadySignedIn: true,
|
|
162
|
+
apiKeyFingerprint: auth.apiKey.slice(-4),
|
|
163
|
+
apiKeyMasked: maskApiKey(auth.apiKey),
|
|
164
|
+
apiKeyPath: authFilePath(),
|
|
165
|
+
accountId: auth.accountId,
|
|
166
|
+
email: auth.email || null,
|
|
167
|
+
phoneNumber: auth.phoneNumber ?? null,
|
|
168
|
+
phoneNumberId: auth.phoneNumberId ?? null,
|
|
169
|
+
dashboardUrl: dashboardUrl(baseUrl()),
|
|
170
|
+
listen: {
|
|
171
|
+
installed: false,
|
|
172
|
+
autoInstalled: false,
|
|
173
|
+
canInstall: supervisor.available,
|
|
174
|
+
unavailableReason: supervisor.available ? null : supervisor.reason,
|
|
175
|
+
},
|
|
176
|
+
skills,
|
|
177
|
+
agentHint: {
|
|
178
|
+
action: "skip",
|
|
179
|
+
kind: "already_signed_in",
|
|
180
|
+
note: "Account is already signed in; verification was skipped and only the requested --agent skills were installed.",
|
|
181
|
+
},
|
|
182
|
+
}));
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
console.log(`already signed in as ${auth.email || "(unknown email)"} — skipped verification.`);
|
|
186
|
+
console.log(` api key: ${maskApiKey(auth.apiKey)} (saved at ${authFilePath()})`);
|
|
187
|
+
printSkills(skills);
|
|
188
|
+
console.log(dashboardHint(dashboardUrl(baseUrl()), auth.email || null));
|
|
189
|
+
}
|
|
190
|
+
return 0;
|
|
191
|
+
}
|
|
192
|
+
function reportFailure(e, opts, label) {
|
|
193
|
+
if (!isDialError(e))
|
|
194
|
+
throw e;
|
|
195
|
+
if (e.code === "no_pending_signup" || e.code === "no_pending_registration") {
|
|
196
|
+
if (opts.json)
|
|
197
|
+
console.log(JSON.stringify({ ok: false, code: e.code, error: e.message }));
|
|
198
|
+
else
|
|
199
|
+
console.error(e.message);
|
|
200
|
+
return 1;
|
|
201
|
+
}
|
|
202
|
+
if (e.code === "verify_failed") {
|
|
203
|
+
if (opts.json)
|
|
204
|
+
console.log(JSON.stringify({ ok: false, code: "verify_failed", status: e.status, error: e.message }));
|
|
205
|
+
else
|
|
206
|
+
console.error(`${label} failed: ${e.message}`);
|
|
207
|
+
// 401 is a wrong/expired code (the user's problem); anything else is ours.
|
|
208
|
+
return e.status === 401 ? 1 : 2;
|
|
209
|
+
}
|
|
210
|
+
// missing_api_key
|
|
211
|
+
if (opts.json)
|
|
212
|
+
console.log(JSON.stringify({ ok: false, code: e.code, error: e.message }));
|
|
213
|
+
else
|
|
214
|
+
console.error(`${label} failed: ${e.message}`);
|
|
215
|
+
return 2;
|
|
216
|
+
}
|
|
217
|
+
export async function runAuthVerifyOtp(opts) {
|
|
218
|
+
// ── SMS code: the step that creates the account ──
|
|
219
|
+
if (opts.number) {
|
|
220
|
+
if (!opts.code) {
|
|
221
|
+
const message = "A --code is required with --number. Check the text message.";
|
|
222
|
+
if (opts.json)
|
|
223
|
+
console.log(JSON.stringify({ ok: false, code: "code_required", error: message }));
|
|
224
|
+
else
|
|
225
|
+
console.error(message);
|
|
226
|
+
return 1;
|
|
227
|
+
}
|
|
228
|
+
if (!opts.registrationId && !readPendingSignup()?.registrationId) {
|
|
229
|
+
const message = `No phone number awaiting verification. Run \`${REGISTER_NUMBER_COMMAND}\` first.`;
|
|
230
|
+
if (opts.json)
|
|
231
|
+
console.log(JSON.stringify({ ok: false, code: "no_pending_registration", error: message }));
|
|
232
|
+
else
|
|
233
|
+
console.error(message);
|
|
234
|
+
return 1;
|
|
235
|
+
}
|
|
236
|
+
try {
|
|
237
|
+
const result = await verifyNumber({
|
|
238
|
+
registrationId: opts.registrationId,
|
|
239
|
+
code: opts.code,
|
|
240
|
+
agents: opts.agents,
|
|
241
|
+
});
|
|
242
|
+
return reportOnboarded(result, !!opts.json);
|
|
243
|
+
}
|
|
244
|
+
catch (e) {
|
|
245
|
+
return reportFailure(e, opts, "verify-otp --number");
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// ── Email code ──
|
|
249
|
+
if (!opts.code)
|
|
250
|
+
return reportAlreadySignedIn(opts);
|
|
251
|
+
try {
|
|
252
|
+
const result = await onboard({
|
|
253
|
+
verificationId: opts.verificationId,
|
|
254
|
+
code: opts.code,
|
|
255
|
+
agents: opts.agents,
|
|
256
|
+
});
|
|
257
|
+
return result.pendingPhone
|
|
258
|
+
? reportPendingPhone(result, !!opts.json)
|
|
259
|
+
: reportOnboarded(result, !!opts.json);
|
|
260
|
+
}
|
|
261
|
+
catch (e) {
|
|
262
|
+
return reportFailure(e, opts, "verify-otp");
|
|
263
|
+
}
|
|
264
|
+
}
|
package/dist/commands/onboard.js
CHANGED
|
@@ -1,217 +1,38 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
// `onboard()` in ops/account.ts. Used by the signed-in-only branch below,
|
|
13
|
-
// which skips OTP verification entirely.
|
|
14
|
-
function installAgentSkills(agents) {
|
|
15
|
-
const skills = [];
|
|
16
|
-
for (const requested of agents) {
|
|
17
|
-
if (!isSupportedAgent(requested)) {
|
|
18
|
-
skills.push({
|
|
19
|
-
agent: requested,
|
|
20
|
-
error: `unknown agent "${requested}". Supported: ${SUPPORTED_AGENTS.join(", ")}.`,
|
|
21
|
-
});
|
|
22
|
-
continue;
|
|
23
|
-
}
|
|
24
|
-
try {
|
|
25
|
-
skills.push(installSkill(requested));
|
|
26
|
-
}
|
|
27
|
-
catch (err) {
|
|
28
|
-
skills.push({ agent: requested, error: err instanceof Error ? err.message : String(err) });
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
return skills;
|
|
32
|
-
}
|
|
33
|
-
const LISTEN_QUESTION = "Install the background notification listening service?";
|
|
34
|
-
const LISTEN_RATIONALE = "The background notification listening service runs on this machine and listens for notifications from Dial — SMS arrivals (2FA codes, replies) and call-ended notifications — so the agent on this machine learns about them in real time. " +
|
|
35
|
-
"Without it the agent is send-only: it can place outbound calls and send messages, but it has no way to know when something arrives at your Dial number. " +
|
|
36
|
-
"It can always be installed later with `dial listen install`.";
|
|
37
|
-
const PRESENTATION_HINT = "When you ask the user, describe it as \"the background notification listening service.\" Don't expose technical details (launchd, plists, daemons, systemd units) — that's not how to talk about it to a non-technical user.";
|
|
38
|
-
const FINALIZATION_HINT = 'To finalize onboarding, ask the user this question before continuing with any other task. If the user\'s original request was something else (e.g. "call X", "send Y"), briefly pause that task, ask the question, get an answer, then resume.';
|
|
1
|
+
/**
|
|
2
|
+
* DEPRECATED. `dial onboard` was replaced by `dial auth verify-otp`, and creating an
|
|
3
|
+
* account now also requires verifying a phone number.
|
|
4
|
+
*
|
|
5
|
+
* The command stays registered rather than being deleted so a stale skill or cached
|
|
6
|
+
* doc gets an instruction it can act on, instead of commander's bare "unknown
|
|
7
|
+
* command". Exits non-zero: nothing was done.
|
|
8
|
+
*/
|
|
9
|
+
const REPLACEMENT = "dial auth verify-otp --code <code>";
|
|
10
|
+
const PHONE_NOTE = "Creating an account now also requires a verified phone number: after the email code, run `dial auth register-number <phone>`, then `dial auth verify-otp --number --code <code>`.";
|
|
11
|
+
const INSTRUCTION_NOTE = "`--inbound-instruction` no longer exists — a new number starts with the default prompt; change it with `dial number set`.";
|
|
39
12
|
export async function runOnboard(opts) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
// is just install any --agent skills the caller asked for. This is what agents
|
|
43
|
-
// following the docs on an already-onboarded account hit when they read the
|
|
44
|
-
// integration page: signup is a no-op, all that's left is the skill drop-in.
|
|
45
|
-
if (!opts.code) {
|
|
46
|
-
const auth = readAuth();
|
|
47
|
-
if (!auth) {
|
|
48
|
-
const message = "Not signed in. Run `dial signup <email>` first, then re-run with --code from your inbox.";
|
|
49
|
-
if (opts.json)
|
|
50
|
-
console.log(JSON.stringify({ ok: false, code: "not_signed_in", error: message }));
|
|
51
|
-
else
|
|
52
|
-
console.error(message);
|
|
53
|
-
return 1;
|
|
54
|
-
}
|
|
55
|
-
const skills = installAgentSkills(opts.agents ?? []);
|
|
56
|
-
const supervisor = supervisorAvailability();
|
|
57
|
-
if (opts.json) {
|
|
58
|
-
console.log(JSON.stringify({
|
|
59
|
-
ok: true,
|
|
60
|
-
alreadySignedIn: true,
|
|
61
|
-
apiKeyFingerprint: auth.apiKey.slice(-4),
|
|
62
|
-
apiKeyMasked: maskApiKey(auth.apiKey),
|
|
63
|
-
apiKeyPath: authFilePath(),
|
|
64
|
-
accountId: auth.accountId,
|
|
65
|
-
email: auth.email || null,
|
|
66
|
-
phoneNumber: auth.phoneNumber ?? null,
|
|
67
|
-
phoneNumberId: auth.phoneNumberId ?? null,
|
|
68
|
-
dashboardUrl: dashboardUrl(baseUrl()),
|
|
69
|
-
listen: {
|
|
70
|
-
installed: false,
|
|
71
|
-
autoInstalled: false,
|
|
72
|
-
canInstall: supervisor.available,
|
|
73
|
-
unavailableReason: supervisor.available ? null : supervisor.reason,
|
|
74
|
-
},
|
|
75
|
-
skills,
|
|
76
|
-
agentHint: {
|
|
77
|
-
action: "skip",
|
|
78
|
-
kind: "already_signed_in",
|
|
79
|
-
note: "Account is already signed in; verification was skipped and only the requested --agent skills were installed.",
|
|
80
|
-
},
|
|
81
|
-
}));
|
|
82
|
-
}
|
|
83
|
-
else {
|
|
84
|
-
console.log(`already signed in as ${auth.email || "(unknown email)"} — skipped verification.`);
|
|
85
|
-
console.log(` api key: ${maskApiKey(auth.apiKey)} (saved at ${authFilePath()})`);
|
|
86
|
-
for (const r of skills) {
|
|
87
|
-
if ("error" in r)
|
|
88
|
-
console.log(` skill (${r.agent}): failed — ${r.error}`);
|
|
89
|
-
else if (r.written)
|
|
90
|
-
console.log(` skill (${r.agent}): installed → ${r.path}`);
|
|
91
|
-
else if (r.unchanged)
|
|
92
|
-
console.log(` skill (${r.agent}): already up to date → ${r.path}`);
|
|
93
|
-
}
|
|
94
|
-
console.log(dashboardHint(dashboardUrl(baseUrl()), auth.email || null));
|
|
95
|
-
}
|
|
96
|
-
return 0;
|
|
97
|
-
}
|
|
98
|
-
let result;
|
|
99
|
-
try {
|
|
100
|
-
result = await onboard({
|
|
101
|
-
verificationId: opts.verificationId,
|
|
102
|
-
code: opts.code,
|
|
103
|
-
inboundInstruction: opts.inboundInstruction,
|
|
104
|
-
agents: opts.agents,
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
catch (e) {
|
|
108
|
-
if (!isDialError(e))
|
|
109
|
-
throw e;
|
|
110
|
-
if (e.code === "no_pending_signup") {
|
|
111
|
-
if (opts.json)
|
|
112
|
-
console.log(JSON.stringify({ ok: false, code: "no_pending_signup" }));
|
|
113
|
-
else
|
|
114
|
-
console.error(e.message);
|
|
115
|
-
return 1;
|
|
116
|
-
}
|
|
117
|
-
if (e.code === "verify_failed") {
|
|
118
|
-
if (opts.json)
|
|
119
|
-
console.log(JSON.stringify({ ok: false, code: "verify_failed", status: e.status, error: e.message }));
|
|
120
|
-
else
|
|
121
|
-
console.error(`onboard failed: ${e.message}`);
|
|
122
|
-
return e.status === 401 ? 1 : 2;
|
|
123
|
-
}
|
|
124
|
-
// missing_api_key
|
|
125
|
-
if (opts.json)
|
|
126
|
-
console.log(JSON.stringify({ ok: false, code: "missing_api_key", error: e.message }));
|
|
127
|
-
else
|
|
128
|
-
console.error(`onboard failed: ${e.message}`);
|
|
129
|
-
return 2;
|
|
130
|
-
}
|
|
131
|
-
const { apiKey, accountId, email, phoneNumber, phoneNumberId, apiKeyPath, skills, supervisor } = result;
|
|
132
|
-
const masked = maskApiKey(apiKey);
|
|
13
|
+
const message = `\`dial onboard\` has been replaced by \`${REPLACEMENT}\`. Nothing was done.`;
|
|
14
|
+
const instructionNote = opts.inboundInstruction ? INSTRUCTION_NOTE : undefined;
|
|
133
15
|
if (opts.json) {
|
|
134
16
|
console.log(JSON.stringify({
|
|
135
|
-
ok:
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
phoneNumber,
|
|
142
|
-
phoneNumberId,
|
|
143
|
-
dashboardUrl: result.dashboardUrl,
|
|
144
|
-
listen: {
|
|
145
|
-
installed: false,
|
|
146
|
-
autoInstalled: false,
|
|
147
|
-
canInstall: supervisor.available,
|
|
148
|
-
unavailableReason: supervisor.available ? null : supervisor.reason,
|
|
149
|
-
},
|
|
150
|
-
skills,
|
|
151
|
-
agentHint: supervisor.available
|
|
152
|
-
? {
|
|
153
|
-
action: "ask_user",
|
|
154
|
-
kind: "onboarding_finalization",
|
|
155
|
-
question: LISTEN_QUESTION,
|
|
156
|
-
rationale: LISTEN_RATIONALE,
|
|
157
|
-
presentation: PRESENTATION_HINT,
|
|
158
|
-
finalizationNote: FINALIZATION_HINT,
|
|
159
|
-
useStructuredToolIfAvailable: true,
|
|
160
|
-
onYes: "dial listen install",
|
|
161
|
-
onNo: null,
|
|
162
|
-
}
|
|
163
|
-
: {
|
|
164
|
-
action: "skip",
|
|
165
|
-
kind: "listen_install_unavailable",
|
|
166
|
-
reason: supervisor.reason,
|
|
167
|
-
note: "No user-level service supervisor on this machine (sandbox/container/CI). Skip the install offer and continue with the original task. Inbound events are still reachable via `dial wait-for` (long-poll); only the always-on background listener and `dial local-target` fan-out are unavailable here.",
|
|
168
|
-
},
|
|
17
|
+
ok: false,
|
|
18
|
+
code: "deprecated_command",
|
|
19
|
+
error: message,
|
|
20
|
+
replacement: REPLACEMENT,
|
|
21
|
+
note: PHONE_NOTE,
|
|
22
|
+
...(instructionNote ? { instructionNote } : {}),
|
|
169
23
|
}));
|
|
170
24
|
}
|
|
171
25
|
else {
|
|
172
|
-
console.
|
|
173
|
-
console.
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
console.log(` skill (${r.agent}): installed → ${r.path}`);
|
|
182
|
-
}
|
|
183
|
-
else if (r.unchanged) {
|
|
184
|
-
console.log(` skill (${r.agent}): already up to date → ${r.path}`);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
// Part of the summary, deliberately above the finalization block so that
|
|
188
|
-
// block stays the last thing an agent reads.
|
|
189
|
-
console.log(dashboardHint(result.dashboardUrl, email));
|
|
190
|
-
console.log(``);
|
|
191
|
-
if (!supervisor.available) {
|
|
192
|
-
console.log(`listen service: not available on this machine (${supervisor.reason}).`);
|
|
193
|
-
console.log(`Skipping the install offer. Inbound events still work via \`dial wait-for\` (long-poll);`);
|
|
194
|
-
console.log(`only the always-on background listener and \`dial local-target\` fan-out are unavailable here.`);
|
|
195
|
-
return 0;
|
|
26
|
+
console.error(message);
|
|
27
|
+
console.error(``);
|
|
28
|
+
console.error(`Run instead:`);
|
|
29
|
+
console.error(` dial auth verify-otp${opts.code ? ` --code ${opts.code}` : ""}`);
|
|
30
|
+
console.error(``);
|
|
31
|
+
console.error(PHONE_NOTE);
|
|
32
|
+
if (instructionNote) {
|
|
33
|
+
console.error(``);
|
|
34
|
+
console.error(instructionNote);
|
|
196
35
|
}
|
|
197
|
-
console.log(`Onboarding finalization (required):`);
|
|
198
|
-
console.log(``);
|
|
199
|
-
console.log(FINALIZATION_HINT);
|
|
200
|
-
console.log(``);
|
|
201
|
-
console.log(`Ask the user:`);
|
|
202
|
-
console.log(``);
|
|
203
|
-
console.log(` "${LISTEN_QUESTION}"`);
|
|
204
|
-
console.log(``);
|
|
205
|
-
console.log(`Background: ${LISTEN_RATIONALE}`);
|
|
206
|
-
console.log(``);
|
|
207
|
-
console.log(`Phrasing rules: ${PRESENTATION_HINT}`);
|
|
208
|
-
console.log(``);
|
|
209
|
-
console.log(`Use your structured ask-user / clarification tool if you have one;`);
|
|
210
|
-
console.log(`otherwise ask in your regular reply and wait for the answer.`);
|
|
211
|
-
console.log(``);
|
|
212
|
-
console.log(`After the user answers:`);
|
|
213
|
-
console.log(` yes → run \`dial listen install\`, then resume the original task.`);
|
|
214
|
-
console.log(` no → resume the original task. They can install later with \`dial listen install\`.`);
|
|
215
36
|
}
|
|
216
|
-
return
|
|
37
|
+
return 2;
|
|
217
38
|
}
|
package/dist/commands/signup.js
CHANGED
|
@@ -1,41 +1,27 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* DEPRECATED. `dial signup` was replaced by `dial auth login`.
|
|
3
|
+
*
|
|
4
|
+
* The command stays registered rather than being deleted so that a stale skill or
|
|
5
|
+
* cached doc gets an instruction it can act on, instead of commander's bare
|
|
6
|
+
* "unknown command". Exits non-zero: nothing was done.
|
|
7
|
+
*/
|
|
8
|
+
const REPLACEMENT = "dial auth login <email>";
|
|
3
9
|
export async function runSignup(email, opts) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
-
return 0;
|
|
10
|
+
const message = `\`dial signup\` has been replaced by \`${REPLACEMENT}\`. Nothing was sent.`;
|
|
11
|
+
if (opts.json) {
|
|
12
|
+
console.log(JSON.stringify({
|
|
13
|
+
ok: false,
|
|
14
|
+
code: "deprecated_command",
|
|
15
|
+
error: message,
|
|
16
|
+
replacement: REPLACEMENT,
|
|
17
|
+
email,
|
|
18
|
+
}));
|
|
14
19
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (opts.json) {
|
|
21
|
-
console.log(JSON.stringify({
|
|
22
|
-
ok: false,
|
|
23
|
-
code: "pending_exists",
|
|
24
|
-
verificationId: d.verificationId,
|
|
25
|
-
email: d.email,
|
|
26
|
-
ageSeconds: d.ageSeconds,
|
|
27
|
-
}));
|
|
28
|
-
}
|
|
29
|
-
else {
|
|
30
|
-
console.error(e.message);
|
|
31
|
-
}
|
|
32
|
-
return 3;
|
|
33
|
-
}
|
|
34
|
-
// signup_failed
|
|
35
|
-
if (opts.json)
|
|
36
|
-
console.log(JSON.stringify({ ok: false, code: e.code, status: e.status, error: e.message }));
|
|
37
|
-
else
|
|
38
|
-
console.error(`signup failed: ${e.message}`);
|
|
39
|
-
return 2;
|
|
20
|
+
else {
|
|
21
|
+
console.error(message);
|
|
22
|
+
console.error(``);
|
|
23
|
+
console.error(`Run instead:`);
|
|
24
|
+
console.error(` dial auth login ${email}`);
|
|
40
25
|
}
|
|
26
|
+
return 2;
|
|
41
27
|
}
|
package/dist/lib/api.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { request, fetch as undiciFetch, FormData as UndiciFormData, setGlobalDispatcher, EnvHttpProxyAgent, } from "undici";
|
|
2
2
|
import { logger } from "./log.js";
|
|
3
|
-
import { VERSION } from "./version.js";
|
|
4
3
|
import { refParamsHeader } from "./ref-params.js";
|
|
4
|
+
import { userAgent } from "./user-agent.js";
|
|
5
5
|
// Route this package's undici requests through HTTP(S)_PROXY when one is set.
|
|
6
6
|
// The `undici` npm package keeps its OWN global dispatcher, which — unlike
|
|
7
7
|
// Node's built-in fetch (wired by NODE_USE_ENV_PROXY) — ignores the proxy env
|
|
@@ -19,9 +19,6 @@ if (process.env.HTTPS_PROXY ||
|
|
|
19
19
|
// check) — Node's global FormData would be coerced to a text/plain string.
|
|
20
20
|
export { UndiciFormData as ApiFormData };
|
|
21
21
|
const DEFAULT_BASE = "https://api.getdial.ai";
|
|
22
|
-
// Identifies the CLI on every request, so server-side request logs attribute
|
|
23
|
-
// provisioning (and everything else) to the client + version that made the call.
|
|
24
|
-
const USER_AGENT = `@getdial/cli/${VERSION}`;
|
|
25
22
|
export function baseUrl() {
|
|
26
23
|
return process.env.DIAL_API_URL ?? DEFAULT_BASE;
|
|
27
24
|
}
|
|
@@ -69,7 +66,7 @@ async function apiRequest(method, path, body, apiKey, extraHeaders) {
|
|
|
69
66
|
const url = `${baseUrl()}${path}`;
|
|
70
67
|
const headers = applyRefParamsHeader({
|
|
71
68
|
"content-type": "application/json",
|
|
72
|
-
"user-agent":
|
|
69
|
+
"user-agent": userAgent(),
|
|
73
70
|
...(extraHeaders ?? {}),
|
|
74
71
|
});
|
|
75
72
|
if (apiKey)
|
|
@@ -99,7 +96,7 @@ async function sendMultipart(method, path, form, apiKey) {
|
|
|
99
96
|
// No content-type here on purpose: undici's fetch sets the multipart boundary.
|
|
100
97
|
// The user-agent must still be set (server request logs key off it — the
|
|
101
98
|
// POST path once regressed by dropping it, see the git history).
|
|
102
|
-
const headers = applyRefParamsHeader({ "user-agent":
|
|
99
|
+
const headers = applyRefParamsHeader({ "user-agent": userAgent() });
|
|
103
100
|
if (apiKey)
|
|
104
101
|
headers.authorization = `Bearer ${apiKey}`;
|
|
105
102
|
try {
|
package/dist/lib/ops/account.js
CHANGED
|
@@ -117,6 +117,28 @@ export async function signup(opts) {
|
|
|
117
117
|
});
|
|
118
118
|
return { verificationId: res.data.verificationId, email: opts.email };
|
|
119
119
|
}
|
|
120
|
+
/** Install the Dial skill into each requested agent's config directory. A local
|
|
121
|
+
* side effect: it writes files and needs no API key, so it runs whether or not
|
|
122
|
+
* the signup has produced an account yet. */
|
|
123
|
+
export function installAgentSkills(agents) {
|
|
124
|
+
const skills = [];
|
|
125
|
+
for (const requested of agents ?? []) {
|
|
126
|
+
if (!isSupportedAgent(requested)) {
|
|
127
|
+
skills.push({
|
|
128
|
+
agent: requested,
|
|
129
|
+
error: `unknown agent "${requested}". Supported: ${SUPPORTED_AGENTS.join(", ")}.`,
|
|
130
|
+
});
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
skills.push(installSkill(requested));
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
skills.push({ agent: requested, error: err instanceof Error ? err.message : String(err) });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return skills;
|
|
141
|
+
}
|
|
120
142
|
export async function onboard(opts) {
|
|
121
143
|
let verificationId = opts.verificationId;
|
|
122
144
|
let email = null;
|
|
@@ -131,10 +153,27 @@ export async function onboard(opts) {
|
|
|
131
153
|
const res = await apiPost("/api/v1/auth/verify", {
|
|
132
154
|
verificationId,
|
|
133
155
|
code: opts.code,
|
|
134
|
-
...(opts.inboundInstruction ? { inboundInstruction: opts.inboundInstruction } : {}),
|
|
135
156
|
});
|
|
136
157
|
if (!res.ok)
|
|
137
158
|
throw new DialError("verify_failed", res.error, res.status);
|
|
159
|
+
// Two outcomes. A registrationId means the email is verified but the account
|
|
160
|
+
// needs a verified phone number first — no key is issued yet, so the authed side
|
|
161
|
+
// effects (saving it, offering the listen service) have nothing to do.
|
|
162
|
+
if (res.data.registrationId) {
|
|
163
|
+
const pending = readPendingSignup();
|
|
164
|
+
writePendingSignup({
|
|
165
|
+
verificationId,
|
|
166
|
+
email: email ?? pending?.email ?? "",
|
|
167
|
+
createdAt: pending?.createdAt ?? new Date().toISOString(),
|
|
168
|
+
registrationId: res.data.registrationId,
|
|
169
|
+
});
|
|
170
|
+
return {
|
|
171
|
+
pendingPhone: true,
|
|
172
|
+
registrationId: res.data.registrationId,
|
|
173
|
+
email: email ?? pending?.email ?? null,
|
|
174
|
+
skills: installAgentSkills(opts.agents),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
138
177
|
const apiKey = res.data.apiKey ?? null;
|
|
139
178
|
if (!apiKey || !res.data.accountId) {
|
|
140
179
|
throw new DialError("missing_api_key", "backend returned no apiKey");
|
|
@@ -147,22 +186,7 @@ export async function onboard(opts) {
|
|
|
147
186
|
phoneNumberId: res.data.phoneNumberId ?? null,
|
|
148
187
|
});
|
|
149
188
|
clearPendingSignup();
|
|
150
|
-
const skills =
|
|
151
|
-
for (const requested of opts.agents ?? []) {
|
|
152
|
-
if (!isSupportedAgent(requested)) {
|
|
153
|
-
skills.push({
|
|
154
|
-
agent: requested,
|
|
155
|
-
error: `unknown agent "${requested}". Supported: ${SUPPORTED_AGENTS.join(", ")}.`,
|
|
156
|
-
});
|
|
157
|
-
continue;
|
|
158
|
-
}
|
|
159
|
-
try {
|
|
160
|
-
skills.push(installSkill(requested));
|
|
161
|
-
}
|
|
162
|
-
catch (err) {
|
|
163
|
-
skills.push({ agent: requested, error: err instanceof Error ? err.message : String(err) });
|
|
164
|
-
}
|
|
165
|
-
}
|
|
189
|
+
const skills = installAgentSkills(opts.agents);
|
|
166
190
|
return {
|
|
167
191
|
apiKey,
|
|
168
192
|
apiKeyFingerprint: apiKey.slice(-4),
|
|
@@ -176,3 +200,68 @@ export async function onboard(opts) {
|
|
|
176
200
|
supervisor: supervisorAvailability(),
|
|
177
201
|
};
|
|
178
202
|
}
|
|
203
|
+
// ── Phone verification ───────────────────────────────────────────────────────
|
|
204
|
+
/**
|
|
205
|
+
* Send a verification code to the phone number that will own the account.
|
|
206
|
+
*
|
|
207
|
+
* The number is passed through as typed: the server canonicalizes it and returns
|
|
208
|
+
* the E.164 form, which is what we store and display. Deliberately no local
|
|
209
|
+
* validation — libphonenumber throws under the CLI's test loader, and duplicating
|
|
210
|
+
* the rule would only let the two disagree.
|
|
211
|
+
*/
|
|
212
|
+
export async function registerNumber(opts) {
|
|
213
|
+
const registrationId = opts.registrationId ?? readPendingSignup()?.registrationId;
|
|
214
|
+
if (!registrationId) {
|
|
215
|
+
throw new DialError("no_pending_registration", "No signup awaiting a phone number. Run `dial auth login <email>` and `dial auth verify-otp --code <code>` first.");
|
|
216
|
+
}
|
|
217
|
+
const res = await apiPost("/api/v1/auth/register-number", { registrationId, phoneNumber: opts.phoneNumber });
|
|
218
|
+
if (!res.ok)
|
|
219
|
+
throw new DialError("register_number_failed", res.error, res.status);
|
|
220
|
+
const pending = readPendingSignup();
|
|
221
|
+
if (pending) {
|
|
222
|
+
writePendingSignup({ ...pending, registrationId, ownerPhoneNumber: res.data.phoneNumber });
|
|
223
|
+
}
|
|
224
|
+
return { registrationId, phoneNumber: res.data.phoneNumber };
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Submit the SMS code, which creates the account. Same result shape as onboard's
|
|
228
|
+
* account outcome, so both paths finish identically — key saved, skills installed,
|
|
229
|
+
* listen service offered.
|
|
230
|
+
*/
|
|
231
|
+
export async function verifyNumber(opts) {
|
|
232
|
+
const pending = readPendingSignup();
|
|
233
|
+
const registrationId = opts.registrationId ?? pending?.registrationId;
|
|
234
|
+
if (!registrationId) {
|
|
235
|
+
throw new DialError("no_pending_registration", "No phone number awaiting verification. Run `dial auth register-number <phone>` first.");
|
|
236
|
+
}
|
|
237
|
+
const res = await apiPost("/api/v1/auth/verify-number", {
|
|
238
|
+
registrationId,
|
|
239
|
+
code: opts.code,
|
|
240
|
+
});
|
|
241
|
+
if (!res.ok)
|
|
242
|
+
throw new DialError("verify_failed", res.error, res.status);
|
|
243
|
+
const apiKey = res.data.apiKey ?? null;
|
|
244
|
+
if (!apiKey || !res.data.accountId) {
|
|
245
|
+
throw new DialError("missing_api_key", "backend returned no apiKey");
|
|
246
|
+
}
|
|
247
|
+
writeAuth({
|
|
248
|
+
apiKey,
|
|
249
|
+
accountId: res.data.accountId,
|
|
250
|
+
email: pending?.email ?? "",
|
|
251
|
+
phoneNumber: res.data.phoneNumber ?? null,
|
|
252
|
+
phoneNumberId: res.data.phoneNumberId ?? null,
|
|
253
|
+
});
|
|
254
|
+
clearPendingSignup();
|
|
255
|
+
return {
|
|
256
|
+
apiKey,
|
|
257
|
+
apiKeyFingerprint: apiKey.slice(-4),
|
|
258
|
+
apiKeyPath: authFilePath(),
|
|
259
|
+
accountId: res.data.accountId,
|
|
260
|
+
email: pending?.email ?? null,
|
|
261
|
+
phoneNumber: res.data.phoneNumber ?? null,
|
|
262
|
+
phoneNumberId: res.data.phoneNumberId ?? null,
|
|
263
|
+
dashboardUrl: dashboardUrl(baseUrl()),
|
|
264
|
+
skills: installAgentSkills(opts.agents),
|
|
265
|
+
supervisor: supervisorAvailability(),
|
|
266
|
+
};
|
|
267
|
+
}
|
package/dist/lib/state.js
CHANGED
|
@@ -12,6 +12,12 @@ export const PendingSignupSchema = z.object({
|
|
|
12
12
|
verificationId: z.string(),
|
|
13
13
|
email: z.string(),
|
|
14
14
|
createdAt: z.string(),
|
|
15
|
+
// Set once the email OTP is verified but the account still needs a verified
|
|
16
|
+
// phone number. Optional so a file written by an older CLI still parses.
|
|
17
|
+
registrationId: z.string().optional(),
|
|
18
|
+
// The number a code was last sent to, so `auth verify-otp --number` can report
|
|
19
|
+
// which one it is verifying.
|
|
20
|
+
ownerPhoneNumber: z.string().optional(),
|
|
15
21
|
});
|
|
16
22
|
const authFile = defineVersionedFile({
|
|
17
23
|
dir: () => paths().dataDir,
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { VERSION } from "./version.js";
|
|
2
|
+
// Identifies the CLI on every request, so server-side request logs attribute
|
|
3
|
+
// provisioning (and everything else) to the client + version that made the call.
|
|
4
|
+
const STOCK_USER_AGENT = `@getdial/cli/${VERSION}`;
|
|
5
|
+
// A caller-supplied token longer than this is almost certainly a bug, and an
|
|
6
|
+
// unbounded one would bloat every server-side request log line it appears in.
|
|
7
|
+
const MAX_PREFIX_LENGTH = 128;
|
|
8
|
+
/**
|
|
9
|
+
* Reduce a caller-supplied identifier to something safe to put in a header.
|
|
10
|
+
*
|
|
11
|
+
* This never throws and never rejects: the prefix is telemetry, so a malformed
|
|
12
|
+
* value degrades to no prefix at all rather than failing the request. That
|
|
13
|
+
* matters concretely — undici refuses a header value containing CR/LF, so
|
|
14
|
+
* passing one through unsanitized would turn a cosmetic misconfiguration into a
|
|
15
|
+
* hard failure of every Dial call the host makes.
|
|
16
|
+
*
|
|
17
|
+
* Returns "" when there is nothing usable left.
|
|
18
|
+
*/
|
|
19
|
+
export function sanitizeUserAgent(raw) {
|
|
20
|
+
if (!raw)
|
|
21
|
+
return "";
|
|
22
|
+
return (raw
|
|
23
|
+
// Anything outside printable ASCII — CR, LF, tabs, other control chars, and
|
|
24
|
+
// non-ASCII bytes — becomes a space, in one pass.
|
|
25
|
+
.replace(/[^\x20-\x7E]/g, " ")
|
|
26
|
+
.replace(/\s+/g, " ")
|
|
27
|
+
.trim()
|
|
28
|
+
.slice(0, MAX_PREFIX_LENGTH)
|
|
29
|
+
// Truncation can land mid-gap and leave a trailing space behind.
|
|
30
|
+
.trim());
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The full User-Agent for an outgoing API request.
|
|
34
|
+
*
|
|
35
|
+
* `DIAL_USER_AGENT` lets an embedding host runtime (an agent platform, a
|
|
36
|
+
* product wrapping the CLI) identify itself: its token is *prepended*, and the
|
|
37
|
+
* CLI's own identifier is always still sent. Unset or unusable, the header goes
|
|
38
|
+
* out exactly as it always has.
|
|
39
|
+
*
|
|
40
|
+
* Read at call time, not at module load — like `baseUrl()` reading
|
|
41
|
+
* DIAL_API_URL, so the environment is still honored if it changes after import.
|
|
42
|
+
*/
|
|
43
|
+
export function userAgent() {
|
|
44
|
+
const prefix = sanitizeUserAgent(process.env.DIAL_USER_AGENT);
|
|
45
|
+
return prefix ? `${prefix} ${STOCK_USER_AGENT}` : STOCK_USER_AGENT;
|
|
46
|
+
}
|
|
@@ -5,12 +5,13 @@ const inputSchema = {
|
|
|
5
5
|
email: z.string().email().describe("Email address to send the sign-up OTP to"),
|
|
6
6
|
force: z.boolean().optional().describe("Overwrite an existing fresh pending signup"),
|
|
7
7
|
};
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
/** Mirrors `dial auth login`. */
|
|
9
|
+
export const authLoginTool = {
|
|
10
|
+
name: "auth_login",
|
|
10
11
|
config: {
|
|
11
|
-
title: "
|
|
12
|
-
description: "Request an email OTP
|
|
13
|
-
"Stores the pending verification locally.",
|
|
12
|
+
title: "Auth Login",
|
|
13
|
+
description: "Request an email OTP to create a Dial account or sign in. The code is emailed; submit it " +
|
|
14
|
+
"with the auth_verify_otp tool. Stores the pending verification locally.",
|
|
14
15
|
inputSchema,
|
|
15
16
|
outputSchema: {
|
|
16
17
|
verificationId: z.string().describe("Pending verification id (also stored locally)"),
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { jsonResult } from "../result.js";
|
|
3
|
+
import { registerNumber } from "../../lib/ops/account.js";
|
|
4
|
+
const inputSchema = {
|
|
5
|
+
phoneNumber: z
|
|
6
|
+
.string()
|
|
7
|
+
.describe("The user's own phone number, in international form with the country code. Must be able to receive SMS. ASK THE USER for it — never invent one, and never use a Dial number (they are refused)."),
|
|
8
|
+
registrationId: z
|
|
9
|
+
.string()
|
|
10
|
+
.optional()
|
|
11
|
+
.describe("Explicit registration id; falls back to the locally stored pending signup"),
|
|
12
|
+
};
|
|
13
|
+
/** Mirrors `dial auth register-number`. */
|
|
14
|
+
export const authRegisterNumberTool = {
|
|
15
|
+
name: "auth_register_number",
|
|
16
|
+
config: {
|
|
17
|
+
title: "Auth Register Number",
|
|
18
|
+
description: "Text a 6-digit verification code to the phone number that will own the account. Required to " +
|
|
19
|
+
"CREATE an account (never needed to sign in), and only valid after auth_verify_otp reported " +
|
|
20
|
+
"that a phone number is still required. The number becomes permanently bound to this " +
|
|
21
|
+
"account — a number can register only one Dial account. Submit the texted code with " +
|
|
22
|
+
"auth_verify_otp using number: true.",
|
|
23
|
+
inputSchema,
|
|
24
|
+
outputSchema: {
|
|
25
|
+
registrationId: z.string(),
|
|
26
|
+
phoneNumber: z.string().describe("The number in canonical E.164 form"),
|
|
27
|
+
},
|
|
28
|
+
annotations: { openWorldHint: true },
|
|
29
|
+
},
|
|
30
|
+
run: async (args) => jsonResult(await registerNumber({
|
|
31
|
+
phoneNumber: args.phoneNumber,
|
|
32
|
+
registrationId: args.registrationId,
|
|
33
|
+
})),
|
|
34
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { jsonResult } from "../result.js";
|
|
3
|
-
import { onboard } from "../../lib/ops/account.js";
|
|
3
|
+
import { onboard, verifyNumber } from "../../lib/ops/account.js";
|
|
4
4
|
import { readAuth, authFilePath } from "../../lib/state.js";
|
|
5
5
|
import { installSkill, isSupportedAgent, SUPPORTED_AGENTS, } from "../../lib/skill-install.js";
|
|
6
6
|
import { supervisorAvailability } from "../../lib/supervisor/index.js";
|
|
@@ -11,22 +11,27 @@ const inputSchema = {
|
|
|
11
11
|
.string()
|
|
12
12
|
.min(1)
|
|
13
13
|
.optional()
|
|
14
|
-
.describe("6-digit OTP
|
|
14
|
+
.describe("The 6-digit OTP. Omit if the account is already signed in — the tool will just install the requested agent skills and skip verification."),
|
|
15
|
+
number: z
|
|
16
|
+
.boolean()
|
|
17
|
+
.optional()
|
|
18
|
+
.describe("Set true to verify the SMS code sent by auth_register_number instead of the emailed code. This is the step that creates the account."),
|
|
15
19
|
verificationId: z
|
|
16
20
|
.string()
|
|
17
21
|
.optional()
|
|
18
|
-
.describe("Explicit verification id (defaults to the local pending signup)"),
|
|
19
|
-
|
|
22
|
+
.describe("Explicit verification id for the email step (defaults to the local pending signup)"),
|
|
23
|
+
registrationId: z
|
|
20
24
|
.string()
|
|
21
25
|
.optional()
|
|
22
|
-
.describe("
|
|
26
|
+
.describe("Explicit registration id for number: true (defaults to the local pending signup)"),
|
|
23
27
|
agents: z
|
|
24
28
|
.array(z.string())
|
|
25
29
|
.optional()
|
|
26
30
|
.describe("Agent names to install the Dial skill into (e.g. claude-code, cursor)"),
|
|
27
31
|
};
|
|
28
|
-
|
|
29
|
-
|
|
32
|
+
/** Mirrors `dial auth verify-otp`. */
|
|
33
|
+
export const authVerifyOtpTool = {
|
|
34
|
+
name: "auth_verify_otp",
|
|
30
35
|
config: {
|
|
31
36
|
title: "Onboard",
|
|
32
37
|
description: "Verify the sign-up OTP and finish onboarding: saves the API key locally and optionally installs " +
|
|
@@ -95,12 +100,30 @@ export const onboardTool = {
|
|
|
95
100
|
listenAvailable: supervisor.available,
|
|
96
101
|
});
|
|
97
102
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
103
|
+
// number: true is the SMS step, which creates the account.
|
|
104
|
+
const r = args.number
|
|
105
|
+
? await verifyNumber({
|
|
106
|
+
code: args.code,
|
|
107
|
+
registrationId: args.registrationId,
|
|
108
|
+
agents: args.agents,
|
|
109
|
+
})
|
|
110
|
+
: await onboard({
|
|
111
|
+
code: args.code,
|
|
112
|
+
verificationId: args.verificationId,
|
|
113
|
+
agents: args.agents,
|
|
114
|
+
});
|
|
115
|
+
// The email step has two outcomes. A pending phone number means no account and
|
|
116
|
+
// no API key yet: report what remains rather than pretending onboarding is done.
|
|
117
|
+
if ("pendingPhone" in r && r.pendingPhone) {
|
|
118
|
+
return jsonResult({
|
|
119
|
+
pendingPhone: true,
|
|
120
|
+
registrationId: r.registrationId,
|
|
121
|
+
email: r.email,
|
|
122
|
+
skills: r.skills,
|
|
123
|
+
nextTool: "auth_register_number",
|
|
124
|
+
note: "The email is verified, but creating an account also requires a verified phone number. ASK THE USER for a phone number that can receive SMS, then call auth_register_number with it. No API key has been issued yet. The number is bound to the account permanently, and a Dial number cannot be used.",
|
|
125
|
+
});
|
|
126
|
+
}
|
|
104
127
|
// Never surface the raw API key to the model; it's saved to disk for the CLI to read.
|
|
105
128
|
const { apiKey: _omit, ...safe } = r;
|
|
106
129
|
void _omit;
|
package/dist/mcp/tools/index.js
CHANGED
|
@@ -10,8 +10,9 @@ import { placeCallTool } from "./place-call.js";
|
|
|
10
10
|
import { listCallsTool } from "./list-calls.js";
|
|
11
11
|
import { getCallTool } from "./get-call.js";
|
|
12
12
|
import { getAccountStatusTool } from "./get-account-status.js";
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
13
|
+
import { authLoginTool } from "./auth-login.js";
|
|
14
|
+
import { authVerifyOtpTool } from "./auth-verify-otp.js";
|
|
15
|
+
import { authRegisterNumberTool } from "./auth-register-number.js";
|
|
15
16
|
import { waitForEventTool } from "./wait-for-event.js";
|
|
16
17
|
import { addUrlTargetTool } from "./add-url-target.js";
|
|
17
18
|
import { addCommandTargetTool } from "./add-command-target.js";
|
|
@@ -34,8 +35,9 @@ export const tools = [
|
|
|
34
35
|
listCallsTool,
|
|
35
36
|
getCallTool,
|
|
36
37
|
getAccountStatusTool,
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
authLoginTool,
|
|
39
|
+
authRegisterNumberTool,
|
|
40
|
+
authVerifyOtpTool,
|
|
39
41
|
waitForEventTool,
|
|
40
42
|
addUrlTargetTool,
|
|
41
43
|
addCommandTargetTool,
|
package/package.json
CHANGED
package/skills.tar.gz
CHANGED
|
Binary file
|