@getdial/cli 0.7.0 → 0.8.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 CHANGED
@@ -9,15 +9,23 @@ import { runListenInstall } from "./commands/listen/install.js";
9
9
  import { runListenUninstall } from "./commands/listen/uninstall.js";
10
10
  import { runListenStatus } from "./commands/listen/status.js";
11
11
  import { runWaitFor } from "./commands/wait-for.js";
12
- import { runNumbersList } from "./commands/numbers/list.js";
13
- import { runNumbersProvision } from "./commands/numbers/provision.js";
14
- import { runSend } from "./commands/send.js";
15
- import { runCall } from "./commands/call.js";
12
+ import { runNumberList } from "./commands/number/list.js";
13
+ import { runNumberPurchase } from "./commands/number/purchase.js";
14
+ import { runMessageSend } from "./commands/message/send.js";
15
+ import { runMessageList } from "./commands/message/list.js";
16
+ import { runCallSend } from "./commands/call/send.js";
17
+ import { runCallList } from "./commands/call/list.js";
18
+ import { runCallGet } from "./commands/call/get.js";
19
+ import { runLocalTargetAddUrl } from "./commands/local-target/add-url.js";
20
+ import { runLocalTargetAddCmd } from "./commands/local-target/add-cmd.js";
21
+ import { runLocalTargetRemove } from "./commands/local-target/remove.js";
22
+ import { runLocalTargetList } from "./commands/local-target/list.js";
16
23
  const program = new Command();
17
24
  program
18
25
  .name("dial")
19
26
  .description("Dial CLI — set up your account and run the listen service.")
20
- .version(VERSION);
27
+ .version(VERSION)
28
+ .enablePositionalOptions();
21
29
  program
22
30
  .command("doctor")
23
31
  .description("Report state and what to do next.")
@@ -34,8 +42,14 @@ program
34
42
  .description("Verify the OTP and finish onboarding.")
35
43
  .option("--verification-id <id>", "explicit verification id (falls back to local pending signup)")
36
44
  .requiredOption("--code <code>", "6-digit OTP from your email")
45
+ .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], [])
37
46
  .option("--json", "machine-readable output")
38
- .action(async (opts) => process.exit(await runOnboard({ verificationId: opts.verificationId, code: opts.code, json: !!opts.json })));
47
+ .action(async (opts) => process.exit(await runOnboard({
48
+ verificationId: opts.verificationId,
49
+ code: opts.code,
50
+ agents: opts.agent,
51
+ json: !!opts.json,
52
+ })));
39
53
  const listen = program
40
54
  .command("listen")
41
55
  .description("Run the listen worker (used by launchd/systemd).")
@@ -55,53 +69,142 @@ listen
55
69
  .description("Report listen daemon state and last events.")
56
70
  .option("--json", "machine-readable output")
57
71
  .action(async (opts) => process.exit(await runListenStatus({ json: !!opts.json })));
58
- const numbers = program
59
- .command("numbers")
72
+ const number = program
73
+ .command("number")
60
74
  .description("Manage your Dial phone numbers.");
61
- numbers
75
+ number
62
76
  .command("list")
63
77
  .description("List the numbers on your account. GET /api/v1/numbers.")
64
78
  .option("--json", "machine-readable output")
65
- .action(async (opts) => process.exit(await runNumbersList({ json: !!opts.json })));
66
- numbers
67
- .command("provision")
68
- .description("Provision an additional phone number. POST /api/v1/numbers.")
79
+ .action(async (opts) => process.exit(await runNumberList({ json: !!opts.json })));
80
+ number
81
+ .command("purchase")
82
+ .description("Purchase an additional phone number. POST /api/v1/numbers.")
69
83
  .option("--country <iso2>", "ISO-3166-1 alpha-2 country code (defaults to US server-side)")
70
84
  .option("--area-code <code>", "preferred area code (US/CA)")
71
85
  .option("--json", "machine-readable output")
72
- .action(async (opts) => process.exit(await runNumbersProvision({
86
+ .action(async (opts) => process.exit(await runNumberPurchase({
73
87
  country: opts.country,
74
88
  areaCode: opts.areaCode,
75
89
  json: !!opts.json,
76
90
  })));
77
- program
78
- .command("send")
91
+ const message = program
92
+ .command("message")
79
93
  .description("Send an SMS. POST /api/v1/messages.")
80
- .requiredOption("--to <e164>", "destination phone number, E.164 (e.g. +14155551234)")
81
- .requiredOption("--body <text>", "message body")
94
+ .option("--to <e164>", "destination phone number, E.164 (e.g. +14155551234)")
95
+ .option("--body <text>", "message body")
82
96
  .option("--from-number-id <id>", "phoneNumberId to send from (defaults to onboard's number)")
83
97
  .option("--json", "machine-readable output")
84
- .action(async (opts) => process.exit(await runSend({
85
- to: opts.to,
86
- body: opts.body,
87
- fromNumberId: opts.fromNumberId,
98
+ .action(async (opts) => {
99
+ if (!opts.to || !opts.body) {
100
+ console.error("error: --to and --body are required to send a message. Use `dial message list` to list, or `dial message --help` for usage.");
101
+ process.exit(2);
102
+ }
103
+ process.exit(await runMessageSend({
104
+ to: opts.to,
105
+ body: opts.body,
106
+ fromNumberId: opts.fromNumberId,
107
+ json: !!opts.json,
108
+ }));
109
+ });
110
+ message
111
+ .command("list")
112
+ .description("List recent messages on your account. GET /api/v1/messages.")
113
+ .option("--number-id <id>", "filter to a single phone number")
114
+ .option("--direction <dir>", "inbound or outbound")
115
+ .option("--since <iso8601>", "only messages created after this timestamp")
116
+ .option("--json", "machine-readable output")
117
+ .action(async (opts) => process.exit(await runMessageList({
118
+ numberId: opts.numberId,
119
+ direction: opts.direction,
120
+ since: opts.since,
88
121
  json: !!opts.json,
89
122
  })));
90
- program
123
+ const call = program
91
124
  .command("call")
92
125
  .description("Place an outbound voice call. POST /api/v1/calls.")
93
- .requiredOption("--to <e164>", "destination phone number, E.164 (e.g. +14155551234)")
94
- .requiredOption("--system-prompt <text>", "system prompt for the agent that will speak")
126
+ .option("--to <e164>", "destination phone number, E.164 (e.g. +14155551234)")
127
+ .option("--system-prompt <text>", "system prompt for the agent that will speak")
95
128
  .option("--language <bcp47>", "BCP-47 language tag for the call", "en-US")
96
129
  .option("--from-number-id <id>", "phoneNumberId to call from (defaults to onboard's number)")
97
130
  .option("--json", "machine-readable output")
98
- .action(async (opts) => process.exit(await runCall({
99
- to: opts.to,
100
- systemPrompt: opts.systemPrompt,
101
- language: opts.language,
102
- fromNumberId: opts.fromNumberId,
131
+ .action(async (opts) => {
132
+ if (!opts.to || !opts.systemPrompt) {
133
+ console.error("error: --to and --system-prompt are required to place a call. Use `dial call list` to list, `dial call get <id>` to fetch one, or `dial call --help` for usage.");
134
+ process.exit(2);
135
+ }
136
+ process.exit(await runCallSend({
137
+ to: opts.to,
138
+ systemPrompt: opts.systemPrompt,
139
+ language: opts.language,
140
+ fromNumberId: opts.fromNumberId,
141
+ json: !!opts.json,
142
+ }));
143
+ });
144
+ call
145
+ .command("list")
146
+ .description("List recent calls on your account. GET /api/v1/calls.")
147
+ .option("--number-id <id>", "filter to a single phone number")
148
+ .option("--direction <dir>", "inbound or outbound")
149
+ .option("--since <iso8601>", "only calls created after this timestamp")
150
+ .option("--json", "machine-readable output")
151
+ .action(async (opts) => process.exit(await runCallList({
152
+ numberId: opts.numberId,
153
+ direction: opts.direction,
154
+ since: opts.since,
155
+ json: !!opts.json,
156
+ })));
157
+ call
158
+ .command("get <call-id>")
159
+ .description("Fetch a single call by id. GET /api/v1/calls/<id>.")
160
+ .option("--json", "machine-readable output")
161
+ .action(async (callId, opts) => process.exit(await runCallGet({ callId, json: !!opts.json })));
162
+ const localTarget = program
163
+ .command("local-target")
164
+ .description("Register local fan-out targets the listen daemon delivers events to.")
165
+ .enablePositionalOptions();
166
+ const localTargetAdd = localTarget
167
+ .command("add")
168
+ .description("Register a new local fan-out target (url or cmd).")
169
+ .enablePositionalOptions();
170
+ localTargetAdd
171
+ .command("url <url>")
172
+ .description("Register a loopback HTTP endpoint. The daemon POSTs each event JSON to <url>.")
173
+ .option("--secret <value>", "HMAC-SHA256 key. The daemon signs each request body and sends the hex digest.")
174
+ .option("--signature-header <name>", "HTTP header for the HMAC signature (defaults to X-Dial-Signature; only used with --secret)")
175
+ .option("--bearer <token>", "static bearer token, sent as `Authorization: Bearer <token>`")
176
+ .option("--timeout <seconds>", "per-attempt timeout (default 5)", (v) => parseInt(v, 10))
177
+ .option("--json", "machine-readable output")
178
+ .action(async (url, opts) => process.exit(await runLocalTargetAddUrl({
179
+ url,
180
+ secret: opts.secret,
181
+ signatureHeader: opts.signatureHeader,
182
+ bearer: opts.bearer,
183
+ timeoutSeconds: opts.timeout,
103
184
  json: !!opts.json,
104
185
  })));
186
+ localTargetAdd
187
+ .command("cmd <path> [args...]")
188
+ .description("Register an executable. The daemon spawns it per event with the event JSON as the final positional argument.")
189
+ .option("--timeout <seconds>", "per-attempt timeout (default 5)", (v) => parseInt(v, 10))
190
+ .option("--json", "machine-readable output")
191
+ .passThroughOptions(true)
192
+ .action(async (path, args, opts) => process.exit(await runLocalTargetAddCmd({
193
+ path,
194
+ args: args ?? [],
195
+ timeoutSeconds: opts.timeout,
196
+ json: !!opts.json,
197
+ })));
198
+ localTarget
199
+ .command("remove <id>")
200
+ .description("Unregister a target by id (URL for url targets, path for cmd targets).")
201
+ .option("--json", "machine-readable output")
202
+ .action(async (id, opts) => process.exit(await runLocalTargetRemove({ id, json: !!opts.json })));
203
+ localTarget
204
+ .command("list")
205
+ .description("List the local targets currently registered for fan-out.")
206
+ .option("--json", "machine-readable output")
207
+ .action(async (opts) => process.exit(await runLocalTargetList({ json: !!opts.json })));
105
208
  program
106
209
  .command("wait-for <event-type>")
107
210
  .description("Wait for the next matching event in the listen log (e.g. call.ended, message.received).")
@@ -0,0 +1,37 @@
1
+ import { readAuth } from "../../lib/state.js";
2
+ import { apiGet } from "../../lib/api.js";
3
+ export async function runCallGet(opts) {
4
+ const auth = readAuth();
5
+ if (!auth) {
6
+ fail(opts.json, "not_signed_in", "Not signed in. Run `dial signup` and `dial onboard` first.");
7
+ return 1;
8
+ }
9
+ const res = await apiGet(`/api/v1/calls/${encodeURIComponent(opts.callId)}`, auth.apiKey);
10
+ if (!res.ok) {
11
+ fail(opts.json, res.status === 404 ? "not_found" : "get_failed", res.error, { status: res.status });
12
+ return res.status === 404 ? 1 : 2;
13
+ }
14
+ const c = res.data.call;
15
+ if (opts.json) {
16
+ console.log(JSON.stringify({ ok: true, call: c }));
17
+ return 0;
18
+ }
19
+ console.log(`id: ${c.id}`);
20
+ console.log(`direction: ${c.direction}`);
21
+ console.log(`from: ${c.from}`);
22
+ console.log(`to: ${c.to}`);
23
+ console.log(`status: ${c.status}`);
24
+ console.log(`duration: ${c.duration}s`);
25
+ console.log(`created: ${c.createdAt}`);
26
+ if (c.transcript) {
27
+ console.log(`transcript:`);
28
+ console.log(c.transcript);
29
+ }
30
+ return 0;
31
+ }
32
+ function fail(json, code, message, extra) {
33
+ if (json)
34
+ console.log(JSON.stringify({ ok: false, code, message, ...extra }));
35
+ else
36
+ console.error(message);
37
+ }
@@ -0,0 +1,42 @@
1
+ import { readAuth } from "../../lib/state.js";
2
+ import { apiGet } from "../../lib/api.js";
3
+ export async function runCallList(opts) {
4
+ const auth = readAuth();
5
+ if (!auth) {
6
+ fail(opts.json, "not_signed_in", "Not signed in. Run `dial signup` and `dial onboard` first.");
7
+ return 1;
8
+ }
9
+ const params = new URLSearchParams();
10
+ if (opts.numberId)
11
+ params.set("numberId", opts.numberId);
12
+ if (opts.direction)
13
+ params.set("direction", opts.direction);
14
+ if (opts.since)
15
+ params.set("since", opts.since);
16
+ const qs = params.toString();
17
+ const path = qs ? `/api/v1/calls?${qs}` : "/api/v1/calls";
18
+ const res = await apiGet(path, auth.apiKey);
19
+ if (!res.ok) {
20
+ fail(opts.json, "list_failed", res.error, { status: res.status });
21
+ return 2;
22
+ }
23
+ const calls = res.data.calls ?? [];
24
+ if (opts.json) {
25
+ console.log(JSON.stringify({ ok: true, calls }));
26
+ return 0;
27
+ }
28
+ if (calls.length === 0) {
29
+ console.log("no calls.");
30
+ return 0;
31
+ }
32
+ for (const c of calls) {
33
+ console.log(`${c.createdAt} ${c.direction.padEnd(8)} ${c.from} -> ${c.to} ${c.status} ${c.duration}s id=${c.id}`);
34
+ }
35
+ return 0;
36
+ }
37
+ function fail(json, code, message, extra) {
38
+ if (json)
39
+ console.log(JSON.stringify({ ok: false, code, message, ...extra }));
40
+ else
41
+ console.error(message);
42
+ }
@@ -1,6 +1,6 @@
1
- import { readAuth } from "../lib/state.js";
2
- import { apiPost } from "../lib/api.js";
3
- export async function runCall(opts) {
1
+ import { readAuth } from "../../lib/state.js";
2
+ import { apiPost } from "../../lib/api.js";
3
+ export async function runCallSend(opts) {
4
4
  const auth = readAuth();
5
5
  if (!auth) {
6
6
  fail(opts.json, "not_signed_in", "Not signed in. Run `dial signup` and `dial onboard` first.");
@@ -0,0 +1,27 @@
1
+ import { addTarget, LocalTargetError } from "../../lib/local-targets.js";
2
+ export async function runLocalTargetAddCmd(opts) {
3
+ try {
4
+ const { added } = addTarget({
5
+ kind: "cmd",
6
+ path: opts.path,
7
+ args: opts.args,
8
+ timeoutSeconds: opts.timeoutSeconds,
9
+ });
10
+ if (opts.json) {
11
+ console.log(JSON.stringify({ ok: true, added, path: opts.path, args: opts.args }));
12
+ }
13
+ else {
14
+ console.log(added ? `added cmd target: ${opts.path}` : `cmd target already registered: ${opts.path}`);
15
+ }
16
+ return 0;
17
+ }
18
+ catch (err) {
19
+ const code = err instanceof LocalTargetError ? err.code : "add_failed";
20
+ const message = err instanceof Error ? err.message : String(err);
21
+ if (opts.json)
22
+ console.log(JSON.stringify({ ok: false, code, message }));
23
+ else
24
+ console.error(`add cmd failed: ${message}`);
25
+ return 2;
26
+ }
27
+ }
@@ -0,0 +1,29 @@
1
+ import { addTarget, LocalTargetError, DEFAULT_SIGNATURE_HEADER } from "../../lib/local-targets.js";
2
+ export async function runLocalTargetAddUrl(opts) {
3
+ try {
4
+ const { added } = addTarget({
5
+ kind: "url",
6
+ url: opts.url,
7
+ secret: opts.secret,
8
+ signatureHeader: opts.signatureHeader ?? (opts.secret ? DEFAULT_SIGNATURE_HEADER : undefined),
9
+ bearer: opts.bearer,
10
+ timeoutSeconds: opts.timeoutSeconds,
11
+ });
12
+ if (opts.json) {
13
+ console.log(JSON.stringify({ ok: true, added, url: opts.url }));
14
+ }
15
+ else {
16
+ console.log(added ? `added url target: ${opts.url}` : `url target already registered: ${opts.url}`);
17
+ }
18
+ return 0;
19
+ }
20
+ catch (err) {
21
+ const code = err instanceof LocalTargetError ? err.code : "add_failed";
22
+ const message = err instanceof Error ? err.message : String(err);
23
+ if (opts.json)
24
+ console.log(JSON.stringify({ ok: false, code, message }));
25
+ else
26
+ console.error(`add url failed: ${message}`);
27
+ return 2;
28
+ }
29
+ }
@@ -0,0 +1,16 @@
1
+ import { listTargets, targetId } from "../../lib/local-targets.js";
2
+ export async function runLocalTargetList(opts) {
3
+ const targets = listTargets();
4
+ if (opts.json) {
5
+ console.log(JSON.stringify({ ok: true, targets }));
6
+ return 0;
7
+ }
8
+ if (targets.length === 0) {
9
+ console.log("no local targets registered. add one with `dial local-target add url <url>` or `dial local-target add cmd <path>`.");
10
+ return 0;
11
+ }
12
+ for (const t of targets) {
13
+ console.log(`${t.kind.padEnd(4)} ${targetId(t)}`);
14
+ }
15
+ return 0;
16
+ }
@@ -0,0 +1,14 @@
1
+ import { removeTarget } from "../../lib/local-targets.js";
2
+ export async function runLocalTargetRemove(opts) {
3
+ const { removed } = removeTarget(opts.id);
4
+ if (opts.json) {
5
+ console.log(JSON.stringify({ ok: removed, removed, id: opts.id }));
6
+ }
7
+ else if (removed) {
8
+ console.log(`removed: ${opts.id}`);
9
+ }
10
+ else {
11
+ console.error(`not found: ${opts.id}`);
12
+ }
13
+ return removed ? 0 : 1;
14
+ }
@@ -0,0 +1,42 @@
1
+ import { readAuth } from "../../lib/state.js";
2
+ import { apiGet } from "../../lib/api.js";
3
+ export async function runMessageList(opts) {
4
+ const auth = readAuth();
5
+ if (!auth) {
6
+ fail(opts.json, "not_signed_in", "Not signed in. Run `dial signup` and `dial onboard` first.");
7
+ return 1;
8
+ }
9
+ const params = new URLSearchParams();
10
+ if (opts.numberId)
11
+ params.set("numberId", opts.numberId);
12
+ if (opts.direction)
13
+ params.set("direction", opts.direction);
14
+ if (opts.since)
15
+ params.set("since", opts.since);
16
+ const qs = params.toString();
17
+ const path = qs ? `/api/v1/messages?${qs}` : "/api/v1/messages";
18
+ const res = await apiGet(path, auth.apiKey);
19
+ if (!res.ok) {
20
+ fail(opts.json, "list_failed", res.error, { status: res.status });
21
+ return 2;
22
+ }
23
+ const messages = res.data.messages ?? [];
24
+ if (opts.json) {
25
+ console.log(JSON.stringify({ ok: true, messages }));
26
+ return 0;
27
+ }
28
+ if (messages.length === 0) {
29
+ console.log("no messages.");
30
+ return 0;
31
+ }
32
+ for (const m of messages) {
33
+ console.log(`${m.createdAt} ${m.direction.padEnd(8)} ${m.from} -> ${m.to} ${m.body}`);
34
+ }
35
+ return 0;
36
+ }
37
+ function fail(json, code, message, extra) {
38
+ if (json)
39
+ console.log(JSON.stringify({ ok: false, code, message, ...extra }));
40
+ else
41
+ console.error(message);
42
+ }
@@ -1,6 +1,6 @@
1
- import { readAuth } from "../lib/state.js";
2
- import { apiPost } from "../lib/api.js";
3
- export async function runSend(opts) {
1
+ import { readAuth } from "../../lib/state.js";
2
+ import { apiPost } from "../../lib/api.js";
3
+ export async function runMessageSend(opts) {
4
4
  const auth = readAuth();
5
5
  if (!auth) {
6
6
  fail(opts.json, "not_signed_in", "Not signed in. Run `dial signup` and `dial onboard` first.");
@@ -1,6 +1,6 @@
1
1
  import { readAuth } from "../../lib/state.js";
2
2
  import { apiGet } from "../../lib/api.js";
3
- export async function runNumbersList(opts) {
3
+ export async function runNumberList(opts) {
4
4
  const auth = readAuth();
5
5
  if (!auth) {
6
6
  fail(opts.json, "not_signed_in", "Not signed in. Run `dial signup` and `dial onboard` first.");
@@ -1,6 +1,6 @@
1
1
  import { readAuth } from "../../lib/state.js";
2
2
  import { apiPost } from "../../lib/api.js";
3
- export async function runNumbersProvision(opts) {
3
+ export async function runNumberPurchase(opts) {
4
4
  const auth = readAuth();
5
5
  if (!auth) {
6
6
  fail(opts.json, "not_signed_in", "Not signed in. Run `dial signup` and `dial onboard` first.");
@@ -13,7 +13,7 @@ export async function runNumbersProvision(opts) {
13
13
  body.areaCode = opts.areaCode;
14
14
  const res = await apiPost("/api/v1/numbers", body, auth.apiKey);
15
15
  if (!res.ok) {
16
- fail(opts.json, "provision_failed", res.error, { status: res.status });
16
+ fail(opts.json, "purchase_failed", res.error, { status: res.status });
17
17
  return 2;
18
18
  }
19
19
  const n = res.data.number;
@@ -21,7 +21,7 @@ export async function runNumbersProvision(opts) {
21
21
  console.log(JSON.stringify({ ok: true, number: n }));
22
22
  }
23
23
  else {
24
- console.log(`provisioned.`);
24
+ console.log(`purchased.`);
25
25
  console.log(` number: ${n.number}`);
26
26
  console.log(` id: ${n.id}`);
27
27
  console.log(` country: ${n.country}`);
@@ -1,6 +1,7 @@
1
1
  import { readPendingSignup, clearPendingSignup, writeAuth } from "../lib/state.js";
2
2
  import { apiPost } from "../lib/api.js";
3
3
  import { paths } from "../lib/paths.js";
4
+ import { installSkill, isSupportedAgent, SUPPORTED_AGENTS } from "../lib/skill-install.js";
4
5
  function maskApiKey(key) {
5
6
  return key.length >= 4 ? `sk_live_***${key.slice(-4)}` : "sk_live_***";
6
7
  }
@@ -51,6 +52,22 @@ export async function runOnboard(opts) {
51
52
  clearPendingSignup();
52
53
  const authFile = paths().authFile;
53
54
  const masked = maskApiKey(apiKey);
55
+ const skillResults = [];
56
+ for (const requested of opts.agents ?? []) {
57
+ if (!isSupportedAgent(requested)) {
58
+ skillResults.push({
59
+ agent: requested,
60
+ error: `unknown agent "${requested}". Supported: ${SUPPORTED_AGENTS.join(", ")}.`,
61
+ });
62
+ continue;
63
+ }
64
+ try {
65
+ skillResults.push(installSkill(requested));
66
+ }
67
+ catch (err) {
68
+ skillResults.push({ agent: requested, error: err instanceof Error ? err.message : String(err) });
69
+ }
70
+ }
54
71
  if (opts.json) {
55
72
  console.log(JSON.stringify({
56
73
  ok: true,
@@ -61,6 +78,7 @@ export async function runOnboard(opts) {
61
78
  phoneNumber: res.data.phoneNumber ?? null,
62
79
  phoneNumberId: res.data.phoneNumberId ?? null,
63
80
  listen: { installed: false, autoInstalled: false },
81
+ skills: skillResults,
64
82
  agentHint: {
65
83
  action: "ask_user",
66
84
  kind: "onboarding_finalization",
@@ -79,6 +97,17 @@ export async function runOnboard(opts) {
79
97
  console.log(` api key: ${masked} (saved to ${authFile})`);
80
98
  if (res.data.phoneNumber)
81
99
  console.log(` phone number: ${res.data.phoneNumber}`);
100
+ for (const r of skillResults) {
101
+ if ("error" in r) {
102
+ console.log(` skill (${r.agent}): failed — ${r.error}`);
103
+ }
104
+ else if (r.written) {
105
+ console.log(` skill (${r.agent}): installed → ${r.path}`);
106
+ }
107
+ else if (r.unchanged) {
108
+ console.log(` skill (${r.agent}): already up to date → ${r.path}`);
109
+ }
110
+ }
82
111
  console.log(``);
83
112
  console.log(`Onboarding finalization (required):`);
84
113
  console.log(``);
@@ -0,0 +1,131 @@
1
+ import { spawn } from "node:child_process";
2
+ import { createHmac } from "node:crypto";
3
+ import { request } from "undici";
4
+ import { DEFAULT_SIGNATURE_HEADER, listTargets, targetId, timeoutMs, } from "./local-targets.js";
5
+ const MAX_CAPTURE_BYTES = 4 * 1024;
6
+ function clipCapture(buf) {
7
+ const s = typeof buf === "string" ? buf : buf.toString("utf8");
8
+ if (s.length <= MAX_CAPTURE_BYTES)
9
+ return s;
10
+ return s.slice(0, MAX_CAPTURE_BYTES) + `…[truncated, total ${s.length} chars]`;
11
+ }
12
+ async function attemptUrl(target, body) {
13
+ const headers = { "Content-Type": "application/json" };
14
+ if (target.bearer)
15
+ headers["Authorization"] = `Bearer ${target.bearer}`;
16
+ if (target.secret) {
17
+ const sig = createHmac("sha256", target.secret).update(body).digest("hex");
18
+ headers[target.signatureHeader ?? DEFAULT_SIGNATURE_HEADER] = sig;
19
+ }
20
+ const controller = new AbortController();
21
+ const t = setTimeout(() => controller.abort(), timeoutMs(target));
22
+ try {
23
+ const res = await request(target.url, {
24
+ method: "POST",
25
+ headers,
26
+ body,
27
+ signal: controller.signal,
28
+ });
29
+ const ok = res.statusCode >= 200 && res.statusCode < 300;
30
+ return { ok, status: res.statusCode };
31
+ }
32
+ catch (err) {
33
+ if (err.name === "AbortError") {
34
+ return { ok: false, timedOut: true, error: "timeout" };
35
+ }
36
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
37
+ }
38
+ finally {
39
+ clearTimeout(t);
40
+ }
41
+ }
42
+ async function attemptCmd(target, eventJson) {
43
+ return new Promise((resolve) => {
44
+ const argv = [...(target.args ?? []), eventJson];
45
+ const child = spawn(target.path, argv, {
46
+ env: {
47
+ PATH: process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin",
48
+ HOME: process.env.HOME ?? "",
49
+ },
50
+ stdio: ["ignore", "pipe", "pipe"],
51
+ });
52
+ let stdout = "";
53
+ let stderr = "";
54
+ let settled = false;
55
+ child.stdout?.on("data", (chunk) => {
56
+ stdout += chunk.toString("utf8");
57
+ });
58
+ child.stderr?.on("data", (chunk) => {
59
+ stderr += chunk.toString("utf8");
60
+ });
61
+ const timer = setTimeout(() => {
62
+ if (settled)
63
+ return;
64
+ settled = true;
65
+ try {
66
+ child.kill("SIGKILL");
67
+ }
68
+ catch { /* already exited */ }
69
+ resolve({
70
+ ok: false,
71
+ timedOut: true,
72
+ error: "timeout",
73
+ stdout: clipCapture(stdout),
74
+ stderr: clipCapture(stderr),
75
+ });
76
+ }, timeoutMs(target));
77
+ child.on("error", (err) => {
78
+ if (settled)
79
+ return;
80
+ settled = true;
81
+ clearTimeout(timer);
82
+ resolve({
83
+ ok: false,
84
+ error: err.message,
85
+ stdout: clipCapture(stdout),
86
+ stderr: clipCapture(stderr),
87
+ });
88
+ });
89
+ child.on("close", (code) => {
90
+ if (settled)
91
+ return;
92
+ settled = true;
93
+ clearTimeout(timer);
94
+ resolve({
95
+ ok: code === 0,
96
+ exitCode: code,
97
+ stdout: clipCapture(stdout),
98
+ stderr: clipCapture(stderr),
99
+ });
100
+ });
101
+ });
102
+ }
103
+ async function dispatchOne(target, body, eventJson) {
104
+ const attempts = [];
105
+ for (let i = 0; i < 2; i += 1) {
106
+ const attempt = target.kind === "url" ? await attemptUrl(target, body) : await attemptCmd(target, eventJson);
107
+ attempts.push(attempt);
108
+ if (attempt.ok)
109
+ return { target, attempts, delivered: true };
110
+ }
111
+ return { target, attempts, delivered: false };
112
+ }
113
+ export async function fanout(event, log) {
114
+ const targets = listTargets();
115
+ if (targets.length === 0)
116
+ return [];
117
+ const eventJson = JSON.stringify(event);
118
+ const results = await Promise.all(targets.map(async (target) => {
119
+ const result = await dispatchOne(target, eventJson, eventJson);
120
+ log({
121
+ ts: new Date().toISOString(),
122
+ lifecycle: "local_target_dispatch",
123
+ kind: target.kind,
124
+ id: targetId(target),
125
+ delivered: result.delivered,
126
+ attempts: result.attempts,
127
+ });
128
+ return result;
129
+ }));
130
+ return results;
131
+ }
@@ -0,0 +1,122 @@
1
+ import { mkdirSync, readFileSync, writeFileSync, chmodSync } from "node:fs";
2
+ import { dirname, isAbsolute } from "node:path";
3
+ import { z } from "zod";
4
+ import { paths } from "./paths.js";
5
+ const LOOPBACK_HOSTS = new Set(["127.0.0.1", "0.0.0.0", "localhost", "::1", "[::1]"]);
6
+ export const DEFAULT_TIMEOUT_SECONDS = 5;
7
+ export const DEFAULT_SIGNATURE_HEADER = "X-Dial-Signature";
8
+ export const UrlTargetSchema = z.object({
9
+ kind: z.literal("url"),
10
+ url: z.string(),
11
+ secret: z.string().optional(),
12
+ signatureHeader: z.string().optional(),
13
+ bearer: z.string().optional(),
14
+ timeoutSeconds: z.number().int().positive().optional(),
15
+ });
16
+ export const CmdTargetSchema = z.object({
17
+ kind: z.literal("cmd"),
18
+ path: z.string(),
19
+ args: z.array(z.string()).default([]),
20
+ timeoutSeconds: z.number().int().positive().optional(),
21
+ });
22
+ export const LocalTargetSchema = z.discriminatedUnion("kind", [UrlTargetSchema, CmdTargetSchema]);
23
+ const RegistrySchema = z.object({
24
+ targets: z.array(LocalTargetSchema).default([]),
25
+ });
26
+ export class LocalTargetError extends Error {
27
+ code;
28
+ constructor(code, message) {
29
+ super(message);
30
+ this.code = code;
31
+ }
32
+ }
33
+ export function assertLoopbackUrl(raw) {
34
+ let parsed;
35
+ try {
36
+ parsed = new URL(raw);
37
+ }
38
+ catch {
39
+ throw new LocalTargetError("invalid_url", `not a valid URL: ${raw}`);
40
+ }
41
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
42
+ throw new LocalTargetError("invalid_scheme", `URL must be http(s): ${raw}`);
43
+ }
44
+ const host = parsed.hostname;
45
+ if (!LOOPBACK_HOSTS.has(host)) {
46
+ throw new LocalTargetError("non_loopback", `URL host must be a loopback (${[...LOOPBACK_HOSTS].join(", ")}); got "${host}".`);
47
+ }
48
+ }
49
+ export function targetId(t) {
50
+ return t.kind === "url" ? t.url : t.path;
51
+ }
52
+ function ensureDir(file) {
53
+ mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
54
+ }
55
+ function readRegistry() {
56
+ const file = paths().localTargetsFile;
57
+ let raw;
58
+ try {
59
+ raw = readFileSync(file, "utf8");
60
+ }
61
+ catch (err) {
62
+ if (err.code === "ENOENT")
63
+ return { targets: [] };
64
+ throw err;
65
+ }
66
+ let parsed;
67
+ try {
68
+ parsed = JSON.parse(raw);
69
+ }
70
+ catch {
71
+ return { targets: [] };
72
+ }
73
+ const result = RegistrySchema.safeParse(parsed);
74
+ if (!result.success)
75
+ return { targets: [] };
76
+ return result.data;
77
+ }
78
+ function writeRegistry(reg) {
79
+ const file = paths().localTargetsFile;
80
+ ensureDir(file);
81
+ writeFileSync(file, JSON.stringify(reg, null, 2), { mode: 0o600 });
82
+ try {
83
+ chmodSync(file, 0o600);
84
+ }
85
+ catch {
86
+ // chmod can fail on some filesystems; the create-mode is the important guarantee
87
+ }
88
+ }
89
+ export function listTargets() {
90
+ return readRegistry().targets;
91
+ }
92
+ export function addTarget(t) {
93
+ if (t.kind === "url") {
94
+ assertLoopbackUrl(t.url);
95
+ }
96
+ else {
97
+ if (!t.path)
98
+ throw new LocalTargetError("invalid_path", "executable path is required");
99
+ }
100
+ const reg = readRegistry();
101
+ const id = targetId(t);
102
+ if (reg.targets.some((existing) => targetId(existing) === id && existing.kind === t.kind)) {
103
+ return { added: false };
104
+ }
105
+ reg.targets.push(t);
106
+ writeRegistry(reg);
107
+ return { added: true };
108
+ }
109
+ export function removeTarget(id) {
110
+ const reg = readRegistry();
111
+ const next = reg.targets.filter((t) => targetId(t) !== id);
112
+ if (next.length === reg.targets.length)
113
+ return { removed: false };
114
+ writeRegistry({ targets: next });
115
+ return { removed: true };
116
+ }
117
+ export function timeoutMs(t) {
118
+ return (t.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS) * 1000;
119
+ }
120
+ export function isAbsolutePath(p) {
121
+ return isAbsolute(p);
122
+ }
package/dist/lib/paths.js CHANGED
@@ -15,6 +15,7 @@ export function paths() {
15
15
  configFile: join(configDir, "config.json"),
16
16
  authFile: join(dataDir, "auth.json"),
17
17
  pendingSignupFile: join(dataDir, "pending-signup.json"),
18
+ localTargetsFile: join(configDir, "local-targets.json"),
18
19
  listenLog: join(stateDir, "listen.log"),
19
20
  listenOutLog: join(stateDir, "listen.out.log"),
20
21
  listenErrLog: join(stateDir, "listen.err.log"),
@@ -2,6 +2,7 @@ import PubNub from "pubnub";
2
2
  import { apiPost } from "./api.js";
3
3
  import { appendJsonl, rotateIfLarge } from "./log.js";
4
4
  import { paths } from "./paths.js";
5
+ import { fanout } from "./fanout.js";
5
6
  const MAX_LOG_BYTES = 10 * 1024 * 1024;
6
7
  const SUBSCRIBE_PATH = "/api/v1/listen/subscribe";
7
8
  const REFRESH_FAILURES_BEFORE_EXIT = 3;
@@ -90,6 +91,9 @@ export function startWorker(apiKey, accountId) {
90
91
  pn.addListener({
91
92
  message: (ev) => {
92
93
  logLine({ ts: new Date().toISOString(), ...ev.message });
94
+ void fanout(ev.message, logLine).catch((err) => {
95
+ logLine({ ts: new Date().toISOString(), lifecycle: "fanout_error", error: err instanceof Error ? err.message : String(err) });
96
+ });
93
97
  },
94
98
  status: (s) => {
95
99
  logLine({ ts: new Date().toISOString(), lifecycle: "status", category: s.category, operation: s.operation ?? null });
@@ -0,0 +1,100 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
3
+ import { homedir, tmpdir } from "node:os";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ export const SUPPORTED_AGENTS = [
7
+ "claude-code",
8
+ "cursor",
9
+ "codex",
10
+ "opencode",
11
+ "pi",
12
+ "openclaw",
13
+ "nanoclaw",
14
+ "hermes",
15
+ ];
16
+ export function isSupportedAgent(name) {
17
+ return SUPPORTED_AGENTS.includes(name);
18
+ }
19
+ /**
20
+ * Where SKILL.md lands for each supported agent. Paths follow each tool's
21
+ * documented skill location, verified against the upstream docs:
22
+ *
23
+ * - claude-code: ~/.claude/skills/<name>/SKILL.md (docs.claude.com)
24
+ * - cursor: ~/.cursor/skills/<name>/SKILL.md (cursor.com/docs/skills)
25
+ * - codex: ~/.agents/skills/<name>/SKILL.md (developers.openai.com/codex/skills)
26
+ * - opencode: ~/.config/opencode/skills/<name>/SKILL.md (opencode.ai/docs/skills)
27
+ * - pi: ~/.pi/agent/skills/<name>/SKILL.md (pi.dev/docs/latest/skills)
28
+ * - openclaw: ~/.openclaw/skills/<name>/SKILL.md (docs.openclaw.ai/tools/skills-config)
29
+ * - hermes: ~/.hermes/skills/<name>/SKILL.md (hermes-agent.nousresearch.com)
30
+ * - nanoclaw: <cwd>/.claude/skills/<name>/SKILL.md (docs.nanoclaw.dev — project-scoped)
31
+ */
32
+ function targetPath(agent, home, cwd) {
33
+ switch (agent) {
34
+ case "claude-code":
35
+ return join(home, ".claude", "skills", "dial", "SKILL.md");
36
+ case "cursor":
37
+ return join(home, ".cursor", "skills", "dial", "SKILL.md");
38
+ case "codex":
39
+ return join(home, ".agents", "skills", "dial", "SKILL.md");
40
+ case "opencode":
41
+ return join(home, ".config", "opencode", "skills", "dial", "SKILL.md");
42
+ case "pi":
43
+ return join(home, ".pi", "agent", "skills", "dial", "SKILL.md");
44
+ case "openclaw":
45
+ return join(home, ".openclaw", "skills", "dial", "SKILL.md");
46
+ case "hermes":
47
+ return join(home, ".hermes", "skills", "dial", "SKILL.md");
48
+ case "nanoclaw":
49
+ return join(cwd, ".claude", "skills", "dial", "SKILL.md");
50
+ }
51
+ }
52
+ function packageRoot() {
53
+ // This file resolves to dist/lib/skill-install.js at runtime. The package
54
+ // root is two levels up. During `tsx` (tests / dev), src/lib/... → also two
55
+ // levels up still lands inside cli/.
56
+ const here = fileURLToPath(import.meta.url);
57
+ return resolve(dirname(here), "..", "..");
58
+ }
59
+ export function tarballPath() {
60
+ return join(packageRoot(), "skill.tar.gz");
61
+ }
62
+ export function readSkillMarkdown(tarball = tarballPath()) {
63
+ if (!existsSync(tarball)) {
64
+ throw new Error(`Dial skill tarball not found at ${tarball}. ` +
65
+ `Re-run \`npm install -g @getdial/cli\` (or, from a checkout, \`npm run build:skill\`).`);
66
+ }
67
+ const tmp = mkdtempSync(join(tmpdir(), "dial-skill-"));
68
+ try {
69
+ execFileSync("tar", ["-xzf", tarball, "-C", tmp], { stdio: "pipe" });
70
+ const skillFile = join(tmp, "skill", "SKILL.md");
71
+ if (!existsSync(skillFile)) {
72
+ throw new Error(`skill.tar.gz does not contain skill/SKILL.md (looked in ${tmp})`);
73
+ }
74
+ return readFileSync(skillFile, "utf8");
75
+ }
76
+ finally {
77
+ rmSync(tmp, { recursive: true, force: true });
78
+ }
79
+ }
80
+ export function installSkill(agent, opts = {}) {
81
+ const home = opts.home ?? process.env.HOME ?? homedir();
82
+ const cwd = opts.cwd ?? process.cwd();
83
+ const body = readSkillMarkdown();
84
+ const path = targetPath(agent, home, cwd);
85
+ mkdirSync(dirname(path), { recursive: true });
86
+ if (existsSync(path)) {
87
+ try {
88
+ const existing = readFileSync(path, "utf8");
89
+ if (existing === body) {
90
+ return { agent, path, written: false, unchanged: true };
91
+ }
92
+ }
93
+ catch {
94
+ // unreadable existing file — overwrite below
95
+ }
96
+ }
97
+ writeFileSync(path, body, { mode: 0o644 });
98
+ statSync(path);
99
+ return { agent, path, written: true };
100
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getdial/cli",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Dial CLI — install, sign up, and run the local listen service.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -13,12 +13,15 @@
13
13
  },
14
14
  "files": [
15
15
  "dist",
16
+ "skill.tar.gz",
16
17
  "README.md"
17
18
  ],
18
19
  "scripts": {
19
20
  "build": "tsc -p .",
20
21
  "test": "node --import tsx --test",
21
- "clean": "rm -rf dist"
22
+ "clean": "rm -rf dist skill.tar.gz",
23
+ "build:skill": "tar -czf skill.tar.gz skill",
24
+ "prepack": "npm run build && npm run build:skill"
22
25
  },
23
26
  "dependencies": {
24
27
  "commander": "^14.0.3",
package/skill.tar.gz ADDED
Binary file