@getdial/cli 0.13.2 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -1
- package/dist/cli.js +5 -0
- package/dist/commands/call/get.js +28 -36
- package/dist/commands/call/list.js +20 -37
- package/dist/commands/call/send.js +52 -65
- package/dist/commands/doctor.js +2 -68
- package/dist/commands/mcp.js +17 -0
- package/dist/commands/message/list.js +20 -37
- package/dist/commands/message/send.js +22 -35
- package/dist/commands/number/list.js +21 -29
- package/dist/commands/number/purchase.js +23 -32
- package/dist/commands/number/set.js +19 -40
- package/dist/commands/onboard.js +36 -67
- package/dist/commands/signup.js +23 -24
- package/dist/commands/wait-for.js +33 -67
- package/dist/lib/cli-error.js +24 -0
- package/dist/lib/ops/account.js +136 -0
- package/dist/lib/ops/auth.js +18 -0
- package/dist/lib/ops/calls.js +33 -0
- package/dist/lib/ops/errors.js +23 -0
- package/dist/lib/ops/events.js +63 -0
- package/dist/lib/ops/listen.js +51 -0
- package/dist/lib/ops/local-targets.js +35 -0
- package/dist/lib/ops/messages.js +26 -0
- package/dist/lib/ops/numbers.js +39 -0
- package/dist/mcp/register.js +27 -0
- package/dist/mcp/result.js +16 -0
- package/dist/mcp/schemas.js +50 -0
- package/dist/mcp/server.js +20 -0
- package/dist/mcp/tool.js +1 -0
- package/dist/mcp/tools/add-command-target.js +27 -0
- package/dist/mcp/tools/add-url-target.js +30 -0
- package/dist/mcp/tools/get-account-status.js +22 -0
- package/dist/mcp/tools/get-call.js +18 -0
- package/dist/mcp/tools/index.js +41 -0
- package/dist/mcp/tools/list-calls.js +26 -0
- package/dist/mcp/tools/list-local-targets.js +15 -0
- package/dist/mcp/tools/list-messages.js +26 -0
- package/dist/mcp/tools/list-numbers.js +18 -0
- package/dist/mcp/tools/listen-install.js +19 -0
- package/dist/mcp/tools/listen-status.js +21 -0
- package/dist/mcp/tools/listen-uninstall.js +14 -0
- package/dist/mcp/tools/onboard.js +44 -0
- package/dist/mcp/tools/place-call.js +35 -0
- package/dist/mcp/tools/purchase-number.js +26 -0
- package/dist/mcp/tools/remove-local-target.js +20 -0
- package/dist/mcp/tools/send-message.js +26 -0
- package/dist/mcp/tools/set-number-properties.js +24 -0
- package/dist/mcp/tools/sign-up.js +22 -0
- package/dist/mcp/tools/wait-for-event.js +32 -0
- package/package.json +2 -1
- package/skills.tar.gz +0 -0
|
@@ -1,44 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { setNumberProperties } from "../../lib/ops/numbers.js";
|
|
2
|
+
import { isDialError } from "../../lib/ops/errors.js";
|
|
3
|
+
import { printDialError } from "../../lib/cli-error.js";
|
|
3
4
|
export async function runNumberSet(opts) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
try {
|
|
6
|
+
const n = await setNumberProperties({ number: opts.number, inboundInstruction: opts.inboundInstruction });
|
|
7
|
+
if (opts.json) {
|
|
8
|
+
console.log(JSON.stringify({ ok: true, number: n }));
|
|
9
|
+
}
|
|
10
|
+
else {
|
|
11
|
+
console.log(`updated.`);
|
|
12
|
+
console.log(` number: ${n.number}`);
|
|
13
|
+
console.log(` id: ${n.id}`);
|
|
14
|
+
console.log(` inbound instruction: ${n.inboundInstruction ?? ""}`);
|
|
15
|
+
}
|
|
16
|
+
return 0;
|
|
8
17
|
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
fail(opts.json, "list_failed", list.error, { status: list.status });
|
|
14
|
-
return 2;
|
|
18
|
+
catch (e) {
|
|
19
|
+
if (isDialError(e))
|
|
20
|
+
return printDialError(opts.json, e);
|
|
21
|
+
throw e;
|
|
15
22
|
}
|
|
16
|
-
const match = list.data.numbers.find((n) => n.number === opts.number);
|
|
17
|
-
if (!match) {
|
|
18
|
-
const known = list.data.numbers.map((n) => n.number).join(", ") || "(none)";
|
|
19
|
-
fail(opts.json, "number_not_found", `No phone number ${opts.number} on your account. Yours: ${known}.`);
|
|
20
|
-
return 1;
|
|
21
|
-
}
|
|
22
|
-
const res = await apiPatch(`/api/v1/numbers/${match.id}`, { inboundInstruction: opts.inboundInstruction }, auth.apiKey);
|
|
23
|
-
if (!res.ok) {
|
|
24
|
-
fail(opts.json, "update_failed", res.error, { status: res.status });
|
|
25
|
-
return 2;
|
|
26
|
-
}
|
|
27
|
-
const n = res.data.number;
|
|
28
|
-
if (opts.json) {
|
|
29
|
-
console.log(JSON.stringify({ ok: true, number: n }));
|
|
30
|
-
}
|
|
31
|
-
else {
|
|
32
|
-
console.log(`updated.`);
|
|
33
|
-
console.log(` number: ${n.number}`);
|
|
34
|
-
console.log(` id: ${n.id}`);
|
|
35
|
-
console.log(` inbound instruction: ${n.inboundInstruction ?? ""}`);
|
|
36
|
-
}
|
|
37
|
-
return 0;
|
|
38
|
-
}
|
|
39
|
-
function fail(json, code, message, extra) {
|
|
40
|
-
if (json)
|
|
41
|
-
console.log(JSON.stringify({ ok: false, code, message, ...extra }));
|
|
42
|
-
else
|
|
43
|
-
console.error(message);
|
|
44
23
|
}
|
package/dist/commands/onboard.js
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { paths } from "../lib/paths.js";
|
|
4
|
-
import { installSkill, isSupportedAgent, SUPPORTED_AGENTS } from "../lib/skill-install.js";
|
|
5
|
-
import { supervisorAvailability } from "../lib/supervisor/index.js";
|
|
1
|
+
import { onboard } from "../lib/ops/account.js";
|
|
2
|
+
import { isDialError } from "../lib/ops/errors.js";
|
|
6
3
|
function maskApiKey(key) {
|
|
7
4
|
return key.length >= 4 ? `sk_live_***${key.slice(-4)}` : "sk_live_***";
|
|
8
5
|
}
|
|
@@ -13,85 +10,57 @@ const LISTEN_RATIONALE = "The background notification listening service runs on
|
|
|
13
10
|
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.";
|
|
14
11
|
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.";
|
|
15
12
|
export async function runOnboard(opts) {
|
|
16
|
-
let
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
13
|
+
let result;
|
|
14
|
+
try {
|
|
15
|
+
result = await onboard({
|
|
16
|
+
verificationId: opts.verificationId,
|
|
17
|
+
code: opts.code,
|
|
18
|
+
inboundInstruction: opts.inboundInstruction,
|
|
19
|
+
agents: opts.agents,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
catch (e) {
|
|
23
|
+
if (!isDialError(e))
|
|
24
|
+
throw e;
|
|
25
|
+
if (e.code === "no_pending_signup") {
|
|
21
26
|
if (opts.json)
|
|
22
27
|
console.log(JSON.stringify({ ok: false, code: "no_pending_signup" }));
|
|
23
28
|
else
|
|
24
|
-
console.error(
|
|
29
|
+
console.error(e.message);
|
|
25
30
|
return 1;
|
|
26
31
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
//
|
|
35
|
-
...(opts.inboundInstruction ? { inboundInstruction: opts.inboundInstruction } : {}),
|
|
36
|
-
});
|
|
37
|
-
if (!res.ok) {
|
|
38
|
-
if (opts.json)
|
|
39
|
-
console.log(JSON.stringify({ ok: false, code: "verify_failed", status: res.status, error: res.error }));
|
|
40
|
-
else
|
|
41
|
-
console.error(`onboard failed: ${res.error}`);
|
|
42
|
-
return res.status === 401 ? 1 : 2;
|
|
43
|
-
}
|
|
44
|
-
const apiKey = res.data.apiKey ?? null;
|
|
45
|
-
if (!apiKey || !res.data.accountId) {
|
|
32
|
+
if (e.code === "verify_failed") {
|
|
33
|
+
if (opts.json)
|
|
34
|
+
console.log(JSON.stringify({ ok: false, code: "verify_failed", status: e.status, error: e.message }));
|
|
35
|
+
else
|
|
36
|
+
console.error(`onboard failed: ${e.message}`);
|
|
37
|
+
return e.status === 401 ? 1 : 2;
|
|
38
|
+
}
|
|
39
|
+
// missing_api_key
|
|
46
40
|
if (opts.json)
|
|
47
|
-
console.log(JSON.stringify({ ok: false, code: "missing_api_key", error:
|
|
41
|
+
console.log(JSON.stringify({ ok: false, code: "missing_api_key", error: e.message }));
|
|
48
42
|
else
|
|
49
|
-
console.error(
|
|
43
|
+
console.error(`onboard failed: ${e.message}`);
|
|
50
44
|
return 2;
|
|
51
45
|
}
|
|
52
|
-
|
|
53
|
-
apiKey,
|
|
54
|
-
accountId: res.data.accountId,
|
|
55
|
-
email: email ?? "",
|
|
56
|
-
phoneNumber: res.data.phoneNumber ?? null,
|
|
57
|
-
phoneNumberId: res.data.phoneNumberId ?? null,
|
|
58
|
-
});
|
|
59
|
-
clearPendingSignup();
|
|
60
|
-
const authFile = paths().authFile;
|
|
46
|
+
const { apiKey, accountId, phoneNumber, phoneNumberId, apiKeyPath, skills, supervisor } = result;
|
|
61
47
|
const masked = maskApiKey(apiKey);
|
|
62
|
-
const skillResults = [];
|
|
63
|
-
for (const requested of opts.agents ?? []) {
|
|
64
|
-
if (!isSupportedAgent(requested)) {
|
|
65
|
-
skillResults.push({
|
|
66
|
-
agent: requested,
|
|
67
|
-
error: `unknown agent "${requested}". Supported: ${SUPPORTED_AGENTS.join(", ")}.`,
|
|
68
|
-
});
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
try {
|
|
72
|
-
skillResults.push(installSkill(requested));
|
|
73
|
-
}
|
|
74
|
-
catch (err) {
|
|
75
|
-
skillResults.push({ agent: requested, error: err instanceof Error ? err.message : String(err) });
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
const supervisor = supervisorAvailability();
|
|
79
48
|
if (opts.json) {
|
|
80
49
|
console.log(JSON.stringify({
|
|
81
50
|
ok: true,
|
|
82
51
|
apiKeyFingerprint: apiKey.slice(-4),
|
|
83
52
|
apiKeyMasked: masked,
|
|
84
|
-
apiKeyPath
|
|
85
|
-
accountId
|
|
86
|
-
phoneNumber
|
|
87
|
-
phoneNumberId
|
|
53
|
+
apiKeyPath,
|
|
54
|
+
accountId,
|
|
55
|
+
phoneNumber,
|
|
56
|
+
phoneNumberId,
|
|
88
57
|
listen: {
|
|
89
58
|
installed: false,
|
|
90
59
|
autoInstalled: false,
|
|
91
60
|
canInstall: supervisor.available,
|
|
92
61
|
unavailableReason: supervisor.available ? null : supervisor.reason,
|
|
93
62
|
},
|
|
94
|
-
skills
|
|
63
|
+
skills,
|
|
95
64
|
agentHint: supervisor.available
|
|
96
65
|
? {
|
|
97
66
|
action: "ask_user",
|
|
@@ -114,10 +83,10 @@ export async function runOnboard(opts) {
|
|
|
114
83
|
}
|
|
115
84
|
else {
|
|
116
85
|
console.log("onboarded.");
|
|
117
|
-
console.log(` api key: ${masked} (saved to ${
|
|
118
|
-
if (
|
|
119
|
-
console.log(` phone number: ${
|
|
120
|
-
for (const r of
|
|
86
|
+
console.log(` api key: ${masked} (saved to ${apiKeyPath})`);
|
|
87
|
+
if (phoneNumber)
|
|
88
|
+
console.log(` phone number: ${phoneNumber}`);
|
|
89
|
+
for (const r of skills) {
|
|
121
90
|
if ("error" in r) {
|
|
122
91
|
console.log(` skill (${r.agent}): failed — ${r.error}`);
|
|
123
92
|
}
|
package/dist/commands/signup.js
CHANGED
|
@@ -1,36 +1,35 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
const PENDING_FRESH_MS = 10 * 60 * 1000;
|
|
1
|
+
import { signup } from "../lib/ops/account.js";
|
|
2
|
+
import { isDialError } from "../lib/ops/errors.js";
|
|
4
3
|
export async function runSignup(email, opts) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
try {
|
|
5
|
+
const { verificationId } = await signup({ email, force: opts.force });
|
|
6
|
+
if (opts.json) {
|
|
7
|
+
console.log(JSON.stringify({ ok: true, verificationId, email }));
|
|
8
|
+
}
|
|
9
|
+
else {
|
|
10
|
+
console.log(`OTP sent to ${email}.`);
|
|
11
|
+
console.log(`Run \`dial onboard --code <code>\` once you have it (verificationId is stored locally).`);
|
|
12
|
+
}
|
|
13
|
+
return 0;
|
|
14
|
+
}
|
|
15
|
+
catch (e) {
|
|
16
|
+
if (!isDialError(e))
|
|
17
|
+
throw e;
|
|
18
|
+
if (e.code === "pending_exists") {
|
|
19
|
+
const d = e.data ?? {};
|
|
10
20
|
if (opts.json) {
|
|
11
|
-
console.log(JSON.stringify({ ok: false, code: "pending_exists", verificationId:
|
|
21
|
+
console.log(JSON.stringify({ ok: false, code: "pending_exists", verificationId: d.verificationId, email: d.email, ageSeconds: d.ageSeconds }));
|
|
12
22
|
}
|
|
13
23
|
else {
|
|
14
|
-
console.error(
|
|
24
|
+
console.error(e.message);
|
|
15
25
|
}
|
|
16
26
|
return 3;
|
|
17
27
|
}
|
|
18
|
-
|
|
19
|
-
const res = await apiPost("/api/v1/auth/signup", { email });
|
|
20
|
-
if (!res.ok) {
|
|
28
|
+
// signup_failed
|
|
21
29
|
if (opts.json)
|
|
22
|
-
console.log(JSON.stringify({ ok: false, code:
|
|
30
|
+
console.log(JSON.stringify({ ok: false, code: e.code, status: e.status, error: e.message }));
|
|
23
31
|
else
|
|
24
|
-
console.error(`signup failed: ${
|
|
32
|
+
console.error(`signup failed: ${e.message}`);
|
|
25
33
|
return 2;
|
|
26
34
|
}
|
|
27
|
-
writePendingSignup({ verificationId: res.data.verificationId, email, createdAt: new Date().toISOString() });
|
|
28
|
-
if (opts.json) {
|
|
29
|
-
console.log(JSON.stringify({ ok: true, verificationId: res.data.verificationId, email }));
|
|
30
|
-
}
|
|
31
|
-
else {
|
|
32
|
-
console.log(`OTP sent to ${email}.`);
|
|
33
|
-
console.log(`Run \`dial onboard --code <code>\` once you have it (verificationId is stored locally).`);
|
|
34
|
-
}
|
|
35
|
-
return 0;
|
|
36
35
|
}
|
|
@@ -1,83 +1,49 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { supervisorStatus } from "../lib/supervisor/index.js";
|
|
4
|
-
import { parseFieldArg, parseRegexArg } from "../lib/event-filter.js";
|
|
5
|
-
import { currentSize, findLatestMatch, tailUntilMatch } from "../lib/log-tail.js";
|
|
6
|
-
import { apiPost } from "../lib/api.js";
|
|
7
|
-
const PER_POLL_SECONDS = 30;
|
|
1
|
+
import { waitForEvent } from "../lib/ops/events.js";
|
|
2
|
+
import { isDialError } from "../lib/ops/errors.js";
|
|
8
3
|
export async function runWaitFor(opts) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
4
|
+
let r;
|
|
5
|
+
try {
|
|
6
|
+
r = await waitForEvent({
|
|
7
|
+
eventType: opts.eventType,
|
|
8
|
+
fields: opts.fields,
|
|
9
|
+
regexes: opts.regexes,
|
|
10
|
+
timeoutSeconds: opts.timeoutSeconds,
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
catch (e) {
|
|
14
|
+
if (!isDialError(e))
|
|
15
|
+
throw e;
|
|
16
|
+
// not_signed_in (no auth on the API path) or api_fallback_failed
|
|
17
|
+
fail(opts.json, e.code, e.message, e.status ? { status: e.status } : undefined);
|
|
18
|
+
return e.code === "api_fallback_failed" ? 4 : 1;
|
|
19
|
+
}
|
|
20
|
+
// Hit: print the raw line and succeed.
|
|
21
|
+
if (!r.timedOut && r.line != null) {
|
|
22
|
+
process.stdout.write(r.line + "\n");
|
|
26
23
|
return 0;
|
|
27
24
|
}
|
|
28
|
-
|
|
29
|
-
if (
|
|
25
|
+
// Timed out tailing the log, but there was an earlier matching entry.
|
|
26
|
+
if (r.source === "log" && r.line != null) {
|
|
30
27
|
if (opts.json) {
|
|
31
|
-
console.log(JSON.stringify({ ok: false, timeout: true, source: "log", event:
|
|
28
|
+
console.log(JSON.stringify({ ok: false, timeout: true, source: "log", event: r.event }));
|
|
32
29
|
}
|
|
33
30
|
else {
|
|
34
31
|
console.error(`timed out after ${opts.timeoutSeconds}s; latest matching entry in log:`);
|
|
35
|
-
process.stdout.write(
|
|
32
|
+
process.stdout.write(r.line + "\n");
|
|
36
33
|
}
|
|
37
34
|
return 1;
|
|
38
35
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
console.error(`timed out after ${opts.timeoutSeconds}s; no matching ${opts.eventType} entry in log.`);
|
|
44
|
-
}
|
|
45
|
-
return 2;
|
|
46
|
-
}
|
|
47
|
-
async function waitFromApi(spec, opts) {
|
|
48
|
-
const auth = readAuth();
|
|
49
|
-
if (!auth) {
|
|
50
|
-
fail(opts.json, "not_signed_in", "Not signed in. Run `dial signup` and `dial onboard` first.");
|
|
51
|
-
return 1;
|
|
52
|
-
}
|
|
53
|
-
const filters = {};
|
|
54
|
-
for (const f of spec.fields)
|
|
55
|
-
filters[f.name] = f.value;
|
|
56
|
-
const regexFilters = {};
|
|
57
|
-
for (const r of spec.regexes)
|
|
58
|
-
regexFilters[r.name] = { pattern: r.regex.source, flags: r.regex.flags };
|
|
59
|
-
const deadline = Date.now() + opts.timeoutSeconds * 1000;
|
|
60
|
-
while (Date.now() < deadline) {
|
|
61
|
-
const remainingSec = Math.max(1, Math.ceil((deadline - Date.now()) / 1000));
|
|
62
|
-
const timeout = Math.min(PER_POLL_SECONDS, remainingSec);
|
|
63
|
-
const res = await apiPost("/api/v1/events/wait", {
|
|
64
|
-
eventType: spec.eventType,
|
|
65
|
-
filters: Object.keys(filters).length > 0 ? filters : undefined,
|
|
66
|
-
regexFilters: Object.keys(regexFilters).length > 0 ? regexFilters : undefined,
|
|
67
|
-
timeout,
|
|
68
|
-
}, auth.apiKey);
|
|
69
|
-
if (res.ok && res.data?.event) {
|
|
70
|
-
process.stdout.write(JSON.stringify(res.data.event) + "\n");
|
|
71
|
-
return 0;
|
|
72
|
-
}
|
|
73
|
-
if (res.ok === false && res.status === 408) {
|
|
74
|
-
continue;
|
|
36
|
+
// Timed out tailing the log with no prior match at all.
|
|
37
|
+
if (r.source === "log") {
|
|
38
|
+
if (opts.json) {
|
|
39
|
+
console.log(JSON.stringify({ ok: false, timeout: true, source: null, event: null }));
|
|
75
40
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
return 4;
|
|
41
|
+
else {
|
|
42
|
+
console.error(`timed out after ${opts.timeoutSeconds}s; no matching ${opts.eventType} entry in log.`);
|
|
79
43
|
}
|
|
44
|
+
return 2;
|
|
80
45
|
}
|
|
46
|
+
// Timed out on the API fallback.
|
|
81
47
|
if (opts.json) {
|
|
82
48
|
console.log(JSON.stringify({ ok: false, timeout: true, source: "api", event: null }));
|
|
83
49
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Codes that map to exit 1 (caller/precondition problems); everything else is 2
|
|
2
|
+
// (operation failure). Matches the exit codes the commands used before the ops refactor.
|
|
3
|
+
const EXIT_1_CODES = new Set([
|
|
4
|
+
"not_signed_in",
|
|
5
|
+
"no_from_number",
|
|
6
|
+
"number_not_found",
|
|
7
|
+
"not_found",
|
|
8
|
+
"no_pending_signup",
|
|
9
|
+
]);
|
|
10
|
+
/**
|
|
11
|
+
* Print a DialError the way the generic REST commands always have — `--json` emits
|
|
12
|
+
* `{ok:false, code, message, status?}`, human mode prints the message to stderr — and
|
|
13
|
+
* return the matching exit code. Commands with bespoke error JSON (signup, onboard,
|
|
14
|
+
* wait-for) handle their own printing instead.
|
|
15
|
+
*/
|
|
16
|
+
export function printDialError(json, e) {
|
|
17
|
+
if (json) {
|
|
18
|
+
console.log(JSON.stringify({ ok: false, code: e.code, message: e.message, ...(e.status ? { status: e.status } : {}) }));
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
console.error(e.message);
|
|
22
|
+
}
|
|
23
|
+
return EXIT_1_CODES.has(e.code) ? 1 : 2;
|
|
24
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { readAuth, readPendingSignup, writePendingSignup, clearPendingSignup, writeAuth } from "../state.js";
|
|
2
|
+
import { apiGet, apiPost, baseUrl, pingBackend } from "../api.js";
|
|
3
|
+
import { supervisorStatus, lastEventAtFromLog, supervisorAvailability } from "../supervisor/index.js";
|
|
4
|
+
import { paths } from "../paths.js";
|
|
5
|
+
import { VERSION } from "../version.js";
|
|
6
|
+
import { installSkill, isSupportedAgent, SUPPORTED_AGENTS } from "../skill-install.js";
|
|
7
|
+
import { DialError } from "./errors.js";
|
|
8
|
+
const OTP_EXPIRY_MS = 10 * 60 * 1000;
|
|
9
|
+
const PENDING_FRESH_MS = 10 * 60 * 1000;
|
|
10
|
+
export async function accountStatus() {
|
|
11
|
+
const ping = await pingBackend();
|
|
12
|
+
const auth = readAuth();
|
|
13
|
+
const pending = readPendingSignup();
|
|
14
|
+
let keyValid = null;
|
|
15
|
+
if (auth?.apiKey) {
|
|
16
|
+
const res = await apiGet("/api/v1/account", auth.apiKey);
|
|
17
|
+
keyValid = res.ok;
|
|
18
|
+
}
|
|
19
|
+
const pendingAgeMs = pending ? Date.now() - Date.parse(pending.createdAt) : null;
|
|
20
|
+
const pendingExpired = pendingAgeMs == null ? null : pendingAgeMs > OTP_EXPIRY_MS;
|
|
21
|
+
let listenState = { installed: false, running: false, lastEventAt: null };
|
|
22
|
+
try {
|
|
23
|
+
const s = supervisorStatus();
|
|
24
|
+
listenState = { installed: s.installed, running: s.running, lastEventAt: lastEventAtFromLog(paths().listenLog) };
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// unsupported platform — leave defaults
|
|
28
|
+
}
|
|
29
|
+
let nextStep;
|
|
30
|
+
if (!auth) {
|
|
31
|
+
if (pending && pendingExpired === false)
|
|
32
|
+
nextStep = "onboard";
|
|
33
|
+
else if (pending && pendingExpired)
|
|
34
|
+
nextStep = "resend_otp";
|
|
35
|
+
else
|
|
36
|
+
nextStep = "signup";
|
|
37
|
+
}
|
|
38
|
+
else if (keyValid === false) {
|
|
39
|
+
nextStep = "signup";
|
|
40
|
+
}
|
|
41
|
+
else if (!listenState.installed || !listenState.running) {
|
|
42
|
+
nextStep = "install_listen";
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
nextStep = "ready";
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
cli: { version: VERSION, node: process.versions.node },
|
|
49
|
+
backend: { url: baseUrl(), reachable: ping.reachable, latencyMs: ping.latencyMs },
|
|
50
|
+
auth: {
|
|
51
|
+
signedIn: Boolean(auth),
|
|
52
|
+
email: auth?.email ?? null,
|
|
53
|
+
accountId: auth?.accountId ?? null,
|
|
54
|
+
apiKeyPresent: Boolean(auth?.apiKey),
|
|
55
|
+
apiKeyFingerprint: auth?.apiKey ? auth.apiKey.slice(-4) : null,
|
|
56
|
+
keyValid,
|
|
57
|
+
},
|
|
58
|
+
pendingOtp: {
|
|
59
|
+
verificationId: pending?.verificationId ?? null,
|
|
60
|
+
ageSeconds: pendingAgeMs == null ? null : Math.round(pendingAgeMs / 1000),
|
|
61
|
+
expired: pendingExpired,
|
|
62
|
+
},
|
|
63
|
+
listen: listenState,
|
|
64
|
+
nextStep,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
// ---- signup ----------------------------------------------------------------
|
|
68
|
+
export async function signup(opts) {
|
|
69
|
+
const existing = readPendingSignup();
|
|
70
|
+
if (existing && !opts.force) {
|
|
71
|
+
const age = Date.now() - Date.parse(existing.createdAt);
|
|
72
|
+
if (Number.isFinite(age) && age < PENDING_FRESH_MS) {
|
|
73
|
+
const ageSeconds = Math.round(age / 1000);
|
|
74
|
+
throw new DialError("pending_exists", `A pending OTP for ${existing.email} is still fresh (${ageSeconds}s old). Use \`dial onboard --code <code>\` or re-run with --force to start a new one.`, undefined, { verificationId: existing.verificationId, email: existing.email, ageSeconds });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const res = await apiPost("/api/v1/auth/signup", { email: opts.email });
|
|
78
|
+
if (!res.ok)
|
|
79
|
+
throw new DialError("signup_failed", res.error, res.status);
|
|
80
|
+
writePendingSignup({ verificationId: res.data.verificationId, email: opts.email, createdAt: new Date().toISOString() });
|
|
81
|
+
return { verificationId: res.data.verificationId, email: opts.email };
|
|
82
|
+
}
|
|
83
|
+
export async function onboard(opts) {
|
|
84
|
+
let verificationId = opts.verificationId;
|
|
85
|
+
let email = null;
|
|
86
|
+
if (!verificationId) {
|
|
87
|
+
const pending = readPendingSignup();
|
|
88
|
+
if (!pending) {
|
|
89
|
+
throw new DialError("no_pending_signup", "No pending signup. Run `dial signup <email>` first, or pass --verification-id.");
|
|
90
|
+
}
|
|
91
|
+
verificationId = pending.verificationId;
|
|
92
|
+
email = pending.email;
|
|
93
|
+
}
|
|
94
|
+
const res = await apiPost("/api/v1/auth/verify", {
|
|
95
|
+
verificationId,
|
|
96
|
+
code: opts.code,
|
|
97
|
+
...(opts.inboundInstruction ? { inboundInstruction: opts.inboundInstruction } : {}),
|
|
98
|
+
});
|
|
99
|
+
if (!res.ok)
|
|
100
|
+
throw new DialError("verify_failed", res.error, res.status);
|
|
101
|
+
const apiKey = res.data.apiKey ?? null;
|
|
102
|
+
if (!apiKey || !res.data.accountId) {
|
|
103
|
+
throw new DialError("missing_api_key", "backend returned no apiKey");
|
|
104
|
+
}
|
|
105
|
+
writeAuth({
|
|
106
|
+
apiKey,
|
|
107
|
+
accountId: res.data.accountId,
|
|
108
|
+
email: email ?? "",
|
|
109
|
+
phoneNumber: res.data.phoneNumber ?? null,
|
|
110
|
+
phoneNumberId: res.data.phoneNumberId ?? null,
|
|
111
|
+
});
|
|
112
|
+
clearPendingSignup();
|
|
113
|
+
const skills = [];
|
|
114
|
+
for (const requested of opts.agents ?? []) {
|
|
115
|
+
if (!isSupportedAgent(requested)) {
|
|
116
|
+
skills.push({ agent: requested, error: `unknown agent "${requested}". Supported: ${SUPPORTED_AGENTS.join(", ")}.` });
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
skills.push(installSkill(requested));
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
skills.push({ agent: requested, error: err instanceof Error ? err.message : String(err) });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
apiKey,
|
|
128
|
+
apiKeyFingerprint: apiKey.slice(-4),
|
|
129
|
+
apiKeyPath: paths().authFile,
|
|
130
|
+
accountId: res.data.accountId,
|
|
131
|
+
phoneNumber: res.data.phoneNumber ?? null,
|
|
132
|
+
phoneNumberId: res.data.phoneNumberId ?? null,
|
|
133
|
+
skills,
|
|
134
|
+
supervisor: supervisorAvailability(),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { readAuth } from "../state.js";
|
|
2
|
+
import { DialError } from "./errors.js";
|
|
3
|
+
/** Resolve the saved auth or throw a `not_signed_in` DialError. */
|
|
4
|
+
export function requireAuth() {
|
|
5
|
+
const auth = readAuth();
|
|
6
|
+
if (!auth) {
|
|
7
|
+
throw new DialError("not_signed_in", "Not signed in. Run `dial signup` and `dial onboard` first.");
|
|
8
|
+
}
|
|
9
|
+
return auth;
|
|
10
|
+
}
|
|
11
|
+
/** Resolve the from-number id: explicit override, else the account default, else throw. */
|
|
12
|
+
export function requireFromNumberId(auth, override) {
|
|
13
|
+
const id = override ?? auth.phoneNumberId;
|
|
14
|
+
if (!id) {
|
|
15
|
+
throw new DialError("no_from_number", "No default phoneNumberId in auth. Pass --from-number-id <id>.");
|
|
16
|
+
}
|
|
17
|
+
return id;
|
|
18
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { apiGet, apiPost } from "../api.js";
|
|
2
|
+
import { requireAuth, requireFromNumberId } from "./auth.js";
|
|
3
|
+
import { DialError } from "./errors.js";
|
|
4
|
+
export async function placeCall(opts) {
|
|
5
|
+
const auth = requireAuth();
|
|
6
|
+
const fromNumberId = requireFromNumberId(auth, opts.fromNumberId);
|
|
7
|
+
const res = await apiPost("/api/v1/calls", { to: opts.to, fromNumberId, outboundInstruction: opts.outboundInstruction, language: opts.language }, auth.apiKey);
|
|
8
|
+
if (!res.ok)
|
|
9
|
+
throw new DialError("call_failed", res.error, res.status);
|
|
10
|
+
return res.data.call;
|
|
11
|
+
}
|
|
12
|
+
export async function listCalls(opts) {
|
|
13
|
+
const auth = requireAuth();
|
|
14
|
+
const params = new URLSearchParams();
|
|
15
|
+
if (opts.numberId)
|
|
16
|
+
params.set("numberId", opts.numberId);
|
|
17
|
+
if (opts.direction)
|
|
18
|
+
params.set("direction", opts.direction);
|
|
19
|
+
if (opts.since)
|
|
20
|
+
params.set("since", opts.since);
|
|
21
|
+
const qs = params.toString();
|
|
22
|
+
const res = await apiGet(qs ? `/api/v1/calls?${qs}` : "/api/v1/calls", auth.apiKey);
|
|
23
|
+
if (!res.ok)
|
|
24
|
+
throw new DialError("list_failed", res.error, res.status);
|
|
25
|
+
return res.data.calls ?? [];
|
|
26
|
+
}
|
|
27
|
+
export async function getCall(callId) {
|
|
28
|
+
const auth = requireAuth();
|
|
29
|
+
const res = await apiGet(`/api/v1/calls/${encodeURIComponent(callId)}`, auth.apiKey);
|
|
30
|
+
if (!res.ok)
|
|
31
|
+
throw new DialError(res.status === 404 ? "not_found" : "get_failed", res.error, res.status);
|
|
32
|
+
return res.data.call;
|
|
33
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed error thrown by ops. Carries a stable machine `code` (reused from the
|
|
3
|
+
* existing CLI command error codes), a human message, and an optional HTTP status.
|
|
4
|
+
* Commands map it to their `--json`/exit-code output; MCP tools map it to an error
|
|
5
|
+
* tool result. Mirrors the server-side `ServiceError`.
|
|
6
|
+
*/
|
|
7
|
+
export class DialError extends Error {
|
|
8
|
+
code;
|
|
9
|
+
status;
|
|
10
|
+
data;
|
|
11
|
+
constructor(code, message, status,
|
|
12
|
+
/** Extra structured context for commands with bespoke error output (e.g. signup's pending_exists). */
|
|
13
|
+
data) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.code = code;
|
|
16
|
+
this.status = status;
|
|
17
|
+
this.data = data;
|
|
18
|
+
this.name = "DialError";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function isDialError(e) {
|
|
22
|
+
return e instanceof DialError;
|
|
23
|
+
}
|