@9000ai/cli 0.5.2 → 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/CHANGELOG.md CHANGED
@@ -1,9 +1,8 @@
1
- ## What's new in v0.5.2
1
+ ## What's new in v0.5.3
2
2
 
3
- - `9000ai init --force` now compares file content only updates changed system files
3
+ - `9000ai update` sync skills after npm update, shows what changed
4
+ - `9000ai init --force` — smart diff, only updates changed system files
4
5
  - User data (profile/, claims/, inbox/, projects/) is never overwritten
5
- - postinstall shows which skill files were added or updated
6
- - Hub SKILL.md: added system update instructions
7
6
 
8
7
  ## v0.5.0
9
8
 
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
  }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function registerUpdateCommand(program: Command): void;
@@ -0,0 +1,85 @@
1
+ import { existsSync, mkdirSync, cpSync, readFileSync, readdirSync, statSync } from "fs";
2
+ import { dirname, join, relative } from "path";
3
+ import { fileURLToPath } from "url";
4
+ import { resolveSkillsPath } from "../config.js";
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ export function registerUpdateCommand(program) {
7
+ program
8
+ .command("update")
9
+ .description("Sync skills to latest version after npm update")
10
+ .option("--path <dir>", "Target skills directory (one-time, does not save to config)")
11
+ .action((opts) => {
12
+ const src = join(__dirname, "..", "skills");
13
+ const dest = resolveSkillsPath(opts.path);
14
+ if (!existsSync(src)) {
15
+ console.error("Error: Skills not found in package. Reinstall: npm install -g @9000ai/cli");
16
+ process.exit(1);
17
+ }
18
+ // Diff before copy
19
+ const created = [];
20
+ const updated = [];
21
+ function diffDir(srcDir, destDir) {
22
+ if (!existsSync(srcDir))
23
+ return;
24
+ for (const entry of readdirSync(srcDir)) {
25
+ const s = join(srcDir, entry);
26
+ const d = join(destDir, entry);
27
+ if (statSync(s).isDirectory()) {
28
+ diffDir(s, d);
29
+ }
30
+ else {
31
+ const rel = relative(dest, d);
32
+ if (!existsSync(d)) {
33
+ created.push(rel);
34
+ }
35
+ else {
36
+ try {
37
+ if (!readFileSync(s).equals(readFileSync(d))) {
38
+ updated.push(rel);
39
+ }
40
+ }
41
+ catch {
42
+ updated.push(rel);
43
+ }
44
+ }
45
+ }
46
+ }
47
+ }
48
+ diffDir(src, dest);
49
+ // Copy
50
+ mkdirSync(dest, { recursive: true });
51
+ cpSync(src, dest, { recursive: true, force: true });
52
+ // Read version
53
+ let version = "unknown";
54
+ try {
55
+ const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
56
+ version = pkg.version;
57
+ }
58
+ catch { }
59
+ // Read changelog
60
+ let changelog = "";
61
+ try {
62
+ changelog = readFileSync(join(__dirname, "..", "CHANGELOG.md"), "utf-8");
63
+ }
64
+ catch { }
65
+ // Output
66
+ console.log(`@9000ai/cli v${version}\n`);
67
+ if (created.length > 0 || updated.length > 0) {
68
+ if (created.length > 0) {
69
+ console.log(`New files:`);
70
+ created.forEach((f) => console.log(` + ${f}`));
71
+ }
72
+ if (updated.length > 0) {
73
+ console.log(`Updated files:`);
74
+ updated.forEach((f) => console.log(` ~ ${f}`));
75
+ }
76
+ console.log("");
77
+ }
78
+ else {
79
+ console.log(`Skills: already up to date\n`);
80
+ }
81
+ if (changelog) {
82
+ console.log(changelog);
83
+ }
84
+ });
85
+ }
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
@@ -9,11 +9,12 @@ import { registerTaskCommands } from "./commands/task.js";
9
9
  import { registerFeedbackCommands } from "./commands/feedback.js";
10
10
  import { registerSkillCommands } from "./commands/skill.js";
11
11
  import { registerInitCommand } from "./commands/init.js";
12
+ import { registerUpdateCommand } from "./commands/update.js";
12
13
  const program = new Command();
13
14
  program
14
15
  .name("9000ai")
15
16
  .description("9000AI Toolbox CLI — unified interface for 9000AI platform")
16
- .version("0.5.2");
17
+ .version("0.5.4");
17
18
  registerConfigCommands(program);
18
19
  registerAuthCommands(program);
19
20
  registerSearchCommands(program);
@@ -23,6 +24,7 @@ registerTaskCommands(program);
23
24
  registerFeedbackCommands(program);
24
25
  registerSkillCommands(program);
25
26
  registerInitCommand(program);
27
+ registerUpdateCommand(program);
26
28
  program.parseAsync(process.argv).catch((err) => {
27
29
  console.error(err);
28
30
  process.exit(1);
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.2",
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": {
@@ -118,13 +118,19 @@ output-format: routing-only
118
118
  ## 更新系统
119
119
 
120
120
  ```bash
121
- # 更新 CLI skills
122
- npm update -g @9000ai/cli
121
+ npm update -g @9000ai/cli && 9000ai update
122
+ ```
123
+
124
+ 第一条更新 CLI,第二条同步 skills 并显示更新内容。
125
+
126
+ 项目目录的模板也要同步时:
123
127
 
124
- # 同步最新模板到项目目录(profile/ 不会被覆盖)
128
+ ```bash
125
129
  9000ai init --path <项目目录> --force
126
130
  ```
127
131
 
132
+ profile/ 和 claims/ 等用户数据不会被覆盖。
133
+
128
134
  ## 参考文档
129
135
 
130
136
  需要更详细的使用规范时查阅: