chub-dev 0.2.0-beta.3 → 0.3.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 ADDED
@@ -0,0 +1,76 @@
1
+ # Context Hub CLI
2
+
3
+ Install the CLI and give your AI agent access to curated, versioned documentation.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g chub-dev
9
+ ```
10
+
11
+ ## Use as an Agent Skill
12
+
13
+ The CLI ships with a bootstrap skill for coding agents. The skill makes sure
14
+ `chub` exists, runs `chub help`, then tells the agent to use chub instead of
15
+ guessing from training data.
16
+
17
+ If your ecosystem supports the Agent Skills installer flow, use:
18
+
19
+ ```bash
20
+ npx skills add chub-dev
21
+ ```
22
+
23
+ Otherwise, install the packaged skill manually into your agent tool of choice:
24
+
25
+ ### Claude Code
26
+
27
+ Copy the skill into your project:
28
+
29
+ ```bash
30
+ mkdir -p .claude/skills
31
+ cp $(npm root -g)/chub-dev/skills/get-api-docs/SKILL.md .claude/skills/get-api-docs.md
32
+ ```
33
+
34
+ Or install it globally (applies to all projects):
35
+
36
+ ```bash
37
+ mkdir -p ~/.claude/skills
38
+ cp $(npm root -g)/chub-dev/skills/get-api-docs/SKILL.md ~/.claude/skills/get-api-docs.md
39
+ ```
40
+
41
+ ### Cursor
42
+
43
+ Copy the skill into your project's rules directory:
44
+
45
+ ```bash
46
+ mkdir -p .cursor/rules
47
+ cp $(npm root -g)/chub-dev/skills/get-api-docs/SKILL.md .cursor/rules/get-api-docs.md
48
+ ```
49
+
50
+ ### Other Agent Tools
51
+
52
+ The skill is a standard markdown file at `skills/get-api-docs/SKILL.md`. Copy it to wherever your agent tool reads custom instructions from.
53
+
54
+ ## Runtime Bootstrap
55
+
56
+ Once chub is installed, start here:
57
+
58
+ ```bash
59
+ chub help
60
+ ```
61
+
62
+ `chub help` fetches the latest bootstrap instructions from the network and falls
63
+ back to the bundled local help if the network is unavailable.
64
+
65
+ ## Commands
66
+
67
+ ```bash
68
+ chub search "stripe" # find docs
69
+ chub get stripe/api # fetch a doc
70
+ chub get stripe/api --lang js # specific language
71
+ chub get stripe/api --version 19.1.0 # specific version
72
+ chub annotate stripe/api "note" # local annotation
73
+ chub feedback stripe/api up # rate a doc
74
+ ```
75
+
76
+ For the full command reference, see [CLI Reference](../docs/cli-reference.md).
package/bin/chub-mcp ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import '../src/mcp/server.js';
package/package.json CHANGED
@@ -1,15 +1,17 @@
1
1
  {
2
2
  "name": "chub-dev",
3
- "version": "0.2.0-beta.3",
3
+ "version": "0.3.0",
4
4
  "description": "CLI for Context Hub - search and retrieve LLM-optimized docs and skills",
5
5
  "type": "module",
6
6
  "bin": {
7
- "chub": "./bin/chub"
7
+ "chub": "./bin/chub",
8
+ "chub-mcp": "./bin/chub-mcp"
8
9
  },
9
10
  "files": [
10
11
  "bin/",
11
12
  "src/",
12
- "dist/"
13
+ "dist/",
14
+ "skills/"
13
15
  ],
14
16
  "engines": {
15
17
  "node": ">=18.0.0"
@@ -21,7 +23,8 @@
21
23
  "agent",
22
24
  "cli",
23
25
  "context",
24
- "skills"
26
+ "skills",
27
+ "mcp"
25
28
  ],
26
29
  "license": "MIT",
27
30
  "repository": {
@@ -39,13 +42,15 @@
39
42
  "test:coverage": "vitest run --coverage"
40
43
  },
41
44
  "dependencies": {
45
+ "@modelcontextprotocol/sdk": "^1.27.1",
42
46
  "chalk": "^5.3.0",
43
47
  "commander": "^12.0.0",
44
48
  "posthog-node": "^5.24.17",
45
49
  "tar": "^7.5.8",
46
- "yaml": "^2.3.0"
50
+ "yaml": "^2.3.0",
51
+ "zod": "^4.3.6"
47
52
  },
48
53
  "devDependencies": {
49
- "vitest": "^3.0.0"
54
+ "vitest": "^4.0.18"
50
55
  }
51
56
  }
@@ -0,0 +1,81 @@
1
+ ---
2
+ name: get-api-docs
3
+ description: >
4
+ Use this skill in any coding agent with terminal access when you need current
5
+ documentation or skills for a third-party library, SDK, API, or developer
6
+ workflow. Make sure chub is installed, run `chub help` first, then use chub
7
+ instead of guessing from training knowledge.
8
+ ---
9
+
10
+ # Bootstrap chub
11
+
12
+ Use this skill in any coding agent that can run shell commands. Its job is to
13
+ bootstrap chub, then defer to `chub help` for the latest instructions.
14
+
15
+ ## Step 1 — Ensure `chub` exists
16
+
17
+ Check whether chub is already installed:
18
+
19
+ ```bash
20
+ command -v chub >/dev/null 2>&1
21
+ ```
22
+
23
+ If that command fails, and the environment has `node`, `npm`, network access,
24
+ and package-manager permission, install chub:
25
+
26
+ ```bash
27
+ npm install -g chub-dev
28
+ ```
29
+
30
+ If installation is blocked because `npm` is unavailable or permissions are
31
+ restricted, tell the user exactly what is needed instead of guessing.
32
+
33
+ ## Step 2 — Start with `chub help`
34
+
35
+ Once chub is available, always begin here:
36
+
37
+ ```bash
38
+ chub help
39
+ ```
40
+
41
+ `chub help` gives the current bootstrap instructions. Follow it instead of
42
+ relying on stale instructions in this file.
43
+
44
+ ## Step 3 — Use chub for external docs and skills
45
+
46
+ Use chub whenever the task depends on current third-party documentation or an
47
+ agent skill. Typical flow:
48
+
49
+ ```bash
50
+ chub search "<library or workflow>" --json
51
+ chub get <id> --lang py
52
+ ```
53
+
54
+ Do not guess API shapes or workflow steps when chub can provide the current doc
55
+ or skill.
56
+
57
+ ## Step 4 — Save annotations when you learn something local
58
+
59
+ ```bash
60
+ chub annotate <id> "Concise project-specific note"
61
+ ```
62
+
63
+ Use annotations for concise, actionable notes that should appear automatically
64
+ the next time the same entry is fetched.
65
+
66
+ ## Step 5 — Always leave feedback after using an entry
67
+
68
+ After using a doc or skill, always run feedback. This is required unless the
69
+ user explicitly tells you not to.
70
+
71
+ ```bash
72
+ chub feedback <id> up --label accurate "What was useful"
73
+ chub feedback <id> down --label outdated "What needs fixing"
74
+ ```
75
+
76
+ Valid labels: `accurate`, `well-structured`, `helpful`, `good-examples`,
77
+ `outdated`, `inaccurate`, `incomplete`, `wrong-examples`, `wrong-version`,
78
+ `poorly-structured`.
79
+
80
+ Do not include source code, secrets, project architecture, or other sensitive
81
+ details in feedback comments.
@@ -0,0 +1,83 @@
1
+ import chalk from 'chalk';
2
+ import { readAnnotation, writeAnnotation, clearAnnotation, listAnnotations } from '../lib/annotations.js';
3
+ import { output, error, info } from '../lib/output.js';
4
+
5
+ export function registerAnnotateCommand(program) {
6
+ program
7
+ .command('annotate [id] [note]')
8
+ .description('Attach agent notes to a doc or skill')
9
+ .option('--clear', 'Remove annotation for this entry')
10
+ .option('--list', 'List all annotations')
11
+ .action((id, note, opts) => {
12
+ const globalOpts = program.optsWithGlobals();
13
+
14
+ if (opts.list) {
15
+ const annotations = listAnnotations();
16
+ output(
17
+ annotations,
18
+ (data) => {
19
+ if (data.length === 0) {
20
+ console.log('No annotations.');
21
+ return;
22
+ }
23
+ for (const a of data) {
24
+ console.log(`${chalk.bold(a.id)} ${chalk.dim(`(${a.updatedAt})`)}`);
25
+ console.log(` ${a.note}`);
26
+ console.log();
27
+ }
28
+ },
29
+ globalOpts
30
+ );
31
+ return;
32
+ }
33
+
34
+ if (!id) {
35
+ error('Missing required argument: <id>. Run: chub annotate <id> <note> | chub annotate <id> --clear | chub annotate --list', globalOpts);
36
+ }
37
+
38
+ if (opts.clear) {
39
+ const removed = clearAnnotation(id);
40
+ output(
41
+ { id, cleared: removed },
42
+ (data) => {
43
+ if (data.cleared) {
44
+ console.log(`Annotation cleared for ${chalk.bold(id)}.`);
45
+ } else {
46
+ console.log(`No annotation found for ${chalk.bold(id)}.`);
47
+ }
48
+ },
49
+ globalOpts
50
+ );
51
+ return;
52
+ }
53
+
54
+ if (!note) {
55
+ // Show existing annotation
56
+ const existing = readAnnotation(id);
57
+ if (existing) {
58
+ output(
59
+ existing,
60
+ (data) => {
61
+ console.log(`${chalk.bold(data.id)} ${chalk.dim(`(${data.updatedAt})`)}`);
62
+ console.log(data.note);
63
+ },
64
+ globalOpts
65
+ );
66
+ } else {
67
+ output(
68
+ { id, note: null },
69
+ () => console.log(`No annotation for ${chalk.bold(id)}.`),
70
+ globalOpts
71
+ );
72
+ }
73
+ return;
74
+ }
75
+
76
+ const data = writeAnnotation(id, note);
77
+ output(
78
+ data,
79
+ (d) => console.log(`Annotation saved for ${chalk.bold(d.id)}.`),
80
+ globalOpts
81
+ );
82
+ });
83
+ }
@@ -1,9 +1,18 @@
1
1
  import { existsSync, readFileSync, readdirSync, statSync, writeFileSync, mkdirSync, cpSync } from 'node:fs';
2
- import { join, relative, dirname } from 'node:path';
2
+ import { join, relative, dirname, basename } from 'node:path';
3
3
  import chalk from 'chalk';
4
4
  import { parseFrontmatter } from '../lib/frontmatter.js';
5
5
  import { info } from '../lib/output.js';
6
6
  import { trackEvent } from '../lib/analytics.js';
7
+ import { buildIndex } from '../lib/bm25.js';
8
+
9
+ /**
10
+ * Normalize a path to use forward slashes so registry.json is
11
+ * consistent regardless of which OS ran the build.
12
+ */
13
+ function toPosix(p) {
14
+ return p.split('\\').join('/');
15
+ }
7
16
 
8
17
  /**
9
18
  * Recursively find all DOC.md and SKILL.md files under a directory.
@@ -30,7 +39,7 @@ function listDirFiles(dir) {
30
39
  for (const entry of readdirSync(d, { withFileTypes: true })) {
31
40
  const full = join(d, entry.name);
32
41
  if (entry.isDirectory()) walk(full);
33
- else results.push(relative(dir, full));
42
+ else results.push(toPosix(relative(dir, full)));
34
43
  }
35
44
  };
36
45
  walk(dir);
@@ -82,7 +91,7 @@ function discoverAuthor(authorDir, authorName, contentDir) {
82
91
  const tags = meta.tags ? meta.tags.split(',').map((t) => t.trim()) : [];
83
92
  const updatedOn = meta['updated-on'] || new Date().toISOString().split('T')[0];
84
93
  const entryDir = dirname(ef.path);
85
- const entryPath = relative(contentDir, entryDir);
94
+ const entryPath = toPosix(relative(contentDir, entryDir));
86
95
  const files = listDirFiles(entryDir);
87
96
  const size = dirSize(entryDir);
88
97
 
@@ -301,6 +310,14 @@ export function registerBuildCommand(program) {
301
310
  mkdirSync(outputDir, { recursive: true });
302
311
  writeFileSync(join(outputDir, 'registry.json'), JSON.stringify(registry, null, 2));
303
312
 
313
+ // Build and write BM25 search index
314
+ const allEntries = [
315
+ ...allDocs.map((d) => ({ ...d, _type: 'doc' })),
316
+ ...allSkills.map((s) => ({ ...s, _type: 'skill' })),
317
+ ];
318
+ const searchIndex = buildIndex(allEntries);
319
+ writeFileSync(join(outputDir, 'search-index.json'), JSON.stringify(searchIndex));
320
+
304
321
  // Copy content tree
305
322
  for (const authorEntry of topLevel) {
306
323
  const src = join(contentDir, authorEntry.name);
@@ -308,7 +325,7 @@ export function registerBuildCommand(program) {
308
325
  // Skip registry.json in author dirs
309
326
  cpSync(src, dest, {
310
327
  recursive: true,
311
- filter: (s) => !s.endsWith('/registry.json'),
328
+ filter: (s) => basename(s) !== 'registry.json',
312
329
  });
313
330
  }
314
331
 
@@ -3,7 +3,7 @@ import { readFileSync } from 'node:fs';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { dirname, join } from 'node:path';
5
5
  import { getEntry } from '../lib/registry.js';
6
- import { sendFeedback, isTelemetryEnabled, getTelemetryUrl } from '../lib/telemetry.js';
6
+ import { sendFeedback, isFeedbackEnabled, isTelemetryEnabled, getTelemetryUrl } from '../lib/telemetry.js';
7
7
  import { getOrCreateClientId } from '../lib/identity.js';
8
8
  import { output, error } from '../lib/output.js';
9
9
  import { trackEvent } from '../lib/analytics.js';
@@ -32,24 +32,27 @@ export function registerFeedbackCommand(program) {
32
32
  .option('--label <label>', 'Feedback label (repeatable: --label outdated --label wrong-examples)', collect, [])
33
33
  .option('--agent <name>', 'AI coding tool name')
34
34
  .option('--model <model>', 'LLM model name')
35
- .option('--status', 'Show telemetry status')
35
+ .option('--status', 'Show feedback and telemetry status')
36
36
  .action(async (id, rating, comment, opts) => {
37
37
  const globalOpts = program.optsWithGlobals();
38
38
 
39
39
  // --status flag
40
40
  if (opts.status) {
41
- const enabled = isTelemetryEnabled();
41
+ const feedbackEnabled = isFeedbackEnabled();
42
+ const telemetryEnabled = isTelemetryEnabled();
42
43
  if (globalOpts.json) {
43
44
  let clientId = null;
44
45
  try { clientId = await getOrCreateClientId(); } catch {}
45
46
  console.log(JSON.stringify({
46
- telemetry: enabled,
47
+ feedback: feedbackEnabled,
48
+ telemetry: telemetryEnabled,
47
49
  client_id_prefix: clientId ? clientId.slice(0, 8) : null,
48
50
  endpoint: getTelemetryUrl(),
49
51
  valid_labels: VALID_LABELS,
50
52
  }));
51
53
  } else {
52
- console.log(`Telemetry: ${enabled ? chalk.green('enabled') : chalk.red('disabled')}`);
54
+ console.log(`Feedback: ${feedbackEnabled ? chalk.green('enabled') : chalk.red('disabled')}`);
55
+ console.log(`Telemetry: ${telemetryEnabled ? chalk.green('enabled') : chalk.red('disabled')}`);
53
56
  try {
54
57
  const cid = await getOrCreateClientId();
55
58
  console.log(`Client ID: ${cid.slice(0, 8)}...`);
@@ -62,17 +65,17 @@ export function registerFeedbackCommand(program) {
62
65
 
63
66
  // BUG #1 FIX: Validation errors respect --json flag
64
67
  if (!id || !rating) {
65
- error('Missing arguments. Usage: chub feedback <id> <up|down> [comment]', globalOpts);
68
+ error('Missing required arguments: <id> and <rating>. Run: chub feedback <id> <up|down> [comment]', globalOpts);
66
69
  }
67
70
 
68
71
  if (rating !== 'up' && rating !== 'down') {
69
72
  error('Rating must be "up" or "down".', globalOpts);
70
73
  }
71
74
 
72
- if (!isTelemetryEnabled()) {
75
+ if (!isFeedbackEnabled()) {
73
76
  output(
74
- { status: 'skipped', reason: 'telemetry_disabled' },
75
- () => console.log(chalk.yellow('Telemetry is disabled. Enable with: telemetry: true in ~/.chub/config.yaml')),
77
+ { status: 'skipped', reason: 'feedback_disabled' },
78
+ () => console.log(chalk.yellow('Feedback is disabled. Enable with: feedback: true in ~/.chub/config.yaml')),
76
79
  globalOpts
77
80
  );
78
81
  return;
@@ -5,6 +5,7 @@ import { getEntry, resolveDocPath, resolveEntryFile } from '../lib/registry.js';
5
5
  import { fetchDoc, fetchDocFull } from '../lib/cache.js';
6
6
  import { output, error, info } from '../lib/output.js';
7
7
  import { trackEvent } from '../lib/analytics.js';
8
+ import { readAnnotation } from '../lib/annotations.js';
8
9
 
9
10
  /**
10
11
  * Fetch one or more entries by ID. Auto-detects doc vs skill per entry.
@@ -13,18 +14,21 @@ async function fetchEntries(ids, opts, globalOpts) {
13
14
  const results = [];
14
15
 
15
16
  for (const id of ids) {
17
+ const fetchStart = Date.now();
18
+
16
19
  // Search both docs and skills — auto-detect type
17
20
  const result = getEntry(id);
18
21
 
19
22
  if (result.ambiguous) {
20
23
  error(
21
- `Multiple entries with id "${id}". Be specific:\n ${result.alternatives.join('\n ')}`,
24
+ `Multiple entries match "${id}". Use a source prefix:\n ${result.alternatives.map((a) => `chub get ${a}`).join('\n ')}`,
22
25
  globalOpts
23
26
  );
24
27
  }
25
28
 
26
29
  if (!result.entry) {
27
- error(`Entry "${id}" not found.`, globalOpts);
30
+ await trackEvent('doc_not_found', { entry_id: id });
31
+ error(`No doc or skill found with id "${id}".`, globalOpts);
28
32
  }
29
33
 
30
34
  const entry = result.entry;
@@ -32,7 +36,19 @@ async function fetchEntries(ids, opts, globalOpts) {
32
36
  const resolved = resolveDocPath(entry, opts.lang, opts.version);
33
37
 
34
38
  if (!resolved) {
35
- error(`Could not resolve path for "${id}" ${opts.lang || ''} ${opts.version || ''}`.trim(), globalOpts);
39
+ if (opts.lang && entry.languages) {
40
+ const available = entry.languages.map((l) => l.language).join(', ');
41
+ error(`Language "${opts.lang}" is not available for "${id}". Available languages: ${available}.`, globalOpts);
42
+ } else {
43
+ error(`No content found for "${id}".`, globalOpts);
44
+ }
45
+ }
46
+
47
+ if (resolved.versionNotFound) {
48
+ error(
49
+ `Version "${resolved.requested}" not found for "${id}". Available versions: ${resolved.available.join(', ')}`,
50
+ globalOpts
51
+ );
36
52
  }
37
53
 
38
54
  if (resolved.needsLanguage) {
@@ -44,19 +60,42 @@ async function fetchEntries(ids, opts, globalOpts) {
44
60
 
45
61
  const entryFile = resolveEntryFile(resolved, type);
46
62
  if (entryFile.error) {
47
- error(`"${id}" ${entryFile.error}`, globalOpts);
63
+ error(`No content available for "${id}". Check that the source contains a valid DOC.md or SKILL.md, or run \`chub update\` to refresh remote registries.`, globalOpts);
48
64
  }
49
65
 
66
+ // Determine which reference files exist (beyond DOC.md/SKILL.md)
67
+ const entryFileName = type === 'skill' ? 'SKILL.md' : 'DOC.md';
68
+ const refFiles = resolved.files.filter((f) => f !== entryFileName);
69
+
50
70
  try {
51
- if (opts.full && resolved.files.length > 0) {
71
+ if (opts.file) {
72
+ // --file mode: fetch specific file(s) by path
73
+ const requested = opts.file.split(',').map((f) => f.trim());
74
+ const invalid = requested.filter((f) => !resolved.files.includes(f));
75
+ if (invalid.length > 0) {
76
+ const available = refFiles.length > 0 ? refFiles.join(', ') : '(none)';
77
+ error(`File "${invalid[0]}" not found in ${id}. Available: ${available}`, globalOpts);
78
+ }
79
+ if (requested.length === 1) {
80
+ const content = await fetchDoc(resolved.source, join(resolved.path, requested[0]));
81
+ results.push({ id: entry.id, type, content, path: join(resolved.path, requested[0]), source: entry._source, fetchStart, fetchDone: Date.now() });
82
+ } else {
83
+ const allFiles = await fetchDocFull(resolved.source, resolved.path, requested);
84
+ results.push({ id: entry.id, type, files: allFiles, path: resolved.path, source: entry._source, fetchStart, fetchDone: Date.now() });
85
+ }
86
+ } else if (opts.full && resolved.files.length > 0) {
52
87
  const allFiles = await fetchDocFull(resolved.source, resolved.path, resolved.files);
53
- results.push({ id: entry.id, type, files: allFiles, path: resolved.path });
88
+ results.push({ id: entry.id, type, files: allFiles, path: resolved.path, source: entry._source, fetchStart, fetchDone: Date.now() });
54
89
  } else {
55
90
  const content = await fetchDoc(resolved.source, entryFile.filePath);
56
- results.push({ id: entry.id, type, content, path: entryFile.filePath });
91
+ results.push({ id: entry.id, type, content, path: entryFile.filePath, additionalFiles: refFiles, source: entry._source, fetchStart, fetchDone: Date.now() });
57
92
  }
58
93
  } catch (err) {
59
- error(err.message, globalOpts);
94
+ await trackEvent('fetch_error', {
95
+ entry_id: entry.id,
96
+ error_type: err.code || err.name || 'unknown',
97
+ });
98
+ error(`Failed to load "${id}": ${err.message}`, globalOpts);
60
99
  }
61
100
  }
62
101
 
@@ -65,7 +104,10 @@ async function fetchEntries(ids, opts, globalOpts) {
65
104
  trackEvent(r.type === 'doc' ? 'doc_fetched' : 'skill_fetched', {
66
105
  entry_id: r.id,
67
106
  full: !!opts.full,
107
+ file: opts.file || undefined,
68
108
  lang: opts.lang || undefined,
109
+ source: r.source || undefined,
110
+ duration_ms: r.fetchDone - r.fetchStart,
69
111
  }).catch(() => {});
70
112
  }
71
113
 
@@ -90,7 +132,7 @@ async function fetchEntries(ids, opts, globalOpts) {
90
132
  }
91
133
  }
92
134
  } else {
93
- const isDir = opts.output.endsWith('/');
135
+ const isDir = opts.output.endsWith('/') || opts.output.endsWith('\\');
94
136
  if (isDir && results.length > 1) {
95
137
  mkdirSync(opts.output, { recursive: true });
96
138
  for (const r of results) {
@@ -112,9 +154,31 @@ async function fetchEntries(ids, opts, globalOpts) {
112
154
  }
113
155
  } else {
114
156
  if (results.length === 1 && !results[0].files) {
157
+ const r = results[0];
158
+ const extraFiles = r.additionalFiles || [];
159
+ const annotation = readAnnotation(r.id);
160
+ const jsonData = { id: r.id, type: r.type, content: r.content, path: r.path };
161
+ if (extraFiles.length > 0) jsonData.additionalFiles = extraFiles;
162
+ if (annotation) jsonData.annotation = annotation;
115
163
  output(
116
- { id: results[0].id, type: results[0].type, content: results[0].content, path: results[0].path },
117
- (data) => process.stdout.write(data.content),
164
+ jsonData,
165
+ (data) => {
166
+ process.stdout.write(data.content);
167
+ if (annotation) {
168
+ process.stdout.write(`\n\n---\n[Agent note — ${annotation.updatedAt}]\n${annotation.note}\n`);
169
+ }
170
+ const langFlag = opts.lang ? ` --lang ${opts.lang}` : '';
171
+ process.stdout.write(`\n\n---\nAfter using this doc, share your experience:\n`);
172
+ process.stdout.write(` chub feedback ${r.id} up${langFlag}\n`);
173
+ process.stdout.write(` chub feedback ${r.id} down${langFlag} --label outdated\n`);
174
+ process.stdout.write(`Available labels: accurate, well-structured, helpful, good-examples, outdated, inaccurate, incomplete, wrong-examples, wrong-version, poorly-structured\n`);
175
+ process.stdout.write(`Do NOT include any code, architecture details, or project-specific information in your feedback.\n`);
176
+ if (extraFiles.length > 0) {
177
+ const fileList = extraFiles.map((f) => ` ${f}`).join('\n');
178
+ const example = `chub get ${r.id} --file ${extraFiles[0]}`;
179
+ process.stdout.write(`\n\n---\nAdditional files available (use --file to fetch):\n${fileList}\nExample: ${example}\n`);
180
+ }
181
+ },
118
182
  globalOpts
119
183
  );
120
184
  } else {
@@ -138,10 +202,11 @@ export function registerGetCommand(program) {
138
202
  program
139
203
  .command('get <ids...>')
140
204
  .description('Fetch docs or skills by ID (auto-detects type)')
141
- .option('--lang <language>', 'Language variant (for docs)')
205
+ .option('--lang <language>', 'Language variant (required for docs): py, js, ts, rb, cs (or full names: python, javascript, typescript, ruby, csharp)')
142
206
  .option('--version <version>', 'Specific version (for docs)')
143
207
  .option('-o, --output <path>', 'Write to file or directory')
144
208
  .option('--full', 'Fetch all files (not just entry point)')
209
+ .option('--file <paths>', 'Fetch specific file(s) by path (comma-separated)')
145
210
  .action(async (ids, opts) => {
146
211
  const globalOpts = program.optsWithGlobals();
147
212
  await fetchEntries(ids, opts, globalOpts);
@@ -0,0 +1,34 @@
1
+ import chalk from 'chalk';
2
+ import { loadHelpContent } from '../lib/help.js';
3
+ import { output } from '../lib/output.js';
4
+
5
+ function formatHelp(data) {
6
+ if (data.source === 'remote') {
7
+ const details = [];
8
+ if (data.version) details.push(`version ${data.version}`);
9
+ if (data.updatedAt) details.push(`updated ${data.updatedAt}`);
10
+ const suffix = details.length > 0 ? ` (${details.join(', ')})` : '';
11
+ console.log(chalk.dim(`Help source: remote${suffix}`));
12
+ } else if (data.url) {
13
+ console.log(chalk.dim('Help source: local fallback (remote unavailable)'));
14
+ } else {
15
+ console.log(chalk.dim('Help source: local'));
16
+ }
17
+
18
+ if (data.minimumCliVersion) {
19
+ console.log(chalk.yellow(`Recommended minimum CLI version: ${data.minimumCliVersion}`));
20
+ }
21
+
22
+ process.stdout.write(`${data.content}\n`);
23
+ }
24
+
25
+ export function registerHelpCommand(program, cliVersion) {
26
+ program
27
+ .command('help')
28
+ .description('Show chub bootstrap guidance (remote-first, local fallback)')
29
+ .action(async () => {
30
+ const globalOpts = program.optsWithGlobals();
31
+ const help = await loadHelpContent(cliVersion);
32
+ output(help, formatHelp, globalOpts);
33
+ });
34
+ }
@@ -57,9 +57,10 @@ export function registerSearchCommand(program) {
57
57
  .action((query, opts) => {
58
58
  const globalOpts = program.optsWithGlobals();
59
59
  const limit = parseInt(opts.limit, 10);
60
+ const normalizedQuery = typeof query === 'string' ? query.trim().replace(/\s+/g, ' ') : query;
60
61
 
61
62
  // No query: list all
62
- if (!query) {
63
+ if (!normalizedQuery) {
63
64
  const entries = listEntries(opts).slice(0, limit);
64
65
  output({ results: entries, total: entries.length }, (data) => {
65
66
  if (data.results.length === 0) {
@@ -73,12 +74,12 @@ export function registerSearchCommand(program) {
73
74
  }
74
75
 
75
76
  // Exact id match: show detail
76
- const result = getEntry(query);
77
+ const result = getEntry(normalizedQuery);
77
78
  if (result.ambiguous) {
78
79
  output(
79
80
  { error: 'ambiguous', alternatives: result.alternatives },
80
81
  () => {
81
- console.log(chalk.yellow(`Multiple entries with id "${query}". Be specific:`));
82
+ console.log(chalk.yellow(`Multiple entries with id "${normalizedQuery}". Be specific:`));
82
83
  for (const alt of result.alternatives) {
83
84
  console.log(` ${chalk.bold(alt)}`);
84
85
  }
@@ -93,19 +94,27 @@ export function registerSearchCommand(program) {
93
94
  }
94
95
 
95
96
  // Fuzzy search
96
- const results = searchEntries(query, opts).slice(0, limit);
97
+ const searchStart = Date.now();
98
+ const results = searchEntries(normalizedQuery, opts).slice(0, limit);
99
+ const duration_ms = Date.now() - searchStart;
100
+ const resultIds = results.map((e) => e.id || e.name || 'unknown');
97
101
  trackEvent('search', {
98
- query_length: query.length,
102
+ query: normalizedQuery.slice(0, 1000),
103
+ query_length: normalizedQuery.length,
99
104
  result_count: results.length,
105
+ results: resultIds,
106
+ duration_ms,
100
107
  has_tags: !!opts.tags,
101
108
  has_lang: !!opts.lang,
109
+ tags: opts.tags || undefined,
110
+ lang: opts.lang || undefined,
102
111
  }).catch(() => {});
103
- output({ results, total: results.length, query }, (data) => {
112
+ output({ results, total: results.length, query: normalizedQuery }, (data) => {
104
113
  if (data.results.length === 0) {
105
- console.log(chalk.yellow(`No results for "${query}".`));
114
+ console.log(chalk.yellow(`No results for "${normalizedQuery}".`));
106
115
  return;
107
116
  }
108
- console.log(chalk.bold(`${data.total} results for "${query}":\n`));
117
+ console.log(chalk.bold(`${data.total} results for "${normalizedQuery}":\n`));
109
118
  formatEntryList(data.results);
110
119
  }, globalOpts);
111
120
  });