@taurgis/bonsai 1.0.6 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -13
- package/agents/agents/official-docs-researcher.agent.md +15 -35
- package/agents/agents/salesforce-docs-researcher.agent.md +17 -44
- package/agents/hooks/README.md +2 -9
- package/agents/hooks/claude-code/hooks/bonsai-web-fetch.sh +1 -1
- package/agents/hooks/codex/hooks/bonsai-user-prompt-context.sh +1 -1
- package/agents/hooks/codex/hooks/bonsai-web-fetch.sh +1 -1
- package/agents/hooks/cursor/hooks/bonsai-web-fetch.sh +1 -1
- package/agents/hooks/github-copilot/bonsai-web-fetch.sh +1 -1
- package/agents/instructions/salesforce-research.instructions.md +7 -8
- package/agents/instructions/web-research.instructions.md +3 -4
- package/agents/skills/web-research/SKILL.md +24 -37
- package/bin/cli.mjs +15 -2
- package/dist/base-command.d.ts +15 -2
- package/dist/base-command.js +32 -4
- package/dist/commands/fetch.d.ts +3 -0
- package/dist/commands/fetch.js +96 -30
- package/dist/commands/import.js +14 -5
- package/dist/commands/inspect.d.ts +4 -0
- package/dist/commands/inspect.js +48 -24
- package/dist/commands/list.d.ts +3 -1
- package/dist/commands/list.js +42 -25
- package/dist/commands/prune.d.ts +1 -0
- package/dist/commands/prune.js +35 -9
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.js +57 -25
- package/dist/commands.d.ts +0 -2
- package/dist/commands.js +0 -2
- package/dist/hooks/command-not-found/suggest.d.ts +7 -1
- package/dist/hooks/command-not-found/suggest.js +82 -57
- package/dist/lib/argv.js +51 -7
- package/dist/lib/color.d.ts +20 -0
- package/dist/lib/color.js +36 -0
- package/dist/lib/envelope.js +8 -2
- package/dist/lib/help-preflight.d.ts +15 -0
- package/dist/lib/help-preflight.js +81 -0
- package/dist/lib/limit-flag.d.ts +1 -0
- package/dist/lib/limit-flag.js +24 -0
- package/dist/lib/research/{search-index.d.ts → artifact-index.d.ts} +5 -5
- package/dist/lib/research/{search-index.js → artifact-index.js} +10 -10
- package/dist/lib/research/browser.d.ts +1 -8
- package/dist/lib/research/browser.js +32 -46
- package/dist/lib/research/docs/markdown-source.js +2 -1
- package/dist/lib/research/extract.js +2 -1
- package/dist/lib/research/fetcher.js +92 -48
- package/dist/lib/research/prompt-injection.d.ts +1 -0
- package/dist/lib/research/prompt-injection.js +149 -0
- package/dist/lib/research/proxy.d.ts +35 -0
- package/dist/lib/research/proxy.js +107 -0
- package/dist/lib/research/regression.d.ts +18 -0
- package/dist/lib/research/regression.js +47 -0
- package/dist/lib/research/storage.d.ts +0 -1
- package/dist/lib/research/storage.js +15 -13
- package/dist/lib/research/url.d.ts +22 -0
- package/dist/lib/research/url.js +63 -1
- package/dist/lib/text.d.ts +3 -1
- package/dist/lib/text.js +9 -1
- package/dist/sites/coveo.d.ts +7 -0
- package/dist/sites/coveo.js +33 -0
- package/dist/sites/index.js +1 -18
- package/dist/sites/salesforce/fetch-page.js +1 -1
- package/dist/sites/salesforce/index.js +3 -8
- package/dist/sites/salesforce-developer/fetch-page.js +3 -1
- package/dist/sites/salesforce-developer/index.js +1 -2
- package/dist/sites/salesforce-doc-fetch.d.ts +11 -0
- package/dist/sites/salesforce-doc-fetch.js +117 -76
- package/dist/sites/salesforce-dom-probe.d.ts +16 -0
- package/dist/sites/salesforce-dom-probe.js +114 -0
- package/dist/sites/types.d.ts +0 -6
- package/oclif.manifest.json +31 -106
- package/package.json +35 -26
- package/dist/commands/index.d.ts +0 -35
- package/dist/commands/index.js +0 -306
- package/dist/commands/research/config/base.d.ts +0 -23
- package/dist/commands/research/config/base.js +0 -46
- package/dist/commands/research/config/get.d.ts +0 -20
- package/dist/commands/research/config/get.js +0 -45
- package/dist/commands/research/config/list.d.ts +0 -17
- package/dist/commands/research/config/list.js +0 -42
- package/dist/commands/research/config/set.d.ts +0 -31
- package/dist/commands/research/config/set.js +0 -94
- package/dist/commands/research/config/unset.d.ts +0 -21
- package/dist/commands/research/config/unset.js +0 -59
- package/dist/commands/research/import.d.ts +0 -38
- package/dist/commands/research/import.js +0 -313
- package/dist/commands/research/inspect.d.ts +0 -13
- package/dist/commands/research/inspect.js +0 -86
- package/dist/commands/research/list.d.ts +0 -25
- package/dist/commands/research/list.js +0 -143
- package/dist/commands/research/prune.d.ts +0 -24
- package/dist/commands/research/prune.js +0 -141
- package/dist/commands/research/search.d.ts +0 -32
- package/dist/commands/research/search.js +0 -334
- package/dist/commands/research/status.d.ts +0 -17
- package/dist/commands/research/status.js +0 -108
- package/dist/commands/research.d.ts +0 -36
- package/dist/commands/research.js +0 -304
- package/dist/commands/search.d.ts +0 -35
- package/dist/commands/search.js +0 -391
- package/dist/lib/research/docs/__fixtures__/sphinx-searchindex.d.ts +0 -1
- package/dist/lib/research/docs/__fixtures__/sphinx-searchindex.js +0 -2
- package/dist/lib/research/docs/discovery-index.d.ts +0 -14
- package/dist/lib/research/docs/discovery-index.js +0 -112
- package/dist/lib/research/docs/remote-search-runner.d.ts +0 -21
- package/dist/lib/research/docs/remote-search-runner.js +0 -114
- package/dist/lib/research/docs/remote-search.d.ts +0 -14
- package/dist/lib/research/docs/remote-search.js +0 -101
- package/dist/lib/research/docs/search-index.d.ts +0 -13
- package/dist/lib/research/docs/search-index.js +0 -176
- package/dist/sites/algolia.d.ts +0 -9
- package/dist/sites/algolia.js +0 -26
- package/dist/sites/auto.d.ts +0 -8
- package/dist/sites/auto.js +0 -30
- package/dist/sites/nextjs/index.d.ts +0 -2
- package/dist/sites/nextjs/index.js +0 -52
- package/dist/sites/react/index.d.ts +0 -2
- package/dist/sites/react/index.js +0 -17
- package/dist/sites/salesforce/browser-search.d.ts +0 -7
- package/dist/sites/salesforce/browser-search.js +0 -82
- package/dist/sites/salesforce/coveo.d.ts +0 -23
- package/dist/sites/salesforce/coveo.js +0 -161
- package/dist/sites/salesforce/token.d.ts +0 -26
- package/dist/sites/salesforce/token.js +0 -154
- package/dist/sites/tailwind/index.d.ts +0 -2
- package/dist/sites/tailwind/index.js +0 -17
- package/dist/sites/vue/index.d.ts +0 -2
- package/dist/sites/vue/index.js +0 -17
package/dist/base-command.js
CHANGED
|
@@ -2,6 +2,7 @@ import { Command, Errors, toConfiguredId } from '@oclif/core';
|
|
|
2
2
|
import { invalidEnvOverrideWarnings } from './lib/config/index.js';
|
|
3
3
|
import { buildEnvelope, formatErrorForJson, normalizeCliErrorMessage, stableErrorCodeFrom, } from './lib/envelope.js';
|
|
4
4
|
import { resolveResearchTarget, } from './lib/research/resolve-target.js';
|
|
5
|
+
import { looksLikeSchemelessUrl } from './lib/research/url.js';
|
|
5
6
|
/**
|
|
6
7
|
* Shared base for every Bonsai command. Enables oclif's native `--json` flag,
|
|
7
8
|
* parses args/flags once in `init()` so commands read `this.args`/`this.flags`
|
|
@@ -12,9 +13,10 @@ export class BaseCommand extends Command {
|
|
|
12
13
|
static enableJsonFlag = true;
|
|
13
14
|
flags;
|
|
14
15
|
args;
|
|
16
|
+
parsedArgv;
|
|
15
17
|
async init() {
|
|
16
18
|
await super.init();
|
|
17
|
-
const { args, flags } = await this.parse({
|
|
19
|
+
const { args, flags, argv } = await this.parse({
|
|
18
20
|
flags: this.ctor.flags,
|
|
19
21
|
enableJsonFlag: this.ctor.enableJsonFlag,
|
|
20
22
|
args: this.ctor.args,
|
|
@@ -22,6 +24,7 @@ export class BaseCommand extends Command {
|
|
|
22
24
|
});
|
|
23
25
|
this.flags = flags;
|
|
24
26
|
this.args = args;
|
|
27
|
+
this.parsedArgv = argv;
|
|
25
28
|
// Surface a set-but-invalid BONSAI_* override once per run. Resolution silently drops such a
|
|
26
29
|
// value, so without this a typo'd env var would take no effect with no signal. Warnings go to
|
|
27
30
|
// stderr (even under --json), so machine output stays clean.
|
|
@@ -30,7 +33,7 @@ export class BaseCommand extends Command {
|
|
|
30
33
|
}
|
|
31
34
|
/**
|
|
32
35
|
* Always surface warnings on stderr, even under `--json`. oclif silences `warn()` in JSON mode,
|
|
33
|
-
* but our warnings (secret-redirect, stale-serve,
|
|
36
|
+
* but our warnings (secret-redirect, stale-serve, prune failures) are
|
|
34
37
|
* security- and freshness-relevant side effects users must see. stderr never pollutes the stdout
|
|
35
38
|
* JSON envelope, so machine output stays clean.
|
|
36
39
|
*/
|
|
@@ -59,6 +62,15 @@ export class BaseCommand extends Command {
|
|
|
59
62
|
// [UnparsedCommand] warning to stderr even under `--json`. CLIParseError subclasses carry `parse`.
|
|
60
63
|
if (err && typeof err === 'object' && 'parse' in err)
|
|
61
64
|
this.parsed = true;
|
|
65
|
+
// Attach the stable code to the error itself so oclif's human pretty-print renders the same
|
|
66
|
+
// `Code:` line that `--json` already reports via stableErrorCodeFrom. Without this, built-in
|
|
67
|
+
// oclif parse errors (invalid enum value, missing arg, unknown flag) printed a code under
|
|
68
|
+
// --json but not for humans — the two output modes disagreed on whether an error had a code.
|
|
69
|
+
if (err && typeof err === 'object' && !err.code) {
|
|
70
|
+
const code = stableErrorCodeFrom(err);
|
|
71
|
+
if (code)
|
|
72
|
+
err.code = code;
|
|
73
|
+
}
|
|
62
74
|
process.exitCode = process.exitCode ?? BaseCommand.exitCodeOf(err);
|
|
63
75
|
return super.catch(err);
|
|
64
76
|
}
|
|
@@ -66,7 +78,12 @@ export class BaseCommand extends Command {
|
|
|
66
78
|
envelopeCommandId() {
|
|
67
79
|
return this.ctor.id ? toConfiguredId(this.ctor.id, this.config) : this.config.bin;
|
|
68
80
|
}
|
|
69
|
-
/**
|
|
81
|
+
/**
|
|
82
|
+
* Resolve a URL against the research cache, exiting on a normalization failure. A scheme-less but
|
|
83
|
+
* domain-shaped input reports MISSING_URL_SCHEME — the same code the root `bonsai <url>` shorthand
|
|
84
|
+
* uses — so an agent sees one stable code for "forgot https://" everywhere; truly unparseable input
|
|
85
|
+
* stays INVALID_URL.
|
|
86
|
+
*/
|
|
70
87
|
resolveResearchTargetOrFail(url, extra) {
|
|
71
88
|
try {
|
|
72
89
|
return resolveResearchTarget({
|
|
@@ -78,8 +95,19 @@ export class BaseCommand extends Command {
|
|
|
78
95
|
});
|
|
79
96
|
}
|
|
80
97
|
catch (err) {
|
|
81
|
-
this.
|
|
98
|
+
this.failInvalidUrl(url, err.message);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Single exit point for a URL that failed normalization, shared by every URL-accepting command.
|
|
103
|
+
* The scheme-less message is already self-contained and actionable, so it is surfaced as-is;
|
|
104
|
+
* other parse failures keep the "Invalid URL:" prefix for context.
|
|
105
|
+
*/
|
|
106
|
+
failInvalidUrl(url, message) {
|
|
107
|
+
if (looksLikeSchemelessUrl(url)) {
|
|
108
|
+
this.error(message, { exit: 2, code: 'MISSING_URL_SCHEME' });
|
|
82
109
|
}
|
|
110
|
+
this.error(`Invalid URL: ${message}`, { exit: 2, code: 'INVALID_URL' });
|
|
83
111
|
}
|
|
84
112
|
/** Single source of truth for the `--json` envelope shape, shared by success and error output. */
|
|
85
113
|
envelope(parts) {
|
package/dist/commands/fetch.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export default class FetchCommand extends BaseCommand<typeof FetchCommand> {
|
|
|
8
8
|
description: string;
|
|
9
9
|
command: string;
|
|
10
10
|
}[];
|
|
11
|
+
static strict: boolean;
|
|
11
12
|
static args: {
|
|
12
13
|
url: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
13
14
|
};
|
|
@@ -35,7 +36,9 @@ export default class FetchCommand extends BaseCommand<typeof FetchCommand> {
|
|
|
35
36
|
private persistFreshArtifact;
|
|
36
37
|
private persistSectionsIfFresh;
|
|
37
38
|
run(): Promise<unknown>;
|
|
39
|
+
private fetchSingleTarget;
|
|
38
40
|
}
|
|
41
|
+
export declare function describeError(err: unknown): string;
|
|
39
42
|
export declare function fetchFailureGuidance(message: string, url: string): {
|
|
40
43
|
suggestions: string[];
|
|
41
44
|
ref?: string;
|
package/dist/commands/fetch.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Args, Flags, ux } from '@oclif/core';
|
|
1
|
+
import { Args, Flags, ux, Errors } from '@oclif/core';
|
|
2
2
|
import { mkdtempSync, rmSync } from 'node:fs';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
@@ -9,6 +9,7 @@ import { loadSummaryLevel } from '../lib/config/index.js';
|
|
|
9
9
|
import { evaluateFreshness, checkMaxAgeExpired, durationFlagError, } from '../lib/research/freshness.js';
|
|
10
10
|
import { revalidateCache, createArtifactFromFetch } from '../lib/research/revalidate.js';
|
|
11
11
|
import { fetchStaticHtml, fetchText } from '../lib/research/fetcher.js';
|
|
12
|
+
import { PROXY_TUNNEL_REJECTION_PATTERN } from '../lib/research/proxy.js';
|
|
12
13
|
import { fetchRenderedHtml } from '../lib/research/browser.js';
|
|
13
14
|
import { capturePage } from '../lib/research/capture.js';
|
|
14
15
|
import { persistSectionArtifacts } from '../lib/research/docs/section-artifacts.js';
|
|
@@ -34,6 +35,7 @@ export default class FetchCommand extends BaseCommand {
|
|
|
34
35
|
command: '<%= config.bin %> https://news.ycombinator.com/ --format compressed --ttl 2h --json',
|
|
35
36
|
},
|
|
36
37
|
];
|
|
38
|
+
static strict = false;
|
|
37
39
|
static args = {
|
|
38
40
|
url: Args.string({
|
|
39
41
|
required: true,
|
|
@@ -193,8 +195,14 @@ export default class FetchCommand extends BaseCommand {
|
|
|
193
195
|
// failure — validation errors (bad flags/URL) exit before the try — so the contract's runtime code
|
|
194
196
|
// (1) applies. Suggestions render on stderr for humans only; under --json the envelope carries just
|
|
195
197
|
// the message (toErrorJson), so machine output is unchanged.
|
|
198
|
+
//
|
|
199
|
+
// Uses `describeError` (below) rather than `err.message` alone: Node/undici's fetch wraps
|
|
200
|
+
// transport failures (DNS, refused connections, a proxy that won't tunnel to the host) in a
|
|
201
|
+
// generic "fetch failed" TypeError with the real reason nested in `.cause`, so reading only the
|
|
202
|
+
// top message would surface a content-free "fetch failed" for exactly the failures a user most
|
|
203
|
+
// needs a hint for.
|
|
196
204
|
emitFetchError(err, url) {
|
|
197
|
-
const message =
|
|
205
|
+
const message = describeError(err);
|
|
198
206
|
const guidance = fetchFailureGuidance(message, url);
|
|
199
207
|
this.error(message, {
|
|
200
208
|
exit: 1,
|
|
@@ -255,49 +263,31 @@ export default class FetchCommand extends BaseCommand {
|
|
|
255
263
|
}
|
|
256
264
|
}
|
|
257
265
|
async run() {
|
|
258
|
-
const
|
|
266
|
+
const urls = this.parsedArgv;
|
|
259
267
|
const { format, ttl, 'max-age': maxAge, 'dry-run': dryRun } = this.flags;
|
|
260
268
|
this.validateDurationFlags(ttl, maxAge);
|
|
261
|
-
const target = this.resolveResearchTargetOrFail(url, {
|
|
262
|
-
flagOverride: this.flags.storage,
|
|
263
|
-
lookup: !this.flags.force,
|
|
264
|
-
});
|
|
265
|
-
const { cacheKey, located, normalizedUrl, roots } = target;
|
|
266
269
|
const summaryLevel = loadSummaryLevel(this.config.configDir, process.cwd());
|
|
267
270
|
const tmpDir = dryRun ? mkdtempSync(join(tmpdir(), 'fnr-dry-run-')) : null;
|
|
268
271
|
const currentTime = new Date();
|
|
272
|
+
const results = [];
|
|
269
273
|
try {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
}
|
|
273
|
-
const { cacheStatus, freshnessState, artifact, storageDir, redirectedToGlobal } = await this.resolveArtifact(normalizedUrl, cacheKey, roots, tmpDir, currentTime, located, summaryLevel);
|
|
274
|
-
this.persistSectionsIfFresh(tmpDir ?? storageDir, artifact, currentTime, cacheStatus, summaryLevel);
|
|
275
|
-
if (!this.jsonEnabled()) {
|
|
276
|
-
const CACHE_STATUS_LABEL = {
|
|
277
|
-
hit: 'cached',
|
|
278
|
-
miss: 'done',
|
|
279
|
-
refreshed: 'refreshed',
|
|
280
|
-
revalidated: 'revalidated',
|
|
281
|
-
stale: 'served stale',
|
|
282
|
-
};
|
|
283
|
-
ux.action.stop(CACHE_STATUS_LABEL[cacheStatus] ?? cacheStatus);
|
|
284
|
-
}
|
|
285
|
-
const content = format === 'compressed' ? artifact.compressed : artifact.detailed;
|
|
286
|
-
const resultData = this.buildResultData(url, normalizedUrl, cacheKey, storageDir, roots.mode, cacheStatus, freshnessState, format, artifact, content, redirectedToGlobal);
|
|
287
|
-
if (!this.jsonEnabled()) {
|
|
288
|
-
this.log(content);
|
|
274
|
+
for (const url of urls) {
|
|
275
|
+
results.push(await this.fetchSingleTarget(url, currentTime, tmpDir, summaryLevel, format, urls.length > 1));
|
|
289
276
|
}
|
|
290
|
-
return
|
|
277
|
+
return urls.length === 1 ? results[0] : results;
|
|
291
278
|
}
|
|
292
279
|
catch (err) {
|
|
293
280
|
if (!this.jsonEnabled()) {
|
|
294
281
|
ux.action.stop('failed');
|
|
295
282
|
}
|
|
283
|
+
if (err instanceof Errors.CLIError)
|
|
284
|
+
throw err;
|
|
296
285
|
// Stale-serve (exit 5) signals via process.exitCode and never throws, so it bypasses this
|
|
297
286
|
// path; only genuine fetch/extract failures land here and get classified guidance. Use the
|
|
298
287
|
// normalized URL in hints so the copy-paste command is canonical (and never echoes raw,
|
|
299
288
|
// unsanitized user input back to the terminal).
|
|
300
|
-
|
|
289
|
+
const failedUrl = urls[results.length] || urls[0] || '';
|
|
290
|
+
this.emitFetchError(err, failedUrl);
|
|
301
291
|
}
|
|
302
292
|
finally {
|
|
303
293
|
if (tmpDir) {
|
|
@@ -305,13 +295,63 @@ export default class FetchCommand extends BaseCommand {
|
|
|
305
295
|
}
|
|
306
296
|
}
|
|
307
297
|
}
|
|
298
|
+
async fetchSingleTarget(url, currentTime, tmpDir, summaryLevel, format, showSeparator) {
|
|
299
|
+
const target = this.resolveResearchTargetOrFail(url, {
|
|
300
|
+
flagOverride: this.flags.storage,
|
|
301
|
+
lookup: !this.flags.force,
|
|
302
|
+
});
|
|
303
|
+
const { cacheKey, located, normalizedUrl, roots } = target;
|
|
304
|
+
if (!this.jsonEnabled()) {
|
|
305
|
+
ux.action.start('Fetching ' + normalizedUrl);
|
|
306
|
+
}
|
|
307
|
+
const { cacheStatus, freshnessState, artifact, storageDir, redirectedToGlobal } = await this.resolveArtifact(normalizedUrl, cacheKey, roots, tmpDir, currentTime, located, summaryLevel);
|
|
308
|
+
this.persistSectionsIfFresh(tmpDir ?? storageDir, artifact, currentTime, cacheStatus, summaryLevel);
|
|
309
|
+
if (!this.jsonEnabled()) {
|
|
310
|
+
const CACHE_STATUS_LABEL = {
|
|
311
|
+
hit: 'cached',
|
|
312
|
+
miss: 'done',
|
|
313
|
+
refreshed: 'refreshed',
|
|
314
|
+
revalidated: 'revalidated',
|
|
315
|
+
stale: 'served stale',
|
|
316
|
+
};
|
|
317
|
+
ux.action.stop(CACHE_STATUS_LABEL[cacheStatus] ?? cacheStatus);
|
|
318
|
+
}
|
|
319
|
+
const content = format === 'compressed' ? artifact.compressed : artifact.detailed;
|
|
320
|
+
const resultData = this.buildResultData(url, normalizedUrl, cacheKey, storageDir, roots.mode, cacheStatus, freshnessState, format, artifact, content, redirectedToGlobal);
|
|
321
|
+
if (!this.jsonEnabled()) {
|
|
322
|
+
this.log(content);
|
|
323
|
+
if (showSeparator) {
|
|
324
|
+
this.log('\n' + '='.repeat(40) + '\n');
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return resultData;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
// How many `.cause` levels to walk. Real undici/Node transport errors nest at most 1-2 deep;
|
|
331
|
+
// the cap exists so a self-referential or cyclic `.cause` chain (Error.cause is an arbitrary,
|
|
332
|
+
// unvalidated property — nothing prevents `e.cause = e`, whether from a bug in this codebase or
|
|
333
|
+
// a dependency) can't hang the process, rather than being a limit expected to matter in practice.
|
|
334
|
+
const MAX_CAUSE_DEPTH = 10;
|
|
335
|
+
// Walks an Error's `.cause` chain and joins every message, deepest last. Node/undici's fetch
|
|
336
|
+
// throws a generic `TypeError: fetch failed` for transport-level failures, with the actionable
|
|
337
|
+
// detail (e.g. a proxy's tunnel rejection) nested one or two `.cause` levels down.
|
|
338
|
+
export function describeError(err) {
|
|
339
|
+
if (!(err instanceof Error))
|
|
340
|
+
return String(err);
|
|
341
|
+
const parts = [err.message];
|
|
342
|
+
let cause = err.cause;
|
|
343
|
+
for (let depth = 0; depth < MAX_CAUSE_DEPTH && cause instanceof Error; depth++) {
|
|
344
|
+
parts.push(cause.message);
|
|
345
|
+
cause = cause.cause;
|
|
346
|
+
}
|
|
347
|
+
return parts.join(': ');
|
|
308
348
|
}
|
|
309
349
|
// Maps a runtime fetch failure to recovery steps keyed off its message. Returns undefined for
|
|
310
350
|
// unrecognized failures, which then surface with their original message and no extra hint. The
|
|
311
351
|
// patterns match the thrown text in fetcher.ts/browser.ts; keep them in sync if those messages
|
|
312
352
|
// change. Resolutions mirror the published troubleshooting guide.
|
|
313
353
|
export function fetchFailureGuidance(message, url) {
|
|
314
|
-
const ref = 'https://
|
|
354
|
+
const ref = 'https://bonsai.rhino-inquisitor.com/troubleshooting';
|
|
315
355
|
// Name the pipe step explicitly: bare `import --stdin` blocks waiting for input, so a reader (or
|
|
316
356
|
// an agent) running the hint verbatim would hang. Show the content being piped in from a file.
|
|
317
357
|
const importHint = `Open it in a browser, save the page, then import it: cat page.md | bonsai import ${url} --stdin`;
|
|
@@ -347,6 +387,32 @@ export function fetchFailureGuidance(message, url) {
|
|
|
347
387
|
suggestions: ['Check the hostname is spelled correctly and resolves on a public network.'],
|
|
348
388
|
};
|
|
349
389
|
}
|
|
390
|
+
// A configured proxy (HTTPS_PROXY/HTTP_PROXY) refused to tunnel to this host — distinct from
|
|
391
|
+
// the destination's own 401/403 above, which is the site itself rejecting the request. Shared
|
|
392
|
+
// with fetcher.ts's retry-fallback check so the two stay in sync by construction.
|
|
393
|
+
if (PROXY_TUNNEL_REJECTION_PATTERN.test(message)) {
|
|
394
|
+
return {
|
|
395
|
+
suggestions: [
|
|
396
|
+
"The configured proxy (HTTPS_PROXY/HTTP_PROXY) won't reach this host. Check its allowlist, or add the host to NO_PROXY to bypass the proxy for it.",
|
|
397
|
+
importHint,
|
|
398
|
+
],
|
|
399
|
+
ref,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
// The connection failed before any HTTP response was received — network-down, DNS, TLS, or a
|
|
403
|
+
// proxy issue not specifically matched above. Node/undici's fetch reports all of these as a
|
|
404
|
+
// generic "fetch failed" TypeError with no further suffix; the negative lookahead keeps this
|
|
405
|
+
// from also matching assertOk's "Fetch failed with status NNN ..." (a real HTTP response was
|
|
406
|
+
// received in that case, so it isn't a transport failure at all).
|
|
407
|
+
if (/^fetch failed\b(?!\s+with status)/i.test(message)) {
|
|
408
|
+
return {
|
|
409
|
+
suggestions: [
|
|
410
|
+
'The connection failed before a response was received. Check network connectivity and any HTTPS_PROXY/HTTP_PROXY/NO_PROXY settings.',
|
|
411
|
+
importHint,
|
|
412
|
+
],
|
|
413
|
+
ref,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
350
416
|
// A hostname that only resolves to a private/local IP at request time (literal private addresses
|
|
351
417
|
// are rejected earlier with exit 2). Blocked to prevent SSRF; only public hosts can be fetched.
|
|
352
418
|
if (/blocked local or private target/.test(message)) {
|
package/dist/commands/import.js
CHANGED
|
@@ -11,6 +11,7 @@ import { writeArtifactSecurely } from '../lib/research/secure-write.js';
|
|
|
11
11
|
import { loadSummaryLevel } from '../lib/config/index.js';
|
|
12
12
|
import { durationFlagError, getPolicy } from '../lib/research/freshness.js';
|
|
13
13
|
import { buildCompressed } from '../lib/research/compress.js';
|
|
14
|
+
import { sanitizePromptInjection } from '../lib/research/prompt-injection.js';
|
|
14
15
|
import { applyAutoTags } from '../lib/research/keywords.js';
|
|
15
16
|
import { estimateTokens } from '../lib/research/token-estimate.js';
|
|
16
17
|
export default class ResearchImport extends BaseCommand {
|
|
@@ -30,6 +31,10 @@ export default class ResearchImport extends BaseCommand {
|
|
|
30
31
|
description: 'import research from a local Markdown file',
|
|
31
32
|
command: '<%= config.bin %> import https://example.com/docs --file path/to/notes.md',
|
|
32
33
|
},
|
|
34
|
+
{
|
|
35
|
+
description: 'import research from stdin using the standard file placeholder',
|
|
36
|
+
command: 'cat notes.md | <%= config.bin %> import https://example.com/docs --file -',
|
|
37
|
+
},
|
|
33
38
|
];
|
|
34
39
|
static args = {
|
|
35
40
|
url: Args.string({
|
|
@@ -47,7 +52,7 @@ export default class ResearchImport extends BaseCommand {
|
|
|
47
52
|
default: false,
|
|
48
53
|
}),
|
|
49
54
|
file: Flags.string({
|
|
50
|
-
description: 'path to a Markdown file containing research notes to import',
|
|
55
|
+
description: 'path to a Markdown file containing research notes to import, or "-" for stdin',
|
|
51
56
|
}),
|
|
52
57
|
'input-format': Flags.option({
|
|
53
58
|
description: 'format of the input provided',
|
|
@@ -121,7 +126,9 @@ export default class ResearchImport extends BaseCommand {
|
|
|
121
126
|
return normalizeUrl(u);
|
|
122
127
|
}
|
|
123
128
|
catch (err) {
|
|
124
|
-
|
|
129
|
+
// Shared exit point: a forgotten scheme reports MISSING_URL_SCHEME like every other command,
|
|
130
|
+
// while genuinely malformed input stays INVALID_URL.
|
|
131
|
+
this.failInvalidUrl(u, err.message);
|
|
125
132
|
}
|
|
126
133
|
});
|
|
127
134
|
return hasMulti ? normalized.sort() : normalized;
|
|
@@ -168,6 +175,7 @@ export default class ResearchImport extends BaseCommand {
|
|
|
168
175
|
const bin = this.config.bin;
|
|
169
176
|
return [
|
|
170
177
|
`Pipe Markdown content, e.g. cat notes.md | ${bin} import <url> --stdin`,
|
|
178
|
+
`Or use the standard stdin placeholder: cat notes.md | ${bin} import <url> --file -`,
|
|
171
179
|
`Or read from a file: ${bin} import <url> --file notes.md`,
|
|
172
180
|
];
|
|
173
181
|
}
|
|
@@ -222,7 +230,8 @@ export default class ResearchImport extends BaseCommand {
|
|
|
222
230
|
return content;
|
|
223
231
|
}
|
|
224
232
|
async readAndValidateInput() {
|
|
225
|
-
|
|
233
|
+
const fileReadsStdin = this.flags.file === '-';
|
|
234
|
+
if (this.flags.stdin || fileReadsStdin) {
|
|
226
235
|
const rawInput = await this.readStdinWithGuard();
|
|
227
236
|
if (!rawInput.trim()) {
|
|
228
237
|
this.error('Empty stdin content provided.', { exit: 2, code: 'EMPTY_INPUT' });
|
|
@@ -260,11 +269,11 @@ export default class ResearchImport extends BaseCommand {
|
|
|
260
269
|
const { freshWindowMs } = getPolicy(tier, ttl);
|
|
261
270
|
const staleAfterTime = new Date(currentTime.getTime() + freshWindowMs);
|
|
262
271
|
const inputFormat = this.flags['input-format'];
|
|
263
|
-
const detailed = rawInput;
|
|
272
|
+
const detailed = sanitizePromptInjection(rawInput);
|
|
264
273
|
// A caller-supplied compressed input is trusted as-is; only a detailed import is condensed here,
|
|
265
274
|
// and it shares the same buildCompressed policy (structural pass + extractive fallback) as fetch.
|
|
266
275
|
const compressed = inputFormat === 'detailed'
|
|
267
|
-
? buildCompressed(
|
|
276
|
+
? buildCompressed(detailed, loadSummaryLevel(this.config.configDir, process.cwd()))
|
|
268
277
|
: rawInput;
|
|
269
278
|
const contentHash = createHash('sha256').update(detailed).digest('hex');
|
|
270
279
|
const metadata = {
|
|
@@ -7,10 +7,14 @@ export default class ResearchInspect extends BaseCommand<typeof ResearchInspect>
|
|
|
7
7
|
description: string;
|
|
8
8
|
command: string;
|
|
9
9
|
}[];
|
|
10
|
+
static strict: boolean;
|
|
10
11
|
static args: {
|
|
11
12
|
url: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
12
13
|
};
|
|
13
14
|
static stdoutIsPrimaryData: boolean;
|
|
14
15
|
run(): Promise<unknown>;
|
|
16
|
+
private logMetadata;
|
|
17
|
+
private logSections;
|
|
18
|
+
private inspectSingleTarget;
|
|
15
19
|
private findSections;
|
|
16
20
|
}
|
package/dist/commands/inspect.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Args } from '@oclif/core';
|
|
2
2
|
import { BaseCommand } from '../base-command.js';
|
|
3
3
|
import { scanCacheDirs } from '../lib/research/storage.js';
|
|
4
|
+
import { colors } from '../lib/color.js';
|
|
4
5
|
export default class ResearchInspect extends BaseCommand {
|
|
5
6
|
static id = 'inspect';
|
|
6
7
|
static summary = 'Inspect cached research metadata for a URL.';
|
|
@@ -15,6 +16,7 @@ export default class ResearchInspect extends BaseCommand {
|
|
|
15
16
|
command: '<%= config.bin %> <%= command.id %> https://example.com/docs --json',
|
|
16
17
|
},
|
|
17
18
|
];
|
|
19
|
+
static strict = false;
|
|
18
20
|
static args = {
|
|
19
21
|
url: Args.string({
|
|
20
22
|
required: true,
|
|
@@ -23,39 +25,61 @@ export default class ResearchInspect extends BaseCommand {
|
|
|
23
25
|
};
|
|
24
26
|
static stdoutIsPrimaryData = true;
|
|
25
27
|
async run() {
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
const urls = this.parsedArgv;
|
|
29
|
+
const results = [];
|
|
30
|
+
const missingUrls = [];
|
|
31
|
+
for (const url of urls) {
|
|
32
|
+
const target = this.resolveResearchTargetOrFail(url);
|
|
33
|
+
if (!target.located) {
|
|
34
|
+
missingUrls.push(target.normalizedUrl);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
results.push(this.inspectSingleTarget(target, urls.length > 1));
|
|
38
|
+
}
|
|
39
|
+
if (missingUrls.length > 0) {
|
|
40
|
+
const firstMissing = missingUrls[0];
|
|
41
|
+
const suggestions = missingUrls.map((u) => `Fetch and cache it first: ${this.config.bin} ${u}`);
|
|
42
|
+
this.error(`No cached research found for URL: ${firstMissing}` +
|
|
43
|
+
(missingUrls.length > 1 ? ` and ${missingUrls.length - 1} other URLs` : ''), {
|
|
33
44
|
exit: 1,
|
|
34
45
|
code: 'CACHE_MISS',
|
|
35
|
-
suggestions
|
|
46
|
+
suggestions,
|
|
36
47
|
});
|
|
37
48
|
}
|
|
49
|
+
return urls.length === 1 ? results[0] : results;
|
|
50
|
+
}
|
|
51
|
+
logMetadata(metadata) {
|
|
52
|
+
this.log(colors.cyan(`--- Metadata ---`));
|
|
53
|
+
for (const [key, val] of Object.entries(metadata)) {
|
|
54
|
+
const paddedKey = colors.cyan((key + ':').padEnd(Math.max(25, key.length + 2)));
|
|
55
|
+
if (typeof val === 'object' && val !== null) {
|
|
56
|
+
this.log(`${paddedKey} ${colors.bold(JSON.stringify(val))}`);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
this.log(`${paddedKey} ${colors.bold(String(val))}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
logSections(sections) {
|
|
64
|
+
this.log(colors.cyan(`--- Sections (${sections.length}) ---`));
|
|
65
|
+
for (const s of sections) {
|
|
66
|
+
this.log(`${colors.cyan(s.headingPath || '')} [${colors.yellow(s.anchor || '')}] (${colors.magenta(String(s.tokenEstimate.detailed || 0))} tokens) ${colors.gray(s.cacheKey)}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
inspectSingleTarget(target, showSeparator) {
|
|
70
|
+
const { cacheKey, located, roots } = target;
|
|
38
71
|
const cached = located.artifact;
|
|
39
72
|
const artifactPath = located.path;
|
|
40
73
|
const sections = this.findSections(roots.readRoots, cacheKey);
|
|
41
74
|
if (!this.jsonEnabled()) {
|
|
42
|
-
this.log(`${'Cache Key:'.padEnd(25)} ${cacheKey}`);
|
|
43
|
-
this.log(`${'Cache Path:'.padEnd(25)} ${artifactPath}`);
|
|
44
|
-
this.
|
|
45
|
-
for (const [key, val] of Object.entries(cached.metadata)) {
|
|
46
|
-
const paddedKey = (key + ':').padEnd(Math.max(25, key.length + 2));
|
|
47
|
-
if (typeof val === 'object' && val !== null) {
|
|
48
|
-
this.log(`${paddedKey} ${JSON.stringify(val)}`);
|
|
49
|
-
}
|
|
50
|
-
else {
|
|
51
|
-
this.log(`${paddedKey} ${val}`);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
75
|
+
this.log(`${colors.cyan('Cache Key:'.padEnd(25))} ${colors.bold(cacheKey)}`);
|
|
76
|
+
this.log(`${colors.cyan('Cache Path:'.padEnd(25))} ${colors.gray(artifactPath)}`);
|
|
77
|
+
this.logMetadata(cached.metadata);
|
|
54
78
|
if (sections.length) {
|
|
55
|
-
this.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
79
|
+
this.logSections(sections);
|
|
80
|
+
}
|
|
81
|
+
if (showSeparator) {
|
|
82
|
+
this.log('='.repeat(40));
|
|
59
83
|
}
|
|
60
84
|
}
|
|
61
85
|
return {
|
package/dist/commands/list.d.ts
CHANGED
|
@@ -10,13 +10,15 @@ export default class ResearchList extends BaseCommand<typeof ResearchList> {
|
|
|
10
10
|
static flags: {
|
|
11
11
|
topic: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
12
|
tags: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
url: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
14
|
freshness: import("@oclif/core/interfaces").OptionFlag<"fresh" | "stale_grace" | "stale_expired" | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
15
|
'artifact-type': import("@oclif/core/interfaces").OptionFlag<"source" | "research_note" | "index" | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
16
|
'capture-method': import("@oclif/core/interfaces").OptionFlag<"static_fetch" | "browser_fallback" | "agent_supplied" | "route_markdown" | "github_source" | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
17
|
limit: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
18
|
};
|
|
18
19
|
static stdoutIsPrimaryData: boolean;
|
|
19
|
-
private
|
|
20
|
+
private matchesTopic;
|
|
21
|
+
private matchesTags;
|
|
20
22
|
private matchesFilters;
|
|
21
23
|
private scanCacheDirForList;
|
|
22
24
|
private logListResults;
|
package/dist/commands/list.js
CHANGED
|
@@ -5,6 +5,9 @@ import { loadStoreRoots } from '../lib/research/store-roots.js';
|
|
|
5
5
|
import { evaluateFreshness } from '../lib/research/freshness.js';
|
|
6
6
|
import { ARTIFACT_TYPES, CAPTURE_METHODS } from '../lib/research/schema.js';
|
|
7
7
|
import { NO_TOPIC_LABEL, resultListHeading, truncationNotice, } from '../lib/text.js';
|
|
8
|
+
import { limitFlag } from '../lib/limit-flag.js';
|
|
9
|
+
import { artifactMatchesUrlFilter } from '../lib/research/url.js';
|
|
10
|
+
import { colors } from '../lib/color.js';
|
|
8
11
|
// Listings are ordered newest-first, so the truncation word is "first"; --limit caps at 100.
|
|
9
12
|
const LIST_LABELS = { noun: 'cached research', order: 'first', maxLimit: 100 };
|
|
10
13
|
// `list` answers "what pages/notes do I have?" and deliberately omits section children (see
|
|
@@ -27,6 +30,10 @@ export default class ResearchList extends BaseCommand {
|
|
|
27
30
|
description: 'list only fresh entries filtered by tags',
|
|
28
31
|
command: '<%= config.bin %> list --freshness fresh --tags node --tags url',
|
|
29
32
|
},
|
|
33
|
+
{
|
|
34
|
+
description: 'list entries matching a specific URL glob',
|
|
35
|
+
command: '<%= config.bin %> list --url "https://react.dev/*"',
|
|
36
|
+
},
|
|
30
37
|
];
|
|
31
38
|
static flags = {
|
|
32
39
|
topic: Flags.string({
|
|
@@ -38,6 +45,9 @@ export default class ResearchList extends BaseCommand {
|
|
|
38
45
|
description: 'filter by tags (must match all tags specified)',
|
|
39
46
|
multiple: true,
|
|
40
47
|
}),
|
|
48
|
+
url: Flags.string({
|
|
49
|
+
description: 'filter by source URL glob pattern (case-insensitive, supports * wildcard)',
|
|
50
|
+
}),
|
|
41
51
|
freshness: Flags.option({
|
|
42
52
|
description: 'filter by freshness state',
|
|
43
53
|
options: ['fresh', 'stale_grace', 'stale_expired'],
|
|
@@ -50,29 +60,29 @@ export default class ResearchList extends BaseCommand {
|
|
|
50
60
|
description: 'filter by capture method',
|
|
51
61
|
options: CAPTURE_METHODS,
|
|
52
62
|
})(),
|
|
53
|
-
limit:
|
|
54
|
-
description: 'maximum number of results to return (default 50, max 100)',
|
|
55
|
-
default: 50,
|
|
56
|
-
}),
|
|
63
|
+
limit: limitFlag(100, 50, 'maximum number of results to return (default 50, max 100)'),
|
|
57
64
|
};
|
|
58
65
|
static stdoutIsPrimaryData = true;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
66
|
+
matchesTopic(meta, topic) {
|
|
67
|
+
if (!topic)
|
|
68
|
+
return true;
|
|
69
|
+
return !!meta.topic && meta.topic.trim().toLowerCase() === topic.trim().toLowerCase();
|
|
70
|
+
}
|
|
71
|
+
matchesTags(meta, tags) {
|
|
72
|
+
if (!tags || tags.length === 0)
|
|
73
|
+
return true;
|
|
74
|
+
const metaTagsLower = (meta.tags || []).map((t) => t.toLowerCase());
|
|
75
|
+
return tags.every((t) => metaTagsLower.includes(t.toLowerCase()));
|
|
64
76
|
}
|
|
65
77
|
matchesFilters(meta, freshness) {
|
|
66
|
-
if (this.flags.topic
|
|
67
|
-
(!meta.topic || meta.topic.trim().toLowerCase() !== this.flags.topic.trim().toLowerCase())) {
|
|
78
|
+
if (!this.matchesTopic(meta, this.flags.topic)) {
|
|
68
79
|
return false;
|
|
69
80
|
}
|
|
70
|
-
if (this.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
81
|
+
if (!this.matchesTags(meta, this.flags.tags)) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
if (this.flags.url && !artifactMatchesUrlFilter(meta, this.flags.url)) {
|
|
85
|
+
return false;
|
|
76
86
|
}
|
|
77
87
|
if (this.flags['artifact-type'] && meta.artifact_type !== this.flags['artifact-type']) {
|
|
78
88
|
return false;
|
|
@@ -91,8 +101,7 @@ export default class ResearchList extends BaseCommand {
|
|
|
91
101
|
return null;
|
|
92
102
|
// Section children are sub-chunks of a page, not artifacts a user "has" — they would flood the
|
|
93
103
|
// listing (one page yields dozens) and aren't in the documented source/research_note contract.
|
|
94
|
-
// They stay discoverable through `
|
|
95
|
-
// page's sections). `list` answers "what pages/notes do I have?", so keep it page-level. This
|
|
104
|
+
// They stay discoverable through `inspect` (which lists a page's sections). `list` answers "what pages/notes do I have?", so keep it page-level. This
|
|
96
105
|
// unconditional guard owns the exclusion (the default no-filter case relies on it);
|
|
97
106
|
// LISTABLE_ARTIFACT_TYPES just hides `section` from --artifact-type so no one filters for a
|
|
98
107
|
// type list can never return. Keep both in sync if section handling ever changes.
|
|
@@ -122,19 +131,27 @@ export default class ResearchList extends BaseCommand {
|
|
|
122
131
|
return;
|
|
123
132
|
if (finalResults.length === 0) {
|
|
124
133
|
this.log('No cached research entries found matching filters.');
|
|
125
|
-
this.log(`\nTip: populate the cache first: ${this.config.bin
|
|
134
|
+
this.log(`\nTip: populate the cache first: ${colors.cyan(this.config.bin + ' <url>')}`);
|
|
126
135
|
return;
|
|
127
136
|
}
|
|
128
137
|
this.log(`${resultListHeading(totalMatched, finalResults.length, LIST_LABELS)}\n`);
|
|
129
138
|
finalResults.forEach((res, index) => {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
this.log(
|
|
133
|
-
|
|
139
|
+
const topicStr = res.topic ? colors.cyan(res.topic) : colors.gray(NO_TOPIC_LABEL);
|
|
140
|
+
const keyStr = colors.bold(res.cacheKey);
|
|
141
|
+
this.log(`${index + 1}. [${topicStr}] Key: ${keyStr}`);
|
|
142
|
+
const freshnessColorMap = {
|
|
143
|
+
fresh: colors.green,
|
|
144
|
+
stale_grace: colors.yellow,
|
|
145
|
+
stale_expired: colors.red,
|
|
146
|
+
};
|
|
147
|
+
const freshnessColor = freshnessColorMap[res.freshness] || ((t) => t);
|
|
148
|
+
const freshnessStr = freshnessColor(res.freshness);
|
|
149
|
+
this.log(` Type: ${colors.bold(res.artifactType)} | Freshness: ${freshnessStr}`);
|
|
150
|
+
this.log(` Tokens: compressed=${colors.bold(String(res.tokenEstimate?.compressed || 0))}, detailed=${colors.bold(String(res.tokenEstimate?.detailed || 0))}`);
|
|
151
|
+
this.log(` Source URLs: ${colors.gray(res.sourceUrls.join(', '))}\n`);
|
|
134
152
|
});
|
|
135
153
|
}
|
|
136
154
|
async run() {
|
|
137
|
-
this.validateListFlags();
|
|
138
155
|
const roots = loadStoreRoots({
|
|
139
156
|
configDir: this.config.configDir,
|
|
140
157
|
cwd: process.cwd(),
|
package/dist/commands/prune.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export default class ResearchPrune extends BaseCommand<typeof ResearchPrune> {
|
|
|
10
10
|
static flags: {
|
|
11
11
|
'older-than': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
12
|
inactive: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
url: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
14
|
'artifact-type': import("@oclif/core/interfaces").OptionFlag<"source" | "research_note" | "index" | "section" | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
15
|
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
16
|
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|