@taurgis/bonsai 1.0.4 → 1.0.6

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 (93) hide show
  1. package/README.md +81 -2
  2. package/agents/agents/official-docs-researcher.agent.md +10 -1
  3. package/agents/agents/salesforce-docs-researcher.agent.md +126 -0
  4. package/agents/hooks/README.md +62 -0
  5. package/agents/hooks/claude-code/hooks/bonsai-web-fetch.sh +22 -0
  6. package/agents/hooks/claude-code/settings.json +16 -0
  7. package/agents/hooks/codex/hooks/bonsai-user-prompt-context.sh +18 -0
  8. package/agents/hooks/codex/hooks/bonsai-web-fetch.sh +22 -0
  9. package/agents/hooks/codex/hooks.json +28 -0
  10. package/agents/hooks/cursor/hooks/bonsai-web-fetch.sh +21 -0
  11. package/agents/hooks/cursor/hooks.json +12 -0
  12. package/agents/hooks/github-copilot/bonsai-web-fetch.json +23 -0
  13. package/agents/hooks/github-copilot/bonsai-web-fetch.sh +19 -0
  14. package/agents/instructions/salesforce-research.instructions.md +59 -0
  15. package/agents/instructions/web-research.instructions.md +5 -2
  16. package/agents/skills/web-research/SKILL.md +8 -3
  17. package/bin/cli.mjs +35 -8
  18. package/dist/base-command.d.ts +23 -0
  19. package/dist/base-command.js +67 -8
  20. package/dist/commands/config/base.d.ts +2 -0
  21. package/dist/commands/config/base.js +15 -1
  22. package/dist/commands/config/get.d.ts +1 -1
  23. package/dist/commands/config/get.js +6 -7
  24. package/dist/commands/config/index.d.ts +12 -0
  25. package/dist/commands/config/index.js +34 -0
  26. package/dist/commands/config/list.js +4 -4
  27. package/dist/commands/config/set.js +20 -14
  28. package/dist/commands/config/unset.d.ts +1 -1
  29. package/dist/commands/config/unset.js +11 -8
  30. package/dist/commands/fetch.d.ts +5 -0
  31. package/dist/commands/fetch.js +122 -47
  32. package/dist/commands/import.d.ts +4 -0
  33. package/dist/commands/import.js +99 -37
  34. package/dist/commands/inspect.d.ts +4 -0
  35. package/dist/commands/inspect.js +24 -19
  36. package/dist/commands/list.d.ts +2 -2
  37. package/dist/commands/list.js +39 -16
  38. package/dist/commands/prune.d.ts +1 -1
  39. package/dist/commands/prune.js +50 -17
  40. package/dist/commands/search.d.ts +4 -0
  41. package/dist/commands/search.js +102 -41
  42. package/dist/commands/status.d.ts +6 -0
  43. package/dist/commands/status.js +58 -31
  44. package/dist/commands.d.ts +2 -0
  45. package/dist/commands.js +2 -0
  46. package/dist/hooks/command-not-found/suggest.d.ts +9 -0
  47. package/dist/hooks/command-not-found/suggest.js +122 -0
  48. package/dist/lib/argv.d.ts +10 -0
  49. package/dist/lib/argv.js +53 -0
  50. package/dist/lib/config/index.d.ts +1 -1
  51. package/dist/lib/config/index.js +1 -1
  52. package/dist/lib/config/resolve.d.ts +7 -0
  53. package/dist/lib/config/resolve.js +23 -0
  54. package/dist/lib/envelope.d.ts +36 -0
  55. package/dist/lib/envelope.js +76 -0
  56. package/dist/lib/json-meta.d.ts +10 -0
  57. package/dist/lib/json-meta.js +98 -0
  58. package/dist/lib/research/browser.d.ts +12 -0
  59. package/dist/lib/research/browser.js +40 -3
  60. package/dist/lib/research/docs/capabilities.d.ts +1 -1
  61. package/dist/lib/research/docs/discovery-index.d.ts +14 -0
  62. package/dist/lib/research/docs/discovery-index.js +112 -0
  63. package/dist/lib/research/docs/machine-readable.d.ts +2 -2
  64. package/dist/lib/research/docs/machine-readable.js +3 -7
  65. package/dist/lib/research/docs/remote-search-runner.js +61 -1
  66. package/dist/lib/research/docs/sections.d.ts +1 -0
  67. package/dist/lib/research/docs/sections.js +19 -1
  68. package/dist/lib/research/freshness.d.ts +9 -1
  69. package/dist/lib/research/freshness.js +24 -2
  70. package/dist/lib/research/keywords.d.ts +10 -3
  71. package/dist/lib/research/keywords.js +32 -5
  72. package/dist/lib/research/revalidate.js +3 -1
  73. package/dist/lib/research/schema.d.ts +5 -2
  74. package/dist/lib/research/schema.js +12 -1
  75. package/dist/lib/research/secret-scan.js +4 -2
  76. package/dist/lib/research/url.js +5 -2
  77. package/dist/lib/text.d.ts +35 -0
  78. package/dist/lib/text.js +65 -0
  79. package/dist/sites/algolia.d.ts +9 -0
  80. package/dist/sites/algolia.js +26 -0
  81. package/dist/sites/auto.d.ts +8 -0
  82. package/dist/sites/auto.js +30 -0
  83. package/dist/sites/index.js +17 -1
  84. package/dist/sites/nextjs/index.d.ts +2 -0
  85. package/dist/sites/nextjs/index.js +52 -0
  86. package/dist/sites/react/index.d.ts +2 -0
  87. package/dist/sites/react/index.js +17 -0
  88. package/dist/sites/tailwind/index.d.ts +2 -0
  89. package/dist/sites/tailwind/index.js +17 -0
  90. package/dist/sites/vue/index.d.ts +2 -0
  91. package/dist/sites/vue/index.js +17 -0
  92. package/oclif.manifest.json +154 -81
  93. package/package.json +24 -4
@@ -0,0 +1,35 @@
1
+ /** Display label when an artifact has no topic — not a filterable topic value. */
2
+ export declare const NO_TOPIC_LABEL = "(no topic)";
3
+ /**
4
+ * Pick the singular or plural noun for a count so human-readable output stays grammatical
5
+ * ("1 entry" / "2 entries"). English has irregular plurals, so the plural is passed explicitly
6
+ * rather than derived.
7
+ */
8
+ export declare function pluralize(count: number, singular: string, plural: string): string;
9
+ /** How a command labels a result listing, so its heading and truncation notice stay consistent. */
10
+ export interface ResultListLabels {
11
+ /** Noun phrase after the count, e.g. "cached research"; the "entry/entries" plural is appended. */
12
+ noun: string;
13
+ /** Ordering word shown when truncated: "first" (recency order) or "top" (ranked order). */
14
+ order: 'first' | 'top';
15
+ /** The command's --limit ceiling, surfaced in the truncation hint. */
16
+ maxLimit: number;
17
+ }
18
+ /** Heading for a result listing; notes truncation inline when more matched than are shown. */
19
+ export declare function resultListHeading(total: number, shown: number, labels: ResultListLabels): string;
20
+ /**
21
+ * One-line truncation notice for stderr, or null when nothing was cut. Used under --json, where the
22
+ * stdout heading is suppressed and an agent would otherwise get no signal that results were capped.
23
+ */
24
+ export declare function truncationNotice(total: number, shown: number, labels: ResultListLabels): string | null;
25
+ /**
26
+ * Levenshtein edit distance: the minimum number of single-character insertions, deletions, or
27
+ * substitutions to turn `s1` into `s2`. Shared by fuzzy search ranking and command-typo suggestion.
28
+ */
29
+ export declare function levenshtein(s1: string, s2: string): number;
30
+ /**
31
+ * The candidate nearest to `input` by edit distance, or null when even the closest is farther than
32
+ * `maxDistance`. The threshold keeps suggestions to plausible typos — input that resembles nothing
33
+ * gets no correction rather than an absurd one.
34
+ */
35
+ export declare function closestMatch(input: string, candidates: readonly string[], maxDistance: number): string | null;
@@ -0,0 +1,65 @@
1
+ /** Display label when an artifact has no topic — not a filterable topic value. */
2
+ export const NO_TOPIC_LABEL = '(no topic)';
3
+ /**
4
+ * Pick the singular or plural noun for a count so human-readable output stays grammatical
5
+ * ("1 entry" / "2 entries"). English has irregular plurals, so the plural is passed explicitly
6
+ * rather than derived.
7
+ */
8
+ export function pluralize(count, singular, plural) {
9
+ return count === 1 ? singular : plural;
10
+ }
11
+ /** Heading for a result listing; notes truncation inline when more matched than are shown. */
12
+ export function resultListHeading(total, shown, labels) {
13
+ const noun = `${labels.noun} ${pluralize(total, 'entry', 'entries')}`;
14
+ return total > shown
15
+ ? `Found ${total} ${noun} (showing ${labels.order} ${shown}; raise --limit to see more):`
16
+ : `Found ${total} ${noun}:`;
17
+ }
18
+ /**
19
+ * One-line truncation notice for stderr, or null when nothing was cut. Used under --json, where the
20
+ * stdout heading is suppressed and an agent would otherwise get no signal that results were capped.
21
+ */
22
+ export function truncationNotice(total, shown, labels) {
23
+ if (total <= shown)
24
+ return null;
25
+ return `${total} entries matched; returning the ${labels.order} ${shown}. Raise --limit (max ${labels.maxLimit}) to see more.`;
26
+ }
27
+ /**
28
+ * Levenshtein edit distance: the minimum number of single-character insertions, deletions, or
29
+ * substitutions to turn `s1` into `s2`. Shared by fuzzy search ranking and command-typo suggestion.
30
+ */
31
+ export function levenshtein(s1, s2) {
32
+ if (s1.length < s2.length)
33
+ return levenshtein(s2, s1);
34
+ if (s2.length === 0)
35
+ return s1.length;
36
+ let prevRow = Array.from({ length: s2.length + 1 }, (_, i) => i);
37
+ for (let i = 0; i < s1.length; i++) {
38
+ const currRow = [i + 1];
39
+ for (let j = 0; j < s2.length; j++) {
40
+ const deletions = prevRow[j + 1] + 1;
41
+ const insertions = currRow[j] + 1;
42
+ const substitutions = prevRow[j] + (s1[i] === s2[j] ? 0 : 1);
43
+ currRow.push(Math.min(deletions, insertions, substitutions));
44
+ }
45
+ prevRow = currRow;
46
+ }
47
+ return prevRow[s2.length];
48
+ }
49
+ /**
50
+ * The candidate nearest to `input` by edit distance, or null when even the closest is farther than
51
+ * `maxDistance`. The threshold keeps suggestions to plausible typos — input that resembles nothing
52
+ * gets no correction rather than an absurd one.
53
+ */
54
+ export function closestMatch(input, candidates, maxDistance) {
55
+ let best = null;
56
+ let bestDistance = Infinity;
57
+ for (const candidate of candidates) {
58
+ const distance = levenshtein(input, candidate);
59
+ if (distance < bestDistance) {
60
+ bestDistance = distance;
61
+ best = candidate;
62
+ }
63
+ }
64
+ return bestDistance <= maxDistance ? best : null;
65
+ }
@@ -0,0 +1,9 @@
1
+ import type { SiteModule } from './types.js';
2
+ export declare function createAlgoliaSite(id: string, name: string, domains: string[], appId: string, apiKey: string, indexName: string): SiteModule;
3
+ export declare const react: SiteModule;
4
+ export declare const vue: SiteModule;
5
+ export declare const tailwind: SiteModule;
6
+ export declare const jest: SiteModule;
7
+ export declare const cypress: SiteModule;
8
+ export declare const vitest: SiteModule;
9
+ export declare const vitepress: SiteModule;
@@ -0,0 +1,26 @@
1
+ import { algoliaQueryUrl, parseAlgoliaResponse } from '../lib/research/docs/remote-search.js';
2
+ import { postJson } from '../lib/research/fetcher.js';
3
+ export function createAlgoliaSite(id, name, domains, appId, apiKey, indexName) {
4
+ return {
5
+ id,
6
+ name,
7
+ domains,
8
+ search: async (query) => {
9
+ const url = algoliaQueryUrl({ appId, apiKey, indexName });
10
+ const bodyParams = { params: `query=${encodeURIComponent(query)}&hitsPerPage=20` };
11
+ const res = await postJson(url, bodyParams, {
12
+ 'Content-Type': 'application/json',
13
+ 'X-Algolia-API-Key': apiKey,
14
+ 'X-Algolia-Application-Id': appId,
15
+ });
16
+ return parseAlgoliaResponse(res, url);
17
+ },
18
+ };
19
+ }
20
+ export const react = createAlgoliaSite('react', 'React', ['react.dev', 'legacy.reactjs.org'], '1FCF9AYYAT', '1b7ad4e1c89e645e351e59d40544eda1', 'beta-react');
21
+ export const vue = createAlgoliaSite('vue', 'Vue.js', ['vuejs.org'], 'ML0LEBN7FQ', '10e7a8b13e6aec4007343338ab134e05', 'vuejs');
22
+ export const tailwind = createAlgoliaSite('tailwind', 'Tailwind CSS', ['tailwindcss.com'], 'KNPXZI5B0M', '5fc87cef58bb80203d2207578309fab6', 'tailwindcss');
23
+ export const jest = createAlgoliaSite('jest', 'Jest', ['jestjs.io'], 'HP439UUSOL', 'e5e670fd16f8f17caada79d6b0931682', 'jest-v2');
24
+ export const cypress = createAlgoliaSite('cypress', 'Cypress', ['docs.cypress.io', 'cypress.io'], 'R9KDA5FMJB', 'b4af59e23bc2fa05281af7dcf13fcae5', 'cypress_docs');
25
+ export const vitest = createAlgoliaSite('vitest', 'Vitest', ['vitest.dev'], 'ZTF29HGJ69', '9c3ced6fed60d2670bb36ab7e8bed8bc', 'vitest');
26
+ export const vitepress = createAlgoliaSite('vitepress', 'VitePress', ['vitepress.dev'], '8J64VVRP8K', '52f578a92b88ad6abde815aae2b0ad7c', 'vitepress');
@@ -0,0 +1,8 @@
1
+ import type { SiteModule } from './types.js';
2
+ export declare function createAutoSite(id: string, name: string, domains: string[], docsUrl: string): SiteModule;
3
+ export declare const angular: SiteModule;
4
+ export declare const redux: SiteModule;
5
+ export declare const vite: SiteModule;
6
+ export declare const fastify: SiteModule;
7
+ export declare const rollup: SiteModule;
8
+ export declare const vueuse: SiteModule;
@@ -0,0 +1,30 @@
1
+ import { runRemoteDocsSearch } from '../lib/research/docs/remote-search-runner.js';
2
+ import { fetchStaticHtml, fetchText, postJson } from '../lib/research/fetcher.js';
3
+ export function createAutoSite(id, name, domains, docsUrl) {
4
+ return {
5
+ id,
6
+ name,
7
+ domains,
8
+ search: async (query) => {
9
+ const deps = {
10
+ fetchStatic: async (url) => {
11
+ const res = await fetchStaticHtml(url);
12
+ return { content: res.content, finalUrl: res.finalUrl };
13
+ },
14
+ fetchText: async (url) => {
15
+ const res = await fetchText(url);
16
+ return { content: res.content, status: res.status };
17
+ },
18
+ postJson: (url, body, headers) => postJson(url, body, headers),
19
+ };
20
+ const { results } = await runRemoteDocsSearch(docsUrl, query, deps);
21
+ return results;
22
+ },
23
+ };
24
+ }
25
+ export const angular = createAutoSite('angular', 'Angular', ['angular.dev'], 'https://angular.dev');
26
+ export const redux = createAutoSite('redux', 'Redux', ['redux.js.org'], 'https://redux.js.org');
27
+ export const vite = createAutoSite('vite', 'Vite', ['vitejs.dev', 'vite.dev'], 'https://vite.dev');
28
+ export const fastify = createAutoSite('fastify', 'Fastify', ['fastify.dev', 'fastify.io'], 'https://fastify.dev');
29
+ export const rollup = createAutoSite('rollup', 'Rollup', ['rollupjs.org'], 'https://rollupjs.org');
30
+ export const vueuse = createAutoSite('vueuse', 'VueUse', ['vueuse.org'], 'https://vueuse.org');
@@ -1,11 +1,27 @@
1
1
  import { salesforce } from './salesforce/index.js';
2
2
  import { salesforceDeveloper } from './salesforce-developer/index.js';
3
+ import { nextjs } from './nextjs/index.js';
4
+ import { react, vue, tailwind, jest, cypress, vitest, vitepress } from './algolia.js';
5
+ import { angular, redux, vite, fastify, rollup, vueuse } from './auto.js';
3
6
  // The known documentation sites. A plain constant, not a registry: these ship
4
7
  // with the CLI, so there is nothing to register at runtime. Sites with custom
5
8
  // fetch/search behavior live in their own module (e.g. ./salesforce). Salesforce Help and
6
9
  // Salesforce Developer are separate modules — different domains, different extraction.
7
10
  export const SITES = [
8
- { id: 'react', name: 'React', domains: ['react.dev', 'legacy.reactjs.org'] },
11
+ react,
12
+ vue,
13
+ tailwind,
14
+ nextjs,
15
+ jest,
16
+ cypress,
17
+ vitest,
18
+ vitepress,
19
+ angular,
20
+ redux,
21
+ vite,
22
+ fastify,
23
+ rollup,
24
+ vueuse,
9
25
  salesforce,
10
26
  salesforceDeveloper,
11
27
  // TanStack docs embed a GitHub "edit this page" link, so capture resolves the page's source
@@ -0,0 +1,2 @@
1
+ import type { SiteModule } from '../types.js';
2
+ export declare const nextjs: SiteModule;
@@ -0,0 +1,52 @@
1
+ import { postJson } from '../../lib/research/fetcher.js';
2
+ export const nextjs = {
3
+ id: 'nextjs',
4
+ name: 'Next.js',
5
+ domains: ['nextjs.org'],
6
+ search: async (query) => {
7
+ const url = 'https://nextjs.org/api/search';
8
+ const bodyStr = await postJson(url, { query, version: 'stable', routerType: 'app' }, {});
9
+ let data;
10
+ try {
11
+ // The Next.js API returns a JSON array sometimes or a JSON object with 'hits'
12
+ const parsed = JSON.parse(bodyStr);
13
+ if (Array.isArray(parsed)) {
14
+ data = parsed;
15
+ }
16
+ else if (parsed && Array.isArray(parsed.hits)) {
17
+ data = parsed.hits;
18
+ }
19
+ else {
20
+ throw new Error('Unexpected response format from Next.js search');
21
+ }
22
+ }
23
+ catch (err) {
24
+ throw new Error(`Failed to parse Next.js search response: ${err.message}`);
25
+ }
26
+ const queryLower = query.toLowerCase();
27
+ return data.map((item) => {
28
+ const content = item.content || '';
29
+ let snippet = content;
30
+ const idx = content.toLowerCase().indexOf(queryLower);
31
+ if (idx !== -1) {
32
+ const start = Math.max(0, idx - 40);
33
+ const end = Math.min(content.length, idx + 110);
34
+ snippet = content.slice(start, end);
35
+ if (start > 0)
36
+ snippet = '...' + snippet;
37
+ if (end < content.length)
38
+ snippet = snippet + '...';
39
+ }
40
+ else {
41
+ snippet = content.slice(0, 150);
42
+ }
43
+ // Clean up whitespace and newlines so it looks like a snippet and not a raw script
44
+ snippet = snippet.replace(/\s+/g, ' ').trim();
45
+ return {
46
+ url: `https://nextjs.org${item.path}${item.anchor ? `#${item.anchor}` : ''}`,
47
+ title: item.title ? `${item.pageTitle} - ${item.title}` : item.pageTitle || 'Untitled',
48
+ snippet,
49
+ };
50
+ });
51
+ },
52
+ };
@@ -0,0 +1,2 @@
1
+ import type { SiteModule } from '../types.js';
2
+ export declare const react: SiteModule;
@@ -0,0 +1,17 @@
1
+ import { algoliaQueryUrl, parseAlgoliaResponse } from '../../lib/research/docs/remote-search.js';
2
+ import { postJson } from '../../lib/research/fetcher.js';
3
+ export const react = {
4
+ id: 'react',
5
+ name: 'React',
6
+ domains: ['react.dev', 'legacy.reactjs.org'],
7
+ search: async (query) => {
8
+ const config = {
9
+ appId: '1FCF9AYYAT',
10
+ apiKey: '1b7ad4e1c89e645e351e59d40544eda1',
11
+ indexName: 'beta-react'
12
+ };
13
+ const url = algoliaQueryUrl(config);
14
+ const body = await postJson(url, { params: `query=${encodeURIComponent(query)}` }, { 'X-Algolia-API-Key': config.apiKey, 'X-Algolia-Application-Id': config.appId });
15
+ return parseAlgoliaResponse(body, url);
16
+ }
17
+ };
@@ -0,0 +1,2 @@
1
+ import type { SiteModule } from '../types.js';
2
+ export declare const tailwind: SiteModule;
@@ -0,0 +1,17 @@
1
+ import { algoliaQueryUrl, parseAlgoliaResponse } from '../../lib/research/docs/remote-search.js';
2
+ import { postJson } from '../../lib/research/fetcher.js';
3
+ export const tailwind = {
4
+ id: 'tailwind',
5
+ name: 'Tailwind CSS',
6
+ domains: ['tailwindcss.com'],
7
+ search: async (query) => {
8
+ const config = {
9
+ appId: 'KNPXZI5B0M',
10
+ apiKey: '5fc87cef58bb80203d2207578309fab6',
11
+ indexName: 'tailwindcss'
12
+ };
13
+ const url = algoliaQueryUrl(config);
14
+ const body = await postJson(url, { params: `query=${encodeURIComponent(query)}` }, { 'X-Algolia-API-Key': config.apiKey, 'X-Algolia-Application-Id': config.appId });
15
+ return parseAlgoliaResponse(body, url);
16
+ }
17
+ };
@@ -0,0 +1,2 @@
1
+ import type { SiteModule } from '../types.js';
2
+ export declare const vue: SiteModule;
@@ -0,0 +1,17 @@
1
+ import { algoliaQueryUrl, parseAlgoliaResponse } from '../../lib/research/docs/remote-search.js';
2
+ import { postJson } from '../../lib/research/fetcher.js';
3
+ export const vue = {
4
+ id: 'vue',
5
+ name: 'Vue.js',
6
+ domains: ['vuejs.org'],
7
+ search: async (query) => {
8
+ const config = {
9
+ appId: 'ML0LEBN7FQ',
10
+ apiKey: '10e7a8b13e6aec4007343338ab134e05',
11
+ indexName: 'vuejs'
12
+ };
13
+ const url = algoliaQueryUrl(config);
14
+ const body = await postJson(url, { params: `query=${encodeURIComponent(query)}` }, { 'X-Algolia-API-Key': config.apiKey, 'X-Algolia-Application-Id': config.appId });
15
+ return parseAlgoliaResponse(body, url);
16
+ }
17
+ };