@getdial/cli 0.34.1 → 0.35.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/commands/onboard.js +11 -1
- package/dist/lib/dashboard.js +38 -0
- package/dist/lib/ops/account.js +3 -0
- package/dist/lib/ref-params.js +48 -10
- package/dist/mcp/tools/onboard.js +13 -0
- package/package.json +1 -1
- package/skills.tar.gz +0 -0
package/dist/commands/onboard.js
CHANGED
|
@@ -3,6 +3,8 @@ import { isDialError } from "../lib/ops/errors.js";
|
|
|
3
3
|
import { readAuth, authFilePath } from "../lib/state.js";
|
|
4
4
|
import { installSkill, isSupportedAgent, SUPPORTED_AGENTS, } from "../lib/skill-install.js";
|
|
5
5
|
import { supervisorAvailability } from "../lib/supervisor/index.js";
|
|
6
|
+
import { baseUrl } from "../lib/api.js";
|
|
7
|
+
import { dashboardHint, dashboardUrl } from "../lib/dashboard.js";
|
|
6
8
|
function maskApiKey(key) {
|
|
7
9
|
return key.length >= 4 ? `sk_live_***${key.slice(-4)}` : "sk_live_***";
|
|
8
10
|
}
|
|
@@ -60,8 +62,10 @@ export async function runOnboard(opts) {
|
|
|
60
62
|
apiKeyMasked: maskApiKey(auth.apiKey),
|
|
61
63
|
apiKeyPath: authFilePath(),
|
|
62
64
|
accountId: auth.accountId,
|
|
65
|
+
email: auth.email || null,
|
|
63
66
|
phoneNumber: auth.phoneNumber ?? null,
|
|
64
67
|
phoneNumberId: auth.phoneNumberId ?? null,
|
|
68
|
+
dashboardUrl: dashboardUrl(baseUrl()),
|
|
65
69
|
listen: {
|
|
66
70
|
installed: false,
|
|
67
71
|
autoInstalled: false,
|
|
@@ -87,6 +91,7 @@ export async function runOnboard(opts) {
|
|
|
87
91
|
else if (r.unchanged)
|
|
88
92
|
console.log(` skill (${r.agent}): already up to date → ${r.path}`);
|
|
89
93
|
}
|
|
94
|
+
console.log(dashboardHint(dashboardUrl(baseUrl()), auth.email || null));
|
|
90
95
|
}
|
|
91
96
|
return 0;
|
|
92
97
|
}
|
|
@@ -123,7 +128,7 @@ export async function runOnboard(opts) {
|
|
|
123
128
|
console.error(`onboard failed: ${e.message}`);
|
|
124
129
|
return 2;
|
|
125
130
|
}
|
|
126
|
-
const { apiKey, accountId, phoneNumber, phoneNumberId, apiKeyPath, skills, supervisor } = result;
|
|
131
|
+
const { apiKey, accountId, email, phoneNumber, phoneNumberId, apiKeyPath, skills, supervisor } = result;
|
|
127
132
|
const masked = maskApiKey(apiKey);
|
|
128
133
|
if (opts.json) {
|
|
129
134
|
console.log(JSON.stringify({
|
|
@@ -132,8 +137,10 @@ export async function runOnboard(opts) {
|
|
|
132
137
|
apiKeyMasked: masked,
|
|
133
138
|
apiKeyPath,
|
|
134
139
|
accountId,
|
|
140
|
+
email,
|
|
135
141
|
phoneNumber,
|
|
136
142
|
phoneNumberId,
|
|
143
|
+
dashboardUrl: result.dashboardUrl,
|
|
137
144
|
listen: {
|
|
138
145
|
installed: false,
|
|
139
146
|
autoInstalled: false,
|
|
@@ -177,6 +184,9 @@ export async function runOnboard(opts) {
|
|
|
177
184
|
console.log(` skill (${r.agent}): already up to date → ${r.path}`);
|
|
178
185
|
}
|
|
179
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));
|
|
180
190
|
console.log(``);
|
|
181
191
|
if (!supervisor.available) {
|
|
182
192
|
console.log(`listen service: not available on this machine (${supervisor.reason}).`);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Where a user manages the account in a browser, derived from whichever API base
|
|
2
|
+
// the CLI is pointed at — so a dev pointing DIAL_API_URL at localhost gets the
|
|
3
|
+
// local dashboard, not production's.
|
|
4
|
+
//
|
|
5
|
+
// Deliberately import-free: api.ts installs a global undici dispatcher at import
|
|
6
|
+
// time, and this is pure string work that shouldn't drag that into its own test.
|
|
7
|
+
// Callers pass `baseUrl()` in.
|
|
8
|
+
/**
|
|
9
|
+
* The dashboard URL for an API base. The web app lives on the same host as the API
|
|
10
|
+
* minus its `api.` label (`api.getdial.ai` → `getdial.ai`); a base without that
|
|
11
|
+
* label — staging, or a localhost dev server serving both — is used as-is.
|
|
12
|
+
*/
|
|
13
|
+
export function dashboardUrl(apiBase) {
|
|
14
|
+
const url = new URL(apiBase);
|
|
15
|
+
// Strip only a leading `api.` LABEL. A substring check would turn a host like
|
|
16
|
+
// `apiary.example.com` into `ary.example.com`.
|
|
17
|
+
if (url.hostname.startsWith("api."))
|
|
18
|
+
url.hostname = url.hostname.slice("api.".length);
|
|
19
|
+
// `pathname` is "/" for a bare origin and keeps a trailing slash when one was
|
|
20
|
+
// given, so join on a trimmed copy rather than concatenating blindly.
|
|
21
|
+
url.pathname = `${url.pathname.replace(/\/+$/, "")}/dashboard`;
|
|
22
|
+
return url.toString();
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The closing line of `dial onboard`: where to manage the account, and which
|
|
26
|
+
* address signs in there.
|
|
27
|
+
*
|
|
28
|
+
* The address matters more than it looks. Sign-in is an emailed code, and when an
|
|
29
|
+
* agent onboarded on the user's behalf the user usually doesn't know which address
|
|
30
|
+
* it used — that's the single biggest reason they never reach the dashboard. Omitted
|
|
31
|
+
* when the CLI genuinely doesn't know it (an explicit `--verification-id` with no
|
|
32
|
+
* pending signup), since offering to sign in as nobody is worse than saying nothing.
|
|
33
|
+
*/
|
|
34
|
+
export function dashboardHint(url, email) {
|
|
35
|
+
return email
|
|
36
|
+
? `manage your account: ${url} (sign in with ${email} — it emails you a code)`
|
|
37
|
+
: `manage your account: ${url} (sign in with the email this account was created under)`;
|
|
38
|
+
}
|
package/dist/lib/ops/account.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readAuth, readPendingSignup, writePendingSignup, clearPendingSignup, writeAuth, authFilePath, } from "../state.js";
|
|
2
2
|
import { apiGet, apiPost, baseUrl, pingBackend } from "../api.js";
|
|
3
|
+
import { dashboardUrl } from "../dashboard.js";
|
|
3
4
|
import { supervisorStatus, lastEventAtFromLog, supervisorAvailability, } from "../supervisor/index.js";
|
|
4
5
|
import { paths } from "../paths.js";
|
|
5
6
|
import { VERSION } from "../version.js";
|
|
@@ -167,8 +168,10 @@ export async function onboard(opts) {
|
|
|
167
168
|
apiKeyFingerprint: apiKey.slice(-4),
|
|
168
169
|
apiKeyPath: authFilePath(),
|
|
169
170
|
accountId: res.data.accountId,
|
|
171
|
+
email,
|
|
170
172
|
phoneNumber: res.data.phoneNumber ?? null,
|
|
171
173
|
phoneNumberId: res.data.phoneNumberId ?? null,
|
|
174
|
+
dashboardUrl: dashboardUrl(baseUrl()),
|
|
172
175
|
skills,
|
|
173
176
|
supervisor: supervisorAvailability(),
|
|
174
177
|
};
|
package/dist/lib/ref-params.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { paths } from "./paths.js";
|
|
4
5
|
import { logger } from "./log.js";
|
|
@@ -7,23 +8,60 @@ import { logger } from "./log.js";
|
|
|
7
8
|
// X-Dial-Ref-Params header. The CLI forwards the file verbatim — no parsing, no
|
|
8
9
|
// allowlist here; the server decodes + validates. Cached per process (the file is
|
|
9
10
|
// write-once and stable for the CLI's lifetime).
|
|
11
|
+
//
|
|
12
|
+
// If the file has no dial_attribution_id, one is minted and appended. That covers
|
|
13
|
+
// every install path — `curl … | bash` writes an id itself, but `npm install -g`,
|
|
14
|
+
// Homebrew, a prebaked image, or an agent installing the CLI do not, and those
|
|
15
|
+
// machines had no attribution spine at all until their eventual signup. Doing it
|
|
16
|
+
// here rather than in an installer means there is exactly one code path to cover,
|
|
17
|
+
// since every API request already passes through this function.
|
|
18
|
+
const ATTRIBUTION_KEY = "dial_attribution_id";
|
|
10
19
|
let cache;
|
|
20
|
+
/** True when the file already carries an attribution id line. */
|
|
21
|
+
function hasAttributionId(text) {
|
|
22
|
+
return text.split("\n").some((l) => l.trim().startsWith(`${ATTRIBUTION_KEY}=`));
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Append an attribution id to `text` and persist it. Write-once per key, mirroring
|
|
26
|
+
* the installer's add_ref: a real browser-originated id is never overwritten (the
|
|
27
|
+
* caller checks first).
|
|
28
|
+
*
|
|
29
|
+
* A write failure is warned and swallowed — the id is still returned so a signup in
|
|
30
|
+
* this same process aliases correctly. An unwritable dataDir is already-broken
|
|
31
|
+
* territory the CLI surfaces elsewhere.
|
|
32
|
+
*/
|
|
33
|
+
function mintAttributionId(file, text) {
|
|
34
|
+
const separator = text.length > 0 && !text.endsWith("\n") ? "\n" : "";
|
|
35
|
+
const next = `${text}${separator}${ATTRIBUTION_KEY}=${randomUUID()}\n`;
|
|
36
|
+
try {
|
|
37
|
+
mkdirSync(paths().dataDir, { recursive: true });
|
|
38
|
+
writeFileSync(file, next, "utf8");
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
logger.warn({ err }, "couldn't persist the attribution id; using it for this process only");
|
|
42
|
+
}
|
|
43
|
+
return next;
|
|
44
|
+
}
|
|
11
45
|
function compute() {
|
|
12
46
|
const file = join(paths().dataDir, "ref-params.txt");
|
|
47
|
+
let text;
|
|
13
48
|
try {
|
|
14
|
-
|
|
15
|
-
if (!text.trim())
|
|
16
|
-
return null;
|
|
17
|
-
return Buffer.from(text, "utf8").toString("base64");
|
|
49
|
+
text = readFileSync(file, "utf8");
|
|
18
50
|
}
|
|
19
51
|
catch (err) {
|
|
20
52
|
// No file is the normal case (the user never went through an attributed
|
|
21
|
-
// install) — not an error worth logging. Anything else is unexpected
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
53
|
+
// install) — not an error worth logging. Anything else is unexpected, but
|
|
54
|
+
// still recoverable: treat it as empty and mint below.
|
|
55
|
+
if (err?.code !== "ENOENT") {
|
|
56
|
+
logger.warn({ err }, "failed to read ref-params.txt");
|
|
57
|
+
}
|
|
58
|
+
text = "";
|
|
26
59
|
}
|
|
60
|
+
if (!hasAttributionId(text))
|
|
61
|
+
text = mintAttributionId(file, text);
|
|
62
|
+
if (!text.trim())
|
|
63
|
+
return null;
|
|
64
|
+
return Buffer.from(text, "utf8").toString("base64");
|
|
27
65
|
}
|
|
28
66
|
/** Base64 of ref-params.txt for the X-Dial-Ref-Params header, or null if absent. */
|
|
29
67
|
export function refParamsHeader() {
|
|
@@ -4,6 +4,8 @@ import { onboard } 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";
|
|
7
|
+
import { baseUrl } from "../../lib/api.js";
|
|
8
|
+
import { dashboardUrl } from "../../lib/dashboard.js";
|
|
7
9
|
const inputSchema = {
|
|
8
10
|
code: z
|
|
9
11
|
.string()
|
|
@@ -34,8 +36,15 @@ export const onboardTool = {
|
|
|
34
36
|
apiKeyFingerprint: z.string().describe("Last 4 chars of the saved API key"),
|
|
35
37
|
apiKeyPath: z.string().describe("Where the key was saved"),
|
|
36
38
|
accountId: z.string(),
|
|
39
|
+
email: z
|
|
40
|
+
.string()
|
|
41
|
+
.nullable()
|
|
42
|
+
.describe("Address that signs in to the dashboard (a code is emailed to it). Null when the CLI never saw the signup — an explicit verificationId. Pass it on to the user: after an agent onboards for them, they usually don't know which address was used."),
|
|
37
43
|
phoneNumber: z.string().nullable(),
|
|
38
44
|
phoneNumberId: z.string().nullable(),
|
|
45
|
+
dashboardUrl: z
|
|
46
|
+
.string()
|
|
47
|
+
.describe("Where the user manages the account in a browser. Tell them about it once onboarding succeeds, then keep working from the CLI — it's only needed for paying, team sharing, and carrier (10DLC) registration."),
|
|
39
48
|
skills: z.array(z.object({}).passthrough()).describe("Per-agent skill install results"),
|
|
40
49
|
supervisor: z.object({}).passthrough().describe("Listen daemon availability on this machine"),
|
|
41
50
|
listenAvailable: z.boolean(),
|
|
@@ -75,8 +84,12 @@ export const onboardTool = {
|
|
|
75
84
|
apiKeyFingerprint: auth.apiKey.slice(-4),
|
|
76
85
|
apiKeyPath: authFilePath(),
|
|
77
86
|
accountId: auth.accountId,
|
|
87
|
+
// Built by hand rather than spread, so both fields have to be added here
|
|
88
|
+
// explicitly to match the full-onboard path below.
|
|
89
|
+
email: auth.email || null,
|
|
78
90
|
phoneNumber: auth.phoneNumber ?? null,
|
|
79
91
|
phoneNumberId: auth.phoneNumberId ?? null,
|
|
92
|
+
dashboardUrl: dashboardUrl(baseUrl()),
|
|
80
93
|
skills,
|
|
81
94
|
supervisor,
|
|
82
95
|
listenAvailable: supervisor.available,
|
package/package.json
CHANGED
package/skills.tar.gz
CHANGED
|
Binary file
|