@getdial/cli 0.31.0 → 0.33.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 +125 -100
- package/dist/commands/onboard.js +68 -0
- package/dist/lib/ops/auth.js +21 -8
- package/dist/lib/ops/billing.js +3 -3
- package/dist/lib/ops/calls.js +7 -7
- package/dist/lib/ops/events.js +3 -3
- package/dist/lib/ops/listen.js +2 -2
- package/dist/lib/ops/messages.js +8 -8
- package/dist/lib/ops/numbers.js +9 -9
- package/dist/lib/ops/typing.js +3 -3
- package/dist/lib/sandbox.js +94 -0
- package/dist/mcp/tools/onboard.js +37 -1
- package/package.json +1 -1
- package/skills.tar.gz +0 -0
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
listen
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
listen
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
listen
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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);
|
package/dist/commands/onboard.js
CHANGED
|
@@ -1,8 +1,30 @@
|
|
|
1
1
|
import { onboard } from "../lib/ops/account.js";
|
|
2
2
|
import { isDialError } from "../lib/ops/errors.js";
|
|
3
|
+
import { readAuth, authFilePath } from "../lib/state.js";
|
|
4
|
+
import { installSkill, isSupportedAgent, SUPPORTED_AGENTS } from "../lib/skill-install.js";
|
|
5
|
+
import { supervisorAvailability } from "../lib/supervisor/index.js";
|
|
3
6
|
function maskApiKey(key) {
|
|
4
7
|
return key.length >= 4 ? `sk_live_***${key.slice(-4)}` : "sk_live_***";
|
|
5
8
|
}
|
|
9
|
+
// Install skills for each requested agent — mirrors the loop inside
|
|
10
|
+
// `onboard()` in ops/account.ts. Used by the signed-in-only branch below,
|
|
11
|
+
// which skips OTP verification entirely.
|
|
12
|
+
function installAgentSkills(agents) {
|
|
13
|
+
const skills = [];
|
|
14
|
+
for (const requested of agents) {
|
|
15
|
+
if (!isSupportedAgent(requested)) {
|
|
16
|
+
skills.push({ agent: requested, error: `unknown agent "${requested}". Supported: ${SUPPORTED_AGENTS.join(", ")}.` });
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
skills.push(installSkill(requested));
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
skills.push({ agent: requested, error: err instanceof Error ? err.message : String(err) });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return skills;
|
|
27
|
+
}
|
|
6
28
|
const LISTEN_QUESTION = "Install the background notification listening service?";
|
|
7
29
|
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. " +
|
|
8
30
|
"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. " +
|
|
@@ -10,6 +32,52 @@ const LISTEN_RATIONALE = "The background notification listening service runs on
|
|
|
10
32
|
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.";
|
|
11
33
|
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.";
|
|
12
34
|
export async function runOnboard(opts) {
|
|
35
|
+
// Skill-install-only branch: when --code isn't supplied, we can't (and shouldn't)
|
|
36
|
+
// re-verify — but if the machine is already signed in, the useful thing to do
|
|
37
|
+
// is just install any --agent skills the caller asked for. This is what agents
|
|
38
|
+
// following the docs on an already-onboarded account hit when they read the
|
|
39
|
+
// integration page: signup is a no-op, all that's left is the skill drop-in.
|
|
40
|
+
if (!opts.code) {
|
|
41
|
+
const auth = readAuth();
|
|
42
|
+
if (!auth) {
|
|
43
|
+
const message = "Not signed in. Run `dial signup <email>` first, then re-run with --code from your inbox.";
|
|
44
|
+
if (opts.json)
|
|
45
|
+
console.log(JSON.stringify({ ok: false, code: "not_signed_in", error: message }));
|
|
46
|
+
else
|
|
47
|
+
console.error(message);
|
|
48
|
+
return 1;
|
|
49
|
+
}
|
|
50
|
+
const skills = installAgentSkills(opts.agents ?? []);
|
|
51
|
+
const supervisor = supervisorAvailability();
|
|
52
|
+
if (opts.json) {
|
|
53
|
+
console.log(JSON.stringify({
|
|
54
|
+
ok: true,
|
|
55
|
+
alreadySignedIn: true,
|
|
56
|
+
apiKeyFingerprint: auth.apiKey.slice(-4),
|
|
57
|
+
apiKeyMasked: maskApiKey(auth.apiKey),
|
|
58
|
+
apiKeyPath: authFilePath(),
|
|
59
|
+
accountId: auth.accountId,
|
|
60
|
+
phoneNumber: auth.phoneNumber ?? null,
|
|
61
|
+
phoneNumberId: auth.phoneNumberId ?? null,
|
|
62
|
+
listen: { installed: false, autoInstalled: false, canInstall: supervisor.available, unavailableReason: supervisor.available ? null : supervisor.reason },
|
|
63
|
+
skills,
|
|
64
|
+
agentHint: { action: "skip", kind: "already_signed_in", note: "Account is already signed in; verification was skipped and only the requested --agent skills were installed." },
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
console.log(`already signed in as ${auth.email || "(unknown email)"} — skipped verification.`);
|
|
69
|
+
console.log(` api key: ${maskApiKey(auth.apiKey)} (saved at ${authFilePath()})`);
|
|
70
|
+
for (const r of skills) {
|
|
71
|
+
if ("error" in r)
|
|
72
|
+
console.log(` skill (${r.agent}): failed — ${r.error}`);
|
|
73
|
+
else if (r.written)
|
|
74
|
+
console.log(` skill (${r.agent}): installed → ${r.path}`);
|
|
75
|
+
else if (r.unchanged)
|
|
76
|
+
console.log(` skill (${r.agent}): already up to date → ${r.path}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
13
81
|
let result;
|
|
14
82
|
try {
|
|
15
83
|
result = await onboard({
|
package/dist/lib/ops/auth.js
CHANGED
|
@@ -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
|
-
/**
|
|
4
|
-
|
|
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 (
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
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
|
|
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
|
}
|
package/dist/lib/ops/billing.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { apiGet } from "../api.js";
|
|
2
|
-
import {
|
|
2
|
+
import { maybeAuth } from "./auth.js";
|
|
3
3
|
import { DialError } from "./errors.js";
|
|
4
4
|
export async function getBilling() {
|
|
5
|
-
const auth =
|
|
6
|
-
const res = await apiGet("/api/v1/billing", auth
|
|
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;
|
package/dist/lib/ops/calls.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { apiGet, apiPost } from "../api.js";
|
|
2
|
-
import {
|
|
2
|
+
import { maybeAuth, resolveFromSelector } from "./auth.js";
|
|
3
3
|
import { DialError } from "./errors.js";
|
|
4
4
|
export async function placeCall(opts) {
|
|
5
|
-
const auth =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
38
|
-
const res = await apiGet(`/api/v1/calls/${encodeURIComponent(callId)}`, auth
|
|
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;
|
package/dist/lib/ops/events.js
CHANGED
|
@@ -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 {
|
|
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 =
|
|
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
|
|
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
|
}
|
package/dist/lib/ops/listen.js
CHANGED
|
@@ -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 {
|
|
4
|
+
import { maybeAuth } from "./auth.js";
|
|
5
5
|
import { DialError } from "./errors.js";
|
|
6
6
|
export function listenInstall() {
|
|
7
|
-
|
|
7
|
+
maybeAuth();
|
|
8
8
|
const supervisor = supervisorAvailability();
|
|
9
9
|
if (!supervisor.available) {
|
|
10
10
|
throw new DialError("supervisor_unavailable", supervisor.reason);
|
package/dist/lib/ops/messages.js
CHANGED
|
@@ -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 {
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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;
|
package/dist/lib/ops/numbers.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { apiGet, apiPost, apiPatch } from "../api.js";
|
|
2
|
-
import {
|
|
2
|
+
import { maybeAuth } from "./auth.js";
|
|
3
3
|
import { DialError } from "./errors.js";
|
|
4
4
|
export async function listNumbers() {
|
|
5
|
-
const auth =
|
|
6
|
-
const res = await apiGet("/api/v1/numbers", auth
|
|
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
|
|
9
|
+
return { numbers: res.data.numbers ?? [], defaultNumberId: auth?.phoneNumberId ?? null };
|
|
10
10
|
}
|
|
11
11
|
export async function purchaseNumber(opts) {
|
|
12
|
-
const auth =
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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;
|
package/dist/lib/ops/typing.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { apiPost } from "../api.js";
|
|
2
|
-
import {
|
|
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 =
|
|
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
|
|
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
|
+
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { jsonResult } from "../result.js";
|
|
3
3
|
import { onboard } from "../../lib/ops/account.js";
|
|
4
|
+
import { readAuth, authFilePath } from "../../lib/state.js";
|
|
5
|
+
import { installSkill, isSupportedAgent, SUPPORTED_AGENTS } from "../../lib/skill-install.js";
|
|
6
|
+
import { supervisorAvailability } from "../../lib/supervisor/index.js";
|
|
4
7
|
const inputSchema = {
|
|
5
|
-
code: z.string().min(1).describe("6-digit OTP from the sign-up email"),
|
|
8
|
+
code: z.string().min(1).optional().describe("6-digit OTP from the sign-up email. Omit if the account is already signed in — the tool will just install the requested --agent skills and skip verification."),
|
|
6
9
|
verificationId: z.string().optional().describe("Explicit verification id (defaults to the local pending signup)"),
|
|
7
10
|
inboundInstruction: z.string().optional().describe("System prompt for inbound calls to a newly provisioned number (new accounts)"),
|
|
8
11
|
agents: z.array(z.string()).optional().describe("Agent names to install the Dial skill into (e.g. claude-code, cursor)"),
|
|
@@ -27,6 +30,39 @@ export const onboardTool = {
|
|
|
27
30
|
annotations: { openWorldHint: true },
|
|
28
31
|
},
|
|
29
32
|
run: async (args) => {
|
|
33
|
+
// Skill-install-only branch — mirror runOnboard(): if no --code and we're
|
|
34
|
+
// already signed in, skip verification and just install the requested skills.
|
|
35
|
+
if (!args.code) {
|
|
36
|
+
const auth = readAuth();
|
|
37
|
+
if (!auth) {
|
|
38
|
+
throw new Error("Not signed in. Run `dial signup <email>` first, then invoke this tool with the OTP as `code`.");
|
|
39
|
+
}
|
|
40
|
+
const skills = [];
|
|
41
|
+
for (const requested of args.agents ?? []) {
|
|
42
|
+
if (!isSupportedAgent(requested)) {
|
|
43
|
+
skills.push({ agent: requested, error: `unknown agent "${requested}". Supported: ${SUPPORTED_AGENTS.join(", ")}.` });
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
skills.push(installSkill(requested));
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
skills.push({ agent: requested, error: err instanceof Error ? err.message : String(err) });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const supervisor = supervisorAvailability();
|
|
54
|
+
return jsonResult({
|
|
55
|
+
alreadySignedIn: true,
|
|
56
|
+
apiKeyFingerprint: auth.apiKey.slice(-4),
|
|
57
|
+
apiKeyPath: authFilePath(),
|
|
58
|
+
accountId: auth.accountId,
|
|
59
|
+
phoneNumber: auth.phoneNumber ?? null,
|
|
60
|
+
phoneNumberId: auth.phoneNumberId ?? null,
|
|
61
|
+
skills,
|
|
62
|
+
supervisor,
|
|
63
|
+
listenAvailable: supervisor.available,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
30
66
|
const r = await onboard({
|
|
31
67
|
code: args.code,
|
|
32
68
|
verificationId: args.verificationId,
|
package/package.json
CHANGED
package/skills.tar.gz
CHANGED
|
Binary file
|