chub-dev 0.2.0-beta.4 → 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 +76 -0
- package/bin/chub-mcp +2 -0
- package/package.json +10 -5
- package/skills/get-api-docs/SKILL.md +81 -0
- package/src/commands/annotate.js +1 -1
- package/src/commands/build.js +12 -4
- package/src/commands/feedback.js +12 -9
- package/src/commands/get.js +32 -11
- package/src/commands/help.js +34 -0
- package/src/commands/search.js +17 -8
- package/src/index.js +31 -65
- package/src/lib/analytics.js +13 -2
- package/src/lib/bm25.js +185 -52
- package/src/lib/cache.js +94 -17
- package/src/lib/config.js +14 -1
- package/src/lib/help.js +158 -0
- package/src/lib/identity.js +12 -1
- package/src/lib/registry.js +236 -63
- package/src/lib/telemetry.js +7 -1
- package/src/lib/welcome.js +42 -0
- package/src/mcp/server.js +184 -0
- package/src/mcp/stdio-lifecycle.js +54 -0
- package/src/mcp/tools.js +286 -0
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
package/package.json
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chub-dev",
|
|
3
|
-
"version": "0.
|
|
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,11 +42,13 @@
|
|
|
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
54
|
"vitest": "^4.0.18"
|
|
@@ -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.
|
package/src/commands/annotate.js
CHANGED
|
@@ -32,7 +32,7 @@ export function registerAnnotateCommand(program) {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
if (!id) {
|
|
35
|
-
error('
|
|
35
|
+
error('Missing required argument: <id>. Run: chub annotate <id> <note> | chub annotate <id> --clear | chub annotate --list', globalOpts);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
if (opts.clear) {
|
package/src/commands/build.js
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
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
7
|
import { buildIndex } from '../lib/bm25.js';
|
|
8
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
|
+
}
|
|
16
|
+
|
|
9
17
|
/**
|
|
10
18
|
* Recursively find all DOC.md and SKILL.md files under a directory.
|
|
11
19
|
*/
|
|
@@ -31,7 +39,7 @@ function listDirFiles(dir) {
|
|
|
31
39
|
for (const entry of readdirSync(d, { withFileTypes: true })) {
|
|
32
40
|
const full = join(d, entry.name);
|
|
33
41
|
if (entry.isDirectory()) walk(full);
|
|
34
|
-
else results.push(relative(dir, full));
|
|
42
|
+
else results.push(toPosix(relative(dir, full)));
|
|
35
43
|
}
|
|
36
44
|
};
|
|
37
45
|
walk(dir);
|
|
@@ -83,7 +91,7 @@ function discoverAuthor(authorDir, authorName, contentDir) {
|
|
|
83
91
|
const tags = meta.tags ? meta.tags.split(',').map((t) => t.trim()) : [];
|
|
84
92
|
const updatedOn = meta['updated-on'] || new Date().toISOString().split('T')[0];
|
|
85
93
|
const entryDir = dirname(ef.path);
|
|
86
|
-
const entryPath = relative(contentDir, entryDir);
|
|
94
|
+
const entryPath = toPosix(relative(contentDir, entryDir));
|
|
87
95
|
const files = listDirFiles(entryDir);
|
|
88
96
|
const size = dirSize(entryDir);
|
|
89
97
|
|
|
@@ -317,7 +325,7 @@ export function registerBuildCommand(program) {
|
|
|
317
325
|
// Skip registry.json in author dirs
|
|
318
326
|
cpSync(src, dest, {
|
|
319
327
|
recursive: true,
|
|
320
|
-
filter: (s) =>
|
|
328
|
+
filter: (s) => basename(s) !== 'registry.json',
|
|
321
329
|
});
|
|
322
330
|
}
|
|
323
331
|
|
package/src/commands/feedback.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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(`
|
|
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
|
|
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 (!
|
|
75
|
+
if (!isFeedbackEnabled()) {
|
|
73
76
|
output(
|
|
74
|
-
{ status: 'skipped', reason: '
|
|
75
|
-
() => console.log(chalk.yellow('
|
|
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;
|
package/src/commands/get.js
CHANGED
|
@@ -14,18 +14,21 @@ async function fetchEntries(ids, opts, globalOpts) {
|
|
|
14
14
|
const results = [];
|
|
15
15
|
|
|
16
16
|
for (const id of ids) {
|
|
17
|
+
const fetchStart = Date.now();
|
|
18
|
+
|
|
17
19
|
// Search both docs and skills — auto-detect type
|
|
18
20
|
const result = getEntry(id);
|
|
19
21
|
|
|
20
22
|
if (result.ambiguous) {
|
|
21
23
|
error(
|
|
22
|
-
`Multiple entries
|
|
24
|
+
`Multiple entries match "${id}". Use a source prefix:\n ${result.alternatives.map((a) => `chub get ${a}`).join('\n ')}`,
|
|
23
25
|
globalOpts
|
|
24
26
|
);
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
if (!result.entry) {
|
|
28
|
-
|
|
30
|
+
await trackEvent('doc_not_found', { entry_id: id });
|
|
31
|
+
error(`No doc or skill found with id "${id}".`, globalOpts);
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
const entry = result.entry;
|
|
@@ -33,7 +36,12 @@ async function fetchEntries(ids, opts, globalOpts) {
|
|
|
33
36
|
const resolved = resolveDocPath(entry, opts.lang, opts.version);
|
|
34
37
|
|
|
35
38
|
if (!resolved) {
|
|
36
|
-
|
|
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
|
+
}
|
|
37
45
|
}
|
|
38
46
|
|
|
39
47
|
if (resolved.versionNotFound) {
|
|
@@ -52,7 +60,7 @@ async function fetchEntries(ids, opts, globalOpts) {
|
|
|
52
60
|
|
|
53
61
|
const entryFile = resolveEntryFile(resolved, type);
|
|
54
62
|
if (entryFile.error) {
|
|
55
|
-
error(`"${id}"
|
|
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);
|
|
56
64
|
}
|
|
57
65
|
|
|
58
66
|
// Determine which reference files exist (beyond DOC.md/SKILL.md)
|
|
@@ -70,20 +78,24 @@ async function fetchEntries(ids, opts, globalOpts) {
|
|
|
70
78
|
}
|
|
71
79
|
if (requested.length === 1) {
|
|
72
80
|
const content = await fetchDoc(resolved.source, join(resolved.path, requested[0]));
|
|
73
|
-
results.push({ id: entry.id, type, content, path: 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() });
|
|
74
82
|
} else {
|
|
75
83
|
const allFiles = await fetchDocFull(resolved.source, resolved.path, requested);
|
|
76
|
-
results.push({ id: entry.id, type, files: allFiles, path: resolved.path });
|
|
84
|
+
results.push({ id: entry.id, type, files: allFiles, path: resolved.path, source: entry._source, fetchStart, fetchDone: Date.now() });
|
|
77
85
|
}
|
|
78
86
|
} else if (opts.full && resolved.files.length > 0) {
|
|
79
87
|
const allFiles = await fetchDocFull(resolved.source, resolved.path, resolved.files);
|
|
80
|
-
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() });
|
|
81
89
|
} else {
|
|
82
90
|
const content = await fetchDoc(resolved.source, entryFile.filePath);
|
|
83
|
-
results.push({ id: entry.id, type, content, path: entryFile.filePath, additionalFiles: refFiles });
|
|
91
|
+
results.push({ id: entry.id, type, content, path: entryFile.filePath, additionalFiles: refFiles, source: entry._source, fetchStart, fetchDone: Date.now() });
|
|
84
92
|
}
|
|
85
93
|
} catch (err) {
|
|
86
|
-
|
|
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);
|
|
87
99
|
}
|
|
88
100
|
}
|
|
89
101
|
|
|
@@ -92,7 +104,10 @@ async function fetchEntries(ids, opts, globalOpts) {
|
|
|
92
104
|
trackEvent(r.type === 'doc' ? 'doc_fetched' : 'skill_fetched', {
|
|
93
105
|
entry_id: r.id,
|
|
94
106
|
full: !!opts.full,
|
|
107
|
+
file: opts.file || undefined,
|
|
95
108
|
lang: opts.lang || undefined,
|
|
109
|
+
source: r.source || undefined,
|
|
110
|
+
duration_ms: r.fetchDone - r.fetchStart,
|
|
96
111
|
}).catch(() => {});
|
|
97
112
|
}
|
|
98
113
|
|
|
@@ -117,7 +132,7 @@ async function fetchEntries(ids, opts, globalOpts) {
|
|
|
117
132
|
}
|
|
118
133
|
}
|
|
119
134
|
} else {
|
|
120
|
-
const isDir = opts.output.endsWith('/');
|
|
135
|
+
const isDir = opts.output.endsWith('/') || opts.output.endsWith('\\');
|
|
121
136
|
if (isDir && results.length > 1) {
|
|
122
137
|
mkdirSync(opts.output, { recursive: true });
|
|
123
138
|
for (const r of results) {
|
|
@@ -152,6 +167,12 @@ async function fetchEntries(ids, opts, globalOpts) {
|
|
|
152
167
|
if (annotation) {
|
|
153
168
|
process.stdout.write(`\n\n---\n[Agent note — ${annotation.updatedAt}]\n${annotation.note}\n`);
|
|
154
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`);
|
|
155
176
|
if (extraFiles.length > 0) {
|
|
156
177
|
const fileList = extraFiles.map((f) => ` ${f}`).join('\n');
|
|
157
178
|
const example = `chub get ${r.id} --file ${extraFiles[0]}`;
|
|
@@ -181,7 +202,7 @@ export function registerGetCommand(program) {
|
|
|
181
202
|
program
|
|
182
203
|
.command('get <ids...>')
|
|
183
204
|
.description('Fetch docs or skills by ID (auto-detects type)')
|
|
184
|
-
.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)')
|
|
185
206
|
.option('--version <version>', 'Specific version (for docs)')
|
|
186
207
|
.option('-o, --output <path>', 'Write to file or directory')
|
|
187
208
|
.option('--full', 'Fetch all files (not just entry point)')
|
|
@@ -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
|
+
}
|
package/src/commands/search.js
CHANGED
|
@@ -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 (!
|
|
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(
|
|
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 "${
|
|
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
|
|
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
|
-
|
|
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 "${
|
|
114
|
+
console.log(chalk.yellow(`No results for "${normalizedQuery}".`));
|
|
106
115
|
return;
|
|
107
116
|
}
|
|
108
|
-
console.log(chalk.bold(`${data.total} results for "${
|
|
117
|
+
console.log(chalk.bold(`${data.total} results for "${normalizedQuery}":\n`));
|
|
109
118
|
formatEntryList(data.results);
|
|
110
119
|
}, globalOpts);
|
|
111
120
|
});
|