@9000ai/cli 0.5.3 → 0.5.4

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.js CHANGED
@@ -83,6 +83,10 @@ export function printJson(data, opts) {
83
83
  function pickFields(data, fields) {
84
84
  if (!data || typeof data !== "object")
85
85
  return data;
86
+ // Bare array — pick from each element
87
+ if (Array.isArray(data)) {
88
+ return data.map((item) => pick(item, fields));
89
+ }
86
90
  const obj = data;
87
91
  // If response has code/message/data wrapper, dig into data
88
92
  if ("code" in obj && "data" in obj) {
@@ -1,19 +1,22 @@
1
- import { loadConfig, saveConfig, resolveBaseUrl } from "../config.js";
1
+ import { loadConfig, saveConfig, resolveBaseUrl, resolveSkillsPath } from "../config.js";
2
2
  export function registerConfigCommands(parent) {
3
3
  const cmd = parent.command("config").description("Manage CLI configuration");
4
4
  cmd
5
5
  .command("set")
6
- .description("Set base URL and/or API key")
6
+ .description("Set base URL, API key, and/or skills path")
7
7
  .option("--base-url <url>", "Hub service address")
8
8
  .option("--api-key <key>", "API key for authentication")
9
+ .option("--skills-path <dir>", "Directory where skills are installed")
9
10
  .action((opts) => {
10
11
  const patch = {};
11
12
  if (opts.baseUrl)
12
13
  patch.base_url = opts.baseUrl;
13
14
  if (opts.apiKey)
14
15
  patch.api_key = opts.apiKey;
16
+ if (opts.skillsPath)
17
+ patch.skills_path = opts.skillsPath;
15
18
  if (Object.keys(patch).length === 0) {
16
- console.error("Provide at least --base-url or --api-key");
19
+ console.error("Provide at least --base-url, --api-key, or --skills-path");
17
20
  process.exit(1);
18
21
  }
19
22
  saveConfig(patch);
@@ -25,6 +28,7 @@ export function registerConfigCommands(parent) {
25
28
  .action(() => {
26
29
  const cfg = loadConfig();
27
30
  const baseUrl = resolveBaseUrl();
28
- console.log(JSON.stringify({ ...cfg, resolved_base_url: baseUrl }, null, 2));
31
+ const skillsPath = resolveSkillsPath();
32
+ console.log(JSON.stringify({ ...cfg, resolved_base_url: baseUrl, resolved_skills_path: skillsPath }, null, 2));
29
33
  });
30
34
  }
@@ -1,7 +1,6 @@
1
1
  import { existsSync, mkdirSync, copyFileSync, readFileSync, readdirSync, statSync } from "fs";
2
- import { homedir } from "os";
3
2
  import { join, relative } from "path";
4
- const TEMPLATES_DIR = join(homedir(), ".9000ai", "skills", "9000AI-hub", "init", "templates");
3
+ import { resolveSkillsPath } from "../config.js";
5
4
  /** User data directories — never overwrite even with --force */
6
5
  const USER_DATA_DIRS = ["profile", "claims", "inbox", "projects"];
7
6
  function filesEqual(a, b) {
@@ -72,9 +71,10 @@ export function registerInitCommand(program) {
72
71
  .option("--force", "Update existing files (except profile/) to latest version")
73
72
  .action((opts) => {
74
73
  const dest = opts.path;
74
+ const templatesDir = join(resolveSkillsPath(), "9000AI-hub", "init", "templates");
75
75
  // Check templates exist
76
- if (!existsSync(TEMPLATES_DIR)) {
77
- console.error(`Error: Templates not found at ${TEMPLATES_DIR}\n` +
76
+ if (!existsSync(templatesDir)) {
77
+ console.error(`Error: Templates not found at ${templatesDir}\n` +
78
78
  `Run: npm install -g @9000ai/cli`);
79
79
  process.exit(1);
80
80
  }
@@ -82,7 +82,7 @@ export function registerInitCommand(program) {
82
82
  if (!existsSync(dest)) {
83
83
  mkdirSync(dest, { recursive: true });
84
84
  }
85
- const { created, updated, skipped, outdated } = copyTemplates(TEMPLATES_DIR, dest, dest, !!opts.force);
85
+ const { created, updated, skipped, outdated } = copyTemplates(templatesDir, dest, dest, !!opts.force);
86
86
  // Output summary
87
87
  if (created.length > 0) {
88
88
  console.log(`Created ${created.length} files:`);
@@ -1,30 +1,61 @@
1
1
  import { request, printJson, pollUntilDone } from "../client.js";
2
- import { writeJson, writeTsv, listOutputFiles, readOutputJson, timestampSlug } from "../output.js";
2
+ import { writeJson, writeTsv, listOutputFiles, readOutputJson, timestampSlug, formatCsv } from "../output.js";
3
+ const ALL_BOARD_TYPES = ["hot", "entertainment", "society", "seeding", "city", "challenge"];
3
4
  export function registerSearchCommands(parent) {
4
5
  const cmd = parent.command("search").description("Douyin topic discovery — trending boards and keyword search");
5
6
  cmd
6
7
  .command("hot")
7
8
  .description("Fetch Douyin trending board")
8
- .option("--type <type>", "Board type: hot|city|seeding|entertainment|society|challenge", "hot")
9
+ .option("--type <type>", "Board type: hot|city|seeding|entertainment|society|challenge or comma-separated or 'all'", "hot")
9
10
  .option("--count <n>", "Number of items", "20")
10
11
  .option("--city <city>", "City filter (only for type=city)")
12
+ .option("--format <fmt>", "Output format: csv|json", "csv")
13
+ .option("--fields <list>", "Comma-separated fields to extract")
14
+ .option("--compact", "One JSON object per line (only for --format json)")
11
15
  .action(async (opts) => {
12
- const params = new URLSearchParams({ type: opts.type, count: opts.count });
13
- if (opts.city)
14
- params.set("city", opts.city);
15
- const data = await request({ method: "GET", path: `/api/v1/douyin/discovery/hot-board?${params}` });
16
- const resp = data;
17
- const inner = resp.data;
18
- const items = (inner?.items ?? []);
16
+ const types = opts.type === "all" ? ALL_BOARD_TYPES : opts.type.split(",").map((t) => t.trim());
17
+ const allItems = [];
18
+ const results = [];
19
+ for (const boardType of types) {
20
+ const params = new URLSearchParams({ type: boardType, count: opts.count });
21
+ if (opts.city)
22
+ params.set("city", opts.city);
23
+ const data = await request({ method: "GET", path: `/api/v1/douyin/discovery/hot-board?${params}` });
24
+ const resp = data;
25
+ const inner = resp.data;
26
+ const items = (inner?.items ?? []);
27
+ // tag each item with board_type when fetching multiple boards
28
+ if (types.length > 1) {
29
+ items.forEach((item) => { item.board_type = boardType; });
30
+ }
31
+ results.push({ type: boardType, items });
32
+ allItems.push(...items);
33
+ }
34
+ // write files
19
35
  const slug = timestampSlug();
20
- writeJson("latest_hot.json", inner);
21
- writeJson(`latest_hot_${slug}.json`, inner);
22
- if (items.length > 0) {
23
- writeTsv("latest_hot.tsv", items);
24
- writeTsv(`latest_hot_${slug}.tsv`, items);
36
+ const mergedData = types.length === 1
37
+ ? results[0].items
38
+ : { boards: results.map((r) => ({ type: r.type, count: r.items.length })), items: allItems };
39
+ writeJson("latest_hot.json", mergedData);
40
+ writeJson(`latest_hot_${slug}.json`, mergedData);
41
+ if (allItems.length > 0) {
42
+ writeTsv("latest_hot.tsv", allItems);
43
+ writeTsv(`latest_hot_${slug}.tsv`, allItems);
44
+ }
45
+ // output
46
+ const boardLabel = types.length === 1 ? types[0] : types.join(",");
47
+ console.error(`Fetched ${allItems.length} items from [${boardLabel}] → output/latest_hot.json`);
48
+ if (opts.format === "csv") {
49
+ const csvFields = opts.fields
50
+ ? opts.fields.split(",").map((f) => f.trim())
51
+ : types.length > 1
52
+ ? ["board_type", "rank", "word", "hot_value"]
53
+ : ["rank", "word", "hot_value"];
54
+ console.log(formatCsv(allItems, csvFields));
55
+ }
56
+ else {
57
+ printJson(allItems, { fields: opts.fields, compact: opts.compact });
25
58
  }
26
- console.log(`Fetched ${items.length} items → output/latest_hot.json`);
27
- printJson(inner);
28
59
  });
29
60
  cmd
30
61
  .command("keyword")
@@ -109,6 +140,37 @@ export function registerSearchCommands(parent) {
109
140
  }
110
141
  printJson(inner, { fields: opts.fields, compact: opts.compact });
111
142
  });
143
+ cmd
144
+ .command("zhihu-hot")
145
+ .description("Fetch Zhihu (知乎) trending hot list")
146
+ .option("--count <n>", "Number of items", "50")
147
+ .option("--format <fmt>", "Output format: csv|json", "csv")
148
+ .option("--fields <list>", "Comma-separated fields to extract")
149
+ .option("--compact", "One JSON object per line (only for --format json)")
150
+ .action(async (opts) => {
151
+ const params = new URLSearchParams({ count: opts.count });
152
+ const data = await request({ method: "GET", path: `/api/v1/zhihu/discovery/hot-list?${params}` });
153
+ const resp = data;
154
+ const inner = resp.data;
155
+ const items = (inner?.items ?? []);
156
+ const slug = timestampSlug();
157
+ writeJson("latest_zhihu_hot.json", inner);
158
+ writeJson(`latest_zhihu_hot_${slug}.json`, inner);
159
+ if (items.length > 0) {
160
+ writeTsv("latest_zhihu_hot.tsv", items);
161
+ writeTsv(`latest_zhihu_hot_${slug}.tsv`, items);
162
+ }
163
+ console.error(`Fetched ${items.length} items from [知乎热榜] → output/latest_zhihu_hot.json`);
164
+ if (opts.format === "csv") {
165
+ const csvFields = opts.fields
166
+ ? opts.fields.split(",").map((f) => f.trim())
167
+ : ["rank", "title", "detail_text", "answer_count"];
168
+ console.log(formatCsv(items, csvFields));
169
+ }
170
+ else {
171
+ printJson(items, { fields: opts.fields, compact: opts.compact });
172
+ }
173
+ });
112
174
  cmd
113
175
  .command("list-output")
114
176
  .description("List result files in output directory")
@@ -1,9 +1,8 @@
1
1
  import { existsSync, readFileSync, readdirSync } from "fs";
2
- import { homedir } from "os";
3
2
  import { join, resolve } from "path";
4
- const SKILLS_DIR = join(homedir(), ".9000ai", "skills");
5
- function getSkillPath(name) {
6
- return resolve(SKILLS_DIR, name, "SKILL.md");
3
+ import { resolveSkillsPath } from "../config.js";
4
+ function getSkillPath(skillsDir, name) {
5
+ return resolve(skillsDir, name, "SKILL.md");
7
6
  }
8
7
  export function registerSkillCommands(program) {
9
8
  const skill = program.command("skill").description("Skill management");
@@ -12,19 +11,20 @@ export function registerSkillCommands(program) {
12
11
  .command("list")
13
12
  .description("List all installed skills")
14
13
  .action(() => {
15
- if (!existsSync(SKILLS_DIR)) {
14
+ const skillsDir = resolveSkillsPath();
15
+ if (!existsSync(skillsDir)) {
16
16
  console.log("No skills installed. Run: npm install -g @9000ai/cli");
17
17
  return;
18
18
  }
19
19
  try {
20
- const dirs = readdirSync(SKILLS_DIR).filter((d) => existsSync(join(SKILLS_DIR, d, "SKILL.md")));
20
+ const dirs = readdirSync(skillsDir).filter((d) => existsSync(join(skillsDir, d, "SKILL.md")));
21
21
  if (dirs.length === 0) {
22
22
  console.log("No skills found.");
23
23
  return;
24
24
  }
25
- console.log(`Installed skills (${SKILLS_DIR}):\n`);
25
+ console.log(`Installed skills (${skillsDir}):\n`);
26
26
  dirs.forEach((d) => {
27
- const path = join(SKILLS_DIR, d, "SKILL.md");
27
+ const path = join(skillsDir, d, "SKILL.md");
28
28
  let description = "";
29
29
  try {
30
30
  const content = readFileSync(path, "utf-8");
@@ -44,18 +44,19 @@ export function registerSkillCommands(program) {
44
44
  .command("load <name>")
45
45
  .description("Load and print a skill's SKILL.md content to stdout")
46
46
  .action((name) => {
47
- const skillPath = getSkillPath(name);
48
- if (!existsSync(SKILLS_DIR)) {
49
- console.error(`Error: Skills directory not found at ${SKILLS_DIR}\n` +
47
+ const skillsDir = resolveSkillsPath();
48
+ const skillPath = getSkillPath(skillsDir, name);
49
+ if (!existsSync(skillsDir)) {
50
+ console.error(`Error: Skills directory not found at ${skillsDir}\n` +
50
51
  `Run: npm install -g @9000ai/cli`);
51
52
  process.exit(1);
52
53
  }
53
54
  if (!existsSync(skillPath)) {
54
55
  // Try fuzzy match
55
- const available = readdirSync(SKILLS_DIR).filter((d) => existsSync(join(SKILLS_DIR, d, "SKILL.md")));
56
+ const available = readdirSync(skillsDir).filter((d) => existsSync(join(skillsDir, d, "SKILL.md")));
56
57
  const matches = available.filter((d) => d.toLowerCase().includes(name.toLowerCase()));
57
58
  if (matches.length === 1) {
58
- const realPath = getSkillPath(matches[0]);
59
+ const realPath = getSkillPath(skillsDir, matches[0]);
59
60
  const content = readFileSync(realPath, "utf-8");
60
61
  console.log(content);
61
62
  return;
@@ -79,11 +80,12 @@ export function registerSkillCommands(program) {
79
80
  .command("path [name]")
80
81
  .description("Show skill directory path. No arg = skills root, name = specific skill path")
81
82
  .action((name) => {
83
+ const skillsDir = resolveSkillsPath();
82
84
  if (name) {
83
- console.log(getSkillPath(name));
85
+ console.log(getSkillPath(skillsDir, name));
84
86
  }
85
87
  else {
86
- console.log(SKILLS_DIR);
88
+ console.log(skillsDir);
87
89
  }
88
90
  });
89
91
  }
@@ -1,15 +1,16 @@
1
1
  import { existsSync, mkdirSync, cpSync, readFileSync, readdirSync, statSync } from "fs";
2
2
  import { dirname, join, relative } from "path";
3
- import { homedir } from "os";
4
3
  import { fileURLToPath } from "url";
4
+ import { resolveSkillsPath } from "../config.js";
5
5
  const __dirname = dirname(fileURLToPath(import.meta.url));
6
6
  export function registerUpdateCommand(program) {
7
7
  program
8
8
  .command("update")
9
9
  .description("Sync skills to latest version after npm update")
10
- .action(() => {
10
+ .option("--path <dir>", "Target skills directory (one-time, does not save to config)")
11
+ .action((opts) => {
11
12
  const src = join(__dirname, "..", "skills");
12
- const dest = join(homedir(), ".9000ai", "skills");
13
+ const dest = resolveSkillsPath(opts.path);
13
14
  if (!existsSync(src)) {
14
15
  console.error("Error: Skills not found in package. Reinstall: npm install -g @9000ai/cli");
15
16
  process.exit(1);
package/dist/config.d.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  export interface AppConfig {
2
2
  base_url?: string;
3
3
  api_key?: string;
4
+ skills_path?: string;
4
5
  }
5
6
  export declare function loadConfig(): AppConfig;
6
7
  export declare function saveConfig(patch: Partial<AppConfig>): void;
7
8
  export declare function resolveBaseUrl(override?: string): string;
8
9
  export declare function resolveApiKey(override?: string): string;
10
+ export declare function resolveSkillsPath(override?: string): string;
package/dist/config.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
- import { join } from "node:path";
3
+ import { join, resolve } from "node:path";
4
4
  const CONFIG_DIR = join(homedir(), ".9000ai");
5
5
  const CONFIG_FILE = join(CONFIG_DIR, "config.json");
6
6
  export function loadConfig() {
@@ -35,3 +35,14 @@ export function resolveApiKey(override) {
35
35
  }
36
36
  return key;
37
37
  }
38
+ export function resolveSkillsPath(override) {
39
+ if (override)
40
+ return resolve(override);
41
+ const env = process.env["9000AI_SKILLS_PATH"];
42
+ if (env)
43
+ return resolve(env);
44
+ const cfg = loadConfig();
45
+ if (cfg.skills_path)
46
+ return resolve(cfg.skills_path);
47
+ return join(homedir(), ".9000ai", "skills");
48
+ }
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ const program = new Command();
14
14
  program
15
15
  .name("9000ai")
16
16
  .description("9000AI Toolbox CLI — unified interface for 9000AI platform")
17
- .version("0.5.3");
17
+ .version("0.5.4");
18
18
  registerConfigCommands(program);
19
19
  registerAuthCommands(program);
20
20
  registerSearchCommands(program);
package/dist/output.d.ts CHANGED
@@ -3,4 +3,5 @@ export declare function writeJson(filename: string, data: unknown): string;
3
3
  export declare function writeTsv(filename: string, rows: Record<string, unknown>[]): string;
4
4
  export declare function listOutputFiles(): string[];
5
5
  export declare function readOutputJson(nameOrPath: string): unknown;
6
+ export declare function formatCsv(rows: Record<string, unknown>[], keys: string[]): string;
6
7
  export declare function timestampSlug(): string;
package/dist/output.js CHANGED
@@ -42,6 +42,16 @@ export function readOutputJson(nameOrPath) {
42
42
  }
43
43
  return JSON.parse(readFileSync(filepath, "utf-8"));
44
44
  }
45
+ export function formatCsv(rows, keys) {
46
+ const header = keys.join(",");
47
+ const lines = rows.map((row) => keys.map((k) => {
48
+ const v = String(row[k] ?? "");
49
+ return v.includes(",") || v.includes('"') || v.includes("\n")
50
+ ? `"${v.replace(/"/g, '""')}"`
51
+ : v;
52
+ }).join(","));
53
+ return [header, ...lines].join("\n");
54
+ }
45
55
  export function timestampSlug() {
46
56
  const now = new Date();
47
57
  const pad = (n) => String(n).padStart(2, "0");
@@ -4,11 +4,11 @@
4
4
  */
5
5
  import { existsSync, mkdirSync, cpSync, readFileSync, readdirSync, statSync } from "fs";
6
6
  import { dirname, join, relative } from "path";
7
- import { homedir } from "os";
8
7
  import { fileURLToPath } from "url";
8
+ import { resolveSkillsPath } from "./config.js";
9
9
  const __dirname = dirname(fileURLToPath(import.meta.url));
10
10
  const src = join(__dirname, "..", "skills");
11
- const dest = join(homedir(), ".9000ai", "skills");
11
+ const dest = resolveSkillsPath();
12
12
  // Read version
13
13
  let version = "unknown";
14
14
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@9000ai/cli",
3
- "version": "0.5.3",
3
+ "version": "0.5.4",
4
4
  "description": "9000AI Toolbox CLI — unified command-line interface for 9000AI platform",
5
5
  "type": "module",
6
6
  "bin": {