@getdial/cli 0.32.0 → 0.33.1

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 CHANGED
@@ -29,6 +29,11 @@ import { runMcp } from "./commands/mcp.js";
29
29
  import { runUpdate } from "./commands/update.js";
30
30
  import { runUninstall } from "./commands/uninstall.js";
31
31
  import { maybeAutoUpdate } from "./lib/update.js";
32
+ import { isSandbox, SANDBOX_DISABLED_COMMANDS, sandboxDisabledMessage } from "./lib/sandbox.js";
33
+ // Sandbox mode (ephemeral agent container behind the OneCLI proxy): hide and
34
+ // disable machine-lifecycle / onboarding commands, and let requests go out
35
+ // keyless so the proxy injects auth. Computed once (memoized in lib/sandbox).
36
+ const sandbox = isSandbox();
32
37
  const program = new Command();
33
38
  program
34
39
  .name("dial")
@@ -50,46 +55,50 @@ program
50
55
  .description("Show account billing: balance, plan, per-number mode, recent activity. GET /api/v1/billing.")
51
56
  .option("--json", "machine-readable output")
52
57
  .action(async (opts) => process.exit(await runBilling({ json: !!opts.json })));
53
- program
54
- .command("signup <email>")
55
- .description("Request an email OTP for the given address.")
56
- .option("--force", "overwrite any pending signup")
57
- .option("--json", "machine-readable output")
58
- .action(async (email, opts) => process.exit(await runSignup(email, { force: !!opts.force, json: !!opts.json })));
59
- program
60
- .command("onboard")
61
- .description("Verify the OTP and finish onboarding.")
62
- .option("--verification-id <id>", "explicit verification id (falls back to local pending signup)")
63
- .option("--code <code>", "6-digit OTP from your email (omit if already signed in — the command just installs the --agent skill and skips verification)")
64
- .option("--inbound-instruction <text>", "system prompt for inbound calls to your auto-provisioned number (required for a new account; ignored when signing in)")
65
- .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], [])
66
- .option("--json", "machine-readable output")
67
- .action(async (opts) => process.exit(await runOnboard({
68
- verificationId: opts.verificationId,
69
- code: opts.code,
70
- inboundInstruction: opts.inboundInstruction,
71
- agents: opts.agent,
72
- json: !!opts.json,
73
- })));
74
- const listen = program
75
- .command("listen")
76
- .description("Run the listen worker (used by launchd/systemd).")
77
- .action(async () => process.exit(await runListen()));
78
- listen
79
- .command("install")
80
- .description("Install the listen daemon (launchd or systemd user unit).")
81
- .option("--json", "machine-readable output")
82
- .action(async (opts) => process.exit(await runListenInstall({ json: !!opts.json })));
83
- listen
84
- .command("uninstall")
85
- .description("Stop and remove the listen daemon.")
86
- .option("--json", "machine-readable output")
87
- .action(async (opts) => process.exit(await runListenUninstall({ json: !!opts.json })));
88
- listen
89
- .command("status")
90
- .description("Report listen daemon state and last events.")
91
- .option("--json", "machine-readable output")
92
- .action(async (opts) => process.exit(await runListenStatus({ json: !!opts.json })));
58
+ if (!sandbox)
59
+ program
60
+ .command("signup <email>")
61
+ .description("Request an email OTP for the given address.")
62
+ .option("--force", "overwrite any pending signup")
63
+ .option("--json", "machine-readable output")
64
+ .action(async (email, opts) => process.exit(await runSignup(email, { force: !!opts.force, json: !!opts.json })));
65
+ if (!sandbox)
66
+ program
67
+ .command("onboard")
68
+ .description("Verify the OTP and finish onboarding.")
69
+ .option("--verification-id <id>", "explicit verification id (falls back to local pending signup)")
70
+ .option("--code <code>", "6-digit OTP from your email (omit if already signed in the command just installs the --agent skill and skips verification)")
71
+ .option("--inbound-instruction <text>", "system prompt for inbound calls to your auto-provisioned number (required for a new account; ignored when signing in)")
72
+ .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], [])
73
+ .option("--json", "machine-readable output")
74
+ .action(async (opts) => process.exit(await runOnboard({
75
+ verificationId: opts.verificationId,
76
+ code: opts.code,
77
+ inboundInstruction: opts.inboundInstruction,
78
+ agents: opts.agent,
79
+ json: !!opts.json,
80
+ })));
81
+ if (!sandbox) {
82
+ const listen = program
83
+ .command("listen")
84
+ .description("Run the listen worker (used by launchd/systemd).")
85
+ .action(async () => process.exit(await runListen()));
86
+ listen
87
+ .command("install")
88
+ .description("Install the listen daemon (launchd or systemd user unit).")
89
+ .option("--json", "machine-readable output")
90
+ .action(async (opts) => process.exit(await runListenInstall({ json: !!opts.json })));
91
+ listen
92
+ .command("uninstall")
93
+ .description("Stop and remove the listen daemon.")
94
+ .option("--json", "machine-readable output")
95
+ .action(async (opts) => process.exit(await runListenUninstall({ json: !!opts.json })));
96
+ listen
97
+ .command("status")
98
+ .description("Report listen daemon state and last events.")
99
+ .option("--json", "machine-readable output")
100
+ .action(async (opts) => process.exit(await runListenStatus({ json: !!opts.json })));
101
+ }
93
102
  const number = program
94
103
  .command("number")
95
104
  .description("Manage your Dial phone numbers.");
@@ -305,52 +314,54 @@ call
305
314
  .description("Fetch a single call by id. GET /api/v1/calls/<id>.")
306
315
  .option("--json", "machine-readable output")
307
316
  .action(async (callId, opts) => process.exit(await runCallGet({ callId, json: !!opts.json })));
308
- const localTarget = program
309
- .command("local-target")
310
- .description("Register local fan-out targets the listen daemon delivers events to.")
311
- .enablePositionalOptions();
312
- const localTargetAdd = localTarget
313
- .command("add")
314
- .description("Register a new local fan-out target (url or cmd).")
315
- .enablePositionalOptions();
316
- localTargetAdd
317
- .command("url <url>")
318
- .description("Register a loopback HTTP endpoint. The daemon POSTs each event JSON to <url>.")
319
- .option("--secret <value>", "HMAC-SHA256 key. The daemon signs each request body and sends the hex digest.")
320
- .option("--signature-header <name>", "HTTP header for the HMAC signature (defaults to X-Dial-Signature; only used with --secret)")
321
- .option("--bearer <token>", "static bearer token, sent as `Authorization: Bearer <token>`")
322
- .option("--timeout <seconds>", "per-attempt timeout (default 5)", (v) => parseInt(v, 10))
323
- .option("--json", "machine-readable output")
324
- .action(async (url, opts) => process.exit(await runLocalTargetAddUrl({
325
- url,
326
- secret: opts.secret,
327
- signatureHeader: opts.signatureHeader,
328
- bearer: opts.bearer,
329
- timeoutSeconds: opts.timeout,
330
- json: !!opts.json,
331
- })));
332
- localTargetAdd
333
- .command("cmd <path> [args...]")
334
- .description("Register an executable. The daemon spawns it per event with the event JSON as the final positional argument.")
335
- .option("--timeout <seconds>", "per-attempt timeout (default 5)", (v) => parseInt(v, 10))
336
- .option("--json", "machine-readable output")
337
- .passThroughOptions(true)
338
- .action(async (path, args, opts) => process.exit(await runLocalTargetAddCmd({
339
- path,
340
- args: args ?? [],
341
- timeoutSeconds: opts.timeout,
342
- json: !!opts.json,
343
- })));
344
- localTarget
345
- .command("remove <id>")
346
- .description("Unregister a target by id (URL for url targets, path for cmd targets).")
347
- .option("--json", "machine-readable output")
348
- .action(async (id, opts) => process.exit(await runLocalTargetRemove({ id, json: !!opts.json })));
349
- localTarget
350
- .command("list")
351
- .description("List the local targets currently registered for fan-out.")
352
- .option("--json", "machine-readable output")
353
- .action(async (opts) => process.exit(await runLocalTargetList({ json: !!opts.json })));
317
+ if (!sandbox) {
318
+ const localTarget = program
319
+ .command("local-target")
320
+ .description("Register local fan-out targets the listen daemon delivers events to.")
321
+ .enablePositionalOptions();
322
+ const localTargetAdd = localTarget
323
+ .command("add")
324
+ .description("Register a new local fan-out target (url or cmd).")
325
+ .enablePositionalOptions();
326
+ localTargetAdd
327
+ .command("url <url>")
328
+ .description("Register a loopback HTTP endpoint. The daemon POSTs each event JSON to <url>.")
329
+ .option("--secret <value>", "HMAC-SHA256 key. The daemon signs each request body and sends the hex digest.")
330
+ .option("--signature-header <name>", "HTTP header for the HMAC signature (defaults to X-Dial-Signature; only used with --secret)")
331
+ .option("--bearer <token>", "static bearer token, sent as `Authorization: Bearer <token>`")
332
+ .option("--timeout <seconds>", "per-attempt timeout (default 5)", (v) => parseInt(v, 10))
333
+ .option("--json", "machine-readable output")
334
+ .action(async (url, opts) => process.exit(await runLocalTargetAddUrl({
335
+ url,
336
+ secret: opts.secret,
337
+ signatureHeader: opts.signatureHeader,
338
+ bearer: opts.bearer,
339
+ timeoutSeconds: opts.timeout,
340
+ json: !!opts.json,
341
+ })));
342
+ localTargetAdd
343
+ .command("cmd <path> [args...]")
344
+ .description("Register an executable. The daemon spawns it per event with the event JSON as the final positional argument.")
345
+ .option("--timeout <seconds>", "per-attempt timeout (default 5)", (v) => parseInt(v, 10))
346
+ .option("--json", "machine-readable output")
347
+ .passThroughOptions(true)
348
+ .action(async (path, args, opts) => process.exit(await runLocalTargetAddCmd({
349
+ path,
350
+ args: args ?? [],
351
+ timeoutSeconds: opts.timeout,
352
+ json: !!opts.json,
353
+ })));
354
+ localTarget
355
+ .command("remove <id>")
356
+ .description("Unregister a target by id (URL for url targets, path for cmd targets).")
357
+ .option("--json", "machine-readable output")
358
+ .action(async (id, opts) => process.exit(await runLocalTargetRemove({ id, json: !!opts.json })));
359
+ localTarget
360
+ .command("list")
361
+ .description("List the local targets currently registered for fan-out.")
362
+ .option("--json", "machine-readable output")
363
+ .action(async (opts) => process.exit(await runLocalTargetList({ json: !!opts.json })));
364
+ }
354
365
  program
355
366
  .command("wait-for <event-type>")
356
367
  .description("Wait for the next matching event in the listen log (e.g. call.ended, message.received).")
@@ -365,20 +376,34 @@ program
365
376
  timeoutSeconds: opts.timeout,
366
377
  json: !!opts.json,
367
378
  })));
368
- program
369
- .command("mcp")
370
- .description("Run a local stdio MCP server exposing Dial as agent tools (reuses your saved API key).")
371
- .action(async () => process.exit(await runMcp()));
372
- program
373
- .command("update")
374
- .description("Update the CLI to the latest published version (global npm installs).")
375
- .option("--json", "machine-readable output")
376
- .action(async (opts) => process.exit(await runUpdate({ json: !!opts.json })));
377
- program
378
- .command("uninstall")
379
- .description("Remove the listen daemon, agent skills, and all local Dial state, then print how to remove the package.")
380
- .option("--json", "machine-readable output")
381
- .action(async (opts) => process.exit(await runUninstall({ json: !!opts.json })));
379
+ if (!sandbox)
380
+ program
381
+ .command("mcp")
382
+ .description("Run a local stdio MCP server exposing Dial as agent tools (reuses your saved API key).")
383
+ .action(async () => process.exit(await runMcp()));
384
+ if (!sandbox)
385
+ program
386
+ .command("update")
387
+ .description("Update the CLI to the latest published version (global npm installs).")
388
+ .option("--json", "machine-readable output")
389
+ .action(async (opts) => process.exit(await runUpdate({ json: !!opts.json })));
390
+ if (!sandbox)
391
+ program
392
+ .command("uninstall")
393
+ .description("Remove the listen daemon, agent skills, and all local Dial state, then print how to remove the package.")
394
+ .option("--json", "machine-readable output")
395
+ .action(async (opts) => process.exit(await runUninstall({ json: !!opts.json })));
396
+ // In sandbox mode the commands above are never registered, so invoking one
397
+ // would otherwise surface commander's generic "unknown command". Intercept the
398
+ // disabled verb first and print a message that names sandbox mode + the escape
399
+ // hatches, so the agent isn't left guessing.
400
+ if (sandbox) {
401
+ const invoked = process.argv.slice(2).find((a) => !a.startsWith("-"));
402
+ if (invoked && SANDBOX_DISABLED_COMMANDS.includes(invoked)) {
403
+ console.error(sandboxDisabledMessage(invoked));
404
+ process.exit(2);
405
+ }
406
+ }
382
407
  program.parseAsync(process.argv).catch((err) => {
383
408
  console.error(err instanceof Error ? err.message : String(err));
384
409
  process.exit(2);
@@ -3,6 +3,17 @@ function humanRender(r) {
3
3
  const lines = [];
4
4
  lines.push(`dial ${r.cli.version} (node ${r.cli.node})`);
5
5
  lines.push(`backend: ${r.backend.url} ${r.backend.reachable ? `reachable${r.backend.latencyMs != null ? ` (${r.backend.latencyMs}ms)` : ""}` : "UNREACHABLE"}`);
6
+ if (r.sandbox) {
7
+ // Sandbox: no local key — the gateway injects credentials. Don't say "not
8
+ // signed in" (misleading) or surface pending-otp/listen (all disabled here).
9
+ lines.push(`mode: sandbox (credentials injected by the gateway)`);
10
+ lines.push(`auth: ${r.auth.keyValid ? "credential connected via the gateway" : "no valid Dial credential connected in the vault"}`);
11
+ lines.push("");
12
+ lines.push(r.nextStep === "ready"
13
+ ? "next: ready"
14
+ : "next: connect the Dial credential in your gateway/vault (this agent has none)");
15
+ return lines.join("\n");
16
+ }
6
17
  if (r.auth.signedIn) {
7
18
  lines.push(`auth: signed in as ${r.auth.email} (account ${r.auth.accountId}, key sk_live_***${r.auth.apiKeyFingerprint})${r.auth.keyValid === false ? " [key rejected by backend]" : ""}`);
8
19
  }
package/dist/lib/api.js CHANGED
@@ -1,6 +1,16 @@
1
- import { request, fetch as undiciFetch, FormData as UndiciFormData } from "undici";
1
+ import { request, fetch as undiciFetch, FormData as UndiciFormData, setGlobalDispatcher, EnvHttpProxyAgent } from "undici";
2
2
  import { logger } from "./log.js";
3
3
  import { VERSION } from "./version.js";
4
+ // Route this package's undici requests through HTTP(S)_PROXY when one is set.
5
+ // The `undici` npm package keeps its OWN global dispatcher, which — unlike
6
+ // Node's built-in fetch (wired by NODE_USE_ENV_PROXY) — ignores the proxy env
7
+ // vars. In a proxied environment (e.g. the NanoClaw agent sandbox, where the
8
+ // OneCLI gateway sits on HTTPS_PROXY and injects the Authorization header for
9
+ // api.getdial.ai) our `request()`/`fetch()` calls would otherwise bypass the
10
+ // gateway entirely and go out UNAUTHENTICATED → 401. Opt in explicitly.
11
+ if (process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy) {
12
+ setGlobalDispatcher(new EnvHttpProxyAgent());
13
+ }
4
14
  // The bundled undici only multipart-encodes its own FormData class (realm
5
15
  // check) — Node's global FormData would be coerced to a text/plain string.
6
16
  export { UndiciFormData as ApiFormData };
@@ -4,11 +4,36 @@ import { supervisorStatus, lastEventAtFromLog, supervisorAvailability } from "..
4
4
  import { paths } from "../paths.js";
5
5
  import { VERSION } from "../version.js";
6
6
  import { installSkill, isSupportedAgent, SUPPORTED_AGENTS } from "../skill-install.js";
7
+ import { isSandbox } from "../sandbox.js";
7
8
  import { DialError } from "./errors.js";
8
9
  const OTP_EXPIRY_MS = 10 * 60 * 1000;
9
10
  const PENDING_FRESH_MS = 10 * 60 * 1000;
10
11
  export async function accountStatus() {
11
12
  const ping = await pingBackend();
13
+ if (isSandbox()) {
14
+ // No local auth file in a sandbox — the gateway injects the credential.
15
+ // Probe keyless (the proxy adds the Authorization header) to report whether
16
+ // the credential is actually connected in the vault. Never suggest
17
+ // signup/onboard/listen here — those are disabled in a sandbox.
18
+ const probe = await apiGet("/api/v1/account");
19
+ const connected = probe.ok;
20
+ return {
21
+ cli: { version: VERSION, node: process.versions.node },
22
+ backend: { url: baseUrl(), reachable: ping.reachable, latencyMs: ping.latencyMs },
23
+ auth: {
24
+ signedIn: connected,
25
+ email: null,
26
+ accountId: null,
27
+ apiKeyPresent: connected,
28
+ apiKeyFingerprint: null,
29
+ keyValid: connected,
30
+ },
31
+ pendingOtp: { verificationId: null, ageSeconds: null, expired: null },
32
+ listen: { installed: false, running: false, lastEventAt: null },
33
+ sandbox: true,
34
+ nextStep: connected ? "ready" : "connect_credential",
35
+ };
36
+ }
12
37
  const auth = readAuth();
13
38
  const pending = readPendingSignup();
14
39
  let keyValid = null;
@@ -61,6 +86,7 @@ export async function accountStatus() {
61
86
  expired: pendingExpired,
62
87
  },
63
88
  listen: listenState,
89
+ sandbox: false,
64
90
  nextStep,
65
91
  };
66
92
  }
@@ -1,16 +1,29 @@
1
1
  import { readAuth } from "../state.js";
2
+ import { isSandbox } from "../sandbox.js";
2
3
  import { DialError } from "./errors.js";
3
- /** Resolve the saved auth or throw a `not_signed_in` DialError. */
4
- export function requireAuth() {
4
+ /**
5
+ * Resolve the saved auth, or `undefined` when running keyless.
6
+ *
7
+ * - Signed in (auth file present): returns the saved {@link Auth}.
8
+ * - Sandbox mode with no saved auth: returns `undefined`. The container has no
9
+ * API key — a transparent HTTPS proxy (OneCLI) injects the real credential at
10
+ * the network boundary, so callers pass `auth?.apiKey` (undefined) and
11
+ * `lib/api.ts` attaches no `Authorization` header. Account-derived state
12
+ * (e.g. a default from-number) is likewise absent and must be supplied
13
+ * explicitly via --from-number(-id).
14
+ * - Not signed in and not sandboxed: throws `not_signed_in`.
15
+ */
16
+ export function maybeAuth() {
5
17
  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;
18
+ if (auth)
19
+ return auth;
20
+ if (isSandbox())
21
+ return undefined;
22
+ throw new DialError("not_signed_in", "Not signed in. Run `dial signup` and `dial onboard` first.");
10
23
  }
11
24
  /** Resolve the from-number id: explicit override, else the account default, else throw. */
12
25
  export function requireFromNumberId(auth, override) {
13
- const id = override ?? auth.phoneNumberId;
26
+ const id = override ?? auth?.phoneNumberId;
14
27
  if (!id) {
15
28
  throw new DialError("no_from_number", "No default phoneNumberId in auth. Pass --from-number-id <id>.");
16
29
  }
@@ -21,7 +34,7 @@ export function requireFromNumberId(auth, override) {
21
34
  * override, else the saved default number id (an id is a valid ref), else throw.
22
35
  */
23
36
  export function requireFromNumber(auth, override) {
24
- const ref = override ?? auth.phoneNumberId;
37
+ const ref = override ?? auth?.phoneNumberId;
25
38
  if (!ref) {
26
39
  throw new DialError("no_from_number", "No default phoneNumberId in auth. Pass --from-number <id|E.164|nickname>.");
27
40
  }
@@ -1,9 +1,9 @@
1
1
  import { apiGet } from "../api.js";
2
- import { requireAuth } from "./auth.js";
2
+ import { maybeAuth } from "./auth.js";
3
3
  import { DialError } from "./errors.js";
4
4
  export async function getBilling() {
5
- const auth = requireAuth();
6
- const res = await apiGet("/api/v1/billing", auth.apiKey);
5
+ const auth = maybeAuth();
6
+ const res = await apiGet("/api/v1/billing", auth?.apiKey);
7
7
  if (!res.ok)
8
8
  throw new DialError("billing_failed", res.error, res.status);
9
9
  return res.data;
@@ -1,8 +1,8 @@
1
1
  import { apiGet, apiPost } from "../api.js";
2
- import { requireAuth, resolveFromSelector } from "./auth.js";
2
+ import { maybeAuth, resolveFromSelector } from "./auth.js";
3
3
  import { DialError } from "./errors.js";
4
4
  export async function placeCall(opts) {
5
- const auth = requireAuth();
5
+ const auth = maybeAuth();
6
6
  const from = resolveFromSelector(auth, opts);
7
7
  const res = await apiPost("/api/v1/calls", {
8
8
  to: opts.to,
@@ -13,13 +13,13 @@ export async function placeCall(opts) {
13
13
  ...(opts.voiceGender ? { voiceGender: opts.voiceGender } : {}),
14
14
  ...(opts.transferTo ? { transferTo: opts.transferTo } : {}),
15
15
  ...(opts.maxCallDurationSeconds !== undefined ? { maxCallDurationSeconds: opts.maxCallDurationSeconds } : {}),
16
- }, auth.apiKey, opts.idempotencyKey ? { "idempotency-key": opts.idempotencyKey } : undefined);
16
+ }, auth?.apiKey, opts.idempotencyKey ? { "idempotency-key": opts.idempotencyKey } : undefined);
17
17
  if (!res.ok)
18
18
  throw new DialError("call_failed", res.error, res.status);
19
19
  return res.data.call;
20
20
  }
21
21
  export async function listCalls(opts) {
22
- const auth = requireAuth();
22
+ const auth = maybeAuth();
23
23
  const params = new URLSearchParams();
24
24
  if (opts.numberId)
25
25
  params.set("numberId", opts.numberId);
@@ -28,14 +28,14 @@ export async function listCalls(opts) {
28
28
  if (opts.since)
29
29
  params.set("since", opts.since);
30
30
  const qs = params.toString();
31
- const res = await apiGet(qs ? `/api/v1/calls?${qs}` : "/api/v1/calls", auth.apiKey);
31
+ const res = await apiGet(qs ? `/api/v1/calls?${qs}` : "/api/v1/calls", auth?.apiKey);
32
32
  if (!res.ok)
33
33
  throw new DialError("list_failed", res.error, res.status);
34
34
  return res.data.calls ?? [];
35
35
  }
36
36
  export async function getCall(callId) {
37
- const auth = requireAuth();
38
- const res = await apiGet(`/api/v1/calls/${encodeURIComponent(callId)}`, auth.apiKey);
37
+ const auth = maybeAuth();
38
+ const res = await apiGet(`/api/v1/calls/${encodeURIComponent(callId)}`, auth?.apiKey);
39
39
  if (!res.ok)
40
40
  throw new DialError(res.status === 404 ? "not_found" : "get_failed", res.error, res.status);
41
41
  return res.data.call;
@@ -3,7 +3,7 @@ import { supervisorStatus } from "../supervisor/index.js";
3
3
  import { parseFieldArg, parseRegexArg } from "../event-filter.js";
4
4
  import { currentSize, findLatestMatch, tailUntilMatch } from "../log-tail.js";
5
5
  import { apiPost } from "../api.js";
6
- import { requireAuth } from "./auth.js";
6
+ import { maybeAuth } from "./auth.js";
7
7
  import { DialError } from "./errors.js";
8
8
  const PER_POLL_SECONDS = 30;
9
9
  /**
@@ -34,7 +34,7 @@ async function waitFromLog(spec, opts) {
34
34
  return { source: null, timedOut: true, event: null, line: null };
35
35
  }
36
36
  async function waitFromApi(spec, opts) {
37
- const auth = requireAuth();
37
+ const auth = maybeAuth();
38
38
  const filters = {};
39
39
  for (const f of spec.fields)
40
40
  filters[f.name] = f.value;
@@ -50,7 +50,7 @@ async function waitFromApi(spec, opts) {
50
50
  filters: Object.keys(filters).length > 0 ? filters : undefined,
51
51
  regexFilters: Object.keys(regexFilters).length > 0 ? regexFilters : undefined,
52
52
  timeout,
53
- }, auth.apiKey);
53
+ }, auth?.apiKey);
54
54
  if (res.ok && res.data?.event) {
55
55
  return { source: "api", timedOut: false, event: res.data.event, line: JSON.stringify(res.data.event) };
56
56
  }
@@ -1,10 +1,10 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { installSupervised, uninstallSupervised, supervisorStatus, supervisorAvailability, lastEventAtFromLog, resolveListenCommand, } from "../supervisor/index.js";
3
3
  import { paths } from "../paths.js";
4
- import { requireAuth } from "./auth.js";
4
+ import { maybeAuth } from "./auth.js";
5
5
  import { DialError } from "./errors.js";
6
6
  export function listenInstall() {
7
- requireAuth();
7
+ maybeAuth();
8
8
  const supervisor = supervisorAvailability();
9
9
  if (!supervisor.available) {
10
10
  throw new DialError("supervisor_unavailable", supervisor.reason);
@@ -1,7 +1,7 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { basename, extname } from "node:path";
3
3
  import { apiGet, apiPost, apiPostMultipart, ApiFormData } from "../api.js";
4
- import { requireAuth, resolveFromSelector } from "./auth.js";
4
+ import { maybeAuth, resolveFromSelector } from "./auth.js";
5
5
  import { DialError } from "./errors.js";
6
6
  export const MAX_MEDIA_ITEMS = 10;
7
7
  // File extensions the API accepts for uploads, mapped to their MIME type
@@ -45,7 +45,7 @@ function readMediaFile(path) {
45
45
  return { data, contentType, name: basename(path) };
46
46
  }
47
47
  export async function sendMessage(opts) {
48
- const auth = requireAuth();
48
+ const auth = maybeAuth();
49
49
  const from = resolveFromSelector(auth, opts);
50
50
  const media = opts.media ?? [];
51
51
  if (media.length > MAX_MEDIA_ITEMS) {
@@ -64,7 +64,7 @@ export async function sendMessage(opts) {
64
64
  ...from,
65
65
  ...(media.length ? { mediaUrls: media } : {}),
66
66
  ...(opts.forceAudioFile ? { forceAudioFile: true } : {}),
67
- }, auth.apiKey);
67
+ }, auth?.apiKey);
68
68
  }
69
69
  else {
70
70
  const form = new ApiFormData();
@@ -84,14 +84,14 @@ export async function sendMessage(opts) {
84
84
  form.append("media", new Blob([new Uint8Array(file.data)], { type: file.contentType }), file.name);
85
85
  }
86
86
  }
87
- res = await apiPostMultipart("/api/v1/messages", form, auth.apiKey);
87
+ res = await apiPostMultipart("/api/v1/messages", form, auth?.apiKey);
88
88
  }
89
89
  if (!res.ok)
90
90
  throw new DialError("send_failed", res.error, res.status);
91
91
  return res.data.message;
92
92
  }
93
93
  export async function listMessages(opts) {
94
- const auth = requireAuth();
94
+ const auth = maybeAuth();
95
95
  const params = new URLSearchParams();
96
96
  if (opts.numberId)
97
97
  params.set("numberId", opts.numberId);
@@ -100,13 +100,13 @@ export async function listMessages(opts) {
100
100
  if (opts.since)
101
101
  params.set("since", opts.since);
102
102
  const qs = params.toString();
103
- const res = await apiGet(qs ? `/api/v1/messages?${qs}` : "/api/v1/messages", auth.apiKey);
103
+ const res = await apiGet(qs ? `/api/v1/messages?${qs}` : "/api/v1/messages", auth?.apiKey);
104
104
  if (!res.ok)
105
105
  throw new DialError("list_failed", res.error, res.status);
106
106
  return res.data.messages ?? [];
107
107
  }
108
108
  export async function replyToMessage(opts) {
109
- const auth = requireAuth();
109
+ const auth = maybeAuth();
110
110
  // No `to`/`fromNumberId`: the server derives both from the target message —
111
111
  // the reply stays in the conversation the target is part of.
112
112
  const payload = {};
@@ -114,7 +114,7 @@ export async function replyToMessage(opts) {
114
114
  payload.body = opts.body;
115
115
  if (opts.reaction !== undefined)
116
116
  payload.reaction = opts.reaction;
117
- const res = await apiPost(`/api/v1/messages/${encodeURIComponent(opts.messageId)}/reply`, payload, auth.apiKey);
117
+ const res = await apiPost(`/api/v1/messages/${encodeURIComponent(opts.messageId)}/reply`, payload, auth?.apiKey);
118
118
  if (!res.ok)
119
119
  throw new DialError("reply_failed", res.error, res.status);
120
120
  return res.data.message;
@@ -1,15 +1,15 @@
1
1
  import { apiGet, apiPost, apiPatch } from "../api.js";
2
- import { requireAuth } from "./auth.js";
2
+ import { maybeAuth } from "./auth.js";
3
3
  import { DialError } from "./errors.js";
4
4
  export async function listNumbers() {
5
- const auth = requireAuth();
6
- const res = await apiGet("/api/v1/numbers", auth.apiKey);
5
+ const auth = maybeAuth();
6
+ const res = await apiGet("/api/v1/numbers", auth?.apiKey);
7
7
  if (!res.ok)
8
8
  throw new DialError("list_failed", res.error, res.status);
9
- return { numbers: res.data.numbers ?? [], defaultNumberId: auth.phoneNumberId ?? null };
9
+ return { numbers: res.data.numbers ?? [], defaultNumberId: auth?.phoneNumberId ?? null };
10
10
  }
11
11
  export async function purchaseNumber(opts) {
12
- const auth = requireAuth();
12
+ const auth = maybeAuth();
13
13
  const body = {
14
14
  inboundInstruction: opts.inboundInstruction,
15
15
  explicitProgrammaticConsent: opts.explicitProgrammaticConsent,
@@ -23,7 +23,7 @@ export async function purchaseNumber(opts) {
23
23
  body.capabilities = ["sms", "call", "imessage"];
24
24
  else if (opts.areaCode)
25
25
  body.areaCode = opts.areaCode;
26
- const res = await apiPost("/api/v1/numbers", body, auth.apiKey);
26
+ const res = await apiPost("/api/v1/numbers", body, auth?.apiKey);
27
27
  if (!res.ok)
28
28
  throw new DialError("purchase_failed", res.error, res.status);
29
29
  return res.data.number;
@@ -44,10 +44,10 @@ export async function setNumberProperties(opts) {
44
44
  if (Object.keys(body).length === 0) {
45
45
  throw new DialError("bad_request", "Provide at least one property to update (inboundInstruction, inboundVoiceGender, inboundLanguage, nickname, or maxCallDurationSeconds).");
46
46
  }
47
- const auth = requireAuth();
47
+ const auth = maybeAuth();
48
48
  // The REST API keys numbers by id; the CLI/tool takes the E.164 number for ergonomics,
49
49
  // so resolve it to its id first.
50
- const list = await apiGet("/api/v1/numbers", auth.apiKey);
50
+ const list = await apiGet("/api/v1/numbers", auth?.apiKey);
51
51
  if (!list.ok)
52
52
  throw new DialError("list_failed", list.error, list.status);
53
53
  const match = list.data.numbers.find((n) => n.number === opts.number);
@@ -55,7 +55,7 @@ export async function setNumberProperties(opts) {
55
55
  const known = list.data.numbers.map((n) => n.number).join(", ") || "(none)";
56
56
  throw new DialError("number_not_found", `No phone number ${opts.number} on your account. Yours: ${known}.`);
57
57
  }
58
- const res = await apiPatch(`/api/v1/numbers/${match.id}`, body, auth.apiKey);
58
+ const res = await apiPatch(`/api/v1/numbers/${match.id}`, body, auth?.apiKey);
59
59
  if (!res.ok)
60
60
  throw new DialError("update_failed", res.error, res.status);
61
61
  return res.data.number;
@@ -1,5 +1,5 @@
1
1
  import { apiPost } from "../api.js";
2
- import { requireAuth, requireFromNumber } from "./auth.js";
2
+ import { maybeAuth, requireFromNumber } from "./auth.js";
3
3
  import { DialError } from "./errors.js";
4
4
  /**
5
5
  * Show or clear a typing indicator (POST /api/v1/typing). iMessage numbers
@@ -7,9 +7,9 @@ import { DialError } from "./errors.js";
7
7
  * no-ops, so calling this unconditionally is safe.
8
8
  */
9
9
  export async function setTyping(opts) {
10
- const auth = requireAuth();
10
+ const auth = maybeAuth();
11
11
  const fromNumber = requireFromNumber(auth, opts.fromNumber);
12
- const res = await apiPost("/api/v1/typing", { toNumber: opts.toNumber, value: opts.value, fromNumber }, auth.apiKey);
12
+ const res = await apiPost("/api/v1/typing", { toNumber: opts.toNumber, value: opts.value, fromNumber }, auth?.apiKey);
13
13
  if (!res.ok)
14
14
  throw new DialError("typing_failed", res.error, res.status);
15
15
  return res.data;
@@ -0,0 +1,94 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { paths } from "./paths.js";
4
+ import { supervisorAvailability } from "./supervisor/index.js";
5
+ /**
6
+ * "Sandbox mode" is the Dial CLI running inside an ephemeral agent container
7
+ * (e.g. NanoClaw) where:
8
+ * - there is no saved auth file / raw API key — a transparent HTTPS proxy
9
+ * (OneCLI) injects the real `Authorization` header for api.getdial.ai, so
10
+ * the CLI must send requests keyless and let the proxy add auth;
11
+ * - there is no service supervisor (no launchd / no `systemd --user`), so
12
+ * machine-lifecycle commands (`listen`, `update`, `uninstall`, …) are
13
+ * meaningless and only confuse the agent;
14
+ * - onboarding/signup/mcp make no sense — the container is pre-provisioned.
15
+ *
16
+ * Detection precedence (see {@link computeSandbox}):
17
+ * 1. Explicit override via DIAL_SANDBOX ("1"/"true" → on, "0"/"false" → off).
18
+ * 2. Inference: no supervisor AND HTTPS_PROXY set AND no `.not-sandbox`
19
+ * sentinel in the Dial data dir.
20
+ *
21
+ * The result is memoized: computed once per process.
22
+ */
23
+ /** Commands (and their subcommands) disabled in sandbox mode. */
24
+ export const SANDBOX_DISABLED_COMMANDS = [
25
+ "listen",
26
+ "signup",
27
+ "onboard",
28
+ "local-target",
29
+ "mcp",
30
+ "update",
31
+ "uninstall",
32
+ ];
33
+ /** Absolute path of the opt-out sentinel file: `<dialDataDir>/.not-sandbox`. */
34
+ export function sentinelPath() {
35
+ return join(paths().dataDir, ".not-sandbox");
36
+ }
37
+ /** Parse the DIAL_SANDBOX override. Returns undefined for unset/unrecognized values. */
38
+ function parseOverride(raw) {
39
+ if (raw === undefined)
40
+ return undefined;
41
+ const v = raw.trim().toLowerCase();
42
+ if (v === "1" || v === "true")
43
+ return true;
44
+ if (v === "0" || v === "false")
45
+ return false;
46
+ return undefined;
47
+ }
48
+ /** existsSync for the sentinel, treating any filesystem error as "absent". */
49
+ function sentinelExists() {
50
+ try {
51
+ return existsSync(sentinelPath());
52
+ }
53
+ catch {
54
+ return false;
55
+ }
56
+ }
57
+ /** Pure computation of sandbox state from the current env/filesystem. Not memoized. */
58
+ export function computeSandbox() {
59
+ const override = parseOverride(process.env.DIAL_SANDBOX);
60
+ if (override === true)
61
+ return { sandbox: true, reason: "forced on via DIAL_SANDBOX" };
62
+ if (override === false)
63
+ return { sandbox: false, reason: "forced off via DIAL_SANDBOX" };
64
+ if (supervisorAvailability().available) {
65
+ return { sandbox: false, reason: "service supervisor available" };
66
+ }
67
+ const httpsProxy = (process.env.HTTPS_PROXY ?? "").trim();
68
+ if (!httpsProxy)
69
+ return { sandbox: false, reason: "HTTPS_PROXY not set" };
70
+ if (sentinelExists())
71
+ return { sandbox: false, reason: `${sentinelPath()} sentinel present` };
72
+ return { sandbox: true, reason: "inferred: HTTPS_PROXY set + no service supervisor" };
73
+ }
74
+ let cached;
75
+ /** Memoized sandbox state (computed once per process). */
76
+ export function sandboxState() {
77
+ if (cached === undefined)
78
+ cached = computeSandbox();
79
+ return cached;
80
+ }
81
+ /** Whether the CLI is running in sandbox mode. Memoized. */
82
+ export function isSandbox() {
83
+ return sandboxState().sandbox;
84
+ }
85
+ /** The message shown when a disabled command is invoked in sandbox mode. */
86
+ export function sandboxDisabledMessage(command) {
87
+ const { reason } = sandboxState();
88
+ return (`'dial ${command}' is disabled in sandbox mode (${reason}). ` +
89
+ `To run it here, set DIAL_SANDBOX=0 or create ${sentinelPath()}.`);
90
+ }
91
+ /** Reset the memoized state. Test-only — the CLI never mutates it at runtime. */
92
+ export function resetSandboxCacheForTests() {
93
+ cached = undefined;
94
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getdial/cli",
3
- "version": "0.32.0",
3
+ "version": "0.33.1",
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