@getdial/cli 0.6.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.
@@ -1,29 +1,28 @@
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.");
7
7
  return 1;
8
8
  }
9
- const res = await apiGet("/api/v1/numbers", auth.api_key);
9
+ const res = await apiGet("/api/v1/numbers", auth.apiKey);
10
10
  if (!res.ok) {
11
11
  fail(opts.json, "list_failed", res.error, { status: res.status });
12
12
  return 2;
13
13
  }
14
14
  const numbers = res.data.numbers ?? [];
15
15
  if (opts.json) {
16
- console.log(JSON.stringify({ ok: true, numbers, default_number_id: auth.phone_number_id ?? null }));
16
+ console.log(JSON.stringify({ ok: true, numbers, defaultNumberId: auth.phoneNumberId ?? null }));
17
17
  return 0;
18
18
  }
19
19
  if (numbers.length === 0) {
20
- console.log("no phone numbers. provision one with `dial numbers provision`.");
20
+ console.log("no phone numbers. provision one with `dial number purchase`.");
21
21
  return 0;
22
22
  }
23
23
  for (const n of numbers) {
24
- const tag = n.id === auth.phone_number_id ? " (default)" : "";
25
- const agent = n.agent ? ` agent=${n.agent.name}` : "";
26
- console.log(`${n.number} id=${n.id} ${n.country}${agent}${tag}`);
24
+ const tag = n.id === auth.phoneNumberId ? " (default)" : "";
25
+ console.log(`${n.number} id=${n.id} ${n.country}${tag}`);
27
26
  }
28
27
  return 0;
29
28
  }
@@ -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.");
@@ -10,12 +10,10 @@ export async function runNumbersProvision(opts) {
10
10
  if (opts.country)
11
11
  body.country = opts.country;
12
12
  if (opts.areaCode)
13
- body.area_code = opts.areaCode;
14
- if (opts.agentId)
15
- body.agent_id = opts.agentId;
16
- const res = await apiPost("/api/v1/numbers", body, auth.api_key);
13
+ body.areaCode = opts.areaCode;
14
+ const res = await apiPost("/api/v1/numbers", body, auth.apiKey);
17
15
  if (!res.ok) {
18
- fail(opts.json, "provision_failed", res.error, { status: res.status });
16
+ fail(opts.json, "purchase_failed", res.error, { status: res.status });
19
17
  return 2;
20
18
  }
21
19
  const n = res.data.number;
@@ -23,7 +21,7 @@ export async function runNumbersProvision(opts) {
23
21
  console.log(JSON.stringify({ ok: true, number: n }));
24
22
  }
25
23
  else {
26
- console.log(`provisioned.`);
24
+ console.log(`purchased.`);
27
25
  console.log(` number: ${n.number}`);
28
26
  console.log(` id: ${n.id}`);
29
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
  }
@@ -22,10 +23,10 @@ export async function runOnboard(opts) {
22
23
  console.error("No pending signup. Run `dial signup <email>` first, or pass --verification-id.");
23
24
  return 1;
24
25
  }
25
- verificationId = pending.verification_id;
26
+ verificationId = pending.verificationId;
26
27
  email = pending.email;
27
28
  }
28
- const res = await apiPost("/api/v1/auth/verify", { verification_id: verificationId, code: opts.code });
29
+ const res = await apiPost("/api/v1/auth/verify", { verificationId, code: opts.code });
29
30
  if (!res.ok) {
30
31
  if (opts.json)
31
32
  console.log(JSON.stringify({ ok: false, code: "verify_failed", status: res.status, error: res.error }));
@@ -33,52 +34,80 @@ export async function runOnboard(opts) {
33
34
  console.error(`onboard failed: ${res.error}`);
34
35
  return res.status === 401 ? 1 : 2;
35
36
  }
36
- const apiKey = res.data.api_key ?? null;
37
- if (!apiKey || !res.data.account_id) {
37
+ const apiKey = res.data.apiKey ?? null;
38
+ if (!apiKey || !res.data.accountId) {
38
39
  if (opts.json)
39
- console.log(JSON.stringify({ ok: false, code: "missing_api_key", error: "backend returned no api_key" }));
40
+ console.log(JSON.stringify({ ok: false, code: "missing_api_key", error: "backend returned no apiKey" }));
40
41
  else
41
- console.error("onboard failed: backend returned no api_key");
42
+ console.error("onboard failed: backend returned no apiKey");
42
43
  return 2;
43
44
  }
44
45
  writeAuth({
45
- api_key: apiKey,
46
- account_id: res.data.account_id,
46
+ apiKey,
47
+ accountId: res.data.accountId,
47
48
  email: email ?? "",
48
- phone_number: res.data.phone_number ?? null,
49
- phone_number_id: res.data.phone_number_id ?? null,
49
+ phoneNumber: res.data.phoneNumber ?? null,
50
+ phoneNumberId: res.data.phoneNumberId ?? null,
50
51
  });
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,
57
- api_key_fingerprint: apiKey.slice(-4),
58
- api_key_masked: masked,
59
- api_key_path: authFile,
60
- account_id: res.data.account_id,
61
- phone_number: res.data.phone_number ?? null,
62
- phone_number_id: res.data.phone_number_id ?? null,
63
- listen: { installed: false, auto_installed: false },
64
- agent_hint: {
74
+ apiKeyFingerprint: apiKey.slice(-4),
75
+ apiKeyMasked: masked,
76
+ apiKeyPath: authFile,
77
+ accountId: res.data.accountId,
78
+ phoneNumber: res.data.phoneNumber ?? null,
79
+ phoneNumberId: res.data.phoneNumberId ?? null,
80
+ listen: { installed: false, autoInstalled: false },
81
+ skills: skillResults,
82
+ agentHint: {
65
83
  action: "ask_user",
66
84
  kind: "onboarding_finalization",
67
85
  question: LISTEN_QUESTION,
68
86
  rationale: LISTEN_RATIONALE,
69
87
  presentation: PRESENTATION_HINT,
70
- finalization_note: FINALIZATION_HINT,
71
- use_structured_tool_if_available: true,
72
- on_yes: "dial listen install",
73
- on_no: null,
88
+ finalizationNote: FINALIZATION_HINT,
89
+ useStructuredToolIfAvailable: true,
90
+ onYes: "dial listen install",
91
+ onNo: null,
74
92
  },
75
93
  }));
76
94
  }
77
95
  else {
78
96
  console.log("onboarded.");
79
97
  console.log(` api key: ${masked} (saved to ${authFile})`);
80
- if (res.data.phone_number)
81
- console.log(` phone number: ${res.data.phone_number}`);
98
+ if (res.data.phoneNumber)
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(``);
@@ -4,11 +4,11 @@ const PENDING_FRESH_MS = 10 * 60 * 1000;
4
4
  export async function runSignup(email, opts) {
5
5
  const existing = readPendingSignup();
6
6
  if (existing && !opts.force) {
7
- const age = Date.now() - Date.parse(existing.created_at);
7
+ const age = Date.now() - Date.parse(existing.createdAt);
8
8
  if (Number.isFinite(age) && age < PENDING_FRESH_MS) {
9
9
  const ageS = Math.round(age / 1000);
10
10
  if (opts.json) {
11
- console.log(JSON.stringify({ ok: false, code: "pending_exists", verification_id: existing.verification_id, email: existing.email, age_seconds: ageS }));
11
+ console.log(JSON.stringify({ ok: false, code: "pending_exists", verificationId: existing.verificationId, email: existing.email, ageSeconds: ageS }));
12
12
  }
13
13
  else {
14
14
  console.error(`A pending OTP for ${existing.email} is still fresh (${ageS}s old). Use \`dial onboard --code <code>\` or re-run with --force to start a new one.`);
@@ -24,13 +24,13 @@ export async function runSignup(email, opts) {
24
24
  console.error(`signup failed: ${res.error}`);
25
25
  return 2;
26
26
  }
27
- writePendingSignup({ verification_id: res.data.verification_id, email, created_at: new Date().toISOString() });
27
+ writePendingSignup({ verificationId: res.data.verificationId, email, createdAt: new Date().toISOString() });
28
28
  if (opts.json) {
29
- console.log(JSON.stringify({ ok: true, verification_id: res.data.verification_id, email }));
29
+ console.log(JSON.stringify({ ok: true, verificationId: res.data.verificationId, email }));
30
30
  }
31
31
  else {
32
32
  console.log(`OTP sent to ${email}.`);
33
- console.log(`Run \`dial onboard --code <code>\` once you have it (verification_id is stored locally).`);
33
+ console.log(`Run \`dial onboard --code <code>\` once you have it (verificationId is stored locally).`);
34
34
  }
35
35
  return 0;
36
36
  }
@@ -61,11 +61,11 @@ async function waitFromApi(spec, opts) {
61
61
  const remainingSec = Math.max(1, Math.ceil((deadline - Date.now()) / 1000));
62
62
  const timeout = Math.min(PER_POLL_SECONDS, remainingSec);
63
63
  const res = await apiPost("/api/v1/events/wait", {
64
- event_type: spec.eventType,
64
+ eventType: spec.eventType,
65
65
  filters: Object.keys(filters).length > 0 ? filters : undefined,
66
- regex_filters: Object.keys(regexFilters).length > 0 ? regexFilters : undefined,
66
+ regexFilters: Object.keys(regexFilters).length > 0 ? regexFilters : undefined,
67
67
  timeout,
68
- }, auth.api_key);
68
+ }, auth.apiKey);
69
69
  if (res.ok && res.data?.event) {
70
70
  process.stdout.write(JSON.stringify(res.data.event) + "\n");
71
71
  return 0;
@@ -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;
@@ -45,7 +46,7 @@ export function startWorker(apiKey, accountId) {
45
46
  }
46
47
  }
47
48
  function scheduleRefresh(creds) {
48
- const ms = Math.max(60, Math.floor(creds.ttl_seconds / 2)) * 1000;
49
+ const ms = Math.max(60, Math.floor(creds.ttlSeconds / 2)) * 1000;
49
50
  refreshTimer = setTimeout(() => refresh(creds), ms);
50
51
  }
51
52
  async function stop() {
@@ -81,7 +82,7 @@ export function startWorker(apiKey, accountId) {
81
82
  return;
82
83
  }
83
84
  pn = new PubNub({
84
- subscribeKey: creds.subscribe_key,
85
+ subscribeKey: creds.subscribeKey,
85
86
  userId: `dial-cli-${accountId}`,
86
87
  ssl: true,
87
88
  authKey: creds.token,
@@ -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/dist/lib/state.js CHANGED
@@ -4,16 +4,16 @@ import { z } from "zod";
4
4
  import { paths } from "./paths.js";
5
5
  import { logger } from "./log.js";
6
6
  export const AuthSchema = z.object({
7
- api_key: z.string(),
8
- account_id: z.string(),
7
+ apiKey: z.string(),
8
+ accountId: z.string(),
9
9
  email: z.string(),
10
- phone_number: z.string().nullable(),
11
- phone_number_id: z.string().nullable(),
10
+ phoneNumber: z.string().nullable(),
11
+ phoneNumberId: z.string().nullable(),
12
12
  });
13
13
  export const PendingSignupSchema = z.object({
14
- verification_id: z.string(),
14
+ verificationId: z.string(),
15
15
  email: z.string(),
16
- created_at: z.string(),
16
+ createdAt: z.string(),
17
17
  });
18
18
  const CHMOD_UNSUPPORTED_CODES = new Set(["ENOTSUP", "EOPNOTSUPP", "EPERM"]);
19
19
  function ensureDir(path) {