@taurgis/bonsai 1.0.6 → 3.0.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 (120) hide show
  1. package/README.md +7 -13
  2. package/agents/agents/official-docs-researcher.agent.md +15 -35
  3. package/agents/agents/salesforce-docs-researcher.agent.md +17 -44
  4. package/agents/hooks/README.md +2 -9
  5. package/agents/hooks/claude-code/hooks/bonsai-web-fetch.sh +1 -1
  6. package/agents/hooks/codex/hooks/bonsai-user-prompt-context.sh +1 -1
  7. package/agents/hooks/codex/hooks/bonsai-web-fetch.sh +1 -1
  8. package/agents/hooks/cursor/hooks/bonsai-web-fetch.sh +1 -1
  9. package/agents/hooks/github-copilot/bonsai-web-fetch.sh +1 -1
  10. package/agents/instructions/salesforce-research.instructions.md +7 -8
  11. package/agents/instructions/web-research.instructions.md +3 -4
  12. package/agents/skills/web-research/SKILL.md +24 -37
  13. package/bin/cli.mjs +15 -2
  14. package/dist/base-command.d.ts +15 -2
  15. package/dist/base-command.js +32 -4
  16. package/dist/commands/fetch.d.ts +2 -0
  17. package/dist/commands/fetch.js +43 -29
  18. package/dist/commands/import.js +11 -3
  19. package/dist/commands/inspect.d.ts +4 -0
  20. package/dist/commands/inspect.js +48 -24
  21. package/dist/commands/list.d.ts +3 -1
  22. package/dist/commands/list.js +42 -25
  23. package/dist/commands/prune.d.ts +1 -0
  24. package/dist/commands/prune.js +35 -9
  25. package/dist/commands/status.d.ts +2 -0
  26. package/dist/commands/status.js +57 -25
  27. package/dist/commands.d.ts +0 -2
  28. package/dist/commands.js +0 -2
  29. package/dist/hooks/command-not-found/suggest.d.ts +7 -1
  30. package/dist/hooks/command-not-found/suggest.js +82 -57
  31. package/dist/lib/argv.js +51 -7
  32. package/dist/lib/color.d.ts +20 -0
  33. package/dist/lib/color.js +36 -0
  34. package/dist/lib/envelope.js +8 -2
  35. package/dist/lib/help-preflight.d.ts +15 -0
  36. package/dist/lib/help-preflight.js +81 -0
  37. package/dist/lib/limit-flag.d.ts +1 -0
  38. package/dist/lib/limit-flag.js +24 -0
  39. package/dist/lib/research/{search-index.d.ts → artifact-index.d.ts} +5 -5
  40. package/dist/lib/research/{search-index.js → artifact-index.js} +10 -10
  41. package/dist/lib/research/browser.d.ts +0 -8
  42. package/dist/lib/research/browser.js +20 -43
  43. package/dist/lib/research/regression.d.ts +18 -0
  44. package/dist/lib/research/regression.js +47 -0
  45. package/dist/lib/research/storage.d.ts +0 -1
  46. package/dist/lib/research/storage.js +15 -13
  47. package/dist/lib/research/url.d.ts +22 -0
  48. package/dist/lib/research/url.js +63 -1
  49. package/dist/lib/text.d.ts +3 -1
  50. package/dist/lib/text.js +9 -1
  51. package/dist/sites/coveo.d.ts +7 -0
  52. package/dist/sites/coveo.js +33 -0
  53. package/dist/sites/index.js +1 -18
  54. package/dist/sites/salesforce/fetch-page.js +1 -1
  55. package/dist/sites/salesforce/index.js +3 -8
  56. package/dist/sites/salesforce-developer/fetch-page.js +3 -1
  57. package/dist/sites/salesforce-developer/index.js +1 -2
  58. package/dist/sites/salesforce-doc-fetch.d.ts +11 -0
  59. package/dist/sites/salesforce-doc-fetch.js +117 -76
  60. package/dist/sites/salesforce-dom-probe.d.ts +16 -0
  61. package/dist/sites/salesforce-dom-probe.js +114 -0
  62. package/dist/sites/types.d.ts +0 -6
  63. package/oclif.manifest.json +31 -106
  64. package/package.json +32 -25
  65. package/dist/commands/index.d.ts +0 -35
  66. package/dist/commands/index.js +0 -306
  67. package/dist/commands/research/config/base.d.ts +0 -23
  68. package/dist/commands/research/config/base.js +0 -46
  69. package/dist/commands/research/config/get.d.ts +0 -20
  70. package/dist/commands/research/config/get.js +0 -45
  71. package/dist/commands/research/config/list.d.ts +0 -17
  72. package/dist/commands/research/config/list.js +0 -42
  73. package/dist/commands/research/config/set.d.ts +0 -31
  74. package/dist/commands/research/config/set.js +0 -94
  75. package/dist/commands/research/config/unset.d.ts +0 -21
  76. package/dist/commands/research/config/unset.js +0 -59
  77. package/dist/commands/research/import.d.ts +0 -38
  78. package/dist/commands/research/import.js +0 -313
  79. package/dist/commands/research/inspect.d.ts +0 -13
  80. package/dist/commands/research/inspect.js +0 -86
  81. package/dist/commands/research/list.d.ts +0 -25
  82. package/dist/commands/research/list.js +0 -143
  83. package/dist/commands/research/prune.d.ts +0 -24
  84. package/dist/commands/research/prune.js +0 -141
  85. package/dist/commands/research/search.d.ts +0 -32
  86. package/dist/commands/research/search.js +0 -334
  87. package/dist/commands/research/status.d.ts +0 -17
  88. package/dist/commands/research/status.js +0 -108
  89. package/dist/commands/research.d.ts +0 -36
  90. package/dist/commands/research.js +0 -304
  91. package/dist/commands/search.d.ts +0 -35
  92. package/dist/commands/search.js +0 -391
  93. package/dist/lib/research/docs/__fixtures__/sphinx-searchindex.d.ts +0 -1
  94. package/dist/lib/research/docs/__fixtures__/sphinx-searchindex.js +0 -2
  95. package/dist/lib/research/docs/discovery-index.d.ts +0 -14
  96. package/dist/lib/research/docs/discovery-index.js +0 -112
  97. package/dist/lib/research/docs/remote-search-runner.d.ts +0 -21
  98. package/dist/lib/research/docs/remote-search-runner.js +0 -114
  99. package/dist/lib/research/docs/remote-search.d.ts +0 -14
  100. package/dist/lib/research/docs/remote-search.js +0 -101
  101. package/dist/lib/research/docs/search-index.d.ts +0 -13
  102. package/dist/lib/research/docs/search-index.js +0 -176
  103. package/dist/sites/algolia.d.ts +0 -9
  104. package/dist/sites/algolia.js +0 -26
  105. package/dist/sites/auto.d.ts +0 -8
  106. package/dist/sites/auto.js +0 -30
  107. package/dist/sites/nextjs/index.d.ts +0 -2
  108. package/dist/sites/nextjs/index.js +0 -52
  109. package/dist/sites/react/index.d.ts +0 -2
  110. package/dist/sites/react/index.js +0 -17
  111. package/dist/sites/salesforce/browser-search.d.ts +0 -7
  112. package/dist/sites/salesforce/browser-search.js +0 -82
  113. package/dist/sites/salesforce/coveo.d.ts +0 -23
  114. package/dist/sites/salesforce/coveo.js +0 -161
  115. package/dist/sites/salesforce/token.d.ts +0 -26
  116. package/dist/sites/salesforce/token.js +0 -154
  117. package/dist/sites/tailwind/index.d.ts +0 -2
  118. package/dist/sites/tailwind/index.js +0 -17
  119. package/dist/sites/vue/index.d.ts +0 -2
  120. package/dist/sites/vue/index.js +0 -17
@@ -1,17 +0,0 @@
1
- import { BaseCommand } from '../../base-command.js';
2
- export default class ResearchStatus extends BaseCommand<typeof ResearchStatus> {
3
- static id: string;
4
- static summary: string;
5
- static description: string;
6
- static args: {
7
- url: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
8
- };
9
- static flags: {
10
- tier: import("@oclif/core/interfaces").OptionFlag<"stable" | "standard" | "volatile", import("@oclif/core/interfaces").CustomOptions>;
11
- ttl: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
- 'max-age': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
- };
14
- static stdoutIsPrimaryData: boolean;
15
- init(): Promise<void>;
16
- execute(): Promise<unknown>;
17
- }
@@ -1,108 +0,0 @@
1
- import { Args, Flags } from '@oclif/core';
2
- import { BaseCommand } from '../../base-command.js';
3
- import { normalizeUrl } from '../../lib/research/url.js';
4
- import { deriveCacheKey } from '../../lib/research/cache-key.js';
5
- import { locateArtifact, getArtifactPath } from '../../lib/research/storage.js';
6
- import { loadStoreRoots } from '../../lib/research/store-roots.js';
7
- import { evaluateFreshness, checkMaxAgeExpired } from '../../lib/research/freshness.js';
8
- export default class ResearchStatus extends BaseCommand {
9
- static id = 'research status';
10
- static summary = 'Check the cache status of a URL without fetching or writing.';
11
- static description = 'Reports whether the URL is cached, its freshness state, and what action (fetch, revalidate, or cached return) the research command would take.';
12
- static args = {
13
- url: Args.string({
14
- required: true,
15
- description: 'The URL to check.',
16
- }),
17
- };
18
- static flags = {
19
- tier: Flags.option({
20
- description: 'Freshness tier policy to evaluate against.',
21
- options: ['stable', 'standard', 'volatile'],
22
- default: 'standard',
23
- })(),
24
- ttl: Flags.string({
25
- char: 'l',
26
- description: 'Custom TTL duration to evaluate against (e.g. "2h", "7d").',
27
- }),
28
- 'max-age': Flags.string({
29
- description: 'Maximum age of cache to accept (e.g. "1d", "30d").',
30
- }),
31
- };
32
- static stdoutIsPrimaryData = true;
33
- async init() {
34
- await super.init();
35
- const { args, flags } = await this.parse(ResearchStatus);
36
- this.args = args;
37
- this.flags = flags;
38
- }
39
- async execute() {
40
- const { url } = this.args;
41
- const { ttl, tier, 'max-age': maxAge } = this.flags;
42
- let normalizedUrl;
43
- let cacheKey;
44
- try {
45
- normalizedUrl = normalizeUrl(url);
46
- cacheKey = deriveCacheKey(normalizedUrl);
47
- }
48
- catch (err) {
49
- this.error(`Invalid URL: ${err.message}`, { exit: 2 });
50
- }
51
- const roots = loadStoreRoots({
52
- configDir: this.config.configDir,
53
- cwd: process.cwd(),
54
- dataDir: this.config.dataDir,
55
- });
56
- const located = locateArtifact(roots.readRoots, cacheKey);
57
- const cached = located?.artifact ?? null;
58
- const currentTime = new Date();
59
- let status;
60
- let freshness;
61
- let action;
62
- if (!cached) {
63
- status = 'miss';
64
- freshness = 'stale_expired';
65
- action = 'would_fetch';
66
- }
67
- else {
68
- let isMaxAgeExpired = false;
69
- if (maxAge) {
70
- try {
71
- isMaxAgeExpired = checkMaxAgeExpired(cached, currentTime, maxAge);
72
- }
73
- catch (err) {
74
- this.error(`Invalid max-age: ${err.message}`, { exit: 2 });
75
- }
76
- }
77
- const freshnessState = isMaxAgeExpired
78
- ? 'stale_expired'
79
- : evaluateFreshness(cached.metadata, currentTime, ttl || cached.metadata.ttl);
80
- freshness = freshnessState;
81
- if (freshnessState === 'fresh') {
82
- status = 'hit';
83
- action = 'would_return_cached';
84
- }
85
- else {
86
- status = 'stale';
87
- action = 'would_revalidate';
88
- }
89
- }
90
- // On a hit, report where it actually lives; on a miss, where a fetch would write it.
91
- const artifactPath = located?.path ?? getArtifactPath(roots.writeRoot, cacheKey);
92
- if (!this.requestedJson()) {
93
- this.log(`URL: ${normalizedUrl}`);
94
- this.log(`Cache Key: ${cacheKey}`);
95
- this.log(`Cache Path: ${artifactPath}`);
96
- this.log(`Status: ${status}`);
97
- this.log(`Freshness: ${freshness}`);
98
- this.log(`Action: ${action}`);
99
- }
100
- return {
101
- cacheKey,
102
- cachePath: artifactPath,
103
- status,
104
- freshness,
105
- action,
106
- };
107
- }
108
- }
@@ -1,36 +0,0 @@
1
- import { BaseCommand } from '../base-command.js';
2
- export default class Research extends BaseCommand<typeof Research> {
3
- static id: string;
4
- static summary: string;
5
- static description: string;
6
- static examples: {
7
- description: string;
8
- command: string;
9
- }[];
10
- static args: {
11
- url: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
12
- };
13
- static flags: {
14
- topic: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
- tags: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
- format: import("@oclif/core/interfaces").OptionFlag<"compressed" | "detailed", import("@oclif/core/interfaces").CustomOptions>;
17
- tier: import("@oclif/core/interfaces").OptionFlag<"stable" | "standard" | "volatile", import("@oclif/core/interfaces").CustomOptions>;
18
- ttl: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
19
- 'max-age': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
20
- force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
21
- 'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
22
- 'allow-stale': import("@oclif/core/interfaces").BooleanFlag<boolean>;
23
- rendered: import("@oclif/core/interfaces").BooleanFlag<boolean>;
24
- storage: import("@oclif/core/interfaces").OptionFlag<"global" | "project" | undefined, import("@oclif/core/interfaces").CustomOptions>;
25
- };
26
- static stdoutIsPrimaryData: boolean;
27
- init(): Promise<void>;
28
- private executeCacheHit;
29
- private executeCacheMiss;
30
- private buildResultData;
31
- private validateDurationFlags;
32
- private resolveArtifact;
33
- private persistFreshArtifact;
34
- private persistSectionsIfFresh;
35
- execute(): Promise<unknown>;
36
- }
@@ -1,304 +0,0 @@
1
- import { Args, Flags } from '@oclif/core';
2
- import { mkdtempSync, rmSync } from 'node:fs';
3
- import { tmpdir } from 'node:os';
4
- import { join } from 'node:path';
5
- import { BaseCommand } from '../base-command.js';
6
- import { normalizeUrl } from '../lib/research/url.js';
7
- import { deriveCacheKey } from '../lib/research/cache-key.js';
8
- import { locateArtifact, writeArtifact, getArtifactPath } from '../lib/research/storage.js';
9
- import { loadStoreRoots } from '../lib/research/store-roots.js';
10
- import { writeArtifactSecurely } from '../lib/research/secure-write.js';
11
- import { evaluateFreshness, parseTtlToMs, checkMaxAgeExpired } from '../lib/research/freshness.js';
12
- import { revalidateCache, createArtifactFromFetch } from '../lib/research/revalidate.js';
13
- import { fetchStaticHtml, fetchText } from '../lib/research/fetcher.js';
14
- import { fetchRenderedHtml } from '../lib/research/browser.js';
15
- import { capturePage } from '../lib/research/capture.js';
16
- import { persistSectionArtifacts } from '../lib/research/docs/section-artifacts.js';
17
- import { detectSite } from '../sites/index.js';
18
- const CAPTURE_DEPS = {
19
- fetchStatic: (url) => fetchStaticHtml(url),
20
- fetchRendered: (url) => fetchRenderedHtml(url),
21
- fetchText: (url) => fetchText(url),
22
- };
23
- export default class Research extends BaseCommand {
24
- static id = 'research';
25
- static summary = 'An advanced, locally cached web research tool optimized for LLM ingestion.';
26
- static description = 'Scrapes the specified URL, strips HTML boilerplate, converts the semantic payload into clean Markdown format, and caches the result locally using dynamic TTL rules.';
27
- static examples = [
28
- {
29
- description: 'Research a URL with detailed output, tagged with topic and tags',
30
- command: '<%= config.bin %> research https://docs.nestjs.com/ --topic "Backend Frameworks" --tags "Node" --tags "NestJS" --format detailed --ttl 30d',
31
- },
32
- {
33
- description: 'Research a volatile page with compressed output and short TTL, returned as JSON',
34
- command: '<%= config.bin %> research https://news.ycombinator.com/ --format compressed --ttl 2h --json',
35
- },
36
- ];
37
- static args = {
38
- url: Args.string({
39
- required: true,
40
- description: 'The full HTTP/HTTPS URL of the web page to research.',
41
- }),
42
- };
43
- static flags = {
44
- topic: Flags.string({
45
- char: 't',
46
- description: 'The main category/topic of the research for metadata tagging.',
47
- }),
48
- tags: Flags.string({
49
- char: 'g',
50
- description: 'Taxonomic tags for this research (can be repeated).',
51
- multiple: true,
52
- }),
53
- format: Flags.option({
54
- char: 'f',
55
- description: 'Desired data density.',
56
- options: ['compressed', 'detailed'],
57
- default: 'compressed',
58
- })(),
59
- tier: Flags.option({
60
- description: 'Freshness tier policy.',
61
- options: ['stable', 'standard', 'volatile'],
62
- default: 'standard',
63
- })(),
64
- ttl: Flags.string({
65
- char: 'l',
66
- description: 'Predicted lifespan of the data (e.g., "2h", "7d", "3m").',
67
- }),
68
- 'max-age': Flags.string({
69
- description: 'Maximum age of cache to accept (e.g., "1d", "30d").',
70
- }),
71
- force: Flags.boolean({
72
- description: 'Force a fresh fetch, ignoring any cached entries.',
73
- default: false,
74
- }),
75
- 'dry-run': Flags.boolean({
76
- description: 'Perform validation and fetch without saving to local cache.',
77
- default: false,
78
- }),
79
- 'allow-stale': Flags.boolean({
80
- description: 'Allow serving stale cache if the remote server is unreachable.',
81
- default: false,
82
- }),
83
- rendered: Flags.boolean({
84
- description: 'Force using a browser-rendered scraping path for dynamic pages.',
85
- default: false,
86
- }),
87
- storage: Flags.option({
88
- description: 'Override where this result is cached (overrides configured default). Secret-bearing pages are always stored globally.',
89
- options: ['global', 'project'],
90
- })(),
91
- };
92
- static stdoutIsPrimaryData = true;
93
- async init() {
94
- await super.init();
95
- const { args, flags } = await this.parse(Research);
96
- this.args = args;
97
- this.flags = flags;
98
- }
99
- async executeCacheHit(cached, targetDir, currentTime) {
100
- const { ttl, 'max-age': maxAge, 'allow-stale': allowStale, rendered } = this.flags;
101
- const isExpired = checkMaxAgeExpired(cached, currentTime, maxAge);
102
- const freshnessState = isExpired
103
- ? 'stale_expired'
104
- : evaluateFreshness(cached.metadata, currentTime, ttl);
105
- if (freshnessState === 'fresh') {
106
- return { cacheStatus: 'hit', freshnessState, artifact: cached };
107
- }
108
- const revalResult = await revalidateCache(targetDir, cached, currentTime, {
109
- allowStale: Boolean(allowStale),
110
- ttlOverride: ttl,
111
- rendered: Boolean(rendered),
112
- });
113
- handleStaleRevalidationResult(this, revalResult);
114
- return {
115
- cacheStatus: revalResult.status,
116
- freshnessState,
117
- artifact: revalResult.artifact,
118
- };
119
- }
120
- async executeCacheMiss(normalizedUrl, currentTime, cacheKey) {
121
- const { topic, tags, tier, ttl, rendered } = this.flags;
122
- const siteModule = detectSite(normalizedUrl);
123
- const useRendered = rendered || Boolean(siteModule?.defaults?.rendered);
124
- let fetchResult;
125
- let extraction;
126
- let capture = null;
127
- if (siteModule?.fetchPage) {
128
- // Custom site modules own their fetch/extract strategy; the generic capture path is skipped.
129
- ({ fetchResult, extraction } = await siteModule.fetchPage(normalizedUrl));
130
- }
131
- else {
132
- capture = await capturePage(normalizedUrl, { forceRendered: useRendered }, CAPTURE_DEPS);
133
- fetchResult = capture.fetchResult;
134
- extraction = capture.extraction;
135
- }
136
- const artifact = createArtifactFromFetch(normalizedUrl, normalizedUrl, cacheKey, fetchResult, extraction, tier, ttl || null, currentTime);
137
- artifact.metadata.topic = topic || null;
138
- artifact.metadata.tags = tags || [];
139
- artifact.metadata.site_module_id = siteModule?.id ?? null;
140
- if (capture) {
141
- applyCaptureMetadata(artifact, capture);
142
- }
143
- else if (useRendered) {
144
- artifact.metadata.capture_method = 'browser_fallback';
145
- }
146
- return artifact;
147
- }
148
- buildResultData(url, normalizedUrl, cacheKey, storageDir, storageMode, cacheStatus, freshnessState, format, artifact, content, redirectedToGlobal) {
149
- return {
150
- schemaVersion: 1,
151
- command: 'research',
152
- cache: {
153
- key: cacheKey,
154
- status: cacheStatus,
155
- freshness: freshnessState,
156
- path: getArtifactPath(storageDir, cacheKey),
157
- storage: storageMode,
158
- redirectedToGlobal,
159
- },
160
- source: {
161
- url,
162
- normalizedUrl,
163
- captureMethod: artifact.metadata.capture_method,
164
- extractionStatus: artifact.metadata.extraction_status,
165
- extractionConfidence: artifact.metadata.extraction_confidence,
166
- qualityNotes: artifact.metadata.quality_notes,
167
- fetchedAt: artifact.metadata.fetched_at,
168
- validatedAt: artifact.metadata.validated_at,
169
- staleAfter: artifact.metadata.stale_after,
170
- },
171
- artifactType: artifact.metadata.artifact_type,
172
- docsEngine: artifact.metadata.docs_engine,
173
- docsFramework: artifact.metadata.docs_framework,
174
- sourceDocUrl: artifact.metadata.source_doc_url,
175
- searchProvider: artifact.metadata.search_provider,
176
- format,
177
- tokenEstimate: format === 'compressed'
178
- ? artifact.metadata.token_estimate.compressed
179
- : artifact.metadata.token_estimate.detailed,
180
- content,
181
- };
182
- }
183
- // Validates the duration flags up front, exiting with code 2 on a malformed value.
184
- validateDurationFlags(ttl, maxAge) {
185
- for (const [label, value] of [
186
- ['TTL', ttl],
187
- ['max-age', maxAge],
188
- ]) {
189
- if (!value)
190
- continue;
191
- try {
192
- parseTtlToMs(value);
193
- }
194
- catch (err) {
195
- this.error(`Invalid ${label}: ${err.message}`, { exit: 2 });
196
- }
197
- }
198
- }
199
- // Serves the cache when a fresh/revalidatable entry exists, otherwise fetches fresh. Reads fall
200
- // back project→global; the resolved artifact is returned with the dir it lives in / landed in.
201
- async resolveArtifact(normalizedUrl, cacheKey, roots, tmpDir, currentTime) {
202
- const located = this.flags.force ? null : locateArtifact(roots.readRoots, cacheKey);
203
- if (located) {
204
- // Revalidate where the entry already lives; on dry-run use the throwaway dir so the cache is
205
- // never mutated. ponytail: revalidation rewrites in place, so a refreshed project entry that
206
- // gains a secret is not re-routed — only first-time project writes are scanned.
207
- const revalDir = tmpDir ?? located.dataDir;
208
- const hit = await this.executeCacheHit(located.artifact, revalDir, currentTime);
209
- return { ...hit, storageDir: located.dataDir, redirectedToGlobal: false };
210
- }
211
- const artifact = await this.executeCacheMiss(normalizedUrl, currentTime, cacheKey);
212
- const { dir, redirectedToGlobal } = this.persistFreshArtifact(roots, tmpDir, cacheKey, artifact);
213
- return { cacheStatus: 'miss', freshnessState: 'stale_expired', artifact, storageDir: dir, redirectedToGlobal };
214
- }
215
- // Writes a freshly fetched artifact, honoring dry-run (throwaway dir) and the secret-safety
216
- // redirect (project→global). Returns the data dir reported to the user and whether a redirect
217
- // occurred (so the JSON envelope mirrors `research import`).
218
- persistFreshArtifact(roots, tmpDir, cacheKey, artifact) {
219
- if (tmpDir) {
220
- writeArtifact(tmpDir, cacheKey, artifact);
221
- // Report the real would-be location, not the throwaway dir that is about to be deleted.
222
- return { dir: roots.writeRoot, redirectedToGlobal: false };
223
- }
224
- const result = writeArtifactSecurely(roots, cacheKey, artifact);
225
- if (result.redirected) {
226
- this.warn(`Detected ${result.secretLabel} in the page content; stored in the global cache instead of the project to avoid committing secrets.`);
227
- }
228
- return { dir: result.dataDir, redirectedToGlobal: result.redirected };
229
- }
230
- // Long references are split into searchable/inspectable section children whenever the page artifact
231
- // is freshly written (T-22). Best-effort: never let chunking break the main result.
232
- persistSectionsIfFresh(targetDir, artifact, currentTime, cacheStatus) {
233
- if (cacheStatus !== 'miss' && cacheStatus !== 'refreshed')
234
- return;
235
- try {
236
- persistSectionArtifacts(targetDir, artifact, currentTime);
237
- }
238
- catch {
239
- /* section generation is non-essential; ignore failures */
240
- }
241
- }
242
- async execute() {
243
- const { url } = this.args;
244
- const { format, ttl, 'max-age': maxAge, 'dry-run': dryRun } = this.flags;
245
- this.validateDurationFlags(ttl, maxAge);
246
- let normalizedUrl;
247
- let cacheKey;
248
- try {
249
- normalizedUrl = normalizeUrl(url);
250
- cacheKey = deriveCacheKey(normalizedUrl);
251
- }
252
- catch (err) {
253
- this.error(`Invalid URL: ${err.message}`, { exit: 2 });
254
- }
255
- const roots = loadStoreRoots({
256
- configDir: this.config.configDir,
257
- cwd: process.cwd(),
258
- dataDir: this.config.dataDir,
259
- flagOverride: this.flags.storage,
260
- });
261
- const tmpDir = dryRun ? mkdtempSync(join(tmpdir(), 'fnr-dry-run-')) : null;
262
- const currentTime = new Date();
263
- try {
264
- const { cacheStatus, freshnessState, artifact, storageDir, redirectedToGlobal } = await this.resolveArtifact(normalizedUrl, cacheKey, roots, tmpDir, currentTime);
265
- this.persistSectionsIfFresh(tmpDir ?? storageDir, artifact, currentTime, cacheStatus);
266
- const content = format === 'compressed' ? artifact.compressed : artifact.detailed;
267
- const resultData = this.buildResultData(url, normalizedUrl, cacheKey, storageDir, roots.mode, cacheStatus, freshnessState, format, artifact, content, redirectedToGlobal);
268
- if (!this.requestedJson()) {
269
- this.log(content);
270
- }
271
- return resultData;
272
- }
273
- finally {
274
- if (tmpDir) {
275
- rmSync(tmpDir, { recursive: true, force: true });
276
- }
277
- }
278
- }
279
- }
280
- // Copies Phase 2 capability provenance from a capture outcome onto the artifact metadata.
281
- function applyCaptureMetadata(artifact, capture) {
282
- const meta = artifact.metadata;
283
- meta.capture_method = capture.captureMethod;
284
- meta.docs_engine = capture.capabilities.docsEngine ?? null;
285
- meta.docs_framework = capture.capabilities.framework ?? null;
286
- meta.source_doc_url = capture.sourceDocUrl;
287
- meta.search_provider = capture.capabilities.search?.provider ?? null;
288
- if (capture.extraction.isIndexHub)
289
- meta.artifact_type = 'index';
290
- if (capture.sourceDocUrl && !meta.source_urls.includes(capture.sourceDocUrl)) {
291
- meta.source_urls.push(capture.sourceDocUrl);
292
- }
293
- }
294
- function handleStaleRevalidationResult(command, revalResult) {
295
- if (revalResult.status !== 'stale')
296
- return;
297
- if (revalResult.allowed) {
298
- command.warn(`Serving stale content: revalidation failed: ${revalResult.error}`);
299
- }
300
- else {
301
- command.warn(`Serving stale content (exit 5): revalidation failed: ${revalResult.error}`);
302
- process.exitCode = 5;
303
- }
304
- }
@@ -1,35 +0,0 @@
1
- import { BaseCommand } from '../base-command.js';
2
- export default class ResearchSearch extends BaseCommand<typeof ResearchSearch> {
3
- static id: string;
4
- static summary: string;
5
- static description: string;
6
- static examples: {
7
- description: string;
8
- command: string;
9
- }[];
10
- static args: {
11
- query: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
12
- };
13
- static flags: {
14
- topic: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
- tags: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
- 'artifact-type': import("@oclif/core/interfaces").OptionFlag<"source" | "research_note" | "index" | "section" | undefined, import("@oclif/core/interfaces").CustomOptions>;
17
- limit: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
18
- 'include-stale': import("@oclif/core/interfaces").BooleanFlag<boolean>;
19
- domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
20
- remote: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
21
- };
22
- static stdoutIsPrimaryData: boolean;
23
- private validateSearchFlags;
24
- private getSearchQueryTerms;
25
- private matchesFilters;
26
- private phraseMatchBonus;
27
- private calculateScore;
28
- private makeSnippet;
29
- private scanCacheDirForResults;
30
- private logSearchResults;
31
- private printResults;
32
- private executeSiteSearch;
33
- private executeRemoteSearch;
34
- run(): Promise<unknown>;
35
- }