chub-dev 0.1.0 → 0.1.2-beta.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 +55 -0
- package/bin/chub-mcp +2 -0
- package/dist/airtable/docs/database/javascript/DOC.md +1437 -0
- package/dist/airtable/docs/database/python/DOC.md +1735 -0
- package/dist/amplitude/docs/analytics/javascript/DOC.md +1282 -0
- package/dist/amplitude/docs/analytics/python/DOC.md +1199 -0
- package/dist/anthropic/docs/claude-api/javascript/DOC.md +503 -0
- package/dist/anthropic/docs/claude-api/python/DOC.md +389 -0
- package/dist/asana/docs/tasks/DOC.md +1396 -0
- package/dist/assemblyai/docs/transcription/DOC.md +1043 -0
- package/dist/atlassian/docs/confluence/javascript/DOC.md +1347 -0
- package/dist/atlassian/docs/confluence/python/DOC.md +1604 -0
- package/dist/auth0/docs/identity/javascript/DOC.md +968 -0
- package/dist/auth0/docs/identity/python/DOC.md +1199 -0
- package/dist/aws/docs/s3/javascript/DOC.md +1773 -0
- package/dist/aws/docs/s3/python/DOC.md +1807 -0
- package/dist/binance/docs/trading/javascript/DOC.md +1315 -0
- package/dist/binance/docs/trading/python/DOC.md +1454 -0
- package/dist/braintree/docs/gateway/javascript/DOC.md +1278 -0
- package/dist/braintree/docs/gateway/python/DOC.md +1179 -0
- package/dist/chromadb/docs/embeddings-db/javascript/DOC.md +1263 -0
- package/dist/chromadb/docs/embeddings-db/python/DOC.md +1707 -0
- package/dist/clerk/docs/auth/javascript/DOC.md +1220 -0
- package/dist/clerk/docs/auth/python/DOC.md +274 -0
- package/dist/cloudflare/docs/workers/javascript/DOC.md +918 -0
- package/dist/cloudflare/docs/workers/python/DOC.md +994 -0
- package/dist/cockroachdb/docs/distributed-db/DOC.md +1500 -0
- package/dist/cohere/docs/llm/DOC.md +1335 -0
- package/dist/datadog/docs/monitoring/javascript/DOC.md +1740 -0
- package/dist/datadog/docs/monitoring/python/DOC.md +1815 -0
- package/dist/deepgram/docs/speech/javascript/DOC.md +885 -0
- package/dist/deepgram/docs/speech/python/DOC.md +685 -0
- package/dist/deepl/docs/translation/javascript/DOC.md +887 -0
- package/dist/deepl/docs/translation/python/DOC.md +944 -0
- package/dist/deepseek/docs/llm/DOC.md +1220 -0
- package/dist/directus/docs/headless-cms/javascript/DOC.md +1128 -0
- package/dist/directus/docs/headless-cms/python/DOC.md +1276 -0
- package/dist/discord/docs/bot/javascript/DOC.md +1090 -0
- package/dist/discord/docs/bot/python/DOC.md +1130 -0
- package/dist/elasticsearch/docs/search/DOC.md +1634 -0
- package/dist/elevenlabs/docs/text-to-speech/javascript/DOC.md +336 -0
- package/dist/elevenlabs/docs/text-to-speech/python/DOC.md +552 -0
- package/dist/firebase/docs/auth/DOC.md +1015 -0
- package/dist/gemini/docs/genai/javascript/DOC.md +691 -0
- package/dist/gemini/docs/genai/python/DOC.md +555 -0
- package/dist/github/docs/octokit/DOC.md +1560 -0
- package/dist/google/docs/bigquery/javascript/DOC.md +1688 -0
- package/dist/google/docs/bigquery/python/DOC.md +1503 -0
- package/dist/hubspot/docs/crm/javascript/DOC.md +1805 -0
- package/dist/hubspot/docs/crm/python/DOC.md +2033 -0
- package/dist/huggingface/docs/transformers/DOC.md +948 -0
- package/dist/intercom/docs/messaging/javascript/DOC.md +1844 -0
- package/dist/intercom/docs/messaging/python/DOC.md +1797 -0
- package/dist/jira/docs/issues/javascript/DOC.md +1420 -0
- package/dist/jira/docs/issues/python/DOC.md +1492 -0
- package/dist/kafka/docs/streaming/javascript/DOC.md +1671 -0
- package/dist/kafka/docs/streaming/python/DOC.md +1464 -0
- package/dist/landingai-ade/docs/api/DOC.md +620 -0
- package/dist/landingai-ade/docs/sdk/python/DOC.md +489 -0
- package/dist/landingai-ade/docs/sdk/typescript/DOC.md +542 -0
- package/dist/landingai-ade/skills/SKILL.md +489 -0
- package/dist/launchdarkly/docs/feature-flags/javascript/DOC.md +1191 -0
- package/dist/launchdarkly/docs/feature-flags/python/DOC.md +1671 -0
- package/dist/linear/docs/tracker/DOC.md +1554 -0
- package/dist/livekit/docs/realtime/javascript/DOC.md +303 -0
- package/dist/livekit/docs/realtime/python/DOC.md +163 -0
- package/dist/mailchimp/docs/marketing/DOC.md +1420 -0
- package/dist/meilisearch/docs/search/DOC.md +1241 -0
- package/dist/microsoft/docs/onedrive/javascript/DOC.md +1421 -0
- package/dist/microsoft/docs/onedrive/python/DOC.md +1549 -0
- package/dist/mongodb/docs/atlas/DOC.md +2041 -0
- package/dist/notion/docs/workspace-api/javascript/DOC.md +1435 -0
- package/dist/notion/docs/workspace-api/python/DOC.md +1400 -0
- package/dist/okta/docs/identity/javascript/DOC.md +1171 -0
- package/dist/okta/docs/identity/python/DOC.md +1401 -0
- package/dist/openai/docs/chat/javascript/DOC.md +407 -0
- package/dist/openai/docs/chat/python/DOC.md +568 -0
- package/dist/paypal/docs/checkout/DOC.md +278 -0
- package/dist/pinecone/docs/sdk/javascript/DOC.md +984 -0
- package/dist/pinecone/docs/sdk/python/DOC.md +1395 -0
- package/dist/plaid/docs/banking/javascript/DOC.md +1163 -0
- package/dist/plaid/docs/banking/python/DOC.md +1203 -0
- package/dist/playwright-community/skills/login-flows/SKILL.md +108 -0
- package/dist/postmark/docs/transactional-email/DOC.md +1168 -0
- package/dist/prisma/docs/orm/javascript/DOC.md +1419 -0
- package/dist/prisma/docs/orm/python/DOC.md +1317 -0
- package/dist/qdrant/docs/vector-search/javascript/DOC.md +1221 -0
- package/dist/qdrant/docs/vector-search/python/DOC.md +1653 -0
- package/dist/rabbitmq/docs/message-queue/javascript/DOC.md +1193 -0
- package/dist/rabbitmq/docs/message-queue/python/DOC.md +1243 -0
- package/dist/razorpay/docs/payments/javascript/DOC.md +1219 -0
- package/dist/razorpay/docs/payments/python/DOC.md +1330 -0
- package/dist/redis/docs/key-value/javascript/DOC.md +1851 -0
- package/dist/redis/docs/key-value/python/DOC.md +2054 -0
- package/dist/registry.json +2817 -0
- package/dist/replicate/docs/model-hosting/DOC.md +1318 -0
- package/dist/resend/docs/email/DOC.md +1271 -0
- package/dist/salesforce/docs/crm/javascript/DOC.md +1241 -0
- package/dist/salesforce/docs/crm/python/DOC.md +1183 -0
- package/dist/search-index.json +1 -0
- package/dist/sendgrid/docs/email-api/javascript/DOC.md +371 -0
- package/dist/sendgrid/docs/email-api/python/DOC.md +656 -0
- package/dist/sentry/docs/error-tracking/javascript/DOC.md +1073 -0
- package/dist/sentry/docs/error-tracking/python/DOC.md +1309 -0
- package/dist/shopify/docs/storefront/DOC.md +457 -0
- package/dist/slack/docs/workspace/javascript/DOC.md +933 -0
- package/dist/slack/docs/workspace/python/DOC.md +271 -0
- package/dist/square/docs/payments/javascript/DOC.md +1855 -0
- package/dist/square/docs/payments/python/DOC.md +1728 -0
- package/dist/stripe/docs/api/DOC.md +1727 -0
- package/dist/stripe/docs/payments/DOC.md +1726 -0
- package/dist/stytch/docs/auth/javascript/DOC.md +1813 -0
- package/dist/stytch/docs/auth/python/DOC.md +1962 -0
- package/dist/supabase/docs/client/DOC.md +1606 -0
- package/dist/twilio/docs/messaging/python/DOC.md +469 -0
- package/dist/twilio/docs/messaging/typescript/DOC.md +946 -0
- package/dist/vercel/docs/platform/DOC.md +1940 -0
- package/dist/weaviate/docs/vector-db/javascript/DOC.md +1268 -0
- package/dist/weaviate/docs/vector-db/python/DOC.md +1388 -0
- package/dist/zendesk/docs/support/javascript/DOC.md +2150 -0
- package/dist/zendesk/docs/support/python/DOC.md +2297 -0
- package/package.json +22 -6
- package/skills/get-api-docs/SKILL.md +84 -0
- package/src/commands/annotate.js +83 -0
- package/src/commands/build.js +12 -1
- package/src/commands/feedback.js +150 -0
- package/src/commands/get.js +83 -42
- package/src/commands/search.js +7 -0
- package/src/index.js +43 -17
- package/src/lib/analytics.js +90 -0
- package/src/lib/annotations.js +57 -0
- package/src/lib/bm25.js +170 -0
- package/src/lib/cache.js +69 -6
- package/src/lib/config.js +8 -3
- package/src/lib/identity.js +99 -0
- package/src/lib/registry.js +103 -20
- package/src/lib/telemetry.js +86 -0
- package/src/mcp/server.js +177 -0
- package/src/mcp/tools.js +251 -0
package/package.json
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chub-dev",
|
|
3
|
-
"version": "0.1.0",
|
|
3
|
+
"version": "0.1.2-beta.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
|
-
"src/"
|
|
12
|
+
"src/",
|
|
13
|
+
"dist/",
|
|
14
|
+
"skills/"
|
|
12
15
|
],
|
|
13
16
|
"engines": {
|
|
14
17
|
"node": ">=18.0.0"
|
|
@@ -20,7 +23,8 @@
|
|
|
20
23
|
"agent",
|
|
21
24
|
"cli",
|
|
22
25
|
"context",
|
|
23
|
-
"skills"
|
|
26
|
+
"skills",
|
|
27
|
+
"mcp"
|
|
24
28
|
],
|
|
25
29
|
"license": "MIT",
|
|
26
30
|
"repository": {
|
|
@@ -31,10 +35,22 @@
|
|
|
31
35
|
"url": "https://github.com/andrewyng/context-hub/issues"
|
|
32
36
|
},
|
|
33
37
|
"homepage": "https://github.com/andrewyng/context-hub#readme",
|
|
38
|
+
"scripts": {
|
|
39
|
+
"prepublish": "node bin/chub build ../content -o dist --base-url https://cdn.aichub.org/v1",
|
|
40
|
+
"test": "vitest run",
|
|
41
|
+
"test:watch": "vitest",
|
|
42
|
+
"test:coverage": "vitest run --coverage"
|
|
43
|
+
},
|
|
34
44
|
"dependencies": {
|
|
35
|
-
"
|
|
45
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
36
46
|
"chalk": "^5.3.0",
|
|
47
|
+
"commander": "^12.0.0",
|
|
48
|
+
"posthog-node": "^5.24.17",
|
|
49
|
+
"tar": "^7.5.8",
|
|
37
50
|
"yaml": "^2.3.0",
|
|
38
|
-
"
|
|
51
|
+
"zod": "^4.3.6"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"vitest": "^4.0.18"
|
|
39
55
|
}
|
|
40
56
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: get-api-docs
|
|
3
|
+
description: >
|
|
4
|
+
Use this skill when you need documentation for a third-party library, SDK, or API
|
|
5
|
+
before writing code that uses it — for example, "use the OpenAI API", "call the
|
|
6
|
+
Stripe API", "use the Anthropic SDK", "query Pinecone", or any time the user asks
|
|
7
|
+
you to write code against an external service and you need current API reference.
|
|
8
|
+
Fetch the docs with chub before answering, rather than relying on training knowledge.
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Get API Docs via chub
|
|
12
|
+
|
|
13
|
+
When you need documentation for a library or API, fetch it with the `chub` CLI
|
|
14
|
+
rather than guessing from training data. This gives you the current, correct API.
|
|
15
|
+
|
|
16
|
+
## Step 1 — Find the right doc ID
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
chub search "<library name>" --json
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Pick the best-matching `id` from the results (e.g. `openai/chat`, `anthropic/sdk`,
|
|
23
|
+
`stripe/api`). If nothing matches, try a broader term.
|
|
24
|
+
|
|
25
|
+
## Step 2 — Fetch the docs
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
chub get <id> --lang py # or --lang js, --lang ts
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Omit `--lang` if the doc has only one language variant — it will be auto-selected.
|
|
32
|
+
|
|
33
|
+
## Step 3 — Use the docs
|
|
34
|
+
|
|
35
|
+
Read the fetched content and use it to write accurate code or answer the question.
|
|
36
|
+
Do not rely on memorized API shapes — use what the docs say.
|
|
37
|
+
|
|
38
|
+
## Step 4 — Annotate what you learned
|
|
39
|
+
|
|
40
|
+
After completing the task, if you discovered something not in the doc — a gotcha,
|
|
41
|
+
workaround, version quirk, or project-specific detail — save it so future sessions
|
|
42
|
+
start smarter:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
chub annotate <id> "Webhook verification requires raw body — do not parse before verifying"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Annotations are local, persist across sessions, and appear automatically on future
|
|
49
|
+
`chub get` calls. Keep notes concise and actionable. Don't repeat what's already in
|
|
50
|
+
the doc.
|
|
51
|
+
|
|
52
|
+
## Step 5 — Give feedback
|
|
53
|
+
|
|
54
|
+
Rate the doc so authors can improve it. Ask the user before sending.
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
chub feedback <id> up # doc worked well
|
|
58
|
+
chub feedback <id> down --label outdated # doc needs updating
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Available labels: `outdated`, `inaccurate`, `incomplete`, `wrong-examples`,
|
|
62
|
+
`wrong-version`, `poorly-structured`, `accurate`, `well-structured`, `helpful`,
|
|
63
|
+
`good-examples`.
|
|
64
|
+
|
|
65
|
+
## Quick reference
|
|
66
|
+
|
|
67
|
+
| Goal | Command |
|
|
68
|
+
|------|---------|
|
|
69
|
+
| List everything | `chub search` |
|
|
70
|
+
| Find a doc | `chub search "stripe"` |
|
|
71
|
+
| Exact id detail | `chub search stripe/api` |
|
|
72
|
+
| Fetch Python docs | `chub get stripe/api --lang py` |
|
|
73
|
+
| Fetch JS docs | `chub get openai/chat --lang js` |
|
|
74
|
+
| Save to file | `chub get anthropic/sdk --lang py -o docs.md` |
|
|
75
|
+
| Fetch multiple | `chub get openai/chat stripe/api --lang py` |
|
|
76
|
+
| Save a note | `chub annotate stripe/api "needs raw body"` |
|
|
77
|
+
| List notes | `chub annotate --list` |
|
|
78
|
+
| Rate a doc | `chub feedback stripe/api up` |
|
|
79
|
+
|
|
80
|
+
## Notes
|
|
81
|
+
|
|
82
|
+
- `chub search` with no query lists everything available
|
|
83
|
+
- IDs are `<author>/<name>` — confirm the ID from search before fetching
|
|
84
|
+
- If multiple languages exist and you don't pass `--lang`, chub will tell you which are available
|
|
@@ -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
|
@@ -3,6 +3,8 @@ import { join, relative, dirname } 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
|
+
import { trackEvent } from '../lib/analytics.js';
|
|
7
|
+
import { buildIndex } from '../lib/bm25.js';
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Recursively find all DOC.md and SKILL.md files under a directory.
|
|
@@ -300,6 +302,14 @@ export function registerBuildCommand(program) {
|
|
|
300
302
|
mkdirSync(outputDir, { recursive: true });
|
|
301
303
|
writeFileSync(join(outputDir, 'registry.json'), JSON.stringify(registry, null, 2));
|
|
302
304
|
|
|
305
|
+
// Build and write BM25 search index
|
|
306
|
+
const allEntries = [
|
|
307
|
+
...allDocs.map((d) => ({ ...d, _type: 'doc' })),
|
|
308
|
+
...allSkills.map((s) => ({ ...s, _type: 'skill' })),
|
|
309
|
+
];
|
|
310
|
+
const searchIndex = buildIndex(allEntries);
|
|
311
|
+
writeFileSync(join(outputDir, 'search-index.json'), JSON.stringify(searchIndex));
|
|
312
|
+
|
|
303
313
|
// Copy content tree
|
|
304
314
|
for (const authorEntry of topLevel) {
|
|
305
315
|
const src = join(contentDir, authorEntry.name);
|
|
@@ -307,11 +317,12 @@ export function registerBuildCommand(program) {
|
|
|
307
317
|
// Skip registry.json in author dirs
|
|
308
318
|
cpSync(src, dest, {
|
|
309
319
|
recursive: true,
|
|
310
|
-
filter: (s) => !s.endsWith('/registry.json')
|
|
320
|
+
filter: (s) => !s.endsWith('/registry.json'),
|
|
311
321
|
});
|
|
312
322
|
}
|
|
313
323
|
|
|
314
324
|
const summary = { docs: allDocs.length, skills: allSkills.length, warnings: allWarnings.length };
|
|
325
|
+
trackEvent('build', { doc_count: allDocs.length, skill_count: allSkills.length }).catch(() => {});
|
|
315
326
|
if (globalOpts.json) {
|
|
316
327
|
console.log(JSON.stringify({ ...summary, output: outputDir }));
|
|
317
328
|
} else {
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { getEntry } from '../lib/registry.js';
|
|
6
|
+
import { sendFeedback, isTelemetryEnabled, getTelemetryUrl } from '../lib/telemetry.js';
|
|
7
|
+
import { getOrCreateClientId } from '../lib/identity.js';
|
|
8
|
+
import { output, error } from '../lib/output.js';
|
|
9
|
+
import { trackEvent } from '../lib/analytics.js';
|
|
10
|
+
|
|
11
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
|
|
13
|
+
const VALID_LABELS = [
|
|
14
|
+
'accurate', 'well-structured', 'helpful', 'good-examples',
|
|
15
|
+
'outdated', 'inaccurate', 'incomplete', 'wrong-examples',
|
|
16
|
+
'wrong-version', 'poorly-structured',
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
function collect(val, acc) {
|
|
20
|
+
acc.push(val);
|
|
21
|
+
return acc;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function registerFeedbackCommand(program) {
|
|
25
|
+
program
|
|
26
|
+
.command('feedback [id] [rating] [comment]')
|
|
27
|
+
.description('Rate a doc or skill (up/down)')
|
|
28
|
+
.option('--type <type>', 'Explicit type: doc or skill')
|
|
29
|
+
.option('--lang <language>', 'Language variant of the doc')
|
|
30
|
+
.option('--doc-version <version>', 'Version of the doc')
|
|
31
|
+
.option('--file <file>', 'Specific file within the entry (e.g. references/streaming.md)')
|
|
32
|
+
.option('--label <label>', 'Feedback label (repeatable: --label outdated --label wrong-examples)', collect, [])
|
|
33
|
+
.option('--agent <name>', 'AI coding tool name')
|
|
34
|
+
.option('--model <model>', 'LLM model name')
|
|
35
|
+
.option('--status', 'Show telemetry status')
|
|
36
|
+
.action(async (id, rating, comment, opts) => {
|
|
37
|
+
const globalOpts = program.optsWithGlobals();
|
|
38
|
+
|
|
39
|
+
// --status flag
|
|
40
|
+
if (opts.status) {
|
|
41
|
+
const enabled = isTelemetryEnabled();
|
|
42
|
+
if (globalOpts.json) {
|
|
43
|
+
let clientId = null;
|
|
44
|
+
try { clientId = await getOrCreateClientId(); } catch {}
|
|
45
|
+
console.log(JSON.stringify({
|
|
46
|
+
telemetry: enabled,
|
|
47
|
+
client_id_prefix: clientId ? clientId.slice(0, 8) : null,
|
|
48
|
+
endpoint: getTelemetryUrl(),
|
|
49
|
+
valid_labels: VALID_LABELS,
|
|
50
|
+
}));
|
|
51
|
+
} else {
|
|
52
|
+
console.log(`Telemetry: ${enabled ? chalk.green('enabled') : chalk.red('disabled')}`);
|
|
53
|
+
try {
|
|
54
|
+
const cid = await getOrCreateClientId();
|
|
55
|
+
console.log(`Client ID: ${cid.slice(0, 8)}...`);
|
|
56
|
+
} catch {}
|
|
57
|
+
console.log(`Endpoint: ${getTelemetryUrl()}`);
|
|
58
|
+
console.log(`Labels: ${VALID_LABELS.join(', ')}`);
|
|
59
|
+
}
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// BUG #1 FIX: Validation errors respect --json flag
|
|
64
|
+
if (!id || !rating) {
|
|
65
|
+
error('Missing required arguments: <id> and <rating>. Run: chub feedback <id> <up|down> [comment]', globalOpts);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (rating !== 'up' && rating !== 'down') {
|
|
69
|
+
error('Rating must be "up" or "down".', globalOpts);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!isTelemetryEnabled()) {
|
|
73
|
+
output(
|
|
74
|
+
{ status: 'skipped', reason: 'telemetry_disabled' },
|
|
75
|
+
() => console.log(chalk.yellow('Telemetry is disabled. Enable with: telemetry: true in ~/.chub/config.yaml')),
|
|
76
|
+
globalOpts
|
|
77
|
+
);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// BUG #2 FIX: Only auto-detect type if --type not explicitly set
|
|
82
|
+
let entryType = opts.type || null;
|
|
83
|
+
let docLang = opts.lang || undefined;
|
|
84
|
+
let docVersion = opts.docVersion || undefined;
|
|
85
|
+
let source;
|
|
86
|
+
try {
|
|
87
|
+
const result = getEntry(id);
|
|
88
|
+
if (result.entry) {
|
|
89
|
+
if (!entryType) {
|
|
90
|
+
entryType = result.entry.languages ? 'doc' : 'skill';
|
|
91
|
+
}
|
|
92
|
+
source = result.entry._source;
|
|
93
|
+
|
|
94
|
+
// If doc and user didn't specify lang/version, try to infer from entry
|
|
95
|
+
if (result.entry.languages && !docLang && result.entry.languages.length === 1) {
|
|
96
|
+
docLang = result.entry.languages[0].language;
|
|
97
|
+
}
|
|
98
|
+
if (result.entry.languages && !docVersion) {
|
|
99
|
+
const lang = result.entry.languages.find((l) => l.language === docLang) || result.entry.languages[0];
|
|
100
|
+
if (lang) docVersion = lang.recommendedVersion;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
} catch {
|
|
104
|
+
// Registry not loaded — use explicit flags
|
|
105
|
+
}
|
|
106
|
+
if (!entryType) entryType = 'doc'; // Final fallback
|
|
107
|
+
|
|
108
|
+
// Parse labels (--label is repeatable, collected into an array)
|
|
109
|
+
let labels;
|
|
110
|
+
if (opts.label && opts.label.length > 0) {
|
|
111
|
+
labels = opts.label.map((l) => l.trim().toLowerCase()).filter((l) => VALID_LABELS.includes(l));
|
|
112
|
+
if (labels.length === 0) labels = undefined;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Read CLI version
|
|
116
|
+
let cliVersion;
|
|
117
|
+
try {
|
|
118
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '..', '..', 'package.json'), 'utf8'));
|
|
119
|
+
cliVersion = pkg.version;
|
|
120
|
+
} catch {}
|
|
121
|
+
|
|
122
|
+
const result = await sendFeedback(id, entryType, rating, {
|
|
123
|
+
comment,
|
|
124
|
+
docLang,
|
|
125
|
+
docVersion,
|
|
126
|
+
targetFile: opts.file,
|
|
127
|
+
labels,
|
|
128
|
+
agent: opts.agent,
|
|
129
|
+
model: opts.model,
|
|
130
|
+
cliVersion,
|
|
131
|
+
source,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
if (result.status === 'sent') {
|
|
135
|
+
trackEvent('feedback_sent', { entry_id: id, rating, entry_type: entryType }).catch(() => {});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
output(result, (data) => {
|
|
139
|
+
if (data.status === 'sent') {
|
|
140
|
+
const parts = [chalk.green(`Feedback recorded for ${id}`)];
|
|
141
|
+
if (docLang) parts.push(chalk.dim(`lang=${docLang}`));
|
|
142
|
+
if (docVersion) parts.push(chalk.dim(`version=${docVersion}`));
|
|
143
|
+
if (opts.file) parts.push(chalk.dim(`file=${opts.file}`));
|
|
144
|
+
console.log(parts.join(' '));
|
|
145
|
+
} else if (data.status === 'error') {
|
|
146
|
+
process.stderr.write(chalk.red(`Failed to send feedback: ${data.reason || `HTTP ${data.code}`}\n`));
|
|
147
|
+
}
|
|
148
|
+
}, globalOpts);
|
|
149
|
+
});
|
|
150
|
+
}
|
package/src/commands/get.js
CHANGED
|
@@ -4,36 +4,48 @@ import chalk from 'chalk';
|
|
|
4
4
|
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
|
+
import { trackEvent } from '../lib/analytics.js';
|
|
8
|
+
import { readAnnotation } from '../lib/annotations.js';
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
|
-
*
|
|
10
|
-
* @param {string} type - "doc" or "skill"
|
|
11
|
-
* @param {string[]} ids - one or more entry ids
|
|
12
|
-
* @param {object} opts - command options (lang, version, output, full)
|
|
13
|
-
* @param {object} globalOpts - global options (json)
|
|
11
|
+
* Fetch one or more entries by ID. Auto-detects doc vs skill per entry.
|
|
14
12
|
*/
|
|
15
|
-
async function fetchEntries(
|
|
13
|
+
async function fetchEntries(ids, opts, globalOpts) {
|
|
16
14
|
const results = [];
|
|
17
15
|
|
|
18
16
|
for (const id of ids) {
|
|
19
|
-
|
|
17
|
+
// Search both docs and skills — auto-detect type
|
|
18
|
+
const result = getEntry(id);
|
|
20
19
|
|
|
21
20
|
if (result.ambiguous) {
|
|
22
21
|
error(
|
|
23
|
-
`Multiple entries
|
|
22
|
+
`Multiple entries match "${id}". Use a source prefix:\n ${result.alternatives.map((a) => `chub get ${a}`).join('\n ')}`,
|
|
24
23
|
globalOpts
|
|
25
24
|
);
|
|
26
25
|
}
|
|
27
26
|
|
|
28
27
|
if (!result.entry) {
|
|
29
|
-
error(`
|
|
28
|
+
error(`No doc or skill found with id "${id}".`, globalOpts);
|
|
30
29
|
}
|
|
31
30
|
|
|
32
31
|
const entry = result.entry;
|
|
32
|
+
const type = entry.languages ? 'doc' : 'skill';
|
|
33
33
|
const resolved = resolveDocPath(entry, opts.lang, opts.version);
|
|
34
34
|
|
|
35
35
|
if (!resolved) {
|
|
36
|
-
|
|
36
|
+
if (opts.lang && entry.languages) {
|
|
37
|
+
const available = entry.languages.map((l) => l.language).join(', ');
|
|
38
|
+
error(`Language "${opts.lang}" is not available for "${id}". Available languages: ${available}.`, globalOpts);
|
|
39
|
+
} else {
|
|
40
|
+
error(`No content found for "${id}".`, globalOpts);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (resolved.versionNotFound) {
|
|
45
|
+
error(
|
|
46
|
+
`Version "${resolved.requested}" not found for "${id}". Available versions: ${resolved.available.join(', ')}`,
|
|
47
|
+
globalOpts
|
|
48
|
+
);
|
|
37
49
|
}
|
|
38
50
|
|
|
39
51
|
if (resolved.needsLanguage) {
|
|
@@ -45,26 +57,53 @@ async function fetchEntries(type, ids, opts, globalOpts) {
|
|
|
45
57
|
|
|
46
58
|
const entryFile = resolveEntryFile(resolved, type);
|
|
47
59
|
if (entryFile.error) {
|
|
48
|
-
error(`"${id}"
|
|
60
|
+
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);
|
|
49
61
|
}
|
|
50
62
|
|
|
63
|
+
// Determine which reference files exist (beyond DOC.md/SKILL.md)
|
|
64
|
+
const entryFileName = type === 'skill' ? 'SKILL.md' : 'DOC.md';
|
|
65
|
+
const refFiles = resolved.files.filter((f) => f !== entryFileName);
|
|
66
|
+
|
|
51
67
|
try {
|
|
52
|
-
if (opts.
|
|
68
|
+
if (opts.file) {
|
|
69
|
+
// --file mode: fetch specific file(s) by path
|
|
70
|
+
const requested = opts.file.split(',').map((f) => f.trim());
|
|
71
|
+
const invalid = requested.filter((f) => !resolved.files.includes(f));
|
|
72
|
+
if (invalid.length > 0) {
|
|
73
|
+
const available = refFiles.length > 0 ? refFiles.join(', ') : '(none)';
|
|
74
|
+
error(`File "${invalid[0]}" not found in ${id}. Available: ${available}`, globalOpts);
|
|
75
|
+
}
|
|
76
|
+
if (requested.length === 1) {
|
|
77
|
+
const content = await fetchDoc(resolved.source, join(resolved.path, requested[0]));
|
|
78
|
+
results.push({ id: entry.id, type, content, path: join(resolved.path, requested[0]) });
|
|
79
|
+
} else {
|
|
80
|
+
const allFiles = await fetchDocFull(resolved.source, resolved.path, requested);
|
|
81
|
+
results.push({ id: entry.id, type, files: allFiles, path: resolved.path });
|
|
82
|
+
}
|
|
83
|
+
} else if (opts.full && resolved.files.length > 0) {
|
|
53
84
|
const allFiles = await fetchDocFull(resolved.source, resolved.path, resolved.files);
|
|
54
|
-
results.push({ id: entry.id, files: allFiles, path: resolved.path });
|
|
85
|
+
results.push({ id: entry.id, type, files: allFiles, path: resolved.path });
|
|
55
86
|
} else {
|
|
56
87
|
const content = await fetchDoc(resolved.source, entryFile.filePath);
|
|
57
|
-
results.push({ id: entry.id, content, path: entryFile.filePath });
|
|
88
|
+
results.push({ id: entry.id, type, content, path: entryFile.filePath, additionalFiles: refFiles });
|
|
58
89
|
}
|
|
59
90
|
} catch (err) {
|
|
60
|
-
error(err.message
|
|
91
|
+
error(`Failed to load "${id}": ${err.message}`, globalOpts);
|
|
61
92
|
}
|
|
62
93
|
}
|
|
63
94
|
|
|
95
|
+
// Track fetches
|
|
96
|
+
for (const r of results) {
|
|
97
|
+
trackEvent(r.type === 'doc' ? 'doc_fetched' : 'skill_fetched', {
|
|
98
|
+
entry_id: r.id,
|
|
99
|
+
full: !!opts.full,
|
|
100
|
+
lang: opts.lang || undefined,
|
|
101
|
+
}).catch(() => {});
|
|
102
|
+
}
|
|
103
|
+
|
|
64
104
|
// Output
|
|
65
105
|
if (opts.output) {
|
|
66
106
|
if (opts.full) {
|
|
67
|
-
// --full -o: write individual files preserving directory structure
|
|
68
107
|
for (const r of results) {
|
|
69
108
|
if (r.files) {
|
|
70
109
|
const baseDir = ids.length > 1 ? join(opts.output, r.id) : opts.output;
|
|
@@ -88,6 +127,7 @@ async function fetchEntries(type, ids, opts, globalOpts) {
|
|
|
88
127
|
mkdirSync(opts.output, { recursive: true });
|
|
89
128
|
for (const r of results) {
|
|
90
129
|
const outPath = join(opts.output, `${r.id}.md`);
|
|
130
|
+
mkdirSync(dirname(outPath), { recursive: true });
|
|
91
131
|
writeFileSync(outPath, r.content);
|
|
92
132
|
info(`Written to ${outPath}`);
|
|
93
133
|
}
|
|
@@ -100,18 +140,32 @@ async function fetchEntries(type, ids, opts, globalOpts) {
|
|
|
100
140
|
}
|
|
101
141
|
}
|
|
102
142
|
if (globalOpts.json) {
|
|
103
|
-
console.log(JSON.stringify(results.map((r) => ({ id: r.id, path: opts.output }))));
|
|
143
|
+
console.log(JSON.stringify(results.map((r) => ({ id: r.id, type: r.type, path: opts.output }))));
|
|
104
144
|
}
|
|
105
145
|
} else {
|
|
106
|
-
// stdout
|
|
107
146
|
if (results.length === 1 && !results[0].files) {
|
|
147
|
+
const r = results[0];
|
|
148
|
+
const extraFiles = r.additionalFiles || [];
|
|
149
|
+
const annotation = readAnnotation(r.id);
|
|
150
|
+
const jsonData = { id: r.id, type: r.type, content: r.content, path: r.path };
|
|
151
|
+
if (extraFiles.length > 0) jsonData.additionalFiles = extraFiles;
|
|
152
|
+
if (annotation) jsonData.annotation = annotation;
|
|
108
153
|
output(
|
|
109
|
-
|
|
110
|
-
(data) =>
|
|
154
|
+
jsonData,
|
|
155
|
+
(data) => {
|
|
156
|
+
process.stdout.write(data.content);
|
|
157
|
+
if (annotation) {
|
|
158
|
+
process.stdout.write(`\n\n---\n[Agent note — ${annotation.updatedAt}]\n${annotation.note}\n`);
|
|
159
|
+
}
|
|
160
|
+
if (extraFiles.length > 0) {
|
|
161
|
+
const fileList = extraFiles.map((f) => ` ${f}`).join('\n');
|
|
162
|
+
const example = `chub get ${r.id} --file ${extraFiles[0]}`;
|
|
163
|
+
process.stdout.write(`\n\n---\nAdditional files available (use --file to fetch):\n${fileList}\nExample: ${example}\n`);
|
|
164
|
+
}
|
|
165
|
+
},
|
|
111
166
|
globalOpts
|
|
112
167
|
);
|
|
113
168
|
} else {
|
|
114
|
-
// Concatenate all content (--full to stdout, or multiple entries)
|
|
115
169
|
const parts = results.flatMap((r) => {
|
|
116
170
|
if (r.files) {
|
|
117
171
|
return r.files.map((f) => `# FILE: ${f.name}\n\n${f.content}`);
|
|
@@ -120,7 +174,7 @@ async function fetchEntries(type, ids, opts, globalOpts) {
|
|
|
120
174
|
});
|
|
121
175
|
const combined = parts.join('\n\n---\n\n');
|
|
122
176
|
output(
|
|
123
|
-
results.map((r) => ({ id: r.id, path: r.path })),
|
|
177
|
+
results.map((r) => ({ id: r.id, type: r.type, path: r.path })),
|
|
124
178
|
() => process.stdout.write(combined),
|
|
125
179
|
globalOpts
|
|
126
180
|
);
|
|
@@ -129,29 +183,16 @@ async function fetchEntries(type, ids, opts, globalOpts) {
|
|
|
129
183
|
}
|
|
130
184
|
|
|
131
185
|
export function registerGetCommand(program) {
|
|
132
|
-
|
|
133
|
-
.command('get')
|
|
134
|
-
.description('
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
.command('docs <ids...>')
|
|
138
|
-
.description('Fetch documentation content')
|
|
139
|
-
.option('--lang <language>', 'Language variant')
|
|
140
|
-
.option('--version <version>', 'Specific version')
|
|
141
|
-
.option('-o, --output <path>', 'Write to file or directory')
|
|
142
|
-
.option('--full', 'Fetch all files (not just entry point)')
|
|
143
|
-
.action(async (ids, opts) => {
|
|
144
|
-
const globalOpts = program.optsWithGlobals();
|
|
145
|
-
await fetchEntries('doc', ids, opts, globalOpts);
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
get
|
|
149
|
-
.command('skills <ids...>')
|
|
150
|
-
.description('Fetch skill content')
|
|
186
|
+
program
|
|
187
|
+
.command('get <ids...>')
|
|
188
|
+
.description('Fetch docs or skills by ID (auto-detects type)')
|
|
189
|
+
.option('--lang <language>', 'Language variant (for docs)')
|
|
190
|
+
.option('--version <version>', 'Specific version (for docs)')
|
|
151
191
|
.option('-o, --output <path>', 'Write to file or directory')
|
|
152
192
|
.option('--full', 'Fetch all files (not just entry point)')
|
|
193
|
+
.option('--file <paths>', 'Fetch specific file(s) by path (comma-separated)')
|
|
153
194
|
.action(async (ids, opts) => {
|
|
154
195
|
const globalOpts = program.optsWithGlobals();
|
|
155
|
-
await fetchEntries(
|
|
196
|
+
await fetchEntries(ids, opts, globalOpts);
|
|
156
197
|
});
|
|
157
198
|
}
|
package/src/commands/search.js
CHANGED
|
@@ -2,6 +2,7 @@ import chalk from 'chalk';
|
|
|
2
2
|
import { searchEntries, listEntries, getEntry, getDisplayId, isMultiSource } from '../lib/registry.js';
|
|
3
3
|
import { displayLanguage } from '../lib/normalize.js';
|
|
4
4
|
import { output } from '../lib/output.js';
|
|
5
|
+
import { trackEvent } from '../lib/analytics.js';
|
|
5
6
|
|
|
6
7
|
function formatEntryList(entries) {
|
|
7
8
|
const multi = isMultiSource();
|
|
@@ -93,6 +94,12 @@ export function registerSearchCommand(program) {
|
|
|
93
94
|
|
|
94
95
|
// Fuzzy search
|
|
95
96
|
const results = searchEntries(query, opts).slice(0, limit);
|
|
97
|
+
trackEvent('search', {
|
|
98
|
+
query_length: query.length,
|
|
99
|
+
result_count: results.length,
|
|
100
|
+
has_tags: !!opts.tags,
|
|
101
|
+
has_lang: !!opts.lang,
|
|
102
|
+
}).catch(() => {});
|
|
96
103
|
output({ results, total: results.length, query }, (data) => {
|
|
97
104
|
if (data.results.length === 0) {
|
|
98
105
|
console.log(chalk.yellow(`No results for "${query}".`));
|