@taurgis/bonsai 3.2.2 → 4.1.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 +25 -2
- package/agents/agents/official-docs-researcher.agent.md +102 -0
- package/agents/agents/salesforce-docs-researcher.agent.md +17 -15
- package/agents/hooks/README.md +18 -2
- 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 +5 -4
- package/agents/instructions/web-research.instructions.md +45 -0
- package/agents/skills/web-research/SKILL.md +139 -0
- package/bin/cli.mjs +7 -4
- package/dist/base-command.d.ts +42 -0
- package/dist/base-command.js +106 -6
- package/dist/commands/config/base.d.ts +2 -7
- package/dist/commands/config/base.js +4 -17
- package/dist/commands/config/get.js +2 -2
- package/dist/commands/config/list.js +2 -2
- package/dist/commands/config/set.js +2 -2
- package/dist/commands/config/unset.js +2 -2
- package/dist/commands/context.d.ts +20 -0
- package/dist/commands/context.js +65 -0
- package/dist/commands/fetch.js +4 -0
- package/dist/commands/inspect.js +11 -2
- package/dist/commands/list.d.ts +23 -11
- package/dist/commands/list.js +110 -121
- package/dist/commands/prune.d.ts +2 -0
- package/dist/commands/prune.js +35 -4
- package/dist/commands/search.d.ts +43 -0
- package/dist/commands/search.js +233 -0
- package/dist/commands/setup.d.ts +27 -0
- package/dist/commands/setup.js +148 -0
- package/dist/commands/status.js +10 -0
- package/dist/commands.d.ts +6 -0
- package/dist/commands.js +6 -0
- package/dist/hooks/command-not-found/suggest.d.ts +1 -1
- package/dist/hooks/command-not-found/suggest.js +12 -2
- package/dist/lib/argv.d.ts +5 -0
- package/dist/lib/argv.js +30 -5
- package/dist/lib/cli-flag-manifest.d.ts +7 -0
- package/dist/lib/cli-flag-manifest.js +7 -0
- package/dist/lib/cli-presentation.d.ts +25 -4
- package/dist/lib/cli-presentation.js +34 -4
- package/dist/lib/cli-result-types.d.ts +84 -1
- package/dist/lib/color.d.ts +2 -0
- package/dist/lib/color.js +6 -0
- package/dist/lib/common-metadata-filter-flags.d.ts +14 -0
- package/dist/lib/common-metadata-filter-flags.js +37 -0
- package/dist/lib/config/index.d.ts +2 -2
- package/dist/lib/config/index.js +2 -2
- package/dist/lib/config/io.d.ts +9 -0
- package/dist/lib/config/io.js +71 -21
- package/dist/lib/config/schema.d.ts +2 -0
- package/dist/lib/config/schema.js +7 -0
- package/dist/lib/envelope.js +11 -4
- package/dist/lib/next-command.d.ts +15 -0
- package/dist/lib/next-command.js +40 -0
- package/dist/lib/prune-flags.d.ts +2 -0
- package/dist/lib/prune-flags.js +18 -6
- package/dist/lib/research/artifact-index.js +7 -1
- package/dist/lib/research/artifact.js +20 -2
- package/dist/lib/research/browser.d.ts +12 -0
- package/dist/lib/research/browser.js +63 -8
- package/dist/lib/research/cli-io.d.ts +7 -0
- package/dist/lib/research/context-dashboard.d.ts +19 -0
- package/dist/lib/research/context-dashboard.js +43 -0
- package/dist/lib/research/fetch-command-service.js +90 -23
- package/dist/lib/research/fetch-result.d.ts +1 -0
- package/dist/lib/research/fetch-result.js +4 -0
- package/dist/lib/research/fetcher.js +3 -1
- package/dist/lib/research/import-command-service.js +22 -1
- package/dist/lib/research/list-row.d.ts +15 -0
- package/dist/lib/research/list-row.js +31 -0
- package/dist/lib/research/metadata-filters.d.ts +39 -0
- package/dist/lib/research/metadata-filters.js +71 -0
- package/dist/lib/research/metadata-flags.d.ts +34 -0
- package/dist/lib/research/metadata-flags.js +45 -0
- package/dist/lib/research/prompt-injection.d.ts +2 -1
- package/dist/lib/research/prompt-injection.js +18 -2
- package/dist/lib/research/resolve-target.d.ts +2 -0
- package/dist/lib/research/resolve-target.js +1 -1
- package/dist/lib/research/schema.d.ts +2 -0
- package/dist/lib/research/schema.js +4 -0
- package/dist/lib/research/search-match.d.ts +39 -0
- package/dist/lib/research/search-match.js +134 -0
- package/dist/lib/research/storage.d.ts +4 -7
- package/dist/lib/research/storage.js +26 -25
- package/dist/lib/scope-flags.d.ts +19 -0
- package/dist/lib/scope-flags.js +27 -0
- package/dist/lib/setup/agents.d.ts +16 -0
- package/dist/lib/setup/agents.js +20 -0
- package/dist/lib/setup/bin-command.d.ts +14 -0
- package/dist/lib/setup/bin-command.js +42 -0
- package/dist/lib/setup/hook-file-io.d.ts +37 -0
- package/dist/lib/setup/hook-file-io.js +80 -0
- package/dist/lib/text.d.ts +20 -8
- package/dist/lib/text.js +32 -9
- package/oclif.manifest.json +406 -13
- package/package.json +2 -1
|
@@ -1,12 +1,30 @@
|
|
|
1
|
+
import { CONTROL_CHAR_PATTERN } from '../text.js';
|
|
2
|
+
// Frontmatter is one field per line. An embedded newline (or tab, treated the same way as
|
|
3
|
+
// legitimate Markdown whitespace elsewhere in this codebase — see sanitizeForTerminal in
|
|
4
|
+
// ../text.ts and stripUnsafeControlChars in ./prompt-injection.ts) in a free-form value (e.g. an
|
|
5
|
+
// agent-supplied --topic or --tags) would inject a bogus extra line — at best a dropped stray
|
|
6
|
+
// line, at worst a fake `key: value` pair, or a bare `---` that closes the frontmatter fence early
|
|
7
|
+
// and splices the remaining metadata fields (and even the closing fence and body sections) into
|
|
8
|
+
// what parseArtifact then treats as free-text content. Collapsing that whitespace to a space
|
|
9
|
+
// (rather than deleting it, which would silently merge adjacent words) keeps every value confined
|
|
10
|
+
// to the single line the format requires, regardless of where the string originated. Other control
|
|
11
|
+
// characters (e.g. a raw ANSI escape byte, whether from an agent-supplied flag or extracted page
|
|
12
|
+
// text) are stripped outright: the cache file is plain text meant to be `cat`ed or displayed by
|
|
13
|
+
// `inspect`/`list`, and an uncaught escape sequence there would reach a real terminal — unlike
|
|
14
|
+
// --json output, which JSON-escapes control characters by construction.
|
|
15
|
+
function sanitizeFrontmatterLine(value) {
|
|
16
|
+
return value.replace(/[\t\r\n]+/g, ' ').replace(CONTROL_CHAR_PATTERN, '');
|
|
17
|
+
}
|
|
1
18
|
function serializeArrayField(lines, name, items) {
|
|
2
19
|
lines.push(`${name}:`);
|
|
3
20
|
for (const item of items) {
|
|
4
|
-
lines.push(` - ${item}`);
|
|
21
|
+
lines.push(` - ${sanitizeFrontmatterLine(item)}`);
|
|
5
22
|
}
|
|
6
23
|
}
|
|
7
24
|
// A scalar frontmatter line, blanking null/undefined. Using `??` (not `||`) preserves a numeric 0.
|
|
8
25
|
function serializeScalarField(lines, name, value) {
|
|
9
|
-
|
|
26
|
+
const safeValue = typeof value === 'string' ? sanitizeFrontmatterLine(value) : value;
|
|
27
|
+
lines.push(`${name}: ${safeValue ?? ''}`);
|
|
10
28
|
}
|
|
11
29
|
/**
|
|
12
30
|
* Serializes artifact metadata to standard YAML frontmatter text.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ChildProcess } from 'node:child_process';
|
|
1
2
|
export interface BrowserFetchOptions {
|
|
2
3
|
timeoutMs?: number;
|
|
3
4
|
bodyLimitBytes?: number;
|
|
@@ -25,6 +26,16 @@ export declare class CdpClient {
|
|
|
25
26
|
}
|
|
26
27
|
export declare function findChromePath(): string;
|
|
27
28
|
export declare function buildChromeArgs(): string[];
|
|
29
|
+
/**
|
|
30
|
+
* Kill Chrome and every child process it spawned (zygote, gpu-process, renderer, crashpad).
|
|
31
|
+
* `chromeProcess.kill()` alone only signals that one PID — per Node's child_process docs, a signal
|
|
32
|
+
* sent to a child is not propagated to *that child's own* children on Linux, so Chrome's
|
|
33
|
+
* multi-process tree survives a plain kill on every rendered fetch, not just an interrupted one.
|
|
34
|
+
* Spawning with `detached: true` makes Chrome the leader of its own process group, so signaling the
|
|
35
|
+
* negative PID reaches the whole tree in one call. Falls back to a direct kill if group-kill fails
|
|
36
|
+
* (already exited, or a platform without POSIX process groups).
|
|
37
|
+
*/
|
|
38
|
+
export declare function killChromeTree(chromeProcess: ChildProcess): void;
|
|
28
39
|
export declare function waitForLoad(client: CdpClient, sessionId: string, timeoutMs: number, settleMs?: number): Promise<void>;
|
|
29
40
|
export declare const BLOCKED_ASSET_URLS: string[];
|
|
30
41
|
export interface CdpPage {
|
|
@@ -106,6 +117,7 @@ export declare const SANDBOX_EGRESS_ERROR_MARKER = "sandboxed execution environm
|
|
|
106
117
|
* against the environment directly and fall back to a manual fetch if it's still unreachable.
|
|
107
118
|
*/
|
|
108
119
|
export declare function describeNavigationFailure(errorText: string, url: string): Error;
|
|
120
|
+
export declare function isTransientBrowserError(err: unknown): boolean;
|
|
109
121
|
/**
|
|
110
122
|
* Navigates an already-open CDP page to `currentUrl` and captures the rendered HTML, honoring the
|
|
111
123
|
* openCdpPage Fetch-domain safety guard at every point a blocked navigation could otherwise be
|
|
@@ -130,13 +130,63 @@ export function buildChromeArgs() {
|
|
|
130
130
|
// CI runners occasionally starve headless Chrome's startup (disk/CPU contention from concurrent
|
|
131
131
|
// jobs), so this is generous enough to absorb that rather than a "typical" local startup time.
|
|
132
132
|
const CHROME_STARTUP_TIMEOUT_MS = 20_000;
|
|
133
|
+
// Node's default SIGINT/SIGTERM disposition kills the process immediately, without waiting for the
|
|
134
|
+
// async `finally { page.close() }` in fetchRenderedHtmlOnce to run — so an interrupted --rendered
|
|
135
|
+
// fetch otherwise abandons a whole Chrome process tree (main + zygote/gpu/renderer children). The
|
|
136
|
+
// plain 'exit' event covers ordinary completion (belt-and-braces alongside close()'s own cleanup);
|
|
137
|
+
// a `once` signal listener runs the same cleanup, then re-sends the signal to this process. Because
|
|
138
|
+
// `once` already removed itself, that re-delivery has no listener left and Node's own default
|
|
139
|
+
// disposition takes over — the process still exits with the normal 128+signum code, with no exit-code
|
|
140
|
+
// bookkeeping of our own.
|
|
141
|
+
const liveChromeProcesses = new Set();
|
|
142
|
+
let chromeCleanupRegistered = false;
|
|
143
|
+
function killLiveChromeProcesses() {
|
|
144
|
+
for (const chromeProcess of liveChromeProcesses)
|
|
145
|
+
killChromeTree(chromeProcess);
|
|
146
|
+
}
|
|
147
|
+
function ensureChromeCleanupRegistered() {
|
|
148
|
+
if (chromeCleanupRegistered)
|
|
149
|
+
return;
|
|
150
|
+
chromeCleanupRegistered = true;
|
|
151
|
+
process.once('exit', killLiveChromeProcesses);
|
|
152
|
+
for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
153
|
+
process.once(signal, () => {
|
|
154
|
+
killLiveChromeProcesses();
|
|
155
|
+
process.kill(process.pid, signal);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Kill Chrome and every child process it spawned (zygote, gpu-process, renderer, crashpad).
|
|
161
|
+
* `chromeProcess.kill()` alone only signals that one PID — per Node's child_process docs, a signal
|
|
162
|
+
* sent to a child is not propagated to *that child's own* children on Linux, so Chrome's
|
|
163
|
+
* multi-process tree survives a plain kill on every rendered fetch, not just an interrupted one.
|
|
164
|
+
* Spawning with `detached: true` makes Chrome the leader of its own process group, so signaling the
|
|
165
|
+
* negative PID reaches the whole tree in one call. Falls back to a direct kill if group-kill fails
|
|
166
|
+
* (already exited, or a platform without POSIX process groups).
|
|
167
|
+
*/
|
|
168
|
+
export function killChromeTree(chromeProcess) {
|
|
169
|
+
if (typeof chromeProcess.pid === 'number') {
|
|
170
|
+
try {
|
|
171
|
+
process.kill(-chromeProcess.pid, 'SIGKILL');
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
// Fall through to a best-effort direct kill below.
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
chromeProcess.kill('SIGKILL');
|
|
179
|
+
}
|
|
133
180
|
async function spawnChrome(chromePath) {
|
|
134
|
-
|
|
181
|
+
ensureChromeCleanupRegistered();
|
|
182
|
+
const chromeProcess = spawn(chromePath, buildChromeArgs(), { detached: true });
|
|
183
|
+
liveChromeProcesses.add(chromeProcess);
|
|
184
|
+
chromeProcess.once('exit', () => liveChromeProcesses.delete(chromeProcess));
|
|
135
185
|
try {
|
|
136
186
|
const wsUrl = await new Promise((resolve, reject) => {
|
|
137
187
|
let output = '';
|
|
138
188
|
const timeoutId = setTimeout(() => {
|
|
139
|
-
chromeProcess
|
|
189
|
+
killChromeTree(chromeProcess);
|
|
140
190
|
reject(new Error('Timed out waiting for Chrome to start.'));
|
|
141
191
|
}, CHROME_STARTUP_TIMEOUT_MS);
|
|
142
192
|
const onData = (data) => {
|
|
@@ -168,7 +218,7 @@ async function spawnChrome(chromePath) {
|
|
|
168
218
|
return { chromeProcess, wsUrl };
|
|
169
219
|
}
|
|
170
220
|
catch (err) {
|
|
171
|
-
chromeProcess
|
|
221
|
+
killChromeTree(chromeProcess);
|
|
172
222
|
throw err;
|
|
173
223
|
}
|
|
174
224
|
}
|
|
@@ -286,7 +336,7 @@ export async function openCdpPage() {
|
|
|
286
336
|
}
|
|
287
337
|
catch { }
|
|
288
338
|
client.close();
|
|
289
|
-
chromeProcess
|
|
339
|
+
killChromeTree(chromeProcess);
|
|
290
340
|
};
|
|
291
341
|
const takeBlockedNavigationError = () => {
|
|
292
342
|
const err = blockedNavigationError;
|
|
@@ -297,7 +347,7 @@ export async function openCdpPage() {
|
|
|
297
347
|
}
|
|
298
348
|
catch (err) {
|
|
299
349
|
client.close();
|
|
300
|
-
chromeProcess
|
|
350
|
+
killChromeTree(chromeProcess);
|
|
301
351
|
throw err;
|
|
302
352
|
}
|
|
303
353
|
}
|
|
@@ -414,8 +464,11 @@ export function describeNavigationFailure(errorText, url) {
|
|
|
414
464
|
`or bypassing the proxy: confirm "${host}" is reachable from this environment (e.g. test the ` +
|
|
415
465
|
`proxy directly), or fetch the page from a network with access and import it instead.`);
|
|
416
466
|
}
|
|
417
|
-
// Chrome can emit
|
|
418
|
-
|
|
467
|
+
// ERR_CERT_VERIFIER_CHANGED: Chrome can emit this on first navigation in headless CI when the cert
|
|
468
|
+
// verifier reloads mid-run. ERR_CONNECTION_CLOSED is Chromium's code for a plain remote TCP FIN
|
|
469
|
+
// (net_error_list.h), which CI runners can hit as a one-off network blip against a real host
|
|
470
|
+
// rather than a real block.
|
|
471
|
+
const TRANSIENT_NAV_ERROR_CODES = ['ERR_CERT_VERIFIER_CHANGED', 'ERR_CONNECTION_CLOSED'];
|
|
419
472
|
// Resource contention in CI (concurrent jobs competing for CPU/disk) can make Chrome itself fail
|
|
420
473
|
// to come up in time, or die before its devtools websocket URL is printed. Neither reflects a
|
|
421
474
|
// problem with the target page, so it's worth a fresh attempt rather than failing the whole fetch.
|
|
@@ -423,7 +476,9 @@ const TRANSIENT_CHROME_STARTUP_MESSAGES = [
|
|
|
423
476
|
'Timed out waiting for Chrome to start.',
|
|
424
477
|
'Chrome exited prematurely',
|
|
425
478
|
];
|
|
426
|
-
|
|
479
|
+
// Exported so tests can assert the transient-error classification directly rather than only
|
|
480
|
+
// indirectly through a live fetchRenderedHtml retry, which needs a real Chrome navigation to fail.
|
|
481
|
+
export function isTransientBrowserError(err) {
|
|
427
482
|
const message = err instanceof Error ? err.message : String(err);
|
|
428
483
|
return (TRANSIENT_NAV_ERROR_CODES.some((code) => message.includes(code)) ||
|
|
429
484
|
TRANSIENT_CHROME_STARTUP_MESSAGES.some((text) => message.includes(text)));
|
|
@@ -18,6 +18,13 @@ export interface CliIo {
|
|
|
18
18
|
warn(msg: string): void;
|
|
19
19
|
log(msg: string): void;
|
|
20
20
|
error(msg: string, opts: CliErrorOptions): never;
|
|
21
|
+
/**
|
|
22
|
+
* Print one row of a batch failure with the same "Error: … / Code: … / Try this: …" rendering as
|
|
23
|
+
* {@link error}, but without exiting the process — a batch (`fetch url1 url2`) must keep
|
|
24
|
+
* processing the remaining rows after one fails. Using `warn` for this mislabels a real failure
|
|
25
|
+
* as non-fatal even though the row still flips the command's exit code and JSON `ok` to false.
|
|
26
|
+
*/
|
|
27
|
+
errorRow(msg: string, opts: Omit<CliErrorOptions, 'exit'>): void;
|
|
21
28
|
}
|
|
22
29
|
/**
|
|
23
30
|
* Single exit point for a URL that failed normalization, shared by BaseCommand and the command
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ContextDashboard } from '../cli-result-types.js';
|
|
2
|
+
/** Entries beyond this count are still counted in `total`/`byFreshness`, just not listed. */
|
|
3
|
+
export declare const CONTEXT_DASHBOARD_DEFAULT_LIMIT = 5;
|
|
4
|
+
/**
|
|
5
|
+
* Builds the directory-scoped cache summary shown by `bonsai context` and injected by the
|
|
6
|
+
* `setup`-installed SessionStart hooks (AXI principle 7: ambient session-start context). Reuses
|
|
7
|
+
* the same scan/freshness primitives as `list` so the two commands can never disagree about what
|
|
8
|
+
* "active" or "fresh" means; unlike `list`, this applies no filters and always includes every
|
|
9
|
+
* matched artifact in `total`/`byFreshness`, previewing only the most recent `limit` in `entries`.
|
|
10
|
+
*
|
|
11
|
+
* @param readRoots - Data dirs to scan, in lookup order (see {@link loadStoreRoots}).
|
|
12
|
+
* @param currentTime - Clock used for freshness evaluation.
|
|
13
|
+
* @param options.limit - Max entries to preview; the count itself is never truncated.
|
|
14
|
+
* @param options.persistIndex - Whether to persist the search-index sidecar (false under read-only).
|
|
15
|
+
*/
|
|
16
|
+
export declare function buildContextDashboard(readRoots: string[], currentTime: Date, options?: {
|
|
17
|
+
limit?: number;
|
|
18
|
+
persistIndex?: boolean;
|
|
19
|
+
}): ContextDashboard;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { scanCacheDirs } from './storage.js';
|
|
2
|
+
import { evaluateFreshness } from './freshness.js';
|
|
3
|
+
/** Entries beyond this count are still counted in `total`/`byFreshness`, just not listed. */
|
|
4
|
+
export const CONTEXT_DASHBOARD_DEFAULT_LIMIT = 5;
|
|
5
|
+
/**
|
|
6
|
+
* Builds the directory-scoped cache summary shown by `bonsai context` and injected by the
|
|
7
|
+
* `setup`-installed SessionStart hooks (AXI principle 7: ambient session-start context). Reuses
|
|
8
|
+
* the same scan/freshness primitives as `list` so the two commands can never disagree about what
|
|
9
|
+
* "active" or "fresh" means; unlike `list`, this applies no filters and always includes every
|
|
10
|
+
* matched artifact in `total`/`byFreshness`, previewing only the most recent `limit` in `entries`.
|
|
11
|
+
*
|
|
12
|
+
* @param readRoots - Data dirs to scan, in lookup order (see {@link loadStoreRoots}).
|
|
13
|
+
* @param currentTime - Clock used for freshness evaluation.
|
|
14
|
+
* @param options.limit - Max entries to preview; the count itself is never truncated.
|
|
15
|
+
* @param options.persistIndex - Whether to persist the search-index sidecar (false under read-only).
|
|
16
|
+
*/
|
|
17
|
+
export function buildContextDashboard(readRoots, currentTime, options = {}) {
|
|
18
|
+
const limit = options.limit ?? CONTEXT_DASHBOARD_DEFAULT_LIMIT;
|
|
19
|
+
const rows = scanCacheDirs(readRoots, (artifact) => {
|
|
20
|
+
if (artifact.metadata.status !== 'active')
|
|
21
|
+
return null;
|
|
22
|
+
// Section children are sub-chunks of a page, not something a dashboard should list on its
|
|
23
|
+
// own — same exclusion `list` applies (see list.ts's scanCacheDirForList).
|
|
24
|
+
if (artifact.metadata.artifact_type === 'section')
|
|
25
|
+
return null;
|
|
26
|
+
return {
|
|
27
|
+
topic: artifact.metadata.topic,
|
|
28
|
+
sourceUrls: artifact.metadata.source_urls,
|
|
29
|
+
freshness: evaluateFreshness(artifact.metadata, currentTime, null),
|
|
30
|
+
recencyKey: artifact.metadata.validated_at ?? artifact.metadata.fetched_at ?? null,
|
|
31
|
+
};
|
|
32
|
+
}, { persistIndex: options.persistIndex ?? true });
|
|
33
|
+
rows.sort((a, b) => new Date(b.recencyKey ?? 0).getTime() - new Date(a.recencyKey ?? 0).getTime());
|
|
34
|
+
const byFreshness = { fresh: 0, stale_grace: 0, stale_expired: 0 };
|
|
35
|
+
for (const row of rows)
|
|
36
|
+
byFreshness[row.freshness]++;
|
|
37
|
+
const entries = rows.slice(0, limit).map((row) => ({
|
|
38
|
+
topic: row.topic,
|
|
39
|
+
sourceUrls: row.sourceUrls,
|
|
40
|
+
freshness: row.freshness,
|
|
41
|
+
}));
|
|
42
|
+
return { total: rows.length, byFreshness, shown: entries.length, entries };
|
|
43
|
+
}
|
|
@@ -3,10 +3,10 @@ import { mkdtempSync, rmSync } from 'node:fs';
|
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { finalizeBatch } from '../batch.js';
|
|
6
|
-
import { formatErrorForJson } from '../envelope.js';
|
|
7
6
|
import { loadSummaryLevel } from '../config/index.js';
|
|
8
7
|
import { buildFetchFailureFromCaught, buildFetchFailureResult, buildFetchResultData, describeError, extractionQualityWarnings, FETCH_STATUS_LABEL, fetchFailureGuidance, reportCacheStatus, } from './fetch-result.js';
|
|
9
8
|
import { durationFlagError, evaluateFreshnessWithMaxAge } from './freshness.js';
|
|
9
|
+
import { metadataLengthError, metadataNewlineError } from './metadata-flags.js';
|
|
10
10
|
import { fetchRenderedHtml } from './browser.js';
|
|
11
11
|
import { capturePage } from './capture.js';
|
|
12
12
|
import { persistSectionArtifacts } from './docs/section-artifacts.js';
|
|
@@ -15,8 +15,10 @@ import { applyAutoTags } from './keywords.js';
|
|
|
15
15
|
import { persistArtifact } from './persist-artifact.js';
|
|
16
16
|
import { createArtifactFromFetch, revalidateCache, } from './revalidate.js';
|
|
17
17
|
import { resolveResearchTarget } from './resolve-target.js';
|
|
18
|
+
import { locateArtifact } from './storage.js';
|
|
18
19
|
import { failInvalidUrl } from './cli-io.js';
|
|
19
20
|
import { batchSeparator } from '../cache-view.js';
|
|
21
|
+
import { formatTip } from '../text.js';
|
|
20
22
|
import { EXIT_STALE_SERVED } from '../cli-error-policy.js';
|
|
21
23
|
import { detectSite } from '../../sites/index.js';
|
|
22
24
|
import { applySiteFetchProvenance } from '../../sites/types.js';
|
|
@@ -34,6 +36,22 @@ export function validateFetchCommandFlags(io, flags) {
|
|
|
34
36
|
if (msg)
|
|
35
37
|
io.error(msg, { exit: 2, code: 'INVALID_DURATION' });
|
|
36
38
|
}
|
|
39
|
+
const newlineErr = metadataNewlineError(flags);
|
|
40
|
+
if (newlineErr) {
|
|
41
|
+
io.error(newlineErr, {
|
|
42
|
+
exit: 2,
|
|
43
|
+
code: 'INVALID_METADATA_VALUE',
|
|
44
|
+
suggestions: ['Remove line breaks from the value.'],
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
const lengthErr = metadataLengthError(flags);
|
|
48
|
+
if (lengthErr) {
|
|
49
|
+
io.error(lengthErr, {
|
|
50
|
+
exit: 2,
|
|
51
|
+
code: 'INVALID_METADATA_VALUE',
|
|
52
|
+
suggestions: ['Shorten the value.'],
|
|
53
|
+
});
|
|
54
|
+
}
|
|
37
55
|
// --force skips cache lookup; --allow-stale only applies when serving a stale entry after a
|
|
38
56
|
// failed revalidation. Together they are a no-op combination that looks intentional -- reject it.
|
|
39
57
|
if (flags.force && flags.allowStale) {
|
|
@@ -112,7 +130,7 @@ async function executeCacheHit(cached, targetDir, run, secure) {
|
|
|
112
130
|
redirectedToGlobal: Boolean(revalidationResult.redirectedToGlobal),
|
|
113
131
|
};
|
|
114
132
|
}
|
|
115
|
-
async function executeCacheMiss(normalizedUrl, cacheKey, run) {
|
|
133
|
+
async function executeCacheMiss(normalizedUrl, cacheKey, readRoots, run) {
|
|
116
134
|
const { currentTime, flags, summaryLevel } = run;
|
|
117
135
|
const siteModule = detectSite(normalizedUrl);
|
|
118
136
|
const useRendered = flags.rendered || Boolean(siteModule?.defaults?.rendered);
|
|
@@ -141,8 +159,13 @@ async function executeCacheMiss(normalizedUrl, cacheKey, run) {
|
|
|
141
159
|
currentTime,
|
|
142
160
|
summaryLevel,
|
|
143
161
|
});
|
|
144
|
-
|
|
145
|
-
|
|
162
|
+
// --force bypasses the normal revalidation-hit path (it always re-fetches instead of
|
|
163
|
+
// conditionally refreshing), so this "miss" may actually be overwriting a previously curated
|
|
164
|
+
// entry. Read-only lookup, purely to carry topic/tags forward like every other refresh path
|
|
165
|
+
// (revalidate.ts's preserveUserMetadata) — explicit --topic/--tags on this call still win.
|
|
166
|
+
const priorArtifact = locateArtifact(readRoots, cacheKey, true)?.artifact ?? null;
|
|
167
|
+
artifact.metadata.topic = flags.topic || priorArtifact?.metadata.topic || null;
|
|
168
|
+
artifact.metadata.tags = flags.tags?.length ? flags.tags : (priorArtifact?.metadata.tags ?? []);
|
|
146
169
|
artifact.metadata.site_module_id = siteModule?.id ?? null;
|
|
147
170
|
if (capture) {
|
|
148
171
|
applyCaptureMetadata(artifact, capture);
|
|
@@ -177,7 +200,7 @@ async function resolveArtifact(target, run) {
|
|
|
177
200
|
redirectedToGlobal: hit.redirectedToGlobal,
|
|
178
201
|
};
|
|
179
202
|
}
|
|
180
|
-
const artifact = await executeCacheMiss(normalizedUrl, cacheKey, run);
|
|
203
|
+
const artifact = await executeCacheMiss(normalizedUrl, cacheKey, roots.readRoots, run);
|
|
181
204
|
const write = persistArtifact({
|
|
182
205
|
roots,
|
|
183
206
|
cacheKey,
|
|
@@ -234,11 +257,17 @@ function failureRowOrRethrow(url, err, run) {
|
|
|
234
257
|
const row = err instanceof Errors.CLIError
|
|
235
258
|
? buildFetchFailureResult({ url, dryRun, err })
|
|
236
259
|
: buildFetchFailureFromCaught(io.bin, url, err, dryRun);
|
|
237
|
-
// Human batches only get the spinner "failed" label unless we echo the reason.
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
|
|
241
|
-
|
|
260
|
+
// Human batches only get the spinner "failed" label unless we echo the reason. Render it with
|
|
261
|
+
// the same "Error:" formatting as a single-URL failure (io.errorRow reuses oclif's own
|
|
262
|
+
// exit:false renderer) rather than io.warn, since a row failure still fails the command overall
|
|
263
|
+
// and should never read as merely a warning.
|
|
264
|
+
if (!io.json) {
|
|
265
|
+
io.errorRow(row.error.message, {
|
|
266
|
+
code: row.error.code,
|
|
267
|
+
suggestions: row.error.suggestions,
|
|
268
|
+
ref: row.error.ref,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
242
271
|
return row;
|
|
243
272
|
}
|
|
244
273
|
async function fetchSingleTarget(url, run) {
|
|
@@ -252,6 +281,10 @@ async function fetchSingleTarget(url, run) {
|
|
|
252
281
|
url,
|
|
253
282
|
flagOverride: flags.storage,
|
|
254
283
|
lookup: !flags.force,
|
|
284
|
+
// `dryRun` already merges explicit --dry-run with global read-only/plan mode
|
|
285
|
+
// (BaseCommand.effectiveDryRun), so a corrupt entry hit while resolving the cache target is
|
|
286
|
+
// never archived (renamed) on disk during either kind of no-write fetch.
|
|
287
|
+
readOnly: dryRun,
|
|
255
288
|
});
|
|
256
289
|
}
|
|
257
290
|
catch (err) {
|
|
@@ -279,21 +312,55 @@ async function fetchSingleTarget(url, run) {
|
|
|
279
312
|
redirectedToGlobal,
|
|
280
313
|
dryRun,
|
|
281
314
|
});
|
|
282
|
-
|
|
283
|
-
if (dryRun && cacheStatus !== 'hit' && cacheStatus !== 'stale') {
|
|
284
|
-
io.log('[dry-run] Preview only — cache was not written.');
|
|
285
|
-
}
|
|
286
|
-
for (const warning of extractionQualityWarnings(resultData.source.qualityNotes)) {
|
|
287
|
-
io.warn(`${normalizedUrl}: ${warning}`);
|
|
288
|
-
}
|
|
289
|
-
io.log(resultData.content);
|
|
290
|
-
const separator = batchSeparator(run.urlCount > 1);
|
|
291
|
-
if (separator) {
|
|
292
|
-
io.log(`\n${separator}\n`);
|
|
293
|
-
}
|
|
294
|
-
}
|
|
315
|
+
logHumanFetchResult({ io, resultData, cacheStatus, dryRun, normalizedUrl, run });
|
|
295
316
|
return resultData;
|
|
296
317
|
}
|
|
318
|
+
/** True when this run launched Chrome on its own (no --rendered flag) to capture `url`, rather than
|
|
319
|
+
* just reporting a stored artifact's capture history from an earlier run (cache 'hit'/'stale'/
|
|
320
|
+
* 'revalidated'). Human mode uses this to decide whether the browser-fallback note would be accurate. */
|
|
321
|
+
function usedAutomaticBrowserFallback(cacheStatus, captureMethod, rendered) {
|
|
322
|
+
const capturedThisRun = cacheStatus === 'miss' || cacheStatus === 'refreshed';
|
|
323
|
+
return capturedThisRun && captureMethod === 'browser_fallback' && !rendered;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Human-mode-only side effects for one fetch result: dry-run notice, the automatic browser-fallback
|
|
327
|
+
* note, extraction-quality warnings, the content itself, and the batch separator. No-op under
|
|
328
|
+
* `--json` (that output goes through the envelope instead).
|
|
329
|
+
*/
|
|
330
|
+
function logHumanFetchResult(input) {
|
|
331
|
+
const { io, resultData, cacheStatus, dryRun, normalizedUrl, run } = input;
|
|
332
|
+
if (io.json)
|
|
333
|
+
return;
|
|
334
|
+
if (dryRun && cacheStatus !== 'hit' && cacheStatus !== 'stale') {
|
|
335
|
+
io.log('[dry-run] Preview only — cache was not written.');
|
|
336
|
+
}
|
|
337
|
+
// Launching Chrome is real added latency and a real dependency that was otherwise invisible
|
|
338
|
+
// outside --json; an explicit --rendered already tells the caller a browser was used.
|
|
339
|
+
if (usedAutomaticBrowserFallback(cacheStatus, resultData.source.captureMethod, run.flags.rendered)) {
|
|
340
|
+
io.log('Note: used browser-rendered capture (static content was insufficient).');
|
|
341
|
+
}
|
|
342
|
+
for (const warning of extractionQualityWarnings(resultData.source.qualityNotes)) {
|
|
343
|
+
io.warn(`${normalizedUrl}: ${warning}`);
|
|
344
|
+
}
|
|
345
|
+
io.log(resultData.content);
|
|
346
|
+
// AXI content-truncation: only nudge toward the escape hatch when something was actually
|
|
347
|
+
// condensed — `detailedTokenEstimate` equals `tokenEstimate` whenever `format` is already
|
|
348
|
+
// `detailed`, or when compression happened to produce no smaller result.
|
|
349
|
+
if (resultData.format === 'compressed' &&
|
|
350
|
+
typeof resultData.tokenEstimate === 'number' &&
|
|
351
|
+
typeof resultData.detailedTokenEstimate === 'number' &&
|
|
352
|
+
resultData.detailedTokenEstimate > resultData.tokenEstimate) {
|
|
353
|
+
io.warn(formatTip(`${io.bin} ${normalizedUrl} --format detailed for the full ${resultData.detailedTokenEstimate}-token ` +
|
|
354
|
+
`version (currently showing ${resultData.tokenEstimate}).`));
|
|
355
|
+
}
|
|
356
|
+
if (!dryRun) {
|
|
357
|
+
io.warn(formatTip(`${io.bin} inspect ${normalizedUrl} for cached metadata.`));
|
|
358
|
+
}
|
|
359
|
+
const separator = batchSeparator(run.urlCount > 1);
|
|
360
|
+
if (separator) {
|
|
361
|
+
io.log(`\n${separator}\n`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
297
364
|
// Copies Phase 2 capability provenance from a capture outcome onto the artifact metadata.
|
|
298
365
|
function applyCaptureMetadata(artifact, capture) {
|
|
299
366
|
const meta = artifact.metadata;
|
|
@@ -57,6 +57,7 @@ export declare function buildFetchResultData(input: FetchResultInput): {
|
|
|
57
57
|
searchProvider: string | null;
|
|
58
58
|
format: "compressed" | "detailed";
|
|
59
59
|
tokenEstimate: number | null;
|
|
60
|
+
detailedTokenEstimate: number | null;
|
|
60
61
|
content: string;
|
|
61
62
|
};
|
|
62
63
|
export interface FetchFailureGuidance {
|
|
@@ -75,6 +75,10 @@ export function buildFetchResultData(input) {
|
|
|
75
75
|
tokenEstimate: format === 'compressed'
|
|
76
76
|
? artifact.metadata.token_estimate.compressed
|
|
77
77
|
: artifact.metadata.token_estimate.detailed,
|
|
78
|
+
// Always the true detailed count, even when `format` is `compressed` (AXI content-truncation
|
|
79
|
+
// principle: show the total size so the agent knows how much a `--format detailed` re-fetch
|
|
80
|
+
// would add, without a second round trip). Equals `tokenEstimate` when nothing was truncated.
|
|
81
|
+
detailedTokenEstimate: artifact.metadata.token_estimate.detailed,
|
|
78
82
|
content,
|
|
79
83
|
};
|
|
80
84
|
}
|
|
@@ -156,7 +156,9 @@ function notModifiedResult(res, currentUrl) {
|
|
|
156
156
|
}
|
|
157
157
|
function assertOk(res) {
|
|
158
158
|
if (!res.ok) {
|
|
159
|
-
|
|
159
|
+
// statusText is often empty (e.g. HTTP/2 responses carry no reason phrase); trim so a
|
|
160
|
+
// reasonless status never leaves a dangling trailing space, matching browser.ts's assertOk.
|
|
161
|
+
throw new Error(`Fetch failed with status ${res.status} ${res.statusText}`.trimEnd());
|
|
160
162
|
}
|
|
161
163
|
}
|
|
162
164
|
/**
|
|
@@ -5,6 +5,7 @@ import { loadSummaryLevel } from '../config/index.js';
|
|
|
5
5
|
import { buildCompressed } from './compress.js';
|
|
6
6
|
import { durationFlagError, resolveFreshnessPolicy } from './freshness.js';
|
|
7
7
|
import { applyAutoTags } from './keywords.js';
|
|
8
|
+
import { metadataLengthError, metadataNewlineError } from './metadata-flags.js';
|
|
8
9
|
import { persistArtifact } from './persist-artifact.js';
|
|
9
10
|
import { sanitizePromptInjection } from './prompt-injection.js';
|
|
10
11
|
import { getArtifactPath } from './storage.js';
|
|
@@ -14,6 +15,7 @@ import { deriveCacheKey } from './cache-key.js';
|
|
|
14
15
|
import { normalizeUrl } from './url.js';
|
|
15
16
|
import { failInvalidUrl } from './cli-io.js';
|
|
16
17
|
import { formatHumanFields } from '../cli-presentation.js';
|
|
18
|
+
import { sanitizeForTerminal } from '../text.js';
|
|
17
19
|
const INPUT_LIMIT_BYTES = 1024 * 1024;
|
|
18
20
|
// ponytail: 1s stdin idle timeout is enough for piped agent input; raise if interactive paste
|
|
19
21
|
// workflows need longer before the read aborts.
|
|
@@ -29,6 +31,22 @@ export async function prepareImportCommandService(opts) {
|
|
|
29
31
|
const ttlErr = durationFlagError('--ttl', flags.ttl);
|
|
30
32
|
if (ttlErr)
|
|
31
33
|
io.error(ttlErr, { exit: 2, code: 'INVALID_DURATION' });
|
|
34
|
+
const newlineErr = metadataNewlineError(flags);
|
|
35
|
+
if (newlineErr) {
|
|
36
|
+
io.error(newlineErr, {
|
|
37
|
+
exit: 2,
|
|
38
|
+
code: 'INVALID_METADATA_VALUE',
|
|
39
|
+
suggestions: ['Remove line breaks from the value.'],
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
const lengthErr = metadataLengthError(flags);
|
|
43
|
+
if (lengthErr) {
|
|
44
|
+
io.error(lengthErr, {
|
|
45
|
+
exit: 2,
|
|
46
|
+
code: 'INVALID_METADATA_VALUE',
|
|
47
|
+
suggestions: ['Shorten the value.'],
|
|
48
|
+
});
|
|
49
|
+
}
|
|
32
50
|
validateSourceMode(hasSingle, hasMulti, flags, io);
|
|
33
51
|
const rawInput = await readAndValidateInput(flags, input, io);
|
|
34
52
|
const sourceUrls = getSourceUrls(hasMulti, flags.sourceUrls, args.url, io);
|
|
@@ -128,9 +146,12 @@ function logImportHumanSuccess(io, opts) {
|
|
|
128
146
|
['Cache Key', cacheKey],
|
|
129
147
|
['Storage Path', storagePath],
|
|
130
148
|
];
|
|
149
|
+
// The agent-supplied --topic is untrusted the same way fetched content is (it may be lifted
|
|
150
|
+
// straight from a page an agent just read), so it gets the same terminal sanitization as
|
|
151
|
+
// list/inspect/prune apply to a cached topic before printing it.
|
|
131
152
|
let topic;
|
|
132
153
|
if (!hasSingle) {
|
|
133
|
-
topic = resolvedTopic(flags);
|
|
154
|
+
topic = sanitizeForTerminal(resolvedTopic(flags));
|
|
134
155
|
fields.push(['Topic', topic], ['Source URLs', sourceUrls.join(', ')]);
|
|
135
156
|
}
|
|
136
157
|
for (const line of formatHumanFields(fields))
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ListFreshness, ListRow } from '../cli-result-types.js';
|
|
2
|
+
import type { ResearchArtifact } from './schema.js';
|
|
3
|
+
/**
|
|
4
|
+
* Maps a parsed artifact to the shared page-level row shape (`ListRow`) that `list` and `search`
|
|
5
|
+
* both return — single source of truth so the two commands' JSON row schemas can never drift on
|
|
6
|
+
* which metadata fields a "row" exposes.
|
|
7
|
+
*/
|
|
8
|
+
export declare function toListRow(artifact: ResearchArtifact, filePath: string, freshness: ListRow['freshness']): ListRow;
|
|
9
|
+
/**
|
|
10
|
+
* Counts rows per freshness tier for the `list`/`search` envelope's `summary.byFreshness` — single
|
|
11
|
+
* source of truth so the two commands' aggregate always agree on the shape and the counting logic.
|
|
12
|
+
*/
|
|
13
|
+
export declare function countByFreshness(rows: readonly {
|
|
14
|
+
freshness: ListFreshness;
|
|
15
|
+
}[]): Record<ListFreshness, number>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps a parsed artifact to the shared page-level row shape (`ListRow`) that `list` and `search`
|
|
3
|
+
* both return — single source of truth so the two commands' JSON row schemas can never drift on
|
|
4
|
+
* which metadata fields a "row" exposes.
|
|
5
|
+
*/
|
|
6
|
+
export function toListRow(artifact, filePath, freshness) {
|
|
7
|
+
return {
|
|
8
|
+
cacheKey: artifact.metadata.cache_key,
|
|
9
|
+
path: filePath,
|
|
10
|
+
artifactType: artifact.metadata.artifact_type,
|
|
11
|
+
sourceUrls: artifact.metadata.source_urls,
|
|
12
|
+
topic: artifact.metadata.topic,
|
|
13
|
+
tags: artifact.metadata.tags,
|
|
14
|
+
freshness,
|
|
15
|
+
captureMethod: artifact.metadata.capture_method,
|
|
16
|
+
tokenEstimate: artifact.metadata.token_estimate,
|
|
17
|
+
qualityNotes: artifact.metadata.quality_notes,
|
|
18
|
+
fetchedAt: artifact.metadata.fetched_at,
|
|
19
|
+
validatedAt: artifact.metadata.validated_at,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Counts rows per freshness tier for the `list`/`search` envelope's `summary.byFreshness` — single
|
|
24
|
+
* source of truth so the two commands' aggregate always agree on the shape and the counting logic.
|
|
25
|
+
*/
|
|
26
|
+
export function countByFreshness(rows) {
|
|
27
|
+
const counts = { fresh: 0, stale_grace: 0, stale_expired: 0 };
|
|
28
|
+
for (const row of rows)
|
|
29
|
+
counts[row.freshness]++;
|
|
30
|
+
return counts;
|
|
31
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ArtifactType, CaptureMethod, ResearchArtifactMetadata } from './schema.js';
|
|
2
|
+
/** Whether an artifact's topic matches a `--topic` filter (case-insensitive exact match). */
|
|
3
|
+
export declare function matchesTopicFilter(meta: Pick<ResearchArtifactMetadata, 'topic'>, topic: string | undefined): boolean;
|
|
4
|
+
/** Whether an artifact's tags satisfy a `--tags` filter (case-insensitive, must match all). */
|
|
5
|
+
export declare function matchesTagsFilter(meta: Pick<ResearchArtifactMetadata, 'tags'>, tags: string[] | undefined): boolean;
|
|
6
|
+
/**
|
|
7
|
+
* Reject an explicitly passed, whitespace-only `--topic` filter. Silently treating it as "no
|
|
8
|
+
* filter" (matching everything, as an empty/falsy value normally would in {@link matchesTopicFilter})
|
|
9
|
+
* would hide what's almost always a shell-quoting mistake — the same reasoning `--url` already
|
|
10
|
+
* applies via `emptyUrlFilterError`.
|
|
11
|
+
*/
|
|
12
|
+
export declare function emptyTopicFilterError(topic: string | undefined): string | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* Reject an explicitly passed, whitespace-only `--tags` filter entry. Unlike a missing/empty
|
|
15
|
+
* `--topic` (which reads as "no filter"), an empty tag string in {@link matchesTagsFilter} never
|
|
16
|
+
* matches any artifact's tags — silently returning zero results with no signal. Fail fast instead.
|
|
17
|
+
*/
|
|
18
|
+
export declare function emptyTagsFilterError(tags: string[] | undefined): string | undefined;
|
|
19
|
+
/** The metadata filter flags `list` and `search` both expose, beyond content/topic search. */
|
|
20
|
+
export interface CommonMetadataFilterFlags {
|
|
21
|
+
topic?: string;
|
|
22
|
+
tags?: string[];
|
|
23
|
+
url?: string;
|
|
24
|
+
artifactType?: ArtifactType;
|
|
25
|
+
captureMethod?: CaptureMethod;
|
|
26
|
+
freshness?: 'fresh' | 'stale_grace' | 'stale_expired';
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Shared page-level metadata filter combinator for `list` and `search` (topic/tags/url/artifact
|
|
30
|
+
* type/capture method/freshness) — single source of truth so the two commands can never drift on
|
|
31
|
+
* what "matches the given filters" means.
|
|
32
|
+
*/
|
|
33
|
+
export declare function matchesCommonMetadataFilters(meta: ResearchArtifactMetadata, freshness: 'fresh' | 'stale_grace' | 'stale_expired', flags: CommonMetadataFilterFlags): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Whether any of the shared metadata filter flags was passed — used to pick empty-result guidance
|
|
36
|
+
* ("no matches for your filters" vs. "the cache is empty"). `search` combines this with its own
|
|
37
|
+
* `--query` check; `list` has no filters beyond these, so it uses the result directly.
|
|
38
|
+
*/
|
|
39
|
+
export declare function hasActiveMetadataFilters(flags: CommonMetadataFilterFlags): boolean;
|