@getdial/cli 0.34.2 → 0.36.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.
@@ -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}).`);
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": 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": USER_AGENT });
99
+ const headers = applyRefParamsHeader({ "user-agent": userAgent() });
103
100
  if (apiKey)
104
101
  headers.authorization = `Bearer ${apiKey}`;
105
102
  try {
@@ -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
+ }
@@ -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
  };
@@ -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
+ }
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getdial/cli",
3
- "version": "0.34.2",
3
+ "version": "0.36.0",
4
4
  "description": "Dial CLI — install, sign up, and run the local listen service.",
5
5
  "license": "MIT",
6
6
  "repository": {
package/skills.tar.gz CHANGED
Binary file