@9000ai/cli 0.3.0 → 0.5.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/client.d.ts CHANGED
@@ -7,4 +7,13 @@ export interface RequestOptions {
7
7
  timeout?: number;
8
8
  }
9
9
  export declare function request(opts: RequestOptions): Promise<unknown>;
10
- export declare function printJson(data: unknown): void;
10
+ /** Poll a task/batch until terminal status. Returns final response data. */
11
+ export declare function pollUntilDone(path: string, opts?: {
12
+ interval?: number;
13
+ timeout?: number;
14
+ quiet?: boolean;
15
+ }): Promise<unknown>;
16
+ export declare function printJson(data: unknown, opts?: {
17
+ fields?: string;
18
+ compact?: boolean;
19
+ }): void;
package/dist/client.js CHANGED
@@ -40,6 +40,115 @@ export async function request(opts) {
40
40
  clearTimeout(timer);
41
41
  }
42
42
  }
43
- export function printJson(data) {
44
- console.log(JSON.stringify(data, null, 2));
43
+ /** Poll a task/batch until terminal status. Returns final response data. */
44
+ export async function pollUntilDone(path, opts) {
45
+ const interval = opts?.interval ?? 3_000;
46
+ const timeout = opts?.timeout ?? 600_000;
47
+ const start = Date.now();
48
+ while (Date.now() - start < timeout) {
49
+ const data = await request({ method: "GET", path });
50
+ const resp = data;
51
+ const inner = (resp.data ?? resp);
52
+ const status = (inner.status ?? "").toUpperCase();
53
+ if (["SUCCESS", "COMPLETED", "FAILED", "ERROR"].includes(status)) {
54
+ return data;
55
+ }
56
+ if (!opts?.quiet) {
57
+ const elapsed = Math.round((Date.now() - start) / 1000);
58
+ console.error(`[${elapsed}s] status: ${status || "PENDING"} — polling...`);
59
+ }
60
+ await new Promise((r) => setTimeout(r, interval));
61
+ }
62
+ console.error("Error: poll timed out");
63
+ process.exit(1);
64
+ }
65
+ export function printJson(data, opts) {
66
+ let output = data;
67
+ // --fields: extract specified fields from items/data
68
+ if (opts?.fields) {
69
+ const fieldList = opts.fields.split(",").map((f) => f.trim());
70
+ output = pickFields(output, fieldList);
71
+ }
72
+ // --compact: one-line-per-item for arrays
73
+ if (opts?.compact) {
74
+ const arr = extractArray(output);
75
+ if (arr) {
76
+ arr.forEach((item) => console.log(JSON.stringify(item)));
77
+ return;
78
+ }
79
+ }
80
+ console.log(JSON.stringify(output, null, 2));
81
+ }
82
+ /** Deep-pick fields from API response. Handles nested data.items / data.tasks structures. */
83
+ function pickFields(data, fields) {
84
+ if (!data || typeof data !== "object")
85
+ return data;
86
+ const obj = data;
87
+ // If response has code/message/data wrapper, dig into data
88
+ if ("code" in obj && "data" in obj) {
89
+ const inner = obj.data;
90
+ if (!inner)
91
+ return obj;
92
+ return pickFieldsFromInner(inner, fields);
93
+ }
94
+ // Already unwrapped (e.g. inner passed directly)
95
+ return pickFieldsFromInner(obj, fields);
96
+ }
97
+ function pickFieldsFromInner(inner, fields) {
98
+ // If has items array, pick from each item
99
+ if (Array.isArray(inner.items)) {
100
+ return {
101
+ ...stripArrays(inner),
102
+ items: inner.items.map((item) => pick(item, fields)),
103
+ };
104
+ }
105
+ // If has tasks array with nested items
106
+ if (Array.isArray(inner.tasks)) {
107
+ return {
108
+ ...stripArrays(inner),
109
+ tasks: inner.tasks.map((task) => {
110
+ const t = { ...task };
111
+ if (Array.isArray(t.items)) {
112
+ t.items = t.items.map((item) => pick(item, fields));
113
+ }
114
+ return t;
115
+ }),
116
+ };
117
+ }
118
+ // Flat data object
119
+ return pick(inner, fields);
120
+ }
121
+ function pick(obj, fields) {
122
+ const result = {};
123
+ for (const field of fields) {
124
+ if (field in obj) {
125
+ result[field] = obj[field];
126
+ }
127
+ }
128
+ return result;
129
+ }
130
+ function stripArrays(obj) {
131
+ const result = {};
132
+ for (const [k, v] of Object.entries(obj)) {
133
+ if (!Array.isArray(v))
134
+ result[k] = v;
135
+ }
136
+ return result;
137
+ }
138
+ function extractArray(data) {
139
+ if (Array.isArray(data))
140
+ return data;
141
+ if (!data || typeof data !== "object")
142
+ return null;
143
+ const obj = data;
144
+ // Wrapped response: { code, data: { items: [...] } }
145
+ if ("code" in obj && "data" in obj) {
146
+ const inner = obj.data;
147
+ if (inner && Array.isArray(inner.items))
148
+ return inner.items;
149
+ }
150
+ // Already unwrapped: { items: [...] }
151
+ if (Array.isArray(obj.items))
152
+ return obj.items;
153
+ return null;
45
154
  }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function registerInitCommand(program: Command): void;
@@ -0,0 +1,83 @@
1
+ import { existsSync, mkdirSync, copyFileSync, readdirSync, statSync } from "fs";
2
+ import { homedir } from "os";
3
+ import { join, relative } from "path";
4
+ const TEMPLATES_DIR = join(homedir(), ".9000ai", "skills", "9000AI-hub", "init", "templates");
5
+ /** Files under profile/ are never overwritten — user may have edited them. */
6
+ const NEVER_OVERWRITE_DIRS = ["profile"];
7
+ function copyTemplates(src, dest, relBase) {
8
+ const created = [];
9
+ const skipped = [];
10
+ if (!existsSync(src))
11
+ return { created, skipped };
12
+ const entries = readdirSync(src);
13
+ for (const entry of entries) {
14
+ const srcPath = join(src, entry);
15
+ const destPath = join(dest, entry);
16
+ const rel = relative(relBase, destPath);
17
+ const stat = statSync(srcPath);
18
+ if (stat.isDirectory()) {
19
+ if (!existsSync(destPath)) {
20
+ mkdirSync(destPath, { recursive: true });
21
+ }
22
+ const sub = copyTemplates(srcPath, destPath, relBase);
23
+ created.push(...sub.created);
24
+ skipped.push(...sub.skipped);
25
+ }
26
+ else {
27
+ if (existsSync(destPath)) {
28
+ // Never overwrite profile files
29
+ const topDir = rel.split(/[/\\]/)[0];
30
+ if (NEVER_OVERWRITE_DIRS.includes(topDir)) {
31
+ skipped.push(rel);
32
+ continue;
33
+ }
34
+ // Other existing files: also skip (idempotent)
35
+ skipped.push(rel);
36
+ }
37
+ else {
38
+ mkdirSync(join(destPath, ".."), { recursive: true });
39
+ copyFileSync(srcPath, destPath);
40
+ created.push(rel);
41
+ }
42
+ }
43
+ }
44
+ return { created, skipped };
45
+ }
46
+ export function registerInitCommand(program) {
47
+ program
48
+ .command("init")
49
+ .description("Initialize content workspace — copy templates to project directory")
50
+ .requiredOption("--path <dir>", "Project directory to initialize")
51
+ .action((opts) => {
52
+ const dest = opts.path;
53
+ // Check templates exist
54
+ if (!existsSync(TEMPLATES_DIR)) {
55
+ console.error(`Error: Templates not found at ${TEMPLATES_DIR}\n` +
56
+ `Run: npm install -g @9000ai/cli`);
57
+ process.exit(1);
58
+ }
59
+ // Create dest if needed
60
+ if (!existsSync(dest)) {
61
+ mkdirSync(dest, { recursive: true });
62
+ }
63
+ const { created, skipped } = copyTemplates(TEMPLATES_DIR, dest, dest);
64
+ // Output summary
65
+ if (created.length > 0) {
66
+ console.log(`Created ${created.length} files:`);
67
+ created.forEach((f) => console.log(` + ${f}`));
68
+ }
69
+ if (skipped.length > 0) {
70
+ console.log(`Skipped ${skipped.length} existing files:`);
71
+ skipped.forEach((f) => console.log(` - ${f}`));
72
+ }
73
+ if (created.length === 0 && skipped.length === 0) {
74
+ console.log("No template files found.");
75
+ return;
76
+ }
77
+ console.log(`\nWorkspace ready: ${dest}`);
78
+ console.log(`\nNext steps:`);
79
+ console.log(` 1. Fill in profile/ (identity.md, voice.md, topics.md, product.md)`);
80
+ console.log(` 2. cd ${dest}`);
81
+ console.log(` 3. Open Claude Code and start working`);
82
+ });
83
+ }
@@ -5,9 +5,11 @@ export function registerMonitorCommands(parent) {
5
5
  cmd
6
6
  .command("list-creators")
7
7
  .description("List all monitoring targets")
8
- .action(async () => {
8
+ .option("--fields <list>", "Comma-separated fields to extract")
9
+ .option("--compact", "One JSON object per line")
10
+ .action(async (opts) => {
9
11
  const data = await request({ method: "GET", path: "/api/v1/douyin/monitor/creators" });
10
- printJson(data);
12
+ printJson(data, { fields: opts.fields, compact: opts.compact });
11
13
  });
12
14
  cmd
13
15
  .command("create-creator")
@@ -83,19 +85,40 @@ export function registerMonitorCommands(parent) {
83
85
  .option("--page <n>", "Page number", "1")
84
86
  .option("--page-size <n>", "Items per page", "20")
85
87
  .option("--status <status>", "Filter by status")
88
+ .option("--fields <list>", "Comma-separated fields to extract")
89
+ .option("--compact", "One JSON object per line")
86
90
  .action(async (opts) => {
87
91
  const params = new URLSearchParams({ page: opts.page, page_size: opts.pageSize });
88
92
  if (opts.status)
89
93
  params.set("status", opts.status);
90
94
  const data = await request({ method: "GET", path: `/api/v1/douyin/monitor/runs?${params}` });
91
- printJson(data);
95
+ printJson(data, { fields: opts.fields, compact: opts.compact });
92
96
  });
93
97
  cmd
94
98
  .command("run-detail")
95
99
  .description("View single run details")
96
100
  .requiredOption("--run-id <id>", "Run ID")
101
+ .option("--fields <list>", "Comma-separated fields to extract")
102
+ .option("--compact", "One JSON object per line")
97
103
  .action(async (opts) => {
98
104
  const data = await request({ method: "GET", path: `/api/v1/douyin/monitor/runs/${opts.runId}` });
99
- printJson(data);
105
+ printJson(data, { fields: opts.fields, compact: opts.compact });
106
+ });
107
+ cmd
108
+ .command("fetch")
109
+ .description("Fetch user homepage videos directly (sync, no queue)")
110
+ .requiredOption("--sec-user <id>", "User sec_user_id or homepage URL")
111
+ .option("--count <n>", "Number of videos to fetch", "20")
112
+ .option("--sort <n>", "Sort: 0=newest 1=hottest", "0")
113
+ .option("--fields <list>", "Comma-separated fields to extract")
114
+ .option("--compact", "One JSON object per line")
115
+ .action(async (opts) => {
116
+ const params = new URLSearchParams({
117
+ sec_user_id: opts.secUser,
118
+ count: opts.count,
119
+ sort_type: opts.sort,
120
+ });
121
+ const data = await request({ method: "GET", path: `/api/v1/douyin/monitor/user-posts?${params}` });
122
+ printJson(data, { fields: opts.fields, compact: opts.compact });
100
123
  });
101
124
  }
@@ -1,4 +1,4 @@
1
- import { request, printJson } from "../client.js";
1
+ import { request, printJson, pollUntilDone } from "../client.js";
2
2
  import { writeJson, writeTsv, listOutputFiles, readOutputJson, timestampSlug } from "../output.js";
3
3
  export function registerSearchCommands(parent) {
4
4
  const cmd = parent.command("search").description("Douyin topic discovery — trending boards and keyword search");
@@ -36,6 +36,9 @@ export function registerSearchCommands(parent) {
36
36
  .option("--filter-duration <v>", "Duration filter", "")
37
37
  .option("--min-likes <n>", "Minimum likes", "0")
38
38
  .option("--min-comments <n>", "Minimum comments", "0")
39
+ .option("--wait", "Poll until batch completes then print results")
40
+ .option("--fields <list>", "Comma-separated fields to extract (used with --wait)")
41
+ .option("--compact", "One JSON object per line (used with --wait)")
39
42
  .action(async (keywords, opts) => {
40
43
  const payload = {
41
44
  keywords,
@@ -55,14 +58,39 @@ export function registerSearchCommands(parent) {
55
58
  });
56
59
  const resp = data;
57
60
  const inner = resp.data;
58
- console.log(`Batch submitted batch_id: ${inner?.batch_id}`);
59
- console.log(`Use: 9000ai search batch-result --batch-id ${inner?.batch_id}`);
60
- printJson(inner);
61
+ const batchId = inner?.batch_id;
62
+ if (!opts.wait || !batchId) {
63
+ console.log(`Batch submitted → batch_id: ${batchId}`);
64
+ console.log(`Use: 9000ai search batch-result --batch-id ${batchId}`);
65
+ printJson(inner);
66
+ return;
67
+ }
68
+ // --wait: poll then fetch results
69
+ console.error(`Batch submitted → ${batchId}, waiting...`);
70
+ await pollUntilDone(`/api/v1/tasks/batch/${batchId}`);
71
+ const resultData = await request({
72
+ method: "GET",
73
+ path: `/api/v1/douyin/discovery/search-videos/batch/${batchId}/results`,
74
+ });
75
+ const resultResp = resultData;
76
+ const resultInner = resultResp.data;
77
+ const items = (resultInner?.items ?? []);
78
+ if (items.length > 0) {
79
+ const slug = timestampSlug();
80
+ writeJson("latest_search.json", resultInner);
81
+ writeJson(`latest_search_${slug}.json`, resultInner);
82
+ writeTsv("latest_search.tsv", items);
83
+ writeTsv(`latest_search_${slug}.tsv`, items);
84
+ console.error(`${items.length} items → output/latest_search.json`);
85
+ }
86
+ printJson(resultInner, { fields: opts.fields, compact: opts.compact });
61
87
  });
62
88
  cmd
63
89
  .command("batch-result")
64
90
  .description("Query async search batch results")
65
91
  .requiredOption("--batch-id <id>", "Batch ID")
92
+ .option("--fields <list>", "Comma-separated fields to extract (e.g. desc,author_name,likes,video_url)")
93
+ .option("--compact", "One JSON object per line, no indentation")
66
94
  .action(async (opts) => {
67
95
  const data = await request({
68
96
  method: "GET",
@@ -79,7 +107,7 @@ export function registerSearchCommands(parent) {
79
107
  writeTsv(`latest_search_${slug}.tsv`, items);
80
108
  console.log(`${items.length} items → output/latest_search.json`);
81
109
  }
82
- printJson(inner);
110
+ printJson(inner, { fields: opts.fields, compact: opts.compact });
83
111
  });
84
112
  cmd
85
113
  .command("list-output")
@@ -5,16 +5,20 @@ export function registerTaskCommands(parent) {
5
5
  .command("status")
6
6
  .description("Check task status")
7
7
  .requiredOption("--task-id <id>", "Task ID")
8
+ .option("--fields <list>", "Comma-separated fields to extract (e.g. desc,video_url,likes)")
9
+ .option("--compact", "One JSON object per line, no indentation")
8
10
  .action(async (opts) => {
9
11
  const data = await request({ method: "GET", path: `/api/v1/tasks/${opts.taskId}` });
10
- printJson(data);
12
+ printJson(data, { fields: opts.fields, compact: opts.compact });
11
13
  });
12
14
  cmd
13
15
  .command("results")
14
16
  .description("Get task results")
15
17
  .requiredOption("--task-id <id>", "Task ID")
18
+ .option("--fields <list>", "Comma-separated fields to extract (e.g. desc,video_url,likes)")
19
+ .option("--compact", "One JSON object per line, no indentation")
16
20
  .action(async (opts) => {
17
21
  const data = await request({ method: "GET", path: `/api/v1/tasks/${opts.taskId}/results` });
18
- printJson(data);
22
+ printJson(data, { fields: opts.fields, compact: opts.compact });
19
23
  });
20
24
  }
@@ -1,4 +1,4 @@
1
- import { request, printJson } from "../client.js";
1
+ import { request, printJson, pollUntilDone } from "../client.js";
2
2
  import { loadJsonFile } from "../utils/format.js";
3
3
  export function registerTranscribeCommands(parent) {
4
4
  const cmd = parent.command("transcribe").description("Video / audio transcription");
@@ -6,6 +6,9 @@ export function registerTranscribeCommands(parent) {
6
6
  .command("submit")
7
7
  .description("Submit batch video-to-text task")
8
8
  .requiredOption("--json-file <path>", "JSON file with video task details")
9
+ .option("--wait", "Poll until task completes then print results")
10
+ .option("--fields <list>", "Comma-separated fields to extract (used with --wait)")
11
+ .option("--compact", "One JSON object per line (used with --wait)")
9
12
  .action(async (opts) => {
10
13
  const payload = loadJsonFile(opts.jsonFile);
11
14
  const data = await request({
@@ -13,7 +16,21 @@ export function registerTranscribeCommands(parent) {
13
16
  path: "/api/v1/media/batch-video-to-text",
14
17
  payload,
15
18
  });
16
- printJson(data);
19
+ const resp = data;
20
+ const inner = resp.data;
21
+ const batchId = inner?.batch_id;
22
+ if (!opts.wait || !batchId) {
23
+ printJson(data);
24
+ return;
25
+ }
26
+ // --wait: poll then fetch results
27
+ console.error(`Batch submitted → ${batchId}, waiting...`);
28
+ await pollUntilDone(`/api/v1/tasks/batch/${batchId}`);
29
+ const resultData = await request({
30
+ method: "GET",
31
+ path: `/api/v1/tasks/batch/${batchId}`,
32
+ });
33
+ printJson(resultData, { fields: opts.fields, compact: opts.compact });
17
34
  });
18
35
  cmd
19
36
  .command("text")
package/dist/index.d.ts CHANGED
@@ -1 +1,2 @@
1
+ #!/usr/bin/env node
1
2
  export {};
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env node
1
2
  import { Command } from "commander";
2
3
  import { registerConfigCommands } from "./commands/config.js";
3
4
  import { registerAuthCommands } from "./commands/auth.js";
@@ -7,11 +8,12 @@ import { registerTranscribeCommands } from "./commands/transcribe.js";
7
8
  import { registerTaskCommands } from "./commands/task.js";
8
9
  import { registerFeedbackCommands } from "./commands/feedback.js";
9
10
  import { registerSkillCommands } from "./commands/skill.js";
11
+ import { registerInitCommand } from "./commands/init.js";
10
12
  const program = new Command();
11
13
  program
12
14
  .name("9000ai")
13
15
  .description("9000AI Toolbox CLI — unified interface for 9000AI platform")
14
- .version("0.1.0");
16
+ .version("0.5.0");
15
17
  registerConfigCommands(program);
16
18
  registerAuthCommands(program);
17
19
  registerSearchCommands(program);
@@ -20,6 +22,7 @@ registerTranscribeCommands(program);
20
22
  registerTaskCommands(program);
21
23
  registerFeedbackCommands(program);
22
24
  registerSkillCommands(program);
25
+ registerInitCommand(program);
23
26
  program.parseAsync(process.argv).catch((err) => {
24
27
  console.error(err);
25
28
  process.exit(1);
@@ -14,4 +14,9 @@ if (!existsSync(src)) {
14
14
  }
15
15
  mkdirSync(dest, { recursive: true });
16
16
  cpSync(src, dest, { recursive: true, force: true });
17
- console.log(`9000AI skills installed → ${dest}`);
17
+ console.log(`\n9000AI CLI installed successfully!\n`);
18
+ console.log(`Skills installed: ${dest}\n`);
19
+ console.log(`Get started:`);
20
+ console.log(` 1. 9000ai config set --base-url <server-url> --api-key <key>`);
21
+ console.log(` 2. 9000ai init --path <your-project-dir>`);
22
+ console.log(` 3. cd <your-project-dir> → open Claude Code → start working\n`);
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@9000ai/cli",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "9000AI Toolbox CLI — unified command-line interface for 9000AI platform",
5
5
  "type": "module",
6
6
  "bin": {
7
- "9000ai": "./dist/index"
7
+ "9000ai": "./dist/index.js"
8
8
  },
9
9
  "scripts": {
10
10
  "build": "tsc",