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 +76 -0
- package/bin/chub-mcp +2 -0
- package/package.json +11 -6
- package/skills/get-api-docs/SKILL.md +81 -0
- package/src/commands/annotate.js +83 -0
- package/src/commands/build.js +21 -4
- package/src/commands/feedback.js +12 -9
- package/src/commands/get.js +77 -12
- package/src/commands/help.js +34 -0
- package/src/commands/search.js +17 -8
- package/src/index.js +35 -67
- package/src/lib/analytics.js +13 -2
- package/src/lib/annotations.js +57 -0
- package/src/lib/bm25.js +303 -0
- package/src/lib/cache.js +108 -17
- package/src/lib/config.js +15 -2
- package/src/lib/help.js +158 -0
- package/src/lib/identity.js +12 -1
- package/src/lib/registry.js +283 -27
- 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/dist/anthropic/docs/sdk/javascript/DOC.md +0 -499
- package/dist/anthropic/docs/sdk/python/DOC.md +0 -382
- package/dist/openai/docs/chat/javascript/DOC.md +0 -350
- package/dist/openai/docs/chat/python/DOC.md +0 -526
- package/dist/pinecone/docs/sdk/javascript/DOC.md +0 -984
- package/dist/pinecone/docs/sdk/python/DOC.md +0 -1395
- package/dist/registry.json +0 -276
- package/dist/resend/docs/sdk/DOC.md +0 -1271
- package/dist/stripe/docs/api/DOC.md +0 -1726
- package/dist/supabase/docs/sdk/DOC.md +0 -1606
- package/dist/twilio/docs/sdk/python/DOC.md +0 -469
- package/dist/twilio/docs/sdk/typescript/DOC.md +0 -946
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,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": "^
|
|
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
|
+
}
|
package/src/commands/build.js
CHANGED
|
@@ -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) =>
|
|
328
|
+
filter: (s) => basename(s) !== 'registry.json',
|
|
312
329
|
});
|
|
313
330
|
}
|
|
314
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
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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}"
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
117
|
-
(data) =>
|
|
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
|
+
}
|
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
|
});
|