@sleepwalkerai/cli 0.1.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/src/config.js ADDED
@@ -0,0 +1,83 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+
5
+ export const DEFAULT_API_BASE_URL = "https://api.sleepwalker.ai";
6
+
7
+ export function configDir(env = process.env) {
8
+ if (env.SLEEPWALKER_CONFIG_DIR) {
9
+ return env.SLEEPWALKER_CONFIG_DIR;
10
+ }
11
+ return path.join(os.homedir(), ".sleepwalker");
12
+ }
13
+
14
+ export function configPath(env = process.env) {
15
+ return path.join(configDir(env), "config.json");
16
+ }
17
+
18
+ export function readConfig(env = process.env) {
19
+ const file = configPath(env);
20
+ try {
21
+ const raw = fs.readFileSync(file, "utf8");
22
+ const parsed = JSON.parse(raw);
23
+ return parsed && typeof parsed === "object" ? parsed : {};
24
+ } catch (error) {
25
+ if (error && error.code === "ENOENT") {
26
+ return {};
27
+ }
28
+ throw new Error(`Could not read Sleepwalker config at ${file}: ${error.message}`);
29
+ }
30
+ }
31
+
32
+ export function writeConfig(config, env = process.env) {
33
+ const dir = configDir(env);
34
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
35
+ fs.chmodSync(dir, 0o700);
36
+ const file = configPath(env);
37
+ fs.writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`, {
38
+ encoding: "utf8",
39
+ mode: 0o600,
40
+ });
41
+ fs.chmodSync(file, 0o600);
42
+ }
43
+
44
+ export function getApiBaseUrl(env = process.env, config = readConfig(env)) {
45
+ return String(env.SLEEPWALKER_API_BASE_URL || config.apiBaseUrl || DEFAULT_API_BASE_URL).replace(/\/+$/, "");
46
+ }
47
+
48
+ export function getApiKey(env = process.env, config = readConfig(env)) {
49
+ return String(env.SLEEPWALKER_API_KEY || config.apiKey || "").trim();
50
+ }
51
+
52
+ export function getApiKeySource(env = process.env, config = readConfig(env)) {
53
+ const envKey = String(env.SLEEPWALKER_API_KEY || "").trim();
54
+ if (envKey) {
55
+ return { source: "env", key: envKey };
56
+ }
57
+ const configKey = String(config.apiKey || "").trim();
58
+ if (configKey) {
59
+ return { source: "config", key: configKey };
60
+ }
61
+ return { source: "missing", key: "" };
62
+ }
63
+
64
+ export function getApiBaseUrlSource(env = process.env, config = readConfig(env)) {
65
+ if (env.SLEEPWALKER_API_BASE_URL) {
66
+ return { source: "env", value: getApiBaseUrl(env, config) };
67
+ }
68
+ if (config.apiBaseUrl) {
69
+ return { source: "config", value: getApiBaseUrl(env, config) };
70
+ }
71
+ return { source: "default", value: DEFAULT_API_BASE_URL };
72
+ }
73
+
74
+ export function maskApiKey(key) {
75
+ const value = String(key || "");
76
+ if (!value) {
77
+ return "";
78
+ }
79
+ if (value.length <= 14) {
80
+ return `${value.slice(0, 5)}...`;
81
+ }
82
+ return `${value.slice(0, 12)}...${value.slice(-4)}`;
83
+ }
package/src/format.js ADDED
@@ -0,0 +1,121 @@
1
+ import { createTheme, sanitizeTerminalText, styleStatus } from "./theme.js";
2
+
3
+ export function printJson(stdout, value) {
4
+ stdout.write(`${JSON.stringify(value, null, 2)}\n`);
5
+ }
6
+
7
+ export function asList(value) {
8
+ if (!value) {
9
+ return [];
10
+ }
11
+ return Array.isArray(value) ? value : [value];
12
+ }
13
+
14
+ export function printKeyValue(stdout, rows, theme = createTheme({ stdout })) {
15
+ const visibleRows = rows.filter((row) => row && row[1] !== undefined && row[1] !== null && row[1] !== "");
16
+ const width = Math.max(...visibleRows.map(([key]) => String(key).length), 0);
17
+ for (const [key, value] of visibleRows) {
18
+ stdout.write(`${theme.muted(String(key).padEnd(width))} ${value ?? ""}\n`);
19
+ }
20
+ }
21
+
22
+ function valueCount(value) {
23
+ if (Array.isArray(value)) {
24
+ return value.length;
25
+ }
26
+ if (typeof value === "number") {
27
+ return value;
28
+ }
29
+ if (typeof value === "string" && value.trim()) {
30
+ return value.split(",").map((item) => item.trim()).filter(Boolean).length;
31
+ }
32
+ return 0;
33
+ }
34
+
35
+ function valueLabel(value) {
36
+ if (Array.isArray(value)) {
37
+ return value.map((item) => sanitizeTerminalText(item)).join(", ");
38
+ }
39
+ return sanitizeTerminalText(value);
40
+ }
41
+
42
+ function hasNonZeroCreditValue(value) {
43
+ if (value === undefined || value === null || value === "") {
44
+ return false;
45
+ }
46
+ const numeric = Number(value);
47
+ return Number.isNaN(numeric) || numeric !== 0;
48
+ }
49
+
50
+ function billingRows(run, theme) {
51
+ const billing = run.billing && typeof run.billing === "object" ? run.billing : {};
52
+ const rows = [];
53
+ const estimated = run.estimated_credits || billing.estimated_credits;
54
+ const reserved = run.reserved_credits || billing.reserved_credits;
55
+ const settled = run.settled_credits || billing.settled_credits;
56
+ const released = run.released_credits || billing.released_credits;
57
+ const status = run.billing_status || billing.status;
58
+ if (estimated) {
59
+ rows.push(["Estimated credits", theme.info(estimated)]);
60
+ }
61
+ if (reserved) {
62
+ rows.push(["Reserved credits", theme.warning(reserved)]);
63
+ }
64
+ if (settled) {
65
+ rows.push(["Settled credits", theme.accent(settled)]);
66
+ }
67
+ if (hasNonZeroCreditValue(released)) {
68
+ rows.push(["Released credits", theme.info(released)]);
69
+ }
70
+ if (status) {
71
+ rows.push(["Billing", styleStatus(theme, status)]);
72
+ }
73
+ return rows;
74
+ }
75
+
76
+ export function printNextCommands(stdout, commands, theme = createTheme({ stdout })) {
77
+ const visible = asList(commands).filter(Boolean);
78
+ if (!visible.length) {
79
+ return;
80
+ }
81
+ stdout.write(`\n${theme.accent("Next")}\n`);
82
+ for (const command of visible) {
83
+ stdout.write(` ${theme.command(command)}\n`);
84
+ }
85
+ }
86
+
87
+ export function printRunSummary(stdout, payload, type, theme = createTheme({ stdout }), options = {}) {
88
+ const run = payload || {};
89
+ const id = run.run_id || run.id;
90
+ const platforms = run.platforms || run.platform || run.ai_platforms;
91
+ const promptCount = run.prompt_count || run.prompts_count || valueCount(run.prompts);
92
+ const platformCount = run.platform_count || valueCount(platforms);
93
+ const probeCount = run.probe_count || run.total_probes || run.summary?.total_probes || valueCount(run.probes);
94
+ const rows = [
95
+ ["Type", type],
96
+ ["Run ID", id ? theme.id(id) : "unknown"],
97
+ ["Status", styleStatus(theme, run.status || "unknown")],
98
+ ["URL", run.url ? theme.info(run.url) : ""],
99
+ ["Target", sanitizeTerminalText(run.target_entity || run.brand_name || run.target || "")],
100
+ ["Platforms", valueLabel(platforms) || ""],
101
+ ["Prompts", promptCount || ""],
102
+ ["Platform count", !platforms && platformCount ? platformCount : ""],
103
+ ["Probes", probeCount || ""],
104
+ ];
105
+ rows.push(...billingRows(run, theme));
106
+ if (run.skipped_probe_count) {
107
+ rows.push(["Skipped probes", theme.warning(run.skipped_probe_count)]);
108
+ }
109
+ printKeyValue(stdout, rows, theme);
110
+ printNextCommands(stdout, options.nextCommands, theme);
111
+ }
112
+
113
+ export function printList(stdout, items, render) {
114
+ if (!items.length) {
115
+ stdout.write("No records found.\n");
116
+ return;
117
+ }
118
+ for (const item of items) {
119
+ stdout.write(`${render(item)}\n`);
120
+ }
121
+ }
package/src/http.js ADDED
@@ -0,0 +1,85 @@
1
+ import { getApiBaseUrl, getApiKey, getApiKeySource, readConfig } from "./config.js";
2
+
3
+ export class SleepwalkerApiError extends Error {
4
+ constructor(message, { status, payload } = {}) {
5
+ super(message);
6
+ this.name = "SleepwalkerApiError";
7
+ this.status = status;
8
+ this.payload = payload;
9
+ }
10
+ }
11
+
12
+ export function createApiClient({ env = process.env, fetchImpl = globalThis.fetch } = {}) {
13
+ if (typeof fetchImpl !== "function") {
14
+ throw new Error("This Node.js runtime does not expose fetch. Use Node 18.17 or newer.");
15
+ }
16
+ const config = readConfig(env);
17
+ const baseUrl = getApiBaseUrl(env, config);
18
+ const apiKey = getApiKey(env, config);
19
+ const apiKeySource = getApiKeySource(env, config).source;
20
+
21
+ async function request(method, pathname, { query, body, requireAuth = true } = {}) {
22
+ if (requireAuth && !apiKey) {
23
+ throw new SleepwalkerApiError(
24
+ "No API key configured. Set SLEEPWALKER_API_KEY or run `sleepwalker auth key set <key>`.",
25
+ { status: 401 },
26
+ );
27
+ }
28
+
29
+ const url = new URL(pathname, `${baseUrl}/`);
30
+ for (const [key, value] of Object.entries(query || {})) {
31
+ if (value !== undefined && value !== null && value !== "") {
32
+ url.searchParams.set(key, String(value));
33
+ }
34
+ }
35
+
36
+ const headers = {
37
+ Accept: "application/json",
38
+ };
39
+ if (body !== undefined) {
40
+ headers["Content-Type"] = "application/json";
41
+ }
42
+ if (apiKey) {
43
+ headers.Authorization = `Bearer ${apiKey}`;
44
+ }
45
+
46
+ const response = await fetchImpl(url, {
47
+ method,
48
+ headers,
49
+ body: body === undefined ? undefined : JSON.stringify(body),
50
+ });
51
+
52
+ const text = await response.text();
53
+ let payload = null;
54
+ if (text) {
55
+ try {
56
+ payload = JSON.parse(text);
57
+ } catch {
58
+ payload = { raw: text };
59
+ }
60
+ }
61
+
62
+ if (!response.ok) {
63
+ const detail = payload && payload.detail ? payload.detail : payload;
64
+ let message = typeof detail === "string"
65
+ ? detail
66
+ : detail && typeof detail === "object" && detail.error
67
+ ? detail.error
68
+ : `Sleepwalker API returned HTTP ${response.status}`;
69
+ if (message === "API key is missing required scope") {
70
+ message = "This API key cannot run the requested action. Create a new key in Sleepwalker Console > API; new keys include the full API action surface.";
71
+ }
72
+ throw new SleepwalkerApiError(message, { status: response.status, payload });
73
+ }
74
+
75
+ return payload;
76
+ }
77
+
78
+ return {
79
+ baseUrl,
80
+ apiKeySource,
81
+ request,
82
+ get: (pathname, options) => request("GET", pathname, options),
83
+ post: (pathname, body, options = {}) => request("POST", pathname, { ...options, body }),
84
+ };
85
+ }
package/src/theme.js ADDED
@@ -0,0 +1,178 @@
1
+ const RESET = "\u001b[0m";
2
+
3
+ const CODES = {
4
+ bold: "\u001b[1m",
5
+ dim: "\u001b[2m",
6
+ green: "\u001b[38;2;34;197;94m",
7
+ greenSoft: "\u001b[38;2;186;247;203m",
8
+ purple: "\u001b[38;2;167;139;250m",
9
+ blue: "\u001b[38;2;96;165;250m",
10
+ cyan: "\u001b[38;2;34;211;238m",
11
+ yellow: "\u001b[38;2;245;158;11m",
12
+ orange: "\u001b[38;2;249;115;22m",
13
+ red: "\u001b[38;2;239;68;68m",
14
+ muted: "\u001b[38;2;148;163;184m",
15
+ };
16
+
17
+ const WORDMARK = [
18
+ String.raw` _ _ _`,
19
+ String.raw` ___| | ___ ___ _ ____ ____ _| | | _____ _ __`,
20
+ "/ __| |/ _ \\/ _ \\ '_ \\ \\ /\\ / / _` | | |/ / _ \\ '__|",
21
+ String.raw`\__ \ | __/ __/ |_) \ V V / (_| | | < __/ |`,
22
+ String.raw`|___/_|\___|\___| .__/ \_/\_/ \__,_|_|_|\_\___|_|`,
23
+ String.raw` |_|`,
24
+ ].join("\n");
25
+
26
+ function shouldUseColor(env, stdout) {
27
+ if (env.NO_COLOR !== undefined) {
28
+ return false;
29
+ }
30
+ if (env.FORCE_COLOR && env.FORCE_COLOR !== "0") {
31
+ return true;
32
+ }
33
+ return Boolean(stdout && stdout.isTTY);
34
+ }
35
+
36
+ function wrap(enabled, code, value) {
37
+ const text = sanitizeTerminalText(value);
38
+ return enabled ? `${code}${text}${RESET}` : text;
39
+ }
40
+
41
+ export function sanitizeTerminalText(value) {
42
+ return String(value ?? "").replace(/[\x00-\x08\x0B-\x1F\x7F-\x9F\u061C\u200E\u200F\u202A-\u202E\u2066-\u2069]/g, "");
43
+ }
44
+
45
+ export function createTheme({ env = process.env, stdout = process.stdout } = {}) {
46
+ const enabled = shouldUseColor(env, stdout);
47
+ return {
48
+ enabled,
49
+ bold: (value) => wrap(enabled, CODES.bold, value),
50
+ dim: (value) => wrap(enabled, CODES.dim, value),
51
+ accent: (value) => wrap(enabled, CODES.green, value),
52
+ accentSoft: (value) => wrap(enabled, CODES.greenSoft, value),
53
+ ci: (value) => wrap(enabled, CODES.purple, value),
54
+ info: (value) => wrap(enabled, CODES.blue, value),
55
+ id: (value) => wrap(enabled, CODES.cyan, value),
56
+ warning: (value) => wrap(enabled, CODES.yellow, value),
57
+ orange: (value) => wrap(enabled, CODES.orange, value),
58
+ error: (value) => wrap(enabled, CODES.red, value),
59
+ muted: (value) => wrap(enabled, CODES.muted, value),
60
+ command: (value) => wrap(enabled, CODES.purple, value),
61
+ };
62
+ }
63
+
64
+ export function styleStatus(theme, status) {
65
+ const value = String(status || "unknown");
66
+ const normalized = value.toLowerCase();
67
+ if (["completed", "settled", "succeeded", "success", "operational"].includes(normalized)) {
68
+ return theme.accent(value);
69
+ }
70
+ if (["queued", "running", "partially_queued", "pending"].includes(normalized)) {
71
+ return theme.warning(value);
72
+ }
73
+ if (["released", "skipped", "cancelled"].includes(normalized)) {
74
+ return theme.info(value);
75
+ }
76
+ if (["failed", "fail", "errored", "error", "denied", "site_error", "blocked"].includes(normalized)) {
77
+ return theme.error(value);
78
+ }
79
+ return theme.muted(value);
80
+ }
81
+
82
+ export function renderWelcome(theme, options = {}) {
83
+ const hasApiKey = Boolean(options.hasApiKey);
84
+ const apiKeySource = options.apiKeySource || "missing";
85
+ const apiBaseUrl = options.apiBaseUrl || "https://api.sleepwalker.ai";
86
+ const configError = options.configError || "";
87
+ const command = (value) => theme.command(value);
88
+ const section = (value) => theme.accent(value);
89
+ const muted = (value) => theme.muted(value);
90
+ return `${theme.accent(WORDMARK)}
91
+ ${theme.accent("Sleepwalker")} ${theme.ci("CLI")} ${theme.muted("command line")}
92
+ ${theme.muted("AI Visibility | Content Intelligence | MCP | API | CLI")}
93
+
94
+ ${section("Start here")}
95
+ ${configError ? ` ${theme.warning("Config needs attention:")} ${configError}\n` : ""}${hasApiKey
96
+ ? ` ${theme.accent("API key detected")} ${muted(`(${apiKeySource})`)}
97
+ ${command("sleepwalker menu")} Open the interactive command menu.
98
+ ${command("sleepwalker doctor")} Check account, API reachability, and credits.
99
+ ${command("sleepwalker credits")} Show prepaid credits.
100
+ `
101
+ : ` ${command("sleepwalker init")} Follow the setup checklist.
102
+ ${command("sleepwalker auth key set <api_key>")}
103
+ ${command("sleepwalker doctor")} Check the connection.
104
+ `}
105
+
106
+ ${hasApiKey
107
+ ? `${section("Common workflows")}
108
+ ${command("sleepwalker page serialize <url>")}
109
+ ${command("sleepwalker visibility run <url>")} ${muted("--brand <brand> --prompt <prompt> --platform <platform>")}
110
+ ${command("sleepwalker ci run <url>")} ${muted("[--depth score|full] [--watch]")}
111
+ ${command("sleepwalker reports by-url <url>")}
112
+ `
113
+ : `${section("After setup")}
114
+ ${command("sleepwalker commands")} Choose an action once the CLI is authenticated.
115
+ `}
116
+
117
+ ${section("More")}
118
+ ${hasApiKey ? ` ${command("sleepwalker commands")} Show every command and option.\n` : ""} ${command("sleepwalker config show")} Show local configuration.
119
+
120
+ ${section("API base")}
121
+ ${theme.info(apiBaseUrl)}
122
+ `;
123
+ }
124
+
125
+ export function renderCommandsHelp(theme) {
126
+ const command = (value) => theme.command(value);
127
+ const section = (value) => theme.accent(value);
128
+ const muted = (value) => theme.muted(value);
129
+ return `${theme.accent(WORDMARK)}
130
+ ${theme.accent("Sleepwalker")} ${theme.ci("CLI")} ${theme.muted("command reference")}
131
+
132
+ ${section("Usage")}
133
+ ${command("sleepwalker <command> [options]")}
134
+
135
+ ${section("Setup")}
136
+ ${command("sleepwalker menu")}
137
+ ${command("sleepwalker init")}
138
+ ${command("sleepwalker auth key set <api_key>")}
139
+ ${command("sleepwalker auth key show")}
140
+ ${command("sleepwalker auth key clear")}
141
+ ${command("sleepwalker auth whoami")}
142
+ ${command("sleepwalker doctor")}
143
+ ${command("sleepwalker config show")}
144
+ ${command("sleepwalker config set api-base-url <url>")}
145
+ ${command("sleepwalker config clear api-base-url")}
146
+
147
+ ${section("Read")}
148
+ ${command("sleepwalker credits")}
149
+ ${command("sleepwalker usage")} ${muted("[--recent-limit 20]")}
150
+ ${command("sleepwalker activity list")} ${muted("[--limit 10] [--kind all]")}
151
+ ${command("sleepwalker tests list")} ${muted("[--limit 20] [--type ai_citations|content_intelligence]")}
152
+ ${command("sleepwalker reports by-url <url>")} ${muted("[--type ai_citations|content_intelligence]")}
153
+
154
+ ${section("Actions")}
155
+ ${command("sleepwalker page serialize <url>")} ${muted("[--max-chars 4000] [--offset 0]")}
156
+ ${command("sleepwalker visibility suggest-prompts <url>")} ${muted("--brand <brand>")}
157
+ ${command("sleepwalker visibility run <url>")} ${muted("--brand <brand> --prompt <prompt> --platform <platform> [--watch] [--idempotency-key <key>]")}
158
+ ${command("sleepwalker visibility status <run_id>")} ${muted("[--results]")}
159
+ ${command("sleepwalker visibility list")} ${muted("[--limit 20] [--status queued|running|completed|failed]")}
160
+ ${command("sleepwalker ci score <url>")}
161
+ ${command("sleepwalker ci run <url>")} ${muted("[--depth score|full] [--watch] [--idempotency-key <key>]")}
162
+ ${command("sleepwalker ci status <run_id>")} ${muted("[--result]")}
163
+ ${command("sleepwalker ci list")} ${muted("[--limit 20] [--status queued|running|completed|failed]")}
164
+
165
+ ${section("Global options")}
166
+ ${theme.info("--json")} Print raw JSON responses.
167
+ ${theme.info("--help")} Show help.
168
+ ${theme.info("--version")} Show version.
169
+
170
+ ${section("Environment")}
171
+ ${theme.info("SLEEPWALKER_API_KEY")} API key from the Sleepwalker Console.
172
+ ${theme.info("SLEEPWALKER_API_BASE_URL")} API base URL. Defaults to https://api.sleepwalker.ai.
173
+ `;
174
+ }
175
+
176
+ export function renderHelp(theme, options = {}) {
177
+ return renderWelcome(theme, options);
178
+ }