@getdial/cli 0.7.0 → 0.9.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 +133 -30
- package/dist/commands/call/get.js +37 -0
- package/dist/commands/call/list.js +42 -0
- package/dist/commands/{call.js → call/send.js} +3 -3
- package/dist/commands/listen/install.js +9 -1
- package/dist/commands/local-target/add-cmd.js +27 -0
- package/dist/commands/local-target/add-url.js +29 -0
- package/dist/commands/local-target/list.js +16 -0
- package/dist/commands/local-target/remove.js +14 -0
- package/dist/commands/message/list.js +42 -0
- package/dist/commands/{send.js → message/send.js} +3 -3
- package/dist/commands/{numbers → number}/list.js +1 -1
- package/dist/commands/{numbers/provision.js → number/purchase.js} +3 -3
- package/dist/commands/onboard.js +60 -11
- package/dist/lib/fanout.js +131 -0
- package/dist/lib/local-targets.js +122 -0
- package/dist/lib/paths.js +1 -0
- package/dist/lib/pubnub.js +4 -0
- package/dist/lib/skill-install.js +100 -0
- package/dist/lib/supervisor/index.js +22 -0
- package/package.json +8 -2
- package/skill.tar.gz +0 -0
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 {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
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({
|
|
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
|
|
59
|
-
.command("
|
|
72
|
+
const number = program
|
|
73
|
+
.command("number")
|
|
60
74
|
.description("Manage your Dial phone numbers.");
|
|
61
|
-
|
|
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
|
|
66
|
-
|
|
67
|
-
.command("
|
|
68
|
-
.description("
|
|
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
|
|
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("
|
|
91
|
+
const message = program
|
|
92
|
+
.command("message")
|
|
79
93
|
.description("Send an SMS. POST /api/v1/messages.")
|
|
80
|
-
.
|
|
81
|
-
.
|
|
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) =>
|
|
85
|
-
to
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
.
|
|
94
|
-
.
|
|
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) =>
|
|
99
|
-
to
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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 "
|
|
2
|
-
import { apiPost } from "
|
|
3
|
-
export async function
|
|
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.");
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readAuth } from "../../lib/state.js";
|
|
2
|
-
import { installSupervised } from "../../lib/supervisor/index.js";
|
|
2
|
+
import { installSupervised, supervisorAvailability } from "../../lib/supervisor/index.js";
|
|
3
3
|
function resolveDialPath() {
|
|
4
4
|
return process.env.DIAL_BIN_OVERRIDE ?? process.argv[1] ?? "dial";
|
|
5
5
|
}
|
|
@@ -12,6 +12,14 @@ export async function runListenInstall(opts) {
|
|
|
12
12
|
console.error("Not signed in. Run `dial signup` and `dial onboard` first.");
|
|
13
13
|
return 1;
|
|
14
14
|
}
|
|
15
|
+
const supervisor = supervisorAvailability();
|
|
16
|
+
if (!supervisor.available) {
|
|
17
|
+
if (opts.json)
|
|
18
|
+
console.log(JSON.stringify({ ok: false, code: "supervisor_unavailable", error: supervisor.reason }));
|
|
19
|
+
else
|
|
20
|
+
console.error(`listen install unavailable: ${supervisor.reason}. This machine has no user-level service supervisor (sandbox/container/CI). Inbound events still work via \`dial wait-for\`; only the always-on background listener and \`dial local-target\` fan-out are unavailable here.`);
|
|
21
|
+
return 2;
|
|
22
|
+
}
|
|
15
23
|
try {
|
|
16
24
|
const result = installSupervised(resolveDialPath());
|
|
17
25
|
if (opts.json)
|
|
@@ -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 "
|
|
2
|
-
import { apiPost } from "
|
|
3
|
-
export async function
|
|
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
|
|
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
|
|
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, "
|
|
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(`
|
|
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}`);
|
package/dist/commands/onboard.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
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";
|
|
5
|
+
import { supervisorAvailability } from "../lib/supervisor/index.js";
|
|
4
6
|
function maskApiKey(key) {
|
|
5
7
|
return key.length >= 4 ? `sk_live_***${key.slice(-4)}` : "sk_live_***";
|
|
6
8
|
}
|
|
@@ -51,6 +53,23 @@ export async function runOnboard(opts) {
|
|
|
51
53
|
clearPendingSignup();
|
|
52
54
|
const authFile = paths().authFile;
|
|
53
55
|
const masked = maskApiKey(apiKey);
|
|
56
|
+
const skillResults = [];
|
|
57
|
+
for (const requested of opts.agents ?? []) {
|
|
58
|
+
if (!isSupportedAgent(requested)) {
|
|
59
|
+
skillResults.push({
|
|
60
|
+
agent: requested,
|
|
61
|
+
error: `unknown agent "${requested}". Supported: ${SUPPORTED_AGENTS.join(", ")}.`,
|
|
62
|
+
});
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
skillResults.push(installSkill(requested));
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
skillResults.push({ agent: requested, error: err instanceof Error ? err.message : String(err) });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const supervisor = supervisorAvailability();
|
|
54
73
|
if (opts.json) {
|
|
55
74
|
console.log(JSON.stringify({
|
|
56
75
|
ok: true,
|
|
@@ -60,18 +79,31 @@ export async function runOnboard(opts) {
|
|
|
60
79
|
accountId: res.data.accountId,
|
|
61
80
|
phoneNumber: res.data.phoneNumber ?? null,
|
|
62
81
|
phoneNumberId: res.data.phoneNumberId ?? null,
|
|
63
|
-
listen: {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
rationale: LISTEN_RATIONALE,
|
|
69
|
-
presentation: PRESENTATION_HINT,
|
|
70
|
-
finalizationNote: FINALIZATION_HINT,
|
|
71
|
-
useStructuredToolIfAvailable: true,
|
|
72
|
-
onYes: "dial listen install",
|
|
73
|
-
onNo: null,
|
|
82
|
+
listen: {
|
|
83
|
+
installed: false,
|
|
84
|
+
autoInstalled: false,
|
|
85
|
+
canInstall: supervisor.available,
|
|
86
|
+
unavailableReason: supervisor.available ? null : supervisor.reason,
|
|
74
87
|
},
|
|
88
|
+
skills: skillResults,
|
|
89
|
+
agentHint: supervisor.available
|
|
90
|
+
? {
|
|
91
|
+
action: "ask_user",
|
|
92
|
+
kind: "onboarding_finalization",
|
|
93
|
+
question: LISTEN_QUESTION,
|
|
94
|
+
rationale: LISTEN_RATIONALE,
|
|
95
|
+
presentation: PRESENTATION_HINT,
|
|
96
|
+
finalizationNote: FINALIZATION_HINT,
|
|
97
|
+
useStructuredToolIfAvailable: true,
|
|
98
|
+
onYes: "dial listen install",
|
|
99
|
+
onNo: null,
|
|
100
|
+
}
|
|
101
|
+
: {
|
|
102
|
+
action: "skip",
|
|
103
|
+
kind: "listen_install_unavailable",
|
|
104
|
+
reason: supervisor.reason,
|
|
105
|
+
note: "No user-level service supervisor on this machine (sandbox/container/CI). Skip the install offer and continue with the original task. Inbound events are still reachable via `dial wait-for` (long-poll); only the always-on background listener and `dial local-target` fan-out are unavailable here.",
|
|
106
|
+
},
|
|
75
107
|
}));
|
|
76
108
|
}
|
|
77
109
|
else {
|
|
@@ -79,7 +111,24 @@ export async function runOnboard(opts) {
|
|
|
79
111
|
console.log(` api key: ${masked} (saved to ${authFile})`);
|
|
80
112
|
if (res.data.phoneNumber)
|
|
81
113
|
console.log(` phone number: ${res.data.phoneNumber}`);
|
|
114
|
+
for (const r of skillResults) {
|
|
115
|
+
if ("error" in r) {
|
|
116
|
+
console.log(` skill (${r.agent}): failed — ${r.error}`);
|
|
117
|
+
}
|
|
118
|
+
else if (r.written) {
|
|
119
|
+
console.log(` skill (${r.agent}): installed → ${r.path}`);
|
|
120
|
+
}
|
|
121
|
+
else if (r.unchanged) {
|
|
122
|
+
console.log(` skill (${r.agent}): already up to date → ${r.path}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
82
125
|
console.log(``);
|
|
126
|
+
if (!supervisor.available) {
|
|
127
|
+
console.log(`listen service: not available on this machine (${supervisor.reason}).`);
|
|
128
|
+
console.log(`Skipping the install offer. Inbound events still work via \`dial wait-for\` (long-poll);`);
|
|
129
|
+
console.log(`only the always-on background listener and \`dial local-target\` fan-out are unavailable here.`);
|
|
130
|
+
return 0;
|
|
131
|
+
}
|
|
83
132
|
console.log(`Onboarding finalization (required):`);
|
|
84
133
|
console.log(``);
|
|
85
134
|
console.log(FINALIZATION_HINT);
|
|
@@ -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"),
|
package/dist/lib/pubnub.js
CHANGED
|
@@ -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
|
+
}
|
|
@@ -11,6 +11,28 @@ export function currentPlatform() {
|
|
|
11
11
|
return "linux";
|
|
12
12
|
throw new Error(`Unsupported platform: ${process.platform} (macOS and Linux only)`);
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* Detects whether a user-level service supervisor (launchd on macOS,
|
|
16
|
+
* systemd --user on Linux) is reachable on this machine. Returns
|
|
17
|
+
* unavailable for sandboxes / containers / CI runners where the user
|
|
18
|
+
* bus is missing — `dial listen install` would fail there with errors
|
|
19
|
+
* like "Failed to connect to bus: No medium found".
|
|
20
|
+
*/
|
|
21
|
+
export function supervisorAvailability() {
|
|
22
|
+
if (process.platform === "darwin")
|
|
23
|
+
return { available: true };
|
|
24
|
+
if (process.platform !== "linux") {
|
|
25
|
+
return { available: false, reason: `unsupported platform: ${process.platform}` };
|
|
26
|
+
}
|
|
27
|
+
const runtimeDir = process.env.XDG_RUNTIME_DIR;
|
|
28
|
+
if (!runtimeDir) {
|
|
29
|
+
return { available: false, reason: "XDG_RUNTIME_DIR is not set (no systemd user session)" };
|
|
30
|
+
}
|
|
31
|
+
if (!existsSync(`${runtimeDir}/systemd/private`)) {
|
|
32
|
+
return { available: false, reason: "systemd user bus socket not found (sandbox or container without systemd --user)" };
|
|
33
|
+
}
|
|
34
|
+
return { available: true };
|
|
35
|
+
}
|
|
14
36
|
export function installSupervised(programPath) {
|
|
15
37
|
const platform = currentPlatform();
|
|
16
38
|
const p = paths();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getdial/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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",
|
|
@@ -32,5 +35,8 @@
|
|
|
32
35
|
"@types/node": "^22.10.0",
|
|
33
36
|
"tsx": "^4.22.3",
|
|
34
37
|
"typescript": "^5.6.0"
|
|
38
|
+
},
|
|
39
|
+
"overrides": {
|
|
40
|
+
"react-native-url-polyfill": "npm:empty-npm-package@1.0.0"
|
|
35
41
|
}
|
|
36
42
|
}
|
package/skill.tar.gz
ADDED
|
Binary file
|