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.
Files changed (139) hide show
  1. package/README.md +55 -0
  2. package/bin/chub-mcp +2 -0
  3. package/dist/airtable/docs/database/javascript/DOC.md +1437 -0
  4. package/dist/airtable/docs/database/python/DOC.md +1735 -0
  5. package/dist/amplitude/docs/analytics/javascript/DOC.md +1282 -0
  6. package/dist/amplitude/docs/analytics/python/DOC.md +1199 -0
  7. package/dist/anthropic/docs/claude-api/javascript/DOC.md +503 -0
  8. package/dist/anthropic/docs/claude-api/python/DOC.md +389 -0
  9. package/dist/asana/docs/tasks/DOC.md +1396 -0
  10. package/dist/assemblyai/docs/transcription/DOC.md +1043 -0
  11. package/dist/atlassian/docs/confluence/javascript/DOC.md +1347 -0
  12. package/dist/atlassian/docs/confluence/python/DOC.md +1604 -0
  13. package/dist/auth0/docs/identity/javascript/DOC.md +968 -0
  14. package/dist/auth0/docs/identity/python/DOC.md +1199 -0
  15. package/dist/aws/docs/s3/javascript/DOC.md +1773 -0
  16. package/dist/aws/docs/s3/python/DOC.md +1807 -0
  17. package/dist/binance/docs/trading/javascript/DOC.md +1315 -0
  18. package/dist/binance/docs/trading/python/DOC.md +1454 -0
  19. package/dist/braintree/docs/gateway/javascript/DOC.md +1278 -0
  20. package/dist/braintree/docs/gateway/python/DOC.md +1179 -0
  21. package/dist/chromadb/docs/embeddings-db/javascript/DOC.md +1263 -0
  22. package/dist/chromadb/docs/embeddings-db/python/DOC.md +1707 -0
  23. package/dist/clerk/docs/auth/javascript/DOC.md +1220 -0
  24. package/dist/clerk/docs/auth/python/DOC.md +274 -0
  25. package/dist/cloudflare/docs/workers/javascript/DOC.md +918 -0
  26. package/dist/cloudflare/docs/workers/python/DOC.md +994 -0
  27. package/dist/cockroachdb/docs/distributed-db/DOC.md +1500 -0
  28. package/dist/cohere/docs/llm/DOC.md +1335 -0
  29. package/dist/datadog/docs/monitoring/javascript/DOC.md +1740 -0
  30. package/dist/datadog/docs/monitoring/python/DOC.md +1815 -0
  31. package/dist/deepgram/docs/speech/javascript/DOC.md +885 -0
  32. package/dist/deepgram/docs/speech/python/DOC.md +685 -0
  33. package/dist/deepl/docs/translation/javascript/DOC.md +887 -0
  34. package/dist/deepl/docs/translation/python/DOC.md +944 -0
  35. package/dist/deepseek/docs/llm/DOC.md +1220 -0
  36. package/dist/directus/docs/headless-cms/javascript/DOC.md +1128 -0
  37. package/dist/directus/docs/headless-cms/python/DOC.md +1276 -0
  38. package/dist/discord/docs/bot/javascript/DOC.md +1090 -0
  39. package/dist/discord/docs/bot/python/DOC.md +1130 -0
  40. package/dist/elasticsearch/docs/search/DOC.md +1634 -0
  41. package/dist/elevenlabs/docs/text-to-speech/javascript/DOC.md +336 -0
  42. package/dist/elevenlabs/docs/text-to-speech/python/DOC.md +552 -0
  43. package/dist/firebase/docs/auth/DOC.md +1015 -0
  44. package/dist/gemini/docs/genai/javascript/DOC.md +691 -0
  45. package/dist/gemini/docs/genai/python/DOC.md +555 -0
  46. package/dist/github/docs/octokit/DOC.md +1560 -0
  47. package/dist/google/docs/bigquery/javascript/DOC.md +1688 -0
  48. package/dist/google/docs/bigquery/python/DOC.md +1503 -0
  49. package/dist/hubspot/docs/crm/javascript/DOC.md +1805 -0
  50. package/dist/hubspot/docs/crm/python/DOC.md +2033 -0
  51. package/dist/huggingface/docs/transformers/DOC.md +948 -0
  52. package/dist/intercom/docs/messaging/javascript/DOC.md +1844 -0
  53. package/dist/intercom/docs/messaging/python/DOC.md +1797 -0
  54. package/dist/jira/docs/issues/javascript/DOC.md +1420 -0
  55. package/dist/jira/docs/issues/python/DOC.md +1492 -0
  56. package/dist/kafka/docs/streaming/javascript/DOC.md +1671 -0
  57. package/dist/kafka/docs/streaming/python/DOC.md +1464 -0
  58. package/dist/landingai-ade/docs/api/DOC.md +620 -0
  59. package/dist/landingai-ade/docs/sdk/python/DOC.md +489 -0
  60. package/dist/landingai-ade/docs/sdk/typescript/DOC.md +542 -0
  61. package/dist/landingai-ade/skills/SKILL.md +489 -0
  62. package/dist/launchdarkly/docs/feature-flags/javascript/DOC.md +1191 -0
  63. package/dist/launchdarkly/docs/feature-flags/python/DOC.md +1671 -0
  64. package/dist/linear/docs/tracker/DOC.md +1554 -0
  65. package/dist/livekit/docs/realtime/javascript/DOC.md +303 -0
  66. package/dist/livekit/docs/realtime/python/DOC.md +163 -0
  67. package/dist/mailchimp/docs/marketing/DOC.md +1420 -0
  68. package/dist/meilisearch/docs/search/DOC.md +1241 -0
  69. package/dist/microsoft/docs/onedrive/javascript/DOC.md +1421 -0
  70. package/dist/microsoft/docs/onedrive/python/DOC.md +1549 -0
  71. package/dist/mongodb/docs/atlas/DOC.md +2041 -0
  72. package/dist/notion/docs/workspace-api/javascript/DOC.md +1435 -0
  73. package/dist/notion/docs/workspace-api/python/DOC.md +1400 -0
  74. package/dist/okta/docs/identity/javascript/DOC.md +1171 -0
  75. package/dist/okta/docs/identity/python/DOC.md +1401 -0
  76. package/dist/openai/docs/chat/javascript/DOC.md +407 -0
  77. package/dist/openai/docs/chat/python/DOC.md +568 -0
  78. package/dist/paypal/docs/checkout/DOC.md +278 -0
  79. package/dist/pinecone/docs/sdk/javascript/DOC.md +984 -0
  80. package/dist/pinecone/docs/sdk/python/DOC.md +1395 -0
  81. package/dist/plaid/docs/banking/javascript/DOC.md +1163 -0
  82. package/dist/plaid/docs/banking/python/DOC.md +1203 -0
  83. package/dist/playwright-community/skills/login-flows/SKILL.md +108 -0
  84. package/dist/postmark/docs/transactional-email/DOC.md +1168 -0
  85. package/dist/prisma/docs/orm/javascript/DOC.md +1419 -0
  86. package/dist/prisma/docs/orm/python/DOC.md +1317 -0
  87. package/dist/qdrant/docs/vector-search/javascript/DOC.md +1221 -0
  88. package/dist/qdrant/docs/vector-search/python/DOC.md +1653 -0
  89. package/dist/rabbitmq/docs/message-queue/javascript/DOC.md +1193 -0
  90. package/dist/rabbitmq/docs/message-queue/python/DOC.md +1243 -0
  91. package/dist/razorpay/docs/payments/javascript/DOC.md +1219 -0
  92. package/dist/razorpay/docs/payments/python/DOC.md +1330 -0
  93. package/dist/redis/docs/key-value/javascript/DOC.md +1851 -0
  94. package/dist/redis/docs/key-value/python/DOC.md +2054 -0
  95. package/dist/registry.json +2817 -0
  96. package/dist/replicate/docs/model-hosting/DOC.md +1318 -0
  97. package/dist/resend/docs/email/DOC.md +1271 -0
  98. package/dist/salesforce/docs/crm/javascript/DOC.md +1241 -0
  99. package/dist/salesforce/docs/crm/python/DOC.md +1183 -0
  100. package/dist/search-index.json +1 -0
  101. package/dist/sendgrid/docs/email-api/javascript/DOC.md +371 -0
  102. package/dist/sendgrid/docs/email-api/python/DOC.md +656 -0
  103. package/dist/sentry/docs/error-tracking/javascript/DOC.md +1073 -0
  104. package/dist/sentry/docs/error-tracking/python/DOC.md +1309 -0
  105. package/dist/shopify/docs/storefront/DOC.md +457 -0
  106. package/dist/slack/docs/workspace/javascript/DOC.md +933 -0
  107. package/dist/slack/docs/workspace/python/DOC.md +271 -0
  108. package/dist/square/docs/payments/javascript/DOC.md +1855 -0
  109. package/dist/square/docs/payments/python/DOC.md +1728 -0
  110. package/dist/stripe/docs/api/DOC.md +1727 -0
  111. package/dist/stripe/docs/payments/DOC.md +1726 -0
  112. package/dist/stytch/docs/auth/javascript/DOC.md +1813 -0
  113. package/dist/stytch/docs/auth/python/DOC.md +1962 -0
  114. package/dist/supabase/docs/client/DOC.md +1606 -0
  115. package/dist/twilio/docs/messaging/python/DOC.md +469 -0
  116. package/dist/twilio/docs/messaging/typescript/DOC.md +946 -0
  117. package/dist/vercel/docs/platform/DOC.md +1940 -0
  118. package/dist/weaviate/docs/vector-db/javascript/DOC.md +1268 -0
  119. package/dist/weaviate/docs/vector-db/python/DOC.md +1388 -0
  120. package/dist/zendesk/docs/support/javascript/DOC.md +2150 -0
  121. package/dist/zendesk/docs/support/python/DOC.md +2297 -0
  122. package/package.json +22 -6
  123. package/skills/get-api-docs/SKILL.md +84 -0
  124. package/src/commands/annotate.js +83 -0
  125. package/src/commands/build.js +12 -1
  126. package/src/commands/feedback.js +150 -0
  127. package/src/commands/get.js +83 -42
  128. package/src/commands/search.js +7 -0
  129. package/src/index.js +43 -17
  130. package/src/lib/analytics.js +90 -0
  131. package/src/lib/annotations.js +57 -0
  132. package/src/lib/bm25.js +170 -0
  133. package/src/lib/cache.js +69 -6
  134. package/src/lib/config.js +8 -3
  135. package/src/lib/identity.js +99 -0
  136. package/src/lib/registry.js +103 -20
  137. package/src/lib/telemetry.js +86 -0
  138. package/src/mcp/server.js +177 -0
  139. 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
- "commander": "^12.0.0",
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
- "tar": "^7.0.0"
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
+ }
@@ -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') || s === join(src, 'registry.json') === false,
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
+ }
@@ -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
- * Core fetch logic shared by `get docs` and `get skills`.
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(type, ids, opts, globalOpts) {
13
+ async function fetchEntries(ids, opts, globalOpts) {
16
14
  const results = [];
17
15
 
18
16
  for (const id of ids) {
19
- const result = getEntry(id, type);
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 with id "${id}". Be specific:\n ${result.alternatives.join('\n ')}`,
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(`Entry "${id}" not found in ${type}s.`, globalOpts);
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
- error(`Could not resolve path for "${id}" ${opts.lang || ''} ${opts.version || ''}`.trim(), globalOpts);
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}" ${entryFile.error}`, globalOpts);
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.full && resolved.files.length > 0) {
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, globalOpts);
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
- { id: results[0].id, content: results[0].content, path: results[0].path },
110
- (data) => process.stdout.write(data.content),
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
- const get = program
133
- .command('get')
134
- .description('Retrieve docs or skills');
135
-
136
- get
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('skill', ids, opts, globalOpts);
196
+ await fetchEntries(ids, opts, globalOpts);
156
197
  });
157
198
  }
@@ -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}".`));