@taurgis/bonsai 1.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 (159) hide show
  1. package/README.md +239 -0
  2. package/agents/agents/official-docs-researcher.agent.md +98 -0
  3. package/agents/instructions/web-research.instructions.md +29 -0
  4. package/agents/skills/web-research/SKILL.md +126 -0
  5. package/bin/cli.mjs +22 -0
  6. package/dist/base-command.d.ts +14 -0
  7. package/dist/base-command.js +56 -0
  8. package/dist/commands/config/base.d.ts +23 -0
  9. package/dist/commands/config/base.js +46 -0
  10. package/dist/commands/config/get.d.ts +20 -0
  11. package/dist/commands/config/get.js +48 -0
  12. package/dist/commands/config/list.d.ts +17 -0
  13. package/dist/commands/config/list.js +48 -0
  14. package/dist/commands/config/set.d.ts +31 -0
  15. package/dist/commands/config/set.js +94 -0
  16. package/dist/commands/config/unset.d.ts +21 -0
  17. package/dist/commands/config/unset.js +65 -0
  18. package/dist/commands/fetch.d.ts +38 -0
  19. package/dist/commands/fetch.js +311 -0
  20. package/dist/commands/import.d.ts +38 -0
  21. package/dist/commands/import.js +313 -0
  22. package/dist/commands/index.d.ts +35 -0
  23. package/dist/commands/index.js +306 -0
  24. package/dist/commands/inspect.d.ts +13 -0
  25. package/dist/commands/inspect.js +82 -0
  26. package/dist/commands/list.d.ts +25 -0
  27. package/dist/commands/list.js +143 -0
  28. package/dist/commands/prune.d.ts +24 -0
  29. package/dist/commands/prune.js +141 -0
  30. package/dist/commands/research/config/base.d.ts +23 -0
  31. package/dist/commands/research/config/base.js +46 -0
  32. package/dist/commands/research/config/get.d.ts +20 -0
  33. package/dist/commands/research/config/get.js +45 -0
  34. package/dist/commands/research/config/list.d.ts +17 -0
  35. package/dist/commands/research/config/list.js +42 -0
  36. package/dist/commands/research/config/set.d.ts +31 -0
  37. package/dist/commands/research/config/set.js +94 -0
  38. package/dist/commands/research/config/unset.d.ts +21 -0
  39. package/dist/commands/research/config/unset.js +59 -0
  40. package/dist/commands/research/import.d.ts +38 -0
  41. package/dist/commands/research/import.js +313 -0
  42. package/dist/commands/research/inspect.d.ts +13 -0
  43. package/dist/commands/research/inspect.js +86 -0
  44. package/dist/commands/research/list.d.ts +25 -0
  45. package/dist/commands/research/list.js +143 -0
  46. package/dist/commands/research/prune.d.ts +24 -0
  47. package/dist/commands/research/prune.js +141 -0
  48. package/dist/commands/research/search.d.ts +32 -0
  49. package/dist/commands/research/search.js +334 -0
  50. package/dist/commands/research/status.d.ts +17 -0
  51. package/dist/commands/research/status.js +108 -0
  52. package/dist/commands/research.d.ts +36 -0
  53. package/dist/commands/research.js +304 -0
  54. package/dist/commands/search.d.ts +32 -0
  55. package/dist/commands/search.js +334 -0
  56. package/dist/commands/status.d.ts +17 -0
  57. package/dist/commands/status.js +95 -0
  58. package/dist/commands.d.ts +24 -0
  59. package/dist/commands.js +24 -0
  60. package/dist/lib/config/index.d.ts +18 -0
  61. package/dist/lib/config/index.js +33 -0
  62. package/dist/lib/config/io.d.ts +8 -0
  63. package/dist/lib/config/io.js +75 -0
  64. package/dist/lib/config/resolve.d.ts +15 -0
  65. package/dist/lib/config/resolve.js +20 -0
  66. package/dist/lib/config/schema.d.ts +28 -0
  67. package/dist/lib/config/schema.js +32 -0
  68. package/dist/lib/research/artifact.d.ts +21 -0
  69. package/dist/lib/research/artifact.js +217 -0
  70. package/dist/lib/research/browser.d.ts +74 -0
  71. package/dist/lib/research/browser.js +359 -0
  72. package/dist/lib/research/cache-key.d.ts +5 -0
  73. package/dist/lib/research/cache-key.js +10 -0
  74. package/dist/lib/research/capture.d.ts +29 -0
  75. package/dist/lib/research/capture.js +181 -0
  76. package/dist/lib/research/compress.d.ts +5 -0
  77. package/dist/lib/research/compress.js +17 -0
  78. package/dist/lib/research/docs/__fixtures__/sphinx-searchindex.d.ts +1 -0
  79. package/dist/lib/research/docs/__fixtures__/sphinx-searchindex.js +2 -0
  80. package/dist/lib/research/docs/capabilities.d.ts +43 -0
  81. package/dist/lib/research/docs/capabilities.js +9 -0
  82. package/dist/lib/research/docs/clean-dom.d.ts +4 -0
  83. package/dist/lib/research/docs/clean-dom.js +41 -0
  84. package/dist/lib/research/docs/code-blocks.d.ts +5 -0
  85. package/dist/lib/research/docs/code-blocks.js +69 -0
  86. package/dist/lib/research/docs/detect.d.ts +6 -0
  87. package/dist/lib/research/docs/detect.js +239 -0
  88. package/dist/lib/research/docs/machine-readable.d.ts +21 -0
  89. package/dist/lib/research/docs/machine-readable.js +73 -0
  90. package/dist/lib/research/docs/markdown-source.d.ts +17 -0
  91. package/dist/lib/research/docs/markdown-source.js +194 -0
  92. package/dist/lib/research/docs/quality-gates.d.ts +9 -0
  93. package/dist/lib/research/docs/quality-gates.js +65 -0
  94. package/dist/lib/research/docs/remote-search-runner.d.ts +21 -0
  95. package/dist/lib/research/docs/remote-search-runner.js +54 -0
  96. package/dist/lib/research/docs/remote-search.d.ts +14 -0
  97. package/dist/lib/research/docs/remote-search.js +101 -0
  98. package/dist/lib/research/docs/rsc.d.ts +10 -0
  99. package/dist/lib/research/docs/rsc.js +30 -0
  100. package/dist/lib/research/docs/search-index.d.ts +13 -0
  101. package/dist/lib/research/docs/search-index.js +176 -0
  102. package/dist/lib/research/docs/section-artifacts.d.ts +12 -0
  103. package/dist/lib/research/docs/section-artifacts.js +75 -0
  104. package/dist/lib/research/docs/sections.d.ts +12 -0
  105. package/dist/lib/research/docs/sections.js +67 -0
  106. package/dist/lib/research/docs/source-map.d.ts +19 -0
  107. package/dist/lib/research/docs/source-map.js +87 -0
  108. package/dist/lib/research/docs/validate.d.ts +15 -0
  109. package/dist/lib/research/docs/validate.js +63 -0
  110. package/dist/lib/research/extract.d.ts +11 -0
  111. package/dist/lib/research/extract.js +83 -0
  112. package/dist/lib/research/fetcher.d.ts +29 -0
  113. package/dist/lib/research/fetcher.js +192 -0
  114. package/dist/lib/research/freshness.d.ts +21 -0
  115. package/dist/lib/research/freshness.js +82 -0
  116. package/dist/lib/research/markdown.d.ts +6 -0
  117. package/dist/lib/research/markdown.js +66 -0
  118. package/dist/lib/research/regression.d.ts +23 -0
  119. package/dist/lib/research/regression.js +93 -0
  120. package/dist/lib/research/resolve-target.d.ts +18 -0
  121. package/dist/lib/research/resolve-target.js +20 -0
  122. package/dist/lib/research/revalidate.d.ts +32 -0
  123. package/dist/lib/research/revalidate.js +152 -0
  124. package/dist/lib/research/schema.d.ts +47 -0
  125. package/dist/lib/research/schema.js +1 -0
  126. package/dist/lib/research/secret-scan.d.ts +5 -0
  127. package/dist/lib/research/secret-scan.js +37 -0
  128. package/dist/lib/research/secure-write.d.ts +16 -0
  129. package/dist/lib/research/secure-write.js +17 -0
  130. package/dist/lib/research/storage.d.ts +52 -0
  131. package/dist/lib/research/storage.js +182 -0
  132. package/dist/lib/research/store-roots.d.ts +29 -0
  133. package/dist/lib/research/store-roots.js +28 -0
  134. package/dist/lib/research/token-estimate.d.ts +5 -0
  135. package/dist/lib/research/token-estimate.js +7 -0
  136. package/dist/lib/research/url.d.ts +24 -0
  137. package/dist/lib/research/url.js +121 -0
  138. package/dist/sites/index.d.ts +4 -0
  139. package/dist/sites/index.js +28 -0
  140. package/dist/sites/salesforce/browser-search.d.ts +7 -0
  141. package/dist/sites/salesforce/browser-search.js +82 -0
  142. package/dist/sites/salesforce/coveo.d.ts +23 -0
  143. package/dist/sites/salesforce/coveo.js +161 -0
  144. package/dist/sites/salesforce/fetch-page.d.ts +6 -0
  145. package/dist/sites/salesforce/fetch-page.js +42 -0
  146. package/dist/sites/salesforce/index.d.ts +2 -0
  147. package/dist/sites/salesforce/index.js +16 -0
  148. package/dist/sites/salesforce/token.d.ts +26 -0
  149. package/dist/sites/salesforce/token.js +154 -0
  150. package/dist/sites/salesforce-developer/fetch-page.d.ts +3 -0
  151. package/dist/sites/salesforce-developer/fetch-page.js +18 -0
  152. package/dist/sites/salesforce-developer/index.d.ts +2 -0
  153. package/dist/sites/salesforce-developer/index.js +12 -0
  154. package/dist/sites/salesforce-doc-fetch.d.ts +14 -0
  155. package/dist/sites/salesforce-doc-fetch.js +321 -0
  156. package/dist/sites/types.d.ts +27 -0
  157. package/dist/sites/types.js +1 -0
  158. package/oclif.manifest.json +845 -0
  159. package/package.json +78 -0
@@ -0,0 +1,313 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import { createHash } from 'node:crypto';
3
+ import * as fs from 'node:fs';
4
+ import * as path 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 { 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 { getPolicy } from '../../lib/research/freshness.js';
12
+ import { compressMarkdown } from '../../lib/research/compress.js';
13
+ import { estimateTokens } from '../../lib/research/token-estimate.js';
14
+ export default class ResearchImport extends BaseCommand {
15
+ static id = 'research import';
16
+ static summary = 'Import agent-supplied clean Markdown notes into the local cache.';
17
+ static description = 'Accepts a synthesized Markdown note via stdin and registers it under the single target URL or multiple source URLs.';
18
+ static examples = [
19
+ {
20
+ description: 'Import detailed research for a single URL via stdin',
21
+ command: 'echo "# My Article" | <%= config.bin %> research import https://example.com/docs --stdin',
22
+ },
23
+ {
24
+ description: 'Import research synthesized from multiple source URLs',
25
+ command: 'echo "# Synthesized" | <%= config.bin %> research import --stdin --topic "React docs" --source-url https://react.dev/a --source-url https://react.dev/b',
26
+ },
27
+ {
28
+ description: 'Import research from a local Markdown file',
29
+ command: '<%= config.bin %> research import https://example.com/docs --file path/to/notes.md',
30
+ },
31
+ ];
32
+ static args = {
33
+ url: Args.string({
34
+ required: false,
35
+ description: 'The single source URL of the page (only for single-source import).',
36
+ }),
37
+ };
38
+ static flags = {
39
+ stdin: Flags.boolean({
40
+ description: 'Read Markdown from stdin.',
41
+ default: false,
42
+ }),
43
+ file: Flags.string({
44
+ description: 'Path to a Markdown file containing research notes to import.',
45
+ }),
46
+ 'input-format': Flags.option({
47
+ description: 'Format of the input provided.',
48
+ options: ['compressed', 'detailed'],
49
+ default: 'detailed',
50
+ })(),
51
+ topic: Flags.string({
52
+ char: 't',
53
+ description: 'The main topic for this research note.',
54
+ }),
55
+ tags: Flags.string({
56
+ char: 'g',
57
+ description: 'Taxonomic tags (can be repeated).',
58
+ multiple: true,
59
+ }),
60
+ tier: Flags.option({
61
+ description: 'Freshness tier policy.',
62
+ options: ['stable', 'standard', 'volatile'],
63
+ default: 'standard',
64
+ })(),
65
+ 'source-url': Flags.string({
66
+ description: 'Repeated source URLs for multi-source import.',
67
+ multiple: true,
68
+ }),
69
+ ttl: Flags.string({
70
+ char: 'l',
71
+ description: 'Predicted lifespan of the data (e.g. "24h", "7d").',
72
+ }),
73
+ storage: Flags.option({
74
+ description: 'Override where this note is cached (overrides configured default). Secret-bearing notes are always stored globally.',
75
+ options: ['global', 'project'],
76
+ })(),
77
+ };
78
+ static stdoutIsPrimaryData = true;
79
+ async init() {
80
+ await super.init();
81
+ const { args, flags } = await this.parse(ResearchImport);
82
+ this.args = args;
83
+ this.flags = flags;
84
+ }
85
+ async readStdin(limitBytes = 1024 * 1024) {
86
+ return new Promise((resolve, reject) => {
87
+ let data = '';
88
+ let bytesRead = 0;
89
+ process.stdin.setEncoding('utf8');
90
+ const onData = (chunk) => {
91
+ bytesRead += Buffer.byteLength(chunk, 'utf8');
92
+ if (bytesRead > limitBytes) {
93
+ process.stdin.removeListener('data', onData);
94
+ reject(new Error('stdin size limit exceeded (max 1 MiB)'));
95
+ return;
96
+ }
97
+ data += chunk;
98
+ };
99
+ process.stdin.on('data', onData);
100
+ const onEnd = () => {
101
+ process.stdin.removeListener('data', onData);
102
+ resolve(data);
103
+ };
104
+ process.stdin.once('end', onEnd);
105
+ process.stdin.once('error', (err) => {
106
+ process.stdin.removeListener('data', onData);
107
+ process.stdin.removeListener('end', onEnd);
108
+ reject(err);
109
+ });
110
+ });
111
+ }
112
+ getSourceUrls(hasMulti, multiUrls, singleUrl) {
113
+ const urls = hasMulti ? multiUrls : singleUrl ? [singleUrl] : [];
114
+ const normalized = urls.map((u) => {
115
+ try {
116
+ return normalizeUrl(u);
117
+ }
118
+ catch (err) {
119
+ this.error(`Invalid URL: ${err.message}`, { exit: 2 });
120
+ }
121
+ });
122
+ return hasMulti ? normalized.sort() : normalized;
123
+ }
124
+ validateSourceMode(hasSingle, hasMulti, multiUrls) {
125
+ if (hasSingle && hasMulti) {
126
+ this.error('Cannot specify both positional <url> and --source-url flags.', { exit: 2 });
127
+ }
128
+ if (!hasSingle && !hasMulti) {
129
+ this.error('Must specify either positional <url> (for single-source) or --source-url (for multi-source) import.', { exit: 2 });
130
+ }
131
+ if (hasMulti && !this.flags.topic) {
132
+ this.error('Multi-source import requires the --topic flag.', { exit: 2 });
133
+ }
134
+ if (!this.flags.stdin && !this.flags.file) {
135
+ this.error('Either --stdin or --file <path> must be specified to import content.', {
136
+ exit: 2,
137
+ });
138
+ }
139
+ if (this.flags.stdin && this.flags.file) {
140
+ this.error('Cannot specify both --stdin and --file <path>. Choose one.', { exit: 2 });
141
+ }
142
+ }
143
+ fsExistsSync(filePath) {
144
+ return fs.existsSync(filePath);
145
+ }
146
+ fsStatSync(filePath) {
147
+ return fs.statSync(filePath);
148
+ }
149
+ fsReadFileSync(filePath) {
150
+ return fs.readFileSync(filePath, 'utf8');
151
+ }
152
+ readAndValidateFile(filePath) {
153
+ if (!this.fsExistsSync(filePath)) {
154
+ this.error(`File does not exist: ${filePath}`, { exit: 2 });
155
+ }
156
+ const stat = this.fsStatSync(filePath);
157
+ if (!stat.isFile()) {
158
+ this.error(`Path is not a file: ${filePath}`, { exit: 2 });
159
+ }
160
+ if (stat.size > 1024 * 1024) {
161
+ this.error('File size limit exceeded (max 1 MiB).', { exit: 1 });
162
+ }
163
+ const content = this.fsReadFileSync(filePath);
164
+ if (!content.trim()) {
165
+ this.error('Empty file content provided.', { exit: 2 });
166
+ }
167
+ return content;
168
+ }
169
+ async readAndValidateInput() {
170
+ if (this.flags.stdin) {
171
+ try {
172
+ const rawInput = await this.readStdin();
173
+ if (!rawInput.trim()) {
174
+ this.error('Empty stdin content provided.', { exit: 2 });
175
+ }
176
+ return rawInput;
177
+ }
178
+ catch (err) {
179
+ this.error(err.message, { exit: 1 });
180
+ }
181
+ }
182
+ if (this.flags.file) {
183
+ const filePath = path.resolve(this.flags.file);
184
+ try {
185
+ return this.readAndValidateFile(filePath);
186
+ }
187
+ catch (err) {
188
+ this.error(`Failed to read file: ${err.message}`, { exit: 1 });
189
+ }
190
+ }
191
+ this.error('No input source specified.', { exit: 2 });
192
+ }
193
+ deriveImportCacheKey(hasSingle, singleNormalizedUrl, sourceUrls) {
194
+ if (hasSingle) {
195
+ return deriveCacheKey(singleNormalizedUrl);
196
+ }
197
+ const normalizedTopic = this.flags.topic.trim().toLowerCase();
198
+ const combinedString = [normalizedTopic, ...sourceUrls].join('|');
199
+ return createHash('sha256').update(combinedString).digest('hex');
200
+ }
201
+ buildImportArtifact(hasSingle, singleNormalizedUrl, sourceUrls, cacheKey, rawInput) {
202
+ const currentTime = new Date();
203
+ const tier = this.flags.tier;
204
+ const ttl = this.flags.ttl || null;
205
+ const { freshWindowMs } = getPolicy(tier, ttl);
206
+ const staleAfterTime = new Date(currentTime.getTime() + freshWindowMs);
207
+ const inputFormat = this.flags['input-format'];
208
+ const detailed = rawInput;
209
+ const compressed = inputFormat === 'detailed' ? compressMarkdown(rawInput) : rawInput;
210
+ const contentHash = createHash('sha256').update(detailed).digest('hex');
211
+ const metadata = {
212
+ schema_version: 1,
213
+ artifact_type: hasSingle ? 'source' : 'research_note',
214
+ source_url: hasSingle ? this.args.url : '',
215
+ source_urls: sourceUrls,
216
+ normalized_url: singleNormalizedUrl,
217
+ cache_key: cacheKey,
218
+ topic: this.flags.topic || null,
219
+ tags: this.flags.tags || [],
220
+ format_available: ['compressed', 'detailed'],
221
+ tier,
222
+ ttl,
223
+ fetched_at: null,
224
+ validated_at: currentTime.toISOString(),
225
+ stale_after: staleAfterTime.toISOString(),
226
+ capture_method: 'agent_supplied',
227
+ extraction_status: 'agent_supplied',
228
+ extraction_confidence: 'high',
229
+ quality_notes: ['agent-supplied research import'],
230
+ supplied_at: currentTime.toISOString(),
231
+ supplied_by: null,
232
+ etag: null,
233
+ last_modified: null,
234
+ content_hash: contentHash,
235
+ token_estimate: {
236
+ compressed: estimateTokens(compressed),
237
+ detailed: estimateTokens(detailed),
238
+ },
239
+ status: 'active',
240
+ site_module_id: null,
241
+ docs_engine: null,
242
+ docs_framework: null,
243
+ source_doc_url: null,
244
+ search_provider: null,
245
+ parent_cache_key: null,
246
+ section_anchor: null,
247
+ section_heading_path: null,
248
+ };
249
+ return {
250
+ metadata,
251
+ summary: `Synthesized research for ${hasSingle ? singleNormalizedUrl : this.flags.topic}`,
252
+ compressed,
253
+ detailed,
254
+ provenance: `Imported via agent-supplied research at ${currentTime.toISOString()}`,
255
+ };
256
+ }
257
+ async execute() {
258
+ const hasSingle = Boolean(this.args.url);
259
+ const multiUrls = this.flags['source-url'] || [];
260
+ const hasMulti = multiUrls.length > 0;
261
+ this.validateSourceMode(hasSingle, hasMulti, multiUrls);
262
+ const rawInput = await this.readAndValidateInput();
263
+ const sourceUrls = this.getSourceUrls(hasMulti, multiUrls, this.args.url);
264
+ const singleNormalizedUrl = hasSingle ? sourceUrls[0] || '' : '';
265
+ const cacheKey = this.deriveImportCacheKey(hasSingle, singleNormalizedUrl, sourceUrls);
266
+ const artifact = this.buildImportArtifact(hasSingle, singleNormalizedUrl, sourceUrls, cacheKey, rawInput);
267
+ const roots = loadStoreRoots({
268
+ configDir: this.config.configDir,
269
+ cwd: process.cwd(),
270
+ dataDir: this.config.dataDir,
271
+ flagOverride: this.flags.storage,
272
+ });
273
+ const writeResult = writeArtifactSecurely(roots, cacheKey, artifact);
274
+ const storagePath = getArtifactPath(writeResult.dataDir, cacheKey);
275
+ if (writeResult.redirected) {
276
+ this.warn(`Detected ${writeResult.secretLabel} in the imported content; stored in the global cache instead of the project to avoid committing secrets.`);
277
+ }
278
+ if (!this.requestedJson()) {
279
+ this.log(`Successfully imported research artifact.`);
280
+ this.log(`Cache Key: ${cacheKey}`);
281
+ this.log(`Storage Path: ${storagePath}`);
282
+ }
283
+ const inputFormat = this.flags['input-format'];
284
+ return {
285
+ schemaVersion: 1,
286
+ command: 'research import',
287
+ cache: {
288
+ key: cacheKey,
289
+ status: 'imported',
290
+ freshness: 'fresh',
291
+ path: storagePath,
292
+ storage: roots.mode,
293
+ redirectedToGlobal: writeResult.redirected,
294
+ },
295
+ source: {
296
+ url: hasSingle ? this.args.url : '',
297
+ normalizedUrl: singleNormalizedUrl,
298
+ captureMethod: 'agent_supplied',
299
+ extractionStatus: 'agent_supplied',
300
+ extractionConfidence: 'high',
301
+ qualityNotes: ['agent-supplied research import'],
302
+ fetchedAt: null,
303
+ validatedAt: artifact.metadata.validated_at,
304
+ staleAfter: artifact.metadata.stale_after,
305
+ },
306
+ format: inputFormat,
307
+ tokenEstimate: inputFormat === 'compressed'
308
+ ? artifact.metadata.token_estimate.compressed
309
+ : artifact.metadata.token_estimate.detailed,
310
+ content: inputFormat === 'compressed' ? artifact.compressed : artifact.detailed,
311
+ };
312
+ }
313
+ }
@@ -0,0 +1,13 @@
1
+ import { BaseCommand } from '../../base-command.js';
2
+ export default class ResearchInspect extends BaseCommand<typeof ResearchInspect> {
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 stdoutIsPrimaryData: boolean;
10
+ init(): Promise<void>;
11
+ execute(): Promise<unknown>;
12
+ private findSections;
13
+ }
@@ -0,0 +1,86 @@
1
+ import { Args } 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, scanCacheDirs } from '../../lib/research/storage.js';
6
+ import { loadStoreRoots } from '../../lib/research/store-roots.js';
7
+ export default class ResearchInspect extends BaseCommand {
8
+ static id = 'research inspect';
9
+ static summary = 'Inspect cached research metadata for a URL.';
10
+ static description = 'Inspects local storage and displays full frontmatter metadata for the URL (without printing the content body).';
11
+ static args = {
12
+ url: Args.string({
13
+ required: true,
14
+ description: 'The URL to inspect.',
15
+ }),
16
+ };
17
+ static stdoutIsPrimaryData = true;
18
+ async init() {
19
+ await super.init();
20
+ const { args } = await this.parse(ResearchInspect);
21
+ this.args = args;
22
+ }
23
+ async execute() {
24
+ const { url } = this.args;
25
+ let normalizedUrl;
26
+ let cacheKey;
27
+ try {
28
+ normalizedUrl = normalizeUrl(url);
29
+ cacheKey = deriveCacheKey(normalizedUrl);
30
+ }
31
+ catch (err) {
32
+ this.error(`Invalid URL: ${err.message}`, { exit: 2 });
33
+ }
34
+ const roots = loadStoreRoots({
35
+ configDir: this.config.configDir,
36
+ cwd: process.cwd(),
37
+ dataDir: this.config.dataDir,
38
+ });
39
+ const located = locateArtifact(roots.readRoots, cacheKey);
40
+ if (!located) {
41
+ this.error(`No cached research found for URL: ${normalizedUrl}`, { exit: 1 });
42
+ }
43
+ const cached = located.artifact;
44
+ const artifactPath = located.path;
45
+ const sections = this.findSections(roots.readRoots, cacheKey);
46
+ if (!this.requestedJson()) {
47
+ this.log(`Cache Key: ${cacheKey}`);
48
+ this.log(`Cache Path: ${artifactPath}`);
49
+ this.log(`--- Metadata ---`);
50
+ for (const [key, val] of Object.entries(cached.metadata)) {
51
+ if (typeof val === 'object' && val !== null) {
52
+ this.log(`${key}: ${JSON.stringify(val)}`);
53
+ }
54
+ else {
55
+ this.log(`${key}: ${val}`);
56
+ }
57
+ }
58
+ if (sections.length) {
59
+ this.log(`--- Sections (${sections.length}) ---`);
60
+ for (const s of sections) {
61
+ this.log(`${s.headingPath} [${s.anchor}] (${s.tokenEstimate.detailed} tokens) ${s.cacheKey}`);
62
+ }
63
+ }
64
+ }
65
+ return {
66
+ cacheKey,
67
+ cachePath: artifactPath,
68
+ metadata: cached.metadata,
69
+ sections,
70
+ };
71
+ }
72
+ // Section children link back via parent_cache_key; list the active ones for this page (T-22).
73
+ findSections(readRoots, parentKey) {
74
+ return scanCacheDirs(readRoots, (artifact) => {
75
+ const meta = artifact.metadata;
76
+ if (meta.parent_cache_key !== parentKey || meta.status !== 'active')
77
+ return null;
78
+ return {
79
+ cacheKey: meta.cache_key,
80
+ anchor: meta.section_anchor,
81
+ headingPath: meta.section_heading_path,
82
+ tokenEstimate: meta.token_estimate,
83
+ };
84
+ }).sort((a, b) => (a.headingPath ?? '').localeCompare(b.headingPath ?? ''));
85
+ }
86
+ }
@@ -0,0 +1,25 @@
1
+ import { BaseCommand } from '../../base-command.js';
2
+ export default class ResearchList extends BaseCommand<typeof ResearchList> {
3
+ static id: string;
4
+ static summary: string;
5
+ static description: string;
6
+ static examples: {
7
+ description: string;
8
+ command: string;
9
+ }[];
10
+ static flags: {
11
+ topic: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ tags: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ freshness: import("@oclif/core/interfaces").OptionFlag<"fresh" | "stale_grace" | "stale_expired" | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
+ 'artifact-type': import("@oclif/core/interfaces").OptionFlag<"source" | "research_note" | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
+ 'capture-method': import("@oclif/core/interfaces").OptionFlag<"static_fetch" | "browser_fallback" | "agent_supplied" | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
+ limit: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
17
+ };
18
+ static stdoutIsPrimaryData: boolean;
19
+ init(): Promise<void>;
20
+ private validateListFlags;
21
+ private matchesFilters;
22
+ private scanCacheDirForList;
23
+ private logListResults;
24
+ execute(): Promise<unknown>;
25
+ }
@@ -0,0 +1,143 @@
1
+ import { Flags } from '@oclif/core';
2
+ import { BaseCommand } from '../../base-command.js';
3
+ import { scanCacheDirs } from '../../lib/research/storage.js';
4
+ import { loadStoreRoots } from '../../lib/research/store-roots.js';
5
+ import { evaluateFreshness } from '../../lib/research/freshness.js';
6
+ export default class ResearchList extends BaseCommand {
7
+ static id = 'research list';
8
+ static summary = 'List cached research artifacts by metadata filters.';
9
+ static description = 'Lists cached research artifacts, including metadata details like path, source count, freshness, token estimates, and quality metrics without printing full content.';
10
+ static examples = [
11
+ {
12
+ description: 'List all cached entries',
13
+ command: '<%= config.bin %> research list',
14
+ },
15
+ {
16
+ description: 'List cached entries for a specific topic with JSON output',
17
+ command: '<%= config.bin %> research list --topic "React Suspense" --json',
18
+ },
19
+ {
20
+ description: 'List only fresh entries filtered by tags',
21
+ command: '<%= config.bin %> research list --freshness fresh --tags node --tags url',
22
+ },
23
+ ];
24
+ static flags = {
25
+ topic: Flags.string({
26
+ char: 't',
27
+ description: 'Filter by exact topic (case-insensitive).',
28
+ }),
29
+ tags: Flags.string({
30
+ char: 'g',
31
+ description: 'Filter by tags (must match all tags specified).',
32
+ multiple: true,
33
+ }),
34
+ freshness: Flags.option({
35
+ description: 'Filter by freshness state.',
36
+ options: ['fresh', 'stale_grace', 'stale_expired'],
37
+ })(),
38
+ 'artifact-type': Flags.option({
39
+ description: 'Filter by artifact type.',
40
+ options: ['source', 'research_note'],
41
+ })(),
42
+ 'capture-method': Flags.option({
43
+ description: 'Filter by capture method.',
44
+ options: ['static_fetch', 'browser_fallback', 'agent_supplied'],
45
+ })(),
46
+ limit: Flags.integer({
47
+ description: 'Maximum number of results to return (default 50, max 100).',
48
+ default: 50,
49
+ }),
50
+ };
51
+ static stdoutIsPrimaryData = true;
52
+ async init() {
53
+ await super.init();
54
+ const { flags } = await this.parse(ResearchList);
55
+ this.flags = flags;
56
+ }
57
+ validateListFlags() {
58
+ const limit = this.flags.limit;
59
+ if (limit !== undefined && (limit < 1 || limit > 100)) {
60
+ this.error('Limit must be between 1 and 100.', { exit: 2 });
61
+ }
62
+ }
63
+ matchesFilters(meta, freshness) {
64
+ if (this.flags.topic &&
65
+ (!meta.topic || meta.topic.trim().toLowerCase() !== this.flags.topic.trim().toLowerCase())) {
66
+ return false;
67
+ }
68
+ if (this.flags.tags && this.flags.tags.length > 0) {
69
+ const metaTagsLower = (meta.tags || []).map((t) => t.toLowerCase());
70
+ const hasAllTags = this.flags.tags.every((t) => metaTagsLower.includes(t.toLowerCase()));
71
+ if (!hasAllTags) {
72
+ return false;
73
+ }
74
+ }
75
+ if (this.flags['artifact-type'] && meta.artifact_type !== this.flags['artifact-type']) {
76
+ return false;
77
+ }
78
+ if (this.flags['capture-method'] && meta.capture_method !== this.flags['capture-method']) {
79
+ return false;
80
+ }
81
+ if (this.flags.freshness && freshness !== this.flags.freshness) {
82
+ return false;
83
+ }
84
+ return true;
85
+ }
86
+ scanCacheDirForList(readRoots, currentTime) {
87
+ return scanCacheDirs(readRoots, (artifact, filePath) => {
88
+ if (artifact.metadata.status !== 'active')
89
+ return null;
90
+ const freshness = evaluateFreshness(artifact.metadata, currentTime, null);
91
+ if (!this.matchesFilters(artifact.metadata, freshness))
92
+ return null;
93
+ return {
94
+ cacheKey: artifact.metadata.cache_key,
95
+ path: filePath,
96
+ artifactType: artifact.metadata.artifact_type,
97
+ sourceUrls: artifact.metadata.source_urls,
98
+ topic: artifact.metadata.topic,
99
+ tags: artifact.metadata.tags,
100
+ freshness,
101
+ captureMethod: artifact.metadata.capture_method,
102
+ tokenEstimate: artifact.metadata.token_estimate,
103
+ qualityNotes: artifact.metadata.quality_notes,
104
+ fetchedAt: artifact.metadata.fetched_at,
105
+ validatedAt: artifact.metadata.validated_at,
106
+ };
107
+ });
108
+ }
109
+ logListResults(finalResults) {
110
+ if (this.requestedJson())
111
+ return;
112
+ if (finalResults.length === 0) {
113
+ this.log('No cached research entries found matching filters.');
114
+ return;
115
+ }
116
+ this.log(`Found ${finalResults.length} cached research entries:\n`);
117
+ finalResults.forEach((res, index) => {
118
+ this.log(`${index + 1}. [${res.topic || 'No Topic'}] Key: ${res.cacheKey}`);
119
+ this.log(` Type: ${res.artifactType} | Freshness: ${res.freshness}`);
120
+ this.log(` Tokens: compressed=${res.tokenEstimate?.compressed || 0}, detailed=${res.tokenEstimate?.detailed || 0}`);
121
+ this.log(` Source URLs: ${res.sourceUrls.join(', ')}\n`);
122
+ });
123
+ }
124
+ async execute() {
125
+ this.validateListFlags();
126
+ const roots = loadStoreRoots({
127
+ configDir: this.config.configDir,
128
+ cwd: process.cwd(),
129
+ dataDir: this.config.dataDir,
130
+ });
131
+ const currentTime = new Date();
132
+ const results = this.scanCacheDirForList(roots.readRoots, currentTime);
133
+ // Sort by validated_at or fetched_at descending (most recent first)
134
+ results.sort((a, b) => {
135
+ const timeA = new Date(a.validatedAt || a.fetchedAt || 0).getTime();
136
+ const timeB = new Date(b.validatedAt || b.fetchedAt || 0).getTime();
137
+ return timeB - timeA;
138
+ });
139
+ const finalResults = results.slice(0, this.flags.limit);
140
+ this.logListResults(finalResults);
141
+ return finalResults;
142
+ }
143
+ }
@@ -0,0 +1,24 @@
1
+ import { BaseCommand } from '../../base-command.js';
2
+ export default class ResearchPrune extends BaseCommand<typeof ResearchPrune> {
3
+ static id: string;
4
+ static summary: string;
5
+ static description: string;
6
+ static examples: {
7
+ description: string;
8
+ command: string;
9
+ }[];
10
+ static flags: {
11
+ 'older-than': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ inactive: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ 'artifact-type': import("@oclif/core/interfaces").OptionFlag<"source" | "research_note" | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
+ 'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
15
+ yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
16
+ };
17
+ static stdoutIsPrimaryData: boolean;
18
+ init(): Promise<void>;
19
+ private validatePruneFlags;
20
+ private shouldPrune;
21
+ private findPruneCandidates;
22
+ private deletePruneCandidates;
23
+ execute(): Promise<unknown>;
24
+ }