@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.
- 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 +2 -0
- package/dist/commands/fetch.js +43 -29
- package/dist/commands/import.js +11 -3
- 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 +0 -8
- package/dist/lib/research/browser.js +20 -43
- 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 +32 -25
- 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,6 +36,7 @@ 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
|
}
|
|
39
41
|
export declare function fetchFailureGuidance(message: string, url: string): {
|
|
40
42
|
suggestions: 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';
|
|
@@ -34,6 +34,7 @@ export default class FetchCommand extends BaseCommand {
|
|
|
34
34
|
command: '<%= config.bin %> https://news.ycombinator.com/ --format compressed --ttl 2h --json',
|
|
35
35
|
},
|
|
36
36
|
];
|
|
37
|
+
static strict = false;
|
|
37
38
|
static args = {
|
|
38
39
|
url: Args.string({
|
|
39
40
|
required: true,
|
|
@@ -255,49 +256,31 @@ export default class FetchCommand extends BaseCommand {
|
|
|
255
256
|
}
|
|
256
257
|
}
|
|
257
258
|
async run() {
|
|
258
|
-
const
|
|
259
|
+
const urls = this.parsedArgv;
|
|
259
260
|
const { format, ttl, 'max-age': maxAge, 'dry-run': dryRun } = this.flags;
|
|
260
261
|
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
262
|
const summaryLevel = loadSummaryLevel(this.config.configDir, process.cwd());
|
|
267
263
|
const tmpDir = dryRun ? mkdtempSync(join(tmpdir(), 'fnr-dry-run-')) : null;
|
|
268
264
|
const currentTime = new Date();
|
|
265
|
+
const results = [];
|
|
269
266
|
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);
|
|
267
|
+
for (const url of urls) {
|
|
268
|
+
results.push(await this.fetchSingleTarget(url, currentTime, tmpDir, summaryLevel, format, urls.length > 1));
|
|
289
269
|
}
|
|
290
|
-
return
|
|
270
|
+
return urls.length === 1 ? results[0] : results;
|
|
291
271
|
}
|
|
292
272
|
catch (err) {
|
|
293
273
|
if (!this.jsonEnabled()) {
|
|
294
274
|
ux.action.stop('failed');
|
|
295
275
|
}
|
|
276
|
+
if (err instanceof Errors.CLIError)
|
|
277
|
+
throw err;
|
|
296
278
|
// Stale-serve (exit 5) signals via process.exitCode and never throws, so it bypasses this
|
|
297
279
|
// path; only genuine fetch/extract failures land here and get classified guidance. Use the
|
|
298
280
|
// normalized URL in hints so the copy-paste command is canonical (and never echoes raw,
|
|
299
281
|
// unsanitized user input back to the terminal).
|
|
300
|
-
|
|
282
|
+
const failedUrl = urls[results.length] || urls[0] || '';
|
|
283
|
+
this.emitFetchError(err, failedUrl);
|
|
301
284
|
}
|
|
302
285
|
finally {
|
|
303
286
|
if (tmpDir) {
|
|
@@ -305,13 +288,44 @@ export default class FetchCommand extends BaseCommand {
|
|
|
305
288
|
}
|
|
306
289
|
}
|
|
307
290
|
}
|
|
291
|
+
async fetchSingleTarget(url, currentTime, tmpDir, summaryLevel, format, showSeparator) {
|
|
292
|
+
const target = this.resolveResearchTargetOrFail(url, {
|
|
293
|
+
flagOverride: this.flags.storage,
|
|
294
|
+
lookup: !this.flags.force,
|
|
295
|
+
});
|
|
296
|
+
const { cacheKey, located, normalizedUrl, roots } = target;
|
|
297
|
+
if (!this.jsonEnabled()) {
|
|
298
|
+
ux.action.start('Fetching ' + normalizedUrl);
|
|
299
|
+
}
|
|
300
|
+
const { cacheStatus, freshnessState, artifact, storageDir, redirectedToGlobal } = await this.resolveArtifact(normalizedUrl, cacheKey, roots, tmpDir, currentTime, located, summaryLevel);
|
|
301
|
+
this.persistSectionsIfFresh(tmpDir ?? storageDir, artifact, currentTime, cacheStatus, summaryLevel);
|
|
302
|
+
if (!this.jsonEnabled()) {
|
|
303
|
+
const CACHE_STATUS_LABEL = {
|
|
304
|
+
hit: 'cached',
|
|
305
|
+
miss: 'done',
|
|
306
|
+
refreshed: 'refreshed',
|
|
307
|
+
revalidated: 'revalidated',
|
|
308
|
+
stale: 'served stale',
|
|
309
|
+
};
|
|
310
|
+
ux.action.stop(CACHE_STATUS_LABEL[cacheStatus] ?? cacheStatus);
|
|
311
|
+
}
|
|
312
|
+
const content = format === 'compressed' ? artifact.compressed : artifact.detailed;
|
|
313
|
+
const resultData = this.buildResultData(url, normalizedUrl, cacheKey, storageDir, roots.mode, cacheStatus, freshnessState, format, artifact, content, redirectedToGlobal);
|
|
314
|
+
if (!this.jsonEnabled()) {
|
|
315
|
+
this.log(content);
|
|
316
|
+
if (showSeparator) {
|
|
317
|
+
this.log('\n' + '='.repeat(40) + '\n');
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return resultData;
|
|
321
|
+
}
|
|
308
322
|
}
|
|
309
323
|
// Maps a runtime fetch failure to recovery steps keyed off its message. Returns undefined for
|
|
310
324
|
// unrecognized failures, which then surface with their original message and no extra hint. The
|
|
311
325
|
// patterns match the thrown text in fetcher.ts/browser.ts; keep them in sync if those messages
|
|
312
326
|
// change. Resolutions mirror the published troubleshooting guide.
|
|
313
327
|
export function fetchFailureGuidance(message, url) {
|
|
314
|
-
const ref = 'https://
|
|
328
|
+
const ref = 'https://bonsai.rhino-inquisitor.com/troubleshooting';
|
|
315
329
|
// Name the pipe step explicitly: bare `import --stdin` blocks waiting for input, so a reader (or
|
|
316
330
|
// an agent) running the hint verbatim would hang. Show the content being piped in from a file.
|
|
317
331
|
const importHint = `Open it in a browser, save the page, then import it: cat page.md | bonsai import ${url} --stdin`;
|
package/dist/commands/import.js
CHANGED
|
@@ -30,6 +30,10 @@ export default class ResearchImport extends BaseCommand {
|
|
|
30
30
|
description: 'import research from a local Markdown file',
|
|
31
31
|
command: '<%= config.bin %> import https://example.com/docs --file path/to/notes.md',
|
|
32
32
|
},
|
|
33
|
+
{
|
|
34
|
+
description: 'import research from stdin using the standard file placeholder',
|
|
35
|
+
command: 'cat notes.md | <%= config.bin %> import https://example.com/docs --file -',
|
|
36
|
+
},
|
|
33
37
|
];
|
|
34
38
|
static args = {
|
|
35
39
|
url: Args.string({
|
|
@@ -47,7 +51,7 @@ export default class ResearchImport extends BaseCommand {
|
|
|
47
51
|
default: false,
|
|
48
52
|
}),
|
|
49
53
|
file: Flags.string({
|
|
50
|
-
description: 'path to a Markdown file containing research notes to import',
|
|
54
|
+
description: 'path to a Markdown file containing research notes to import, or "-" for stdin',
|
|
51
55
|
}),
|
|
52
56
|
'input-format': Flags.option({
|
|
53
57
|
description: 'format of the input provided',
|
|
@@ -121,7 +125,9 @@ export default class ResearchImport extends BaseCommand {
|
|
|
121
125
|
return normalizeUrl(u);
|
|
122
126
|
}
|
|
123
127
|
catch (err) {
|
|
124
|
-
|
|
128
|
+
// Shared exit point: a forgotten scheme reports MISSING_URL_SCHEME like every other command,
|
|
129
|
+
// while genuinely malformed input stays INVALID_URL.
|
|
130
|
+
this.failInvalidUrl(u, err.message);
|
|
125
131
|
}
|
|
126
132
|
});
|
|
127
133
|
return hasMulti ? normalized.sort() : normalized;
|
|
@@ -168,6 +174,7 @@ export default class ResearchImport extends BaseCommand {
|
|
|
168
174
|
const bin = this.config.bin;
|
|
169
175
|
return [
|
|
170
176
|
`Pipe Markdown content, e.g. cat notes.md | ${bin} import <url> --stdin`,
|
|
177
|
+
`Or use the standard stdin placeholder: cat notes.md | ${bin} import <url> --file -`,
|
|
171
178
|
`Or read from a file: ${bin} import <url> --file notes.md`,
|
|
172
179
|
];
|
|
173
180
|
}
|
|
@@ -222,7 +229,8 @@ export default class ResearchImport extends BaseCommand {
|
|
|
222
229
|
return content;
|
|
223
230
|
}
|
|
224
231
|
async readAndValidateInput() {
|
|
225
|
-
|
|
232
|
+
const fileReadsStdin = this.flags.file === '-';
|
|
233
|
+
if (this.flags.stdin || fileReadsStdin) {
|
|
226
234
|
const rawInput = await this.readStdinWithGuard();
|
|
227
235
|
if (!rawInput.trim()) {
|
|
228
236
|
this.error('Empty stdin content provided.', { exit: 2, code: 'EMPTY_INPUT' });
|
|
@@ -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>;
|
package/dist/commands/prune.js
CHANGED
|
@@ -7,6 +7,8 @@ import { loadStoreRoots } from '../lib/research/store-roots.js';
|
|
|
7
7
|
import { parseTtlToMs, durationFlagError } from '../lib/research/freshness.js';
|
|
8
8
|
import { ARTIFACT_TYPES } from '../lib/research/schema.js';
|
|
9
9
|
import { NO_TOPIC_LABEL, pluralize } from '../lib/text.js';
|
|
10
|
+
import { artifactMatchesUrlFilter } from '../lib/research/url.js';
|
|
11
|
+
import { colors } from '../lib/color.js';
|
|
10
12
|
export default class ResearchPrune extends BaseCommand {
|
|
11
13
|
static id = 'prune';
|
|
12
14
|
static summary = 'Clean up old or inactive research cache entries.';
|
|
@@ -20,6 +22,10 @@ export default class ResearchPrune extends BaseCommand {
|
|
|
20
22
|
description: 'actually prune entries older than 30 days that are source scrapes',
|
|
21
23
|
command: '<%= config.bin %> prune --older-than 30d --artifact-type source --yes',
|
|
22
24
|
},
|
|
25
|
+
{
|
|
26
|
+
description: 'prune entries matching a URL glob pattern',
|
|
27
|
+
command: '<%= config.bin %> prune --url "https://react.dev/*" --yes',
|
|
28
|
+
},
|
|
23
29
|
];
|
|
24
30
|
static flags = {
|
|
25
31
|
'older-than': Flags.string({
|
|
@@ -28,6 +34,9 @@ export default class ResearchPrune extends BaseCommand {
|
|
|
28
34
|
inactive: Flags.string({
|
|
29
35
|
description: 'prune entries inactive (unvalidated/unfetched) for duration (e.g. "14d", "30d")',
|
|
30
36
|
}),
|
|
37
|
+
url: Flags.string({
|
|
38
|
+
description: 'filter pruning to source URL glob pattern (case-insensitive, supports * wildcard)',
|
|
39
|
+
}),
|
|
31
40
|
'artifact-type': Flags.option({
|
|
32
41
|
// Prune operates on every cached file, so it can target any artifact type — including the
|
|
33
42
|
// `section`/`index` children a page generates (e.g. to clear orphans left after a source is
|
|
@@ -47,8 +56,11 @@ export default class ResearchPrune extends BaseCommand {
|
|
|
47
56
|
};
|
|
48
57
|
static stdoutIsPrimaryData = true;
|
|
49
58
|
validatePruneFlags() {
|
|
50
|
-
if (!this.flags['older-than'] &&
|
|
51
|
-
this.
|
|
59
|
+
if (!this.flags['older-than'] &&
|
|
60
|
+
!this.flags.inactive &&
|
|
61
|
+
!this.flags['artifact-type'] &&
|
|
62
|
+
!this.flags.url) {
|
|
63
|
+
this.error('Must specify at least one pruning filter: --older-than, --inactive, --artifact-type, or --url.', {
|
|
52
64
|
exit: 2,
|
|
53
65
|
code: 'MISSING_FILTER',
|
|
54
66
|
suggestions: [
|
|
@@ -56,14 +68,20 @@ export default class ResearchPrune extends BaseCommand {
|
|
|
56
68
|
],
|
|
57
69
|
});
|
|
58
70
|
}
|
|
71
|
+
if (this.flags['dry-run'] && this.flags.yes) {
|
|
72
|
+
this.error('--dry-run and --yes are mutually exclusive: --dry-run previews without deleting, --yes confirms deletion. Choose one.', { exit: 2, code: 'CONFLICTING_FLAGS' });
|
|
73
|
+
}
|
|
59
74
|
if (!this.flags['dry-run'] && !this.flags.yes) {
|
|
60
75
|
const olderThanPart = this.flags['older-than']
|
|
61
76
|
? ` --older-than ${this.flags['older-than']}`
|
|
62
77
|
: '';
|
|
78
|
+
const urlPart = this.flags.url ? ` --url "${this.flags.url}"` : '';
|
|
63
79
|
this.error('Safety check: use --yes to confirm pruning, or --dry-run to preview files that would be deleted.', {
|
|
64
80
|
exit: 2,
|
|
65
81
|
code: 'SAFETY_CHECK_REQUIRED',
|
|
66
|
-
suggestions: [
|
|
82
|
+
suggestions: [
|
|
83
|
+
`Preview first: ${this.config.bin} prune --dry-run${olderThanPart}${urlPart}`,
|
|
84
|
+
],
|
|
67
85
|
});
|
|
68
86
|
}
|
|
69
87
|
// Validate durations up front: scanCacheDir swallows per-file errors, so a malformed
|
|
@@ -84,6 +102,9 @@ export default class ResearchPrune extends BaseCommand {
|
|
|
84
102
|
if (this.flags['artifact-type'] && meta.artifact_type !== this.flags['artifact-type']) {
|
|
85
103
|
return false;
|
|
86
104
|
}
|
|
105
|
+
if (this.flags.url && !artifactMatchesUrlFilter(meta, this.flags.url)) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
87
108
|
if (this.flags['older-than']) {
|
|
88
109
|
const olderThanMs = parseTtlToMs(this.flags['older-than']);
|
|
89
110
|
if (ageMs < olderThanMs) {
|
|
@@ -142,18 +163,23 @@ export default class ResearchPrune extends BaseCommand {
|
|
|
142
163
|
let prunedCount = 0;
|
|
143
164
|
if (dryRun) {
|
|
144
165
|
if (!this.jsonEnabled()) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
166
|
+
if (count === 0) {
|
|
167
|
+
this.log(colors.yellow('[Dry Run] No research cache entries match the given filters.'));
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
const noun = pluralize(count, 'entry', 'entries');
|
|
171
|
+
this.log(colors.yellow(`[Dry Run] Found ${count} research cache ${noun} that would be pruned:\n`));
|
|
172
|
+
filesToPrune.forEach((f) => {
|
|
173
|
+
this.log(`- [${f.topic ? colors.cyan(f.topic) : colors.gray(NO_TOPIC_LABEL)}] Key: ${colors.bold(f.cacheKey)} (${colors.gray(f.url || 'Imported note')})`);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
150
176
|
}
|
|
151
177
|
}
|
|
152
178
|
else {
|
|
153
179
|
prunedCount = this.deletePruneCandidates(filesToPrune);
|
|
154
180
|
if (!this.jsonEnabled()) {
|
|
155
181
|
const noun = pluralize(count, 'entry', 'entries');
|
|
156
|
-
this.log(`Successfully pruned ${prunedCount} of ${count} research cache ${noun}.`);
|
|
182
|
+
this.log(colors.green(`Successfully pruned ${prunedCount} of ${count} research cache ${noun}.`));
|
|
157
183
|
}
|
|
158
184
|
}
|
|
159
185
|
return {
|