spook-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/README.md +81 -0
- package/bin/spook.js +358 -0
- package/package.json +39 -0
package/README.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Spook CLI
|
|
2
|
+
|
|
3
|
+
Agent-first CLI for the [Spook](https://www.tryspook.com) agent API. Spook is an
|
|
4
|
+
SEO autopilot: it finds winnable Google & ChatGPT queries, writes SEO content,
|
|
5
|
+
and publishes it automatically. This CLI is a thin, zero-dependency wrapper over
|
|
6
|
+
Spook's REST agent API (`/api/agent`, described at `/openapi.json`), so terminal
|
|
7
|
+
agents (Claude Code, Cursor, CI) and shell scripts can drive Spook. Everything
|
|
8
|
+
prints JSON.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
Requires Node.js 18+.
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install -g spook-cli
|
|
16
|
+
spook help
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Get an API key
|
|
20
|
+
|
|
21
|
+
1. Sign in to Spook and open **Settings → Agent API**.
|
|
22
|
+
2. Generate a key (`spook_agent_...`) and save it:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
spook auth login --key spook_agent_xxxxxxxx
|
|
26
|
+
spook auth whoami
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The key is stored in `~/.spook/config.json` (chmod 600). You can instead set
|
|
30
|
+
`SPOOK_API_KEY` in the environment. Point at a different host with `--base` or
|
|
31
|
+
`SPOOK_BASE_URL` (default `https://www.tryspook.com`).
|
|
32
|
+
|
|
33
|
+
## Try it free (no key)
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
spook analyze example.com # quick-win queries any domain already ranks for
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Commands
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
Sites
|
|
43
|
+
spook sites List your sites
|
|
44
|
+
spook site <siteId> Site details
|
|
45
|
+
spook sites:create --name <n> --domain <d> [--publish nextjs] [--webhook <url>]
|
|
46
|
+
spook publishing <siteId> --integration nextjs [--mode draft|live]
|
|
47
|
+
spook autopilot <siteId> on|off
|
|
48
|
+
spook metrics <siteId> [--days 28]
|
|
49
|
+
spook checkout Stripe Checkout URL to add a paid site
|
|
50
|
+
|
|
51
|
+
Content
|
|
52
|
+
spook opportunities <siteId> [--questions] [--limit 20]
|
|
53
|
+
spook articles <siteId> [--limit 20]
|
|
54
|
+
spook article <articleId>
|
|
55
|
+
spook generate <siteId> "<query>" [--callback <https-url>]
|
|
56
|
+
spook publish <articleId>
|
|
57
|
+
|
|
58
|
+
Any tool (incl. the free AI writers)
|
|
59
|
+
spook tools List every tool + input schema
|
|
60
|
+
spook run <tool> '<json>' e.g. spook run blog-outline-generator '{"topic":"cold brew"}'
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Examples
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# Stand up a site, let Spook run it
|
|
67
|
+
spook sites:create --name "Acme" --domain acme.com --publish nextjs
|
|
68
|
+
spook opportunities <siteId> --limit 10
|
|
69
|
+
spook generate <siteId> "how to brew cold brew coffee"
|
|
70
|
+
spook article <articleId> # review the draft
|
|
71
|
+
spook publish <articleId>
|
|
72
|
+
spook autopilot <siteId> on
|
|
73
|
+
|
|
74
|
+
# Use a free AI tool
|
|
75
|
+
spook run content-brief-generator '{"keyword":"cold brew ratios"}'
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## MCP
|
|
79
|
+
|
|
80
|
+
Prefer MCP? Spook is also an MCP server at `https://www.tryspook.com/api/mcp` —
|
|
81
|
+
point any MCP client there. The CLI and MCP expose the same tools.
|
package/bin/spook.js
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Spook CLI — agent-first command line for the Spook agent API.
|
|
3
|
+
// Zero dependencies; Node 18+ (uses global fetch). Thin wrapper over the REST
|
|
4
|
+
// endpoints at <base>/api/agent (see <base>/openapi.json). Prints JSON so it
|
|
5
|
+
// composes in shells and is trivial for terminal agents (Claude Code, Cursor,
|
|
6
|
+
// CI) to parse.
|
|
7
|
+
|
|
8
|
+
import { readFileSync, writeFileSync, mkdirSync, rmSync, existsSync } from "node:fs";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
|
|
12
|
+
const DEFAULT_BASE = "https://www.tryspook.com";
|
|
13
|
+
const CONFIG_DIR = join(homedir(), ".spook");
|
|
14
|
+
const CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
15
|
+
const VERSION = "0.1.0";
|
|
16
|
+
|
|
17
|
+
// ── config ────────────────────────────────────────────────────────────────
|
|
18
|
+
function readConfig() {
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
|
|
21
|
+
} catch {
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function writeConfig(cfg) {
|
|
26
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
27
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2) + "\n", { mode: 0o600 });
|
|
28
|
+
}
|
|
29
|
+
function config() {
|
|
30
|
+
const file = readConfig();
|
|
31
|
+
return {
|
|
32
|
+
apiKey: process.env.SPOOK_API_KEY || file.apiKey || "",
|
|
33
|
+
baseUrl: (process.env.SPOOK_BASE_URL || file.baseUrl || DEFAULT_BASE).replace(/\/$/, ""),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ── output ────────────────────────────────────────────────────────────────
|
|
38
|
+
function out(data) {
|
|
39
|
+
process.stdout.write(JSON.stringify(data, null, 2) + "\n");
|
|
40
|
+
}
|
|
41
|
+
function fail(message, extra) {
|
|
42
|
+
process.stderr.write(
|
|
43
|
+
JSON.stringify({ error: message, ...(extra || {}) }, null, 2) + "\n",
|
|
44
|
+
);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── arg parsing ─────────────────────────────────────────────────────────────
|
|
49
|
+
// Splits argv into { _: positionals, flags: {name: value|true} }.
|
|
50
|
+
function parseArgs(argv) {
|
|
51
|
+
const _ = [];
|
|
52
|
+
const flags = {};
|
|
53
|
+
for (let i = 0; i < argv.length; i++) {
|
|
54
|
+
const a = argv[i];
|
|
55
|
+
if (a.startsWith("--")) {
|
|
56
|
+
const key = a.slice(2);
|
|
57
|
+
const next = argv[i + 1];
|
|
58
|
+
if (next === undefined || next.startsWith("--")) {
|
|
59
|
+
flags[key] = true;
|
|
60
|
+
} else {
|
|
61
|
+
flags[key] = next;
|
|
62
|
+
i++;
|
|
63
|
+
}
|
|
64
|
+
} else {
|
|
65
|
+
_.push(a);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return { _, flags };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── HTTP ────────────────────────────────────────────────────────────────────
|
|
72
|
+
// Non-throwing: returns { status, json }. status 0 == network error.
|
|
73
|
+
async function tryRequest(path, { method = "POST", body, auth = true } = {}) {
|
|
74
|
+
const { apiKey, baseUrl } = config();
|
|
75
|
+
const headers = { "Content-Type": "application/json" };
|
|
76
|
+
if (auth && apiKey) headers.Authorization = `Bearer ${apiKey}`;
|
|
77
|
+
let res;
|
|
78
|
+
try {
|
|
79
|
+
res = await fetch(`${baseUrl}${path}`, {
|
|
80
|
+
method,
|
|
81
|
+
headers,
|
|
82
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
83
|
+
});
|
|
84
|
+
} catch (e) {
|
|
85
|
+
return { status: 0, json: { error: `Network error reaching ${baseUrl}${path}`, detail: String(e) } };
|
|
86
|
+
}
|
|
87
|
+
const text = await res.text();
|
|
88
|
+
let json;
|
|
89
|
+
try {
|
|
90
|
+
json = text ? JSON.parse(text) : {};
|
|
91
|
+
} catch {
|
|
92
|
+
json = { raw: text };
|
|
93
|
+
}
|
|
94
|
+
return { status: res.status, json };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Throwing variant: exits non-zero on any error.
|
|
98
|
+
async function request(path, opts = {}) {
|
|
99
|
+
const { status, json } = await tryRequest(path, opts);
|
|
100
|
+
if (status === 0 || status >= 400) {
|
|
101
|
+
fail(json.error || `Request failed (${status})`, { status, code: json.code });
|
|
102
|
+
}
|
|
103
|
+
return json;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// POST a tool at /api/agent/<tool>, returning its `result`.
|
|
107
|
+
async function runTool(tool, args) {
|
|
108
|
+
const json = await request(`/api/agent/${tool}`, { body: args });
|
|
109
|
+
return json.result !== undefined ? json.result : json;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Run any tool by name — tries the agent surface, then the free-tools surface
|
|
113
|
+
// (/api/tools/<slug>), so deterministic tools work here too.
|
|
114
|
+
async function runAnyTool(tool, args) {
|
|
115
|
+
let r = await tryRequest(`/api/agent/${tool}`, { body: args });
|
|
116
|
+
if (r.status === 404) r = await tryRequest(`/api/tools/${tool}`, { body: args });
|
|
117
|
+
if (r.status === 0 || r.status >= 400) {
|
|
118
|
+
fail(r.json.error || `Request failed (${r.status})`, { status: r.status, code: r.json.code });
|
|
119
|
+
}
|
|
120
|
+
return r.json.result !== undefined ? r.json.result : r.json;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function requireKey() {
|
|
124
|
+
if (!config().apiKey) {
|
|
125
|
+
fail(
|
|
126
|
+
"No API key. Run `spook auth login --key spook_agent_...` or set SPOOK_API_KEY. Generate a key in Spook → Settings → Agent API.",
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ── commands ──────────────────────────────────────────────────────────────
|
|
132
|
+
const HELP = `Spook CLI ${VERSION} — agent-first CLI for the Spook agent API
|
|
133
|
+
|
|
134
|
+
Usage: spook <command> [args] [--flags]
|
|
135
|
+
|
|
136
|
+
Auth
|
|
137
|
+
auth login --key <spook_agent_...> [--base <url>] Save your API key
|
|
138
|
+
auth whoami Verify the key (lists sites)
|
|
139
|
+
auth logout Remove saved config
|
|
140
|
+
|
|
141
|
+
Free (no key)
|
|
142
|
+
analyze <domain> Quick-win queries for any domain
|
|
143
|
+
|
|
144
|
+
Sites
|
|
145
|
+
sites List your sites
|
|
146
|
+
site <siteId> Site details
|
|
147
|
+
sites:create --name <n> --domain <d> [--description <t>]
|
|
148
|
+
[--publish <nextjs|webhook|wordpress|ghost|shopify|rest>] [--webhook <url>]
|
|
149
|
+
publishing <siteId> --integration <type> [--mode <draft|live>] [--webhook <url>]
|
|
150
|
+
autopilot <siteId> <on|off>
|
|
151
|
+
metrics <siteId> [--days <n>]
|
|
152
|
+
checkout Get a Stripe Checkout URL to add a paid site
|
|
153
|
+
|
|
154
|
+
Content
|
|
155
|
+
opportunities <siteId> [--questions] [--limit <n>] Winnable queries
|
|
156
|
+
articles <siteId> [--limit <n>] Recent articles
|
|
157
|
+
article <articleId> One article (full body + SEO)
|
|
158
|
+
generate <siteId> <query...> [--callback <https-url>]
|
|
159
|
+
publish <articleId>
|
|
160
|
+
|
|
161
|
+
Any tool (incl. the free AI tools)
|
|
162
|
+
tools List every tool + schema
|
|
163
|
+
run <tool> [jsonBody] Run any tool by name
|
|
164
|
+
e.g. spook run blog-outline-generator '{"topic":"cold brew"}'
|
|
165
|
+
|
|
166
|
+
Global: --base <url> and SPOOK_BASE_URL override the API host (default ${DEFAULT_BASE}).
|
|
167
|
+
SPOOK_API_KEY overrides the saved key. Output is JSON.`;
|
|
168
|
+
|
|
169
|
+
async function main() {
|
|
170
|
+
const argv = process.argv.slice(2);
|
|
171
|
+
const { _, flags } = parseArgs(argv);
|
|
172
|
+
const cmd = _[0];
|
|
173
|
+
|
|
174
|
+
if (!cmd || cmd === "help" || flags.help || cmd === "--help") return out({ help: HELP });
|
|
175
|
+
if (cmd === "version" || flags.version) return out({ version: VERSION });
|
|
176
|
+
|
|
177
|
+
// Allow --base to override for a single invocation.
|
|
178
|
+
if (typeof flags.base === "string") process.env.SPOOK_BASE_URL = flags.base;
|
|
179
|
+
|
|
180
|
+
switch (cmd) {
|
|
181
|
+
case "auth": {
|
|
182
|
+
const sub = _[1];
|
|
183
|
+
if (sub === "login") {
|
|
184
|
+
const key = typeof flags.key === "string" ? flags.key : "";
|
|
185
|
+
if (!key) fail("Pass --key spook_agent_...");
|
|
186
|
+
const cfg = readConfig();
|
|
187
|
+
cfg.apiKey = key;
|
|
188
|
+
if (typeof flags.base === "string") cfg.baseUrl = flags.base;
|
|
189
|
+
writeConfig(cfg);
|
|
190
|
+
return out({ ok: true, savedTo: CONFIG_PATH, baseUrl: config().baseUrl });
|
|
191
|
+
}
|
|
192
|
+
if (sub === "logout") {
|
|
193
|
+
if (existsSync(CONFIG_PATH)) rmSync(CONFIG_PATH);
|
|
194
|
+
return out({ ok: true });
|
|
195
|
+
}
|
|
196
|
+
if (sub === "whoami") {
|
|
197
|
+
requireKey();
|
|
198
|
+
const r = await request(`/api/agent/list_sites`, { body: {} });
|
|
199
|
+
const sites = r.result?.sites ?? [];
|
|
200
|
+
return out({ authenticated: true, sites: sites.length, baseUrl: config().baseUrl });
|
|
201
|
+
}
|
|
202
|
+
return fail("Usage: spook auth <login|whoami|logout>");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
case "analyze": {
|
|
206
|
+
const domain = _[1];
|
|
207
|
+
if (!domain) fail("Usage: spook analyze <domain>");
|
|
208
|
+
return out(await runTool("analyze_domain", { domain }));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
case "sites":
|
|
212
|
+
requireKey();
|
|
213
|
+
return out(await runTool("list_sites", {}));
|
|
214
|
+
|
|
215
|
+
case "sites:create": {
|
|
216
|
+
requireKey();
|
|
217
|
+
const { name, domain } = flags;
|
|
218
|
+
if (typeof name !== "string" || typeof domain !== "string") {
|
|
219
|
+
fail("Usage: spook sites:create --name <n> --domain <d>");
|
|
220
|
+
}
|
|
221
|
+
return out(
|
|
222
|
+
await runTool("create_site", {
|
|
223
|
+
name,
|
|
224
|
+
domain,
|
|
225
|
+
description: typeof flags.description === "string" ? flags.description : undefined,
|
|
226
|
+
brandVoice: typeof flags.voice === "string" ? flags.voice : undefined,
|
|
227
|
+
niche: typeof flags.niche === "string" ? flags.niche : undefined,
|
|
228
|
+
publishIntegration: typeof flags.publish === "string" ? flags.publish : undefined,
|
|
229
|
+
publishWebhookUrl: typeof flags.webhook === "string" ? flags.webhook : undefined,
|
|
230
|
+
}),
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
case "site": {
|
|
235
|
+
requireKey();
|
|
236
|
+
const siteId = _[1];
|
|
237
|
+
if (!siteId) fail("Usage: spook site <siteId>");
|
|
238
|
+
return out(await runTool("get_site", { siteId }));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
case "publishing": {
|
|
242
|
+
requireKey();
|
|
243
|
+
const siteId = _[1];
|
|
244
|
+
if (!siteId) fail("Usage: spook publishing <siteId> --integration <type>");
|
|
245
|
+
return out(
|
|
246
|
+
await runTool("update_publishing", {
|
|
247
|
+
siteId,
|
|
248
|
+
publishIntegration: typeof flags.integration === "string" ? flags.integration : undefined,
|
|
249
|
+
publishMode: typeof flags.mode === "string" ? flags.mode : undefined,
|
|
250
|
+
publishWebhookUrl: typeof flags.webhook === "string" ? flags.webhook : undefined,
|
|
251
|
+
publishWebhookSecret: typeof flags.secret === "string" ? flags.secret : undefined,
|
|
252
|
+
}),
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
case "autopilot": {
|
|
257
|
+
requireKey();
|
|
258
|
+
const siteId = _[1];
|
|
259
|
+
const state = _[2];
|
|
260
|
+
if (!siteId || !["on", "off"].includes(state)) {
|
|
261
|
+
fail("Usage: spook autopilot <siteId> <on|off>");
|
|
262
|
+
}
|
|
263
|
+
return out(await runTool("set_autopilot", { siteId, enabled: state === "on" }));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
case "metrics": {
|
|
267
|
+
requireKey();
|
|
268
|
+
const siteId = _[1];
|
|
269
|
+
if (!siteId) fail("Usage: spook metrics <siteId> [--days <n>]");
|
|
270
|
+
return out(
|
|
271
|
+
await runTool("get_site_metrics", {
|
|
272
|
+
siteId,
|
|
273
|
+
days: typeof flags.days === "string" ? Number(flags.days) : undefined,
|
|
274
|
+
}),
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
case "checkout":
|
|
279
|
+
requireKey();
|
|
280
|
+
return out(await runTool("create_checkout", {}));
|
|
281
|
+
|
|
282
|
+
case "opportunities": {
|
|
283
|
+
requireKey();
|
|
284
|
+
const siteId = _[1];
|
|
285
|
+
if (!siteId) fail("Usage: spook opportunities <siteId> [--questions] [--limit <n>]");
|
|
286
|
+
return out(
|
|
287
|
+
await runTool("list_opportunities", {
|
|
288
|
+
siteId,
|
|
289
|
+
questionsOnly: flags.questions === true,
|
|
290
|
+
limit: typeof flags.limit === "string" ? Number(flags.limit) : undefined,
|
|
291
|
+
}),
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
case "articles": {
|
|
296
|
+
requireKey();
|
|
297
|
+
const siteId = _[1];
|
|
298
|
+
if (!siteId) fail("Usage: spook articles <siteId> [--limit <n>]");
|
|
299
|
+
return out(
|
|
300
|
+
await runTool("list_articles", {
|
|
301
|
+
siteId,
|
|
302
|
+
limit: typeof flags.limit === "string" ? Number(flags.limit) : undefined,
|
|
303
|
+
}),
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
case "article": {
|
|
308
|
+
requireKey();
|
|
309
|
+
const articleId = _[1];
|
|
310
|
+
if (!articleId) fail("Usage: spook article <articleId>");
|
|
311
|
+
return out(await runTool("get_article", { articleId }));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
case "generate": {
|
|
315
|
+
requireKey();
|
|
316
|
+
const siteId = _[1];
|
|
317
|
+
const query = _.slice(2).join(" ");
|
|
318
|
+
if (!siteId || !query) fail('Usage: spook generate <siteId> "<query>" [--callback <url>]');
|
|
319
|
+
return out(
|
|
320
|
+
await runTool("generate_article", {
|
|
321
|
+
siteId,
|
|
322
|
+
query,
|
|
323
|
+
callbackUrl: typeof flags.callback === "string" ? flags.callback : undefined,
|
|
324
|
+
}),
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
case "publish": {
|
|
329
|
+
requireKey();
|
|
330
|
+
const articleId = _[1];
|
|
331
|
+
if (!articleId) fail("Usage: spook publish <articleId>");
|
|
332
|
+
return out(await runTool("publish_article", { articleId }));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
case "tools":
|
|
336
|
+
return out(await request(`/api/agent`, { method: "GET", auth: false }));
|
|
337
|
+
|
|
338
|
+
case "run": {
|
|
339
|
+
const tool = _[1];
|
|
340
|
+
if (!tool) fail('Usage: spook run <tool> [jsonBody]');
|
|
341
|
+
let body = {};
|
|
342
|
+
const raw = _[2];
|
|
343
|
+
if (raw) {
|
|
344
|
+
try {
|
|
345
|
+
body = JSON.parse(raw);
|
|
346
|
+
} catch {
|
|
347
|
+
fail("Invalid JSON body. Wrap it in single quotes, e.g. '{\"topic\":\"x\"}'");
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return out(await runAnyTool(tool, body));
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
default:
|
|
354
|
+
return fail(`Unknown command: ${cmd}. Run \`spook help\`.`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
main().catch((e) => fail("Unexpected error", { detail: String(e?.stack || e) }));
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "spook-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Agent-first CLI for the Spook agent API (SEO autopilot: find winnable queries, generate & publish SEO content).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"spook": "bin/spook.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"spook",
|
|
18
|
+
"cli",
|
|
19
|
+
"agent",
|
|
20
|
+
"seo",
|
|
21
|
+
"content",
|
|
22
|
+
"ai",
|
|
23
|
+
"mcp"
|
|
24
|
+
],
|
|
25
|
+
"author": "Spook (https://www.tryspook.com)",
|
|
26
|
+
"homepage": "https://www.tryspook.com",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/VictorKildahl/spook.git",
|
|
30
|
+
"directory": "cli"
|
|
31
|
+
},
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/VictorKildahl/spook/issues"
|
|
34
|
+
},
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
}
|
|
39
|
+
}
|