contentkong 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/dist/index.js +2339 -0
- package/package.json +19 -0
- package/src/client.ts +75 -0
- package/src/commands/export.ts +36 -0
- package/src/commands/generate.ts +64 -0
- package/src/commands/get.ts +27 -0
- package/src/commands/list.ts +53 -0
- package/src/commands/status.ts +24 -0
- package/src/commands/voices.ts +55 -0
- package/src/index.ts +31 -0
- package/tsconfig.json +17 -0
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "contentkong",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"bin": {
|
|
6
|
+
"ckx": "./dist/index.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "bun build src/index.ts --outfile dist/index.js --target node",
|
|
10
|
+
"dev": "bun run src/index.ts"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@contentkong/shared": "workspace:*",
|
|
14
|
+
"commander": "^13.0.0"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/node": "^25.5.0"
|
|
18
|
+
}
|
|
19
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
type HttpMethod = "GET" | "POST" | "DELETE";
|
|
2
|
+
|
|
3
|
+
interface ClientConfig {
|
|
4
|
+
apiKey?: string;
|
|
5
|
+
apiUrl?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
let globalConfig: ClientConfig = {};
|
|
9
|
+
|
|
10
|
+
export function configureClient(config: ClientConfig): void {
|
|
11
|
+
globalConfig = config;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getApiKey(): string {
|
|
15
|
+
const key = globalConfig.apiKey || process.env.CK_API_KEY;
|
|
16
|
+
if (!key) {
|
|
17
|
+
console.error("Error: API key required. Set CK_API_KEY or use --api-key.");
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
return key;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getBaseUrl(): string {
|
|
24
|
+
return (
|
|
25
|
+
globalConfig.apiUrl ||
|
|
26
|
+
process.env.CK_API_URL ||
|
|
27
|
+
"https://api.contentkong.com"
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function request<T = unknown>(
|
|
32
|
+
method: HttpMethod,
|
|
33
|
+
path: string,
|
|
34
|
+
body?: unknown,
|
|
35
|
+
): Promise<T> {
|
|
36
|
+
const url = `${getBaseUrl()}${path}`;
|
|
37
|
+
const headers: Record<string, string> = {
|
|
38
|
+
Authorization: `Bearer ${getApiKey()}`,
|
|
39
|
+
"Content-Type": "application/json",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const res = await fetch(url, {
|
|
43
|
+
method,
|
|
44
|
+
headers,
|
|
45
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (!res.ok) {
|
|
49
|
+
let msg = `HTTP ${res.status}`;
|
|
50
|
+
try {
|
|
51
|
+
const err = (await res.json()) as { error?: string; message?: string };
|
|
52
|
+
msg = err.error || err.message || msg;
|
|
53
|
+
} catch {
|
|
54
|
+
// ignore parse failure
|
|
55
|
+
}
|
|
56
|
+
throw new Error(msg);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// handle 204 no content
|
|
60
|
+
if (res.status === 204) return undefined as T;
|
|
61
|
+
|
|
62
|
+
return (await res.json()) as T;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function apiGet<T = unknown>(path: string): Promise<T> {
|
|
66
|
+
return request<T>("GET", path);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function apiPost<T = unknown>(path: string, body?: unknown): Promise<T> {
|
|
70
|
+
return request<T>("POST", path, body);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function apiDelete<T = unknown>(path: string): Promise<T> {
|
|
74
|
+
return request<T>("DELETE", path);
|
|
75
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { writeFile } from "node:fs/promises";
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { apiGet } from "../client.js";
|
|
4
|
+
export const exportCommand = new Command("export")
|
|
5
|
+
.description("Export article content to file or stdout")
|
|
6
|
+
.argument("<articleId>", "Article ID")
|
|
7
|
+
.option("--format <format>", "Output format: markdown|json", "markdown")
|
|
8
|
+
.option("--output <path>", "Write to file instead of stdout")
|
|
9
|
+
.action(async (articleId: string, opts) => {
|
|
10
|
+
try {
|
|
11
|
+
const data = await apiGet<{ content?: unknown[]; contentMarkdown?: string }>(
|
|
12
|
+
`/v1/articles/${articleId}/content`,
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
let output: string;
|
|
16
|
+
switch (opts.format) {
|
|
17
|
+
case "json":
|
|
18
|
+
output = JSON.stringify(data.content ?? [], null, 2);
|
|
19
|
+
break;
|
|
20
|
+
case "markdown":
|
|
21
|
+
default:
|
|
22
|
+
output = data.contentMarkdown ?? "";
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (opts.output) {
|
|
27
|
+
await writeFile(opts.output, output, "utf-8");
|
|
28
|
+
console.log(`Written to ${opts.output}`);
|
|
29
|
+
} else {
|
|
30
|
+
console.log(output);
|
|
31
|
+
}
|
|
32
|
+
} catch (e) {
|
|
33
|
+
console.error(`Error: ${(e as Error).message}`);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { apiGet, apiPost } from "../client.js";
|
|
3
|
+
import type { Article } from "@contentkong/shared";
|
|
4
|
+
|
|
5
|
+
function sleep(ms: number): Promise<void> {
|
|
6
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const generateCommand = new Command("generate")
|
|
10
|
+
.description("Generate a new article")
|
|
11
|
+
.requiredOption("--topic <topic>", "Article topic")
|
|
12
|
+
.requiredOption("--site <siteId>", "Site ID")
|
|
13
|
+
.option("--type <type>", "Article type", "blog_post")
|
|
14
|
+
.option("--length <length>", "Article length", "medium")
|
|
15
|
+
.option("--voice <voiceId>", "Voice ID")
|
|
16
|
+
.option("--keyword <keywords...>", "Target keywords")
|
|
17
|
+
.option("--wait", "Wait for completion")
|
|
18
|
+
.action(async (opts) => {
|
|
19
|
+
try {
|
|
20
|
+
const body: Record<string, unknown> = {
|
|
21
|
+
topic: opts.topic,
|
|
22
|
+
siteId: opts.site,
|
|
23
|
+
type: opts.type,
|
|
24
|
+
length: opts.length,
|
|
25
|
+
};
|
|
26
|
+
if (opts.voice) body.voiceId = opts.voice;
|
|
27
|
+
if (opts.keyword) body.keyword = opts.keyword[0];
|
|
28
|
+
|
|
29
|
+
const article = await apiPost<Article>("/v1/articles", body);
|
|
30
|
+
console.log(`Article created: ${article.id}`);
|
|
31
|
+
console.log(`Status: ${article.status}`);
|
|
32
|
+
|
|
33
|
+
if (!opts.wait) return;
|
|
34
|
+
|
|
35
|
+
let prev = article.status;
|
|
36
|
+
console.log(`Waiting for completion...`);
|
|
37
|
+
|
|
38
|
+
while (true) {
|
|
39
|
+
await sleep(3000);
|
|
40
|
+
const current = await apiGet<Article>(`/v1/articles/${article.id}`);
|
|
41
|
+
|
|
42
|
+
if (current.status !== prev) {
|
|
43
|
+
console.log(`Phase: ${current.status}`);
|
|
44
|
+
prev = current.status;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (current.status === "completed") {
|
|
48
|
+
console.log(`\nCompleted!`);
|
|
49
|
+
console.log(`Title: ${current.title ?? current.topic}`);
|
|
50
|
+
console.log(`Words: ${current.wordCount}`);
|
|
51
|
+
console.log(`ID: ${current.id}`);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (current.status === "failed") {
|
|
56
|
+
console.error(`\nArticle generation failed.`);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
} catch (e) {
|
|
61
|
+
console.error(`Error: ${(e as Error).message}`);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { apiGet } from "../client.js";
|
|
3
|
+
|
|
4
|
+
export const getCommand = new Command("get")
|
|
5
|
+
.description("Get article content")
|
|
6
|
+
.argument("<articleId>", "Article ID")
|
|
7
|
+
.option("--format <format>", "Output format: json|markdown|html", "markdown")
|
|
8
|
+
.action(async (articleId: string, opts) => {
|
|
9
|
+
try {
|
|
10
|
+
const data = await apiGet<{ content?: unknown[]; contentMarkdown?: string }>(
|
|
11
|
+
`/v1/articles/${articleId}/content`,
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
switch (opts.format) {
|
|
15
|
+
case "json":
|
|
16
|
+
console.log(JSON.stringify(data.content ?? [], null, 2));
|
|
17
|
+
break;
|
|
18
|
+
case "markdown":
|
|
19
|
+
default:
|
|
20
|
+
console.log(data.contentMarkdown ?? "");
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
} catch (e) {
|
|
24
|
+
console.error(`Error: ${(e as Error).message}`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { apiGet } from "../client.js";
|
|
3
|
+
import type { Article } from "@contentkong/shared";
|
|
4
|
+
|
|
5
|
+
function truncate(s: string, len: number): string {
|
|
6
|
+
return s.length > len ? `${s.slice(0, len - 1)}…` : s;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function pad(s: string, len: number): string {
|
|
10
|
+
return s.padEnd(len);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const listCommand = new Command("list")
|
|
14
|
+
.description("List articles")
|
|
15
|
+
.option("--status <status>", "Filter by status")
|
|
16
|
+
.option("--site <siteId>", "Filter by site ID")
|
|
17
|
+
.option("--limit <n>", "Max results", "20")
|
|
18
|
+
.action(async (opts) => {
|
|
19
|
+
try {
|
|
20
|
+
const params = new URLSearchParams();
|
|
21
|
+
if (opts.status) params.set("status", opts.status);
|
|
22
|
+
if (opts.site) params.set("siteId", opts.site);
|
|
23
|
+
params.set("limit", opts.limit);
|
|
24
|
+
|
|
25
|
+
const qs = params.toString();
|
|
26
|
+
const data = await apiGet<{ data: Article[]; count: number }>(
|
|
27
|
+
`/v1/articles${qs ? `?${qs}` : ""}`,
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const articles: Article[] = data.data ?? [];
|
|
31
|
+
|
|
32
|
+
if (articles.length === 0) {
|
|
33
|
+
console.log("No articles found.");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// header
|
|
38
|
+
console.log(
|
|
39
|
+
`${pad("ID", 38)}${pad("Title/Topic", 40)}${pad("Status", 14)}${pad("Words", 8)}Created`,
|
|
40
|
+
);
|
|
41
|
+
console.log("-".repeat(110));
|
|
42
|
+
|
|
43
|
+
for (const a of articles) {
|
|
44
|
+
const label = a.title ?? a.topic;
|
|
45
|
+
console.log(
|
|
46
|
+
`${pad(a.id, 38)}${pad(truncate(label, 38), 40)}${pad(a.status, 14)}${pad(String(a.wordCount), 8)}${a.createdAt}`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
} catch (e) {
|
|
50
|
+
console.error(`Error: ${(e as Error).message}`);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { apiGet } from "../client.js";
|
|
3
|
+
import type { Article } from "@contentkong/shared";
|
|
4
|
+
|
|
5
|
+
export const statusCommand = new Command("status")
|
|
6
|
+
.description("Check article generation status")
|
|
7
|
+
.argument("<articleId>", "Article ID")
|
|
8
|
+
.action(async (articleId: string) => {
|
|
9
|
+
try {
|
|
10
|
+
const a = await apiGet<Article>(`/v1/articles/${articleId}`);
|
|
11
|
+
console.log(`ID: ${a.id}`);
|
|
12
|
+
console.log(`Status: ${a.status}`);
|
|
13
|
+
console.log(`Topic: ${a.topic}`);
|
|
14
|
+
if (a.title) console.log(`Title: ${a.title}`);
|
|
15
|
+
console.log(`Words: ${a.wordCount}`);
|
|
16
|
+
console.log(`Type: ${a.articleType}`);
|
|
17
|
+
console.log(`Length: ${a.articleLength}`);
|
|
18
|
+
console.log(`Created: ${a.createdAt}`);
|
|
19
|
+
if (a.completedAt) console.log(`Completed: ${a.completedAt}`);
|
|
20
|
+
} catch (e) {
|
|
21
|
+
console.error(`Error: ${(e as Error).message}`);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { apiGet } from "../client.js";
|
|
3
|
+
import type { Voice } from "@contentkong/shared";
|
|
4
|
+
|
|
5
|
+
function pad(s: string, len: number): string {
|
|
6
|
+
return s.padEnd(len);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const voicesList = new Command("list")
|
|
10
|
+
.description("List all voices")
|
|
11
|
+
.action(async () => {
|
|
12
|
+
try {
|
|
13
|
+
const data = await apiGet<{ data: Voice[]; count: number }>("/v1/voices");
|
|
14
|
+
const voices: Voice[] = data.data ?? [];
|
|
15
|
+
|
|
16
|
+
if (voices.length === 0) {
|
|
17
|
+
console.log("No voices found.");
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
console.log(`${pad("ID", 38)}${pad("Name", 30)}Preset`);
|
|
22
|
+
console.log("-".repeat(80));
|
|
23
|
+
|
|
24
|
+
for (const v of voices) {
|
|
25
|
+
console.log(
|
|
26
|
+
`${pad(v.id, 38)}${pad(v.name, 30)}${v.preset ?? "-"}`,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
} catch (e) {
|
|
30
|
+
console.error(`Error: ${(e as Error).message}`);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const voicesGet = new Command("get")
|
|
36
|
+
.description("Get voice details")
|
|
37
|
+
.argument("<id>", "Voice ID")
|
|
38
|
+
.action(async (id: string) => {
|
|
39
|
+
try {
|
|
40
|
+
const v = await apiGet<Voice>(`/v1/voices/${id}`);
|
|
41
|
+
console.log(`ID: ${v.id}`);
|
|
42
|
+
console.log(`Name: ${v.name}`);
|
|
43
|
+
if (v.preset) console.log(`Preset: ${v.preset}`);
|
|
44
|
+
if (v.customDescription)
|
|
45
|
+
console.log(`Description: ${v.customDescription}`);
|
|
46
|
+
} catch (e) {
|
|
47
|
+
console.error(`Error: ${(e as Error).message}`);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export const voicesCommand = new Command("voices")
|
|
53
|
+
.description("Manage voices")
|
|
54
|
+
.addCommand(voicesList)
|
|
55
|
+
.addCommand(voicesGet);
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { configureClient } from "./client.js";
|
|
4
|
+
import { generateCommand } from "./commands/generate.js";
|
|
5
|
+
import { statusCommand } from "./commands/status.js";
|
|
6
|
+
import { getCommand } from "./commands/get.js";
|
|
7
|
+
import { listCommand } from "./commands/list.js";
|
|
8
|
+
import { exportCommand } from "./commands/export.js";
|
|
9
|
+
import { voicesCommand } from "./commands/voices.js";
|
|
10
|
+
|
|
11
|
+
const program = new Command();
|
|
12
|
+
|
|
13
|
+
program
|
|
14
|
+
.name("ckx")
|
|
15
|
+
.description("ContentKong CLI")
|
|
16
|
+
.version("0.1.0")
|
|
17
|
+
.option("--api-key <key>", "API key (or set CK_API_KEY)")
|
|
18
|
+
.option("--api-url <url>", "API base URL (or set CK_API_URL)")
|
|
19
|
+
.hook("preAction", (_thisCommand) => {
|
|
20
|
+
const opts = program.opts<{ apiKey?: string; apiUrl?: string }>();
|
|
21
|
+
configureClient({ apiKey: opts.apiKey, apiUrl: opts.apiUrl });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
program.addCommand(generateCommand);
|
|
25
|
+
program.addCommand(statusCommand);
|
|
26
|
+
program.addCommand(getCommand);
|
|
27
|
+
program.addCommand(listCommand);
|
|
28
|
+
program.addCommand(exportCommand);
|
|
29
|
+
program.addCommand(voicesCommand);
|
|
30
|
+
|
|
31
|
+
program.parse();
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"isolatedModules": true,
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"outDir": "dist",
|
|
14
|
+
"rootDir": "src"
|
|
15
|
+
},
|
|
16
|
+
"include": ["src"]
|
|
17
|
+
}
|