@taurgis/bonsai 3.2.0 → 4.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 +9 -6
- package/agents/agents/salesforce-docs-researcher.agent.md +17 -4
- package/agents/instructions/salesforce-research.instructions.md +2 -1
- package/bin/cli.mjs +13 -18
- package/dist/base-command.d.ts +59 -12
- package/dist/base-command.js +179 -51
- package/dist/commands/config/base.js +12 -2
- package/dist/commands/config/get.js +8 -12
- package/dist/commands/config/index.js +2 -2
- package/dist/commands/config/list.js +11 -17
- package/dist/commands/config/set.d.ts +2 -1
- package/dist/commands/config/set.js +35 -28
- package/dist/commands/config/unset.d.ts +2 -1
- package/dist/commands/config/unset.js +26 -25
- package/dist/commands/fetch.d.ts +7 -15
- package/dist/commands/fetch.js +61 -401
- package/dist/commands/import.d.ts +4 -9
- package/dist/commands/import.js +56 -284
- package/dist/commands/inspect.d.ts +7 -1
- package/dist/commands/inspect.js +112 -52
- package/dist/commands/list.d.ts +18 -3
- package/dist/commands/list.js +121 -46
- package/dist/commands/prune.d.ts +6 -1
- package/dist/commands/prune.js +89 -78
- package/dist/commands/status.d.ts +3 -2
- package/dist/commands/status.js +62 -79
- package/dist/hooks/command-not-found/suggest.d.ts +2 -1
- package/dist/hooks/command-not-found/suggest.js +78 -41
- package/dist/lib/argv.d.ts +29 -3
- package/dist/lib/argv.js +147 -54
- package/dist/lib/batch.d.ts +41 -0
- package/dist/lib/batch.js +39 -0
- package/dist/lib/cache-view.d.ts +21 -0
- package/dist/lib/cache-view.js +30 -0
- package/dist/lib/cli-emit.d.ts +28 -0
- package/dist/lib/cli-emit.js +44 -0
- package/dist/lib/cli-error-policy.d.ts +57 -0
- package/dist/lib/cli-error-policy.js +74 -0
- package/dist/lib/cli-flag-manifest.d.ts +12 -0
- package/dist/lib/cli-flag-manifest.js +15 -0
- package/dist/lib/cli-presentation.d.ts +41 -0
- package/dist/lib/cli-presentation.js +53 -0
- package/dist/lib/cli-result-types.d.ts +112 -0
- package/dist/lib/cli-result-types.js +2 -0
- package/dist/lib/config/index.d.ts +23 -5
- package/dist/lib/config/index.js +60 -5
- 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/config-persist.d.ts +28 -0
- package/dist/lib/config-persist.js +29 -0
- package/dist/lib/envelope.d.ts +41 -3
- package/dist/lib/envelope.js +154 -4
- package/dist/lib/help-preflight.d.ts +5 -8
- package/dist/lib/help-preflight.js +13 -32
- package/dist/lib/json-meta.d.ts +0 -2
- package/dist/lib/json-meta.js +21 -25
- package/dist/lib/limit-flag.d.ts +11 -1
- package/dist/lib/limit-flag.js +19 -6
- package/dist/lib/parse-error-ux.d.ts +5 -0
- package/dist/lib/parse-error-ux.js +73 -0
- package/dist/lib/prune-flags.d.ts +30 -0
- package/dist/lib/prune-flags.js +86 -0
- package/dist/lib/research/artifact-index.d.ts +11 -2
- package/dist/lib/research/artifact-index.js +14 -5
- package/dist/lib/research/artifact.js +20 -2
- package/dist/lib/research/browser.d.ts +53 -2
- package/dist/lib/research/browser.js +202 -58
- package/dist/lib/research/capture.d.ts +24 -2
- package/dist/lib/research/capture.js +24 -4
- package/dist/lib/research/cli-io.d.ts +40 -0
- package/dist/lib/research/cli-io.js +27 -0
- package/dist/lib/research/compress.d.ts +7 -0
- package/dist/lib/research/compress.js +7 -3
- package/dist/lib/research/docs/capabilities.d.ts +15 -0
- package/dist/lib/research/docs/capabilities.js +5 -0
- package/dist/lib/research/docs/clean-dom.d.ts +2 -0
- package/dist/lib/research/docs/clean-dom.js +2 -0
- package/dist/lib/research/docs/code-blocks.d.ts +2 -0
- package/dist/lib/research/docs/code-blocks.js +2 -0
- package/dist/lib/research/docs/detect.d.ts +4 -0
- package/dist/lib/research/docs/detect.js +4 -0
- package/dist/lib/research/docs/machine-readable.d.ts +18 -0
- package/dist/lib/research/docs/machine-readable.js +10 -0
- package/dist/lib/research/docs/markdown-source.d.ts +20 -0
- package/dist/lib/research/docs/markdown-source.js +19 -2
- package/dist/lib/research/docs/quality-gates.d.ts +5 -0
- package/dist/lib/research/docs/quality-gates.js +4 -0
- package/dist/lib/research/docs/rsc.d.ts +9 -2
- package/dist/lib/research/docs/rsc.js +5 -2
- package/dist/lib/research/docs/section-artifacts.d.ts +12 -0
- package/dist/lib/research/docs/section-artifacts.js +12 -0
- package/dist/lib/research/docs/sections.d.ts +18 -0
- package/dist/lib/research/docs/sections.js +17 -1
- package/dist/lib/research/docs/source-map.d.ts +28 -0
- package/dist/lib/research/docs/source-map.js +27 -0
- package/dist/lib/research/docs/validate.d.ts +35 -2
- package/dist/lib/research/docs/validate.js +47 -2
- package/dist/lib/research/extract.d.ts +16 -0
- package/dist/lib/research/extract.js +12 -1
- package/dist/lib/research/fetch-command-service.d.ts +36 -0
- package/dist/lib/research/fetch-command-service.js +367 -0
- package/dist/lib/research/fetch-result.d.ts +114 -0
- package/dist/lib/research/fetch-result.js +212 -0
- package/dist/lib/research/fetcher.d.ts +46 -8
- package/dist/lib/research/fetcher.js +61 -31
- package/dist/lib/research/freshness.d.ts +54 -11
- package/dist/lib/research/freshness.js +91 -31
- package/dist/lib/research/import-command-service.d.ts +56 -0
- package/dist/lib/research/import-command-service.js +402 -0
- package/dist/lib/research/keywords.js +5 -1
- package/dist/lib/research/markdown.d.ts +12 -1
- package/dist/lib/research/markdown.js +13 -5
- package/dist/lib/research/metadata-filters.d.ts +18 -0
- package/dist/lib/research/metadata-filters.js +37 -0
- package/dist/lib/research/metadata-flags.d.ts +34 -0
- package/dist/lib/research/metadata-flags.js +45 -0
- package/dist/lib/research/persist-artifact.d.ts +23 -0
- package/dist/lib/research/persist-artifact.js +27 -0
- package/dist/lib/research/prompt-injection.d.ts +9 -0
- package/dist/lib/research/prompt-injection.js +59 -14
- package/dist/lib/research/proxy.d.ts +14 -8
- package/dist/lib/research/proxy.js +14 -10
- package/dist/lib/research/resolve-target.d.ts +2 -0
- package/dist/lib/research/resolve-target.js +1 -1
- package/dist/lib/research/revalidate.d.ts +49 -20
- package/dist/lib/research/revalidate.js +80 -15
- package/dist/lib/research/schema.d.ts +7 -0
- package/dist/lib/research/schema.js +2 -0
- package/dist/lib/research/secret-scan.d.ts +6 -0
- package/dist/lib/research/secret-scan.js +8 -0
- package/dist/lib/research/secure-write.d.ts +4 -0
- package/dist/lib/research/secure-write.js +31 -2
- package/dist/lib/research/storage.d.ts +9 -8
- package/dist/lib/research/storage.js +34 -28
- package/dist/lib/research/summarize.d.ts +15 -0
- package/dist/lib/research/summarize.js +11 -0
- package/dist/lib/research/url.d.ts +9 -6
- package/dist/lib/research/url.js +33 -21
- package/dist/lib/text.d.ts +19 -8
- package/dist/lib/text.js +32 -9
- package/dist/lib/value-flags.d.ts +14 -0
- package/dist/lib/value-flags.js +28 -0
- package/dist/sites/coveo.d.ts +10 -0
- package/dist/sites/coveo.js +10 -0
- package/dist/sites/index.d.ts +16 -0
- package/dist/sites/index.js +16 -4
- package/dist/sites/markdown-twin.d.ts +31 -0
- package/dist/sites/markdown-twin.js +56 -0
- package/dist/sites/salesforce/fetch-page.d.ts +8 -2
- package/dist/sites/salesforce/fetch-page.js +14 -3
- package/dist/sites/salesforce/help-tooling-markdown.d.ts +30 -0
- package/dist/sites/salesforce/help-tooling-markdown.js +74 -0
- package/dist/sites/salesforce/index.d.ts +1 -0
- package/dist/sites/salesforce/index.js +1 -0
- package/dist/sites/salesforce-developer/fetch-page.d.ts +6 -1
- package/dist/sites/salesforce-developer/fetch-page.js +6 -1
- package/dist/sites/salesforce-developer/index.d.ts +1 -0
- package/dist/sites/salesforce-developer/index.js +1 -0
- package/dist/sites/salesforce-developer/route-markdown.d.ts +18 -6
- package/dist/sites/salesforce-developer/route-markdown.js +29 -67
- package/dist/sites/salesforce-doc-fetch.d.ts +39 -5
- package/dist/sites/salesforce-doc-fetch.js +52 -7
- package/dist/sites/salesforce-dom-probe.d.ts +20 -2
- package/dist/sites/salesforce-dom-probe.js +20 -2
- package/dist/sites/types.d.ts +14 -8
- package/dist/sites/types.js +3 -0
- package/oclif.manifest.json +216 -102
- package/package.json +20 -7
- package/agents/agents/official-docs-researcher.agent.md +0 -87
- package/agents/instructions/web-research.instructions.md +0 -44
- package/agents/skills/web-research/SKILL.md +0 -118
package/dist/commands/list.d.ts
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import { BaseCommand } from '../base-command.js';
|
|
2
|
+
import type { ListRow, ListRowMinimal } from '../lib/cli-result-types.js';
|
|
3
|
+
/** List cached research artifacts by metadata filters. */
|
|
2
4
|
export default class ResearchList extends BaseCommand<typeof ResearchList> {
|
|
3
5
|
static id: string;
|
|
4
6
|
static summary: string;
|
|
5
7
|
static description: string;
|
|
8
|
+
/**
|
|
9
|
+
* Aggregate counts (and an explicit empty/truncation signal) attached to the success envelope so
|
|
10
|
+
* agents get a cache-wide summary without a separate round trip (AXI principles: pre-computed
|
|
11
|
+
* aggregates, definitive empty states). Always set by `run()` before it returns.
|
|
12
|
+
*/
|
|
13
|
+
private jsonSummary;
|
|
6
14
|
static examples: {
|
|
7
15
|
description: string;
|
|
8
16
|
command: string;
|
|
@@ -15,12 +23,19 @@ export default class ResearchList extends BaseCommand<typeof ResearchList> {
|
|
|
15
23
|
'artifact-type': import("@oclif/core/interfaces").OptionFlag<"source" | "research_note" | "index" | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
24
|
'capture-method': import("@oclif/core/interfaces").OptionFlag<"static_fetch" | "browser_fallback" | "agent_supplied" | "route_markdown" | "github_source" | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
25
|
limit: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
26
|
+
full: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
18
27
|
};
|
|
19
28
|
static stdoutIsPrimaryData: boolean;
|
|
20
|
-
private matchesTopic;
|
|
21
|
-
private matchesTags;
|
|
22
29
|
private matchesFilters;
|
|
23
30
|
private scanCacheDirForList;
|
|
31
|
+
private hasActiveFilters;
|
|
24
32
|
private logListResults;
|
|
25
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Empty-cache / no-match tip (human mode only). Under `--json`/`--toon`, `data: []` is paired with
|
|
35
|
+
* `summary.empty: true` instead of prose — see {@link toSuccessJson}.
|
|
36
|
+
*/
|
|
37
|
+
private emitEmptyListGuidance;
|
|
38
|
+
run(): Promise<ListRow[] | ListRowMinimal[]>;
|
|
39
|
+
/** Attach the aggregate `summary` computed in `run()` (see {@link ListSummary}). */
|
|
40
|
+
protected toSuccessJson(data: unknown): Record<string, unknown>;
|
|
26
41
|
}
|
package/dist/commands/list.js
CHANGED
|
@@ -3,82 +3,109 @@ import { BaseCommand } from '../base-command.js';
|
|
|
3
3
|
import { scanCacheDirs } from '../lib/research/storage.js';
|
|
4
4
|
import { loadStoreRoots } from '../lib/research/store-roots.js';
|
|
5
5
|
import { evaluateFreshness } from '../lib/research/freshness.js';
|
|
6
|
-
import { ARTIFACT_TYPES, CAPTURE_METHODS } from '../lib/research/schema.js';
|
|
7
|
-
import { NO_TOPIC_LABEL, resultListHeading,
|
|
6
|
+
import { ARTIFACT_TYPES, CAPTURE_METHODS, } from '../lib/research/schema.js';
|
|
7
|
+
import { NO_TOPIC_LABEL, formatTip, resultListHeading, sanitizeForTerminal, } from '../lib/text.js';
|
|
8
8
|
import { limitFlag } from '../lib/limit-flag.js';
|
|
9
|
-
import { artifactMatchesUrlFilter } from '../lib/research/url.js';
|
|
9
|
+
import { artifactMatchesUrlFilter, emptyUrlFilterError } from '../lib/research/url.js';
|
|
10
|
+
import { matchesTopicFilter, matchesTagsFilter, emptyTopicFilterError, emptyTagsFilterError, } from '../lib/research/metadata-filters.js';
|
|
10
11
|
import { colors } from '../lib/color.js';
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
import { CLI_FLAG_DESCRIPTIONS } from '../lib/cli-presentation.js';
|
|
13
|
+
// Listings are ordered newest-first, so the truncation word is "first"; --limit caps at this value.
|
|
14
|
+
const LIST_DEFAULT_MAX_LIMIT = 100;
|
|
15
|
+
const LIST_LABELS = {
|
|
16
|
+
noun: 'cached research',
|
|
17
|
+
order: 'first',
|
|
18
|
+
};
|
|
13
19
|
// `list` answers "what pages/notes do I have?" and deliberately omits section children (see
|
|
14
20
|
// scanCacheDirForList), so `section` is not an offered filter — every other artifact type can appear.
|
|
15
21
|
const LISTABLE_ARTIFACT_TYPES = ARTIFACT_TYPES.filter((type) => type !== 'section');
|
|
22
|
+
/** Project a full row down to the default minimal shape (see {@link ListRowMinimal}). */
|
|
23
|
+
function toMinimalListRow(row) {
|
|
24
|
+
return {
|
|
25
|
+
sourceUrls: row.sourceUrls,
|
|
26
|
+
topic: row.topic,
|
|
27
|
+
freshness: row.freshness,
|
|
28
|
+
tokenEstimate: row.tokenEstimate,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** Count matched rows per freshness tier for the envelope's `summary.byFreshness`. */
|
|
32
|
+
function countByFreshness(rows) {
|
|
33
|
+
const counts = { fresh: 0, stale_grace: 0, stale_expired: 0 };
|
|
34
|
+
for (const row of rows)
|
|
35
|
+
counts[row.freshness]++;
|
|
36
|
+
return counts;
|
|
37
|
+
}
|
|
38
|
+
/** List cached research artifacts by metadata filters. */
|
|
16
39
|
export default class ResearchList extends BaseCommand {
|
|
17
40
|
static id = 'list';
|
|
18
|
-
static summary = 'List cached research artifacts by metadata
|
|
19
|
-
static description = '
|
|
41
|
+
static summary = 'List cached research artifacts by metadata';
|
|
42
|
+
static description = 'List page-level cached artifacts by source URL, topic, freshness, and token estimate. ' +
|
|
43
|
+
'Pass --full for every metadata field (cache key, path, artifact type, tags, capture method, quality notes, timestamps).';
|
|
44
|
+
/**
|
|
45
|
+
* Aggregate counts (and an explicit empty/truncation signal) attached to the success envelope so
|
|
46
|
+
* agents get a cache-wide summary without a separate round trip (AXI principles: pre-computed
|
|
47
|
+
* aggregates, definitive empty states). Always set by `run()` before it returns.
|
|
48
|
+
*/
|
|
49
|
+
jsonSummary = null;
|
|
20
50
|
static examples = [
|
|
21
51
|
{
|
|
22
|
-
description: 'list
|
|
52
|
+
description: 'list cached entries',
|
|
23
53
|
command: '<%= config.bin %> list',
|
|
24
54
|
},
|
|
25
55
|
{
|
|
26
|
-
description: 'list
|
|
56
|
+
description: 'list entries for a topic as JSON',
|
|
27
57
|
command: '<%= config.bin %> list --topic "React Suspense" --json',
|
|
28
58
|
},
|
|
29
59
|
{
|
|
30
|
-
description: 'list
|
|
60
|
+
description: 'list fresh entries matching tags',
|
|
31
61
|
command: '<%= config.bin %> list --freshness fresh --tags node --tags url',
|
|
32
62
|
},
|
|
33
63
|
{
|
|
34
|
-
description: 'list entries matching a
|
|
64
|
+
description: 'list entries matching a URL glob',
|
|
35
65
|
command: '<%= config.bin %> list --url "https://react.dev/*"',
|
|
36
66
|
},
|
|
67
|
+
{
|
|
68
|
+
description: 'list every metadata field instead of the minimal default row',
|
|
69
|
+
command: '<%= config.bin %> list --full --json',
|
|
70
|
+
},
|
|
37
71
|
];
|
|
38
72
|
static flags = {
|
|
39
73
|
topic: Flags.string({
|
|
40
74
|
char: 't',
|
|
41
|
-
description:
|
|
75
|
+
description: CLI_FLAG_DESCRIPTIONS.filterTopic,
|
|
42
76
|
}),
|
|
43
77
|
tags: Flags.string({
|
|
44
78
|
char: 'g',
|
|
45
|
-
description:
|
|
79
|
+
description: CLI_FLAG_DESCRIPTIONS.filterTags,
|
|
46
80
|
multiple: true,
|
|
47
81
|
}),
|
|
48
82
|
url: Flags.string({
|
|
49
|
-
description:
|
|
83
|
+
description: CLI_FLAG_DESCRIPTIONS.sourceUrlGlob,
|
|
50
84
|
}),
|
|
51
85
|
freshness: Flags.option({
|
|
52
|
-
description: '
|
|
86
|
+
description: 'freshness state',
|
|
53
87
|
options: ['fresh', 'stale_grace', 'stale_expired'],
|
|
54
88
|
})(),
|
|
55
89
|
'artifact-type': Flags.option({
|
|
56
|
-
description:
|
|
90
|
+
description: CLI_FLAG_DESCRIPTIONS.listArtifactType,
|
|
57
91
|
options: LISTABLE_ARTIFACT_TYPES,
|
|
58
92
|
})(),
|
|
59
93
|
'capture-method': Flags.option({
|
|
60
|
-
description: '
|
|
94
|
+
description: 'capture method',
|
|
61
95
|
options: CAPTURE_METHODS,
|
|
62
96
|
})(),
|
|
63
|
-
limit: limitFlag(
|
|
97
|
+
limit: limitFlag(LIST_DEFAULT_MAX_LIMIT, 50, `result count (max ${LIST_DEFAULT_MAX_LIMIT})`),
|
|
98
|
+
full: Flags.boolean({
|
|
99
|
+
default: false,
|
|
100
|
+
description: CLI_FLAG_DESCRIPTIONS.listFull,
|
|
101
|
+
}),
|
|
64
102
|
};
|
|
65
103
|
static stdoutIsPrimaryData = true;
|
|
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()));
|
|
76
|
-
}
|
|
77
104
|
matchesFilters(meta, freshness) {
|
|
78
|
-
if (!
|
|
105
|
+
if (!matchesTopicFilter(meta, this.flags.topic)) {
|
|
79
106
|
return false;
|
|
80
107
|
}
|
|
81
|
-
if (!
|
|
108
|
+
if (!matchesTagsFilter(meta, this.flags.tags)) {
|
|
82
109
|
return false;
|
|
83
110
|
}
|
|
84
111
|
if (this.flags.url && !artifactMatchesUrlFilter(meta, this.flags.url)) {
|
|
@@ -96,6 +123,7 @@ export default class ResearchList extends BaseCommand {
|
|
|
96
123
|
return true;
|
|
97
124
|
}
|
|
98
125
|
scanCacheDirForList(readRoots, currentTime) {
|
|
126
|
+
// Honor effective read-only: listing must not persist the derived search-index sidecar.
|
|
99
127
|
return scanCacheDirs(readRoots, (artifact, filePath) => {
|
|
100
128
|
if (artifact.metadata.status !== 'active')
|
|
101
129
|
return null;
|
|
@@ -124,19 +152,28 @@ export default class ResearchList extends BaseCommand {
|
|
|
124
152
|
fetchedAt: artifact.metadata.fetched_at,
|
|
125
153
|
validatedAt: artifact.metadata.validated_at,
|
|
126
154
|
};
|
|
127
|
-
});
|
|
155
|
+
}, { persistIndex: !this.readOnly });
|
|
156
|
+
}
|
|
157
|
+
hasActiveFilters() {
|
|
158
|
+
return Boolean(this.flags.topic ||
|
|
159
|
+
(this.flags.tags && this.flags.tags.length > 0) ||
|
|
160
|
+
this.flags.freshness ||
|
|
161
|
+
this.flags['artifact-type'] ||
|
|
162
|
+
this.flags['capture-method'] ||
|
|
163
|
+
this.flags.url);
|
|
128
164
|
}
|
|
129
165
|
logListResults(finalResults, totalMatched) {
|
|
130
|
-
if (this.jsonEnabled())
|
|
131
|
-
return;
|
|
132
166
|
if (finalResults.length === 0) {
|
|
133
|
-
this.
|
|
134
|
-
this.log(`\nTip: populate the cache first: ${colors.cyan(this.config.bin + ' <url>')}`);
|
|
167
|
+
this.emitEmptyListGuidance();
|
|
135
168
|
return;
|
|
136
169
|
}
|
|
170
|
+
if (this.jsonEnabled())
|
|
171
|
+
return;
|
|
137
172
|
this.log(`${resultListHeading(totalMatched, finalResults.length, LIST_LABELS)}\n`);
|
|
138
173
|
finalResults.forEach((res, index) => {
|
|
139
|
-
const topicStr = res.topic
|
|
174
|
+
const topicStr = res.topic
|
|
175
|
+
? colors.cyan(sanitizeForTerminal(res.topic))
|
|
176
|
+
: colors.gray(NO_TOPIC_LABEL);
|
|
140
177
|
const keyStr = colors.bold(res.cacheKey);
|
|
141
178
|
this.log(`${index + 1}. [${topicStr}] Key: ${keyStr}`);
|
|
142
179
|
const freshnessColorMap = {
|
|
@@ -144,14 +181,42 @@ export default class ResearchList extends BaseCommand {
|
|
|
144
181
|
stale_grace: colors.yellow,
|
|
145
182
|
stale_expired: colors.red,
|
|
146
183
|
};
|
|
147
|
-
const freshnessColor = freshnessColorMap[res.freshness]
|
|
184
|
+
const freshnessColor = freshnessColorMap[res.freshness];
|
|
148
185
|
const freshnessStr = freshnessColor(res.freshness);
|
|
149
186
|
this.log(` Type: ${colors.bold(res.artifactType)} | Freshness: ${freshnessStr}`);
|
|
150
187
|
this.log(` Tokens: compressed=${colors.bold(String(res.tokenEstimate?.compressed || 0))}, detailed=${colors.bold(String(res.tokenEstimate?.detailed || 0))}`);
|
|
151
188
|
this.log(` Source URLs: ${colors.gray(res.sourceUrls.join(', '))}\n`);
|
|
152
189
|
});
|
|
153
190
|
}
|
|
191
|
+
/**
|
|
192
|
+
* Empty-cache / no-match tip (human mode only). Under `--json`/`--toon`, `data: []` is paired with
|
|
193
|
+
* `summary.empty: true` instead of prose — see {@link toSuccessJson}.
|
|
194
|
+
*/
|
|
195
|
+
emitEmptyListGuidance() {
|
|
196
|
+
if (this.jsonEnabled())
|
|
197
|
+
return;
|
|
198
|
+
const filtered = this.hasActiveFilters();
|
|
199
|
+
const headline = filtered
|
|
200
|
+
? 'No cached research entries match the given filters.'
|
|
201
|
+
: 'No cached research entries found.';
|
|
202
|
+
const tipCmd = filtered ? `${this.config.bin} list` : `${this.config.bin} <url>`;
|
|
203
|
+
const tipLead = filtered
|
|
204
|
+
? 'try relaxing filters, or list everything: '
|
|
205
|
+
: 'populate the cache first: ';
|
|
206
|
+
this.log(headline);
|
|
207
|
+
this.log(`\n${formatTip(`${tipLead}${colors.cyan(tipCmd)}`)}`);
|
|
208
|
+
// Bare `bonsai` (no args) redirects here (content-first default), so an empty, unfiltered cache
|
|
209
|
+
// is also the first thing a brand-new user sees — point at --help so command discovery isn't lost.
|
|
210
|
+
if (!filtered) {
|
|
211
|
+
this.log(formatTip(`see every command: ${colors.cyan(`${this.config.bin} --help`)}`));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
154
214
|
async run() {
|
|
215
|
+
const flagErr = emptyUrlFilterError(this.flags.url) ??
|
|
216
|
+
emptyTopicFilterError(this.flags.topic) ??
|
|
217
|
+
emptyTagsFilterError(this.flags.tags);
|
|
218
|
+
if (flagErr)
|
|
219
|
+
this.error(flagErr, { exit: 2, code: 'INVALID_FLAG_VALUE' });
|
|
155
220
|
const roots = loadStoreRoots({
|
|
156
221
|
configDir: this.config.configDir,
|
|
157
222
|
cwd: process.cwd(),
|
|
@@ -159,20 +224,30 @@ export default class ResearchList extends BaseCommand {
|
|
|
159
224
|
});
|
|
160
225
|
const currentTime = new Date();
|
|
161
226
|
const results = this.scanCacheDirForList(roots.readRoots, currentTime);
|
|
162
|
-
// Sort by validated_at or fetched_at descending (most recent first)
|
|
163
227
|
results.sort((a, b) => {
|
|
164
228
|
const timeA = new Date(a.validatedAt || a.fetchedAt || 0).getTime();
|
|
165
229
|
const timeB = new Date(b.validatedAt || b.fetchedAt || 0).getTime();
|
|
166
230
|
return timeB - timeA;
|
|
167
231
|
});
|
|
168
232
|
const finalResults = results.slice(0, this.flags.limit);
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
this.
|
|
233
|
+
// Human heading carries truncation inline; --json/--toon get the same counts (plus a freshness
|
|
234
|
+
// breakdown and an explicit `empty` flag) as an always-present envelope `summary` object.
|
|
235
|
+
this.jsonSummary = {
|
|
236
|
+
total: results.length,
|
|
237
|
+
shown: finalResults.length,
|
|
238
|
+
limit: this.flags.limit,
|
|
239
|
+
truncated: results.length > finalResults.length,
|
|
240
|
+
empty: results.length === 0,
|
|
241
|
+
byFreshness: countByFreshness(results),
|
|
242
|
+
};
|
|
175
243
|
this.logListResults(finalResults, results.length);
|
|
176
|
-
return finalResults;
|
|
244
|
+
return this.flags.full ? finalResults : finalResults.map(toMinimalListRow);
|
|
245
|
+
}
|
|
246
|
+
/** Attach the aggregate `summary` computed in `run()` (see {@link ListSummary}). */
|
|
247
|
+
toSuccessJson(data) {
|
|
248
|
+
const envelope = this.baseSuccessJson(data);
|
|
249
|
+
if (!this.jsonSummary)
|
|
250
|
+
return envelope;
|
|
251
|
+
return { ...envelope, summary: this.jsonSummary };
|
|
177
252
|
}
|
|
178
253
|
}
|
package/dist/commands/prune.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { BaseCommand } from '../base-command.js';
|
|
2
|
+
import type { PruneWriteResult } from '../lib/cli-result-types.js';
|
|
3
|
+
/** Prune cached artifacts by age, idle time, or filters. */
|
|
2
4
|
export default class ResearchPrune extends BaseCommand<typeof ResearchPrune> {
|
|
3
5
|
static id: string;
|
|
4
6
|
static summary: string;
|
|
@@ -11,14 +13,17 @@ export default class ResearchPrune extends BaseCommand<typeof ResearchPrune> {
|
|
|
11
13
|
'older-than': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
14
|
inactive: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
15
|
url: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
+
topic: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
|
+
tags: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
18
|
'artifact-type': import("@oclif/core/interfaces").OptionFlag<"source" | "research_note" | "index" | "section" | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
19
|
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
16
20
|
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
17
21
|
};
|
|
18
22
|
static stdoutIsPrimaryData: boolean;
|
|
23
|
+
protected toSuccessJson(data: unknown): Record<string, unknown>;
|
|
19
24
|
private validatePruneFlags;
|
|
20
25
|
private shouldPrune;
|
|
21
26
|
private findPruneCandidates;
|
|
22
27
|
private deletePruneCandidates;
|
|
23
|
-
run(): Promise<
|
|
28
|
+
run(): Promise<PruneWriteResult>;
|
|
24
29
|
}
|
package/dist/commands/prune.js
CHANGED
|
@@ -2,131 +2,119 @@ import { Flags } from '@oclif/core';
|
|
|
2
2
|
import { unlinkSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { BaseCommand } from '../base-command.js';
|
|
5
|
+
import { enrichPrunePartialEnvelope } from '../lib/envelope.js';
|
|
5
6
|
import { scanCacheDir } from '../lib/research/storage.js';
|
|
6
7
|
import { loadStoreRoots } from '../lib/research/store-roots.js';
|
|
7
|
-
import { parseTtlToMs
|
|
8
|
+
import { parseTtlToMs } from '../lib/research/freshness.js';
|
|
8
9
|
import { ARTIFACT_TYPES } from '../lib/research/schema.js';
|
|
9
|
-
import { NO_TOPIC_LABEL, pluralize } from '../lib/text.js';
|
|
10
|
+
import { NO_TOPIC_LABEL, pluralize, sanitizeForTerminal } from '../lib/text.js';
|
|
10
11
|
import { artifactMatchesUrlFilter } from '../lib/research/url.js';
|
|
12
|
+
import { matchesTopicFilter, matchesTagsFilter } from '../lib/research/metadata-filters.js';
|
|
13
|
+
import { pruneFlagError } from '../lib/prune-flags.js';
|
|
11
14
|
import { colors } from '../lib/color.js';
|
|
15
|
+
import { CLI_FLAG_DESCRIPTIONS } from '../lib/cli-presentation.js';
|
|
16
|
+
/** Prune cached artifacts by age, idle time, or filters. */
|
|
12
17
|
export default class ResearchPrune extends BaseCommand {
|
|
13
18
|
static id = 'prune';
|
|
14
|
-
static summary = '
|
|
15
|
-
static description = '
|
|
19
|
+
static summary = 'Prune cached research artifacts';
|
|
20
|
+
static description = 'Delete cached artifacts by content age, idle time, URL glob, artifact type, topic, or tags.';
|
|
16
21
|
static examples = [
|
|
17
22
|
{
|
|
18
|
-
description: '
|
|
19
|
-
command: '<%= config.bin %> prune --older-than
|
|
23
|
+
description: 'preview pruning entries older than 30 days',
|
|
24
|
+
command: '<%= config.bin %> prune --older-than 30d --dry-run',
|
|
20
25
|
},
|
|
21
26
|
{
|
|
22
|
-
description: '
|
|
27
|
+
description: 'prune source artifacts older than 30 days',
|
|
23
28
|
command: '<%= config.bin %> prune --older-than 30d --artifact-type source --yes',
|
|
24
29
|
},
|
|
25
30
|
{
|
|
26
|
-
description: 'prune entries matching a URL glob
|
|
31
|
+
description: 'prune entries matching a URL glob',
|
|
27
32
|
command: '<%= config.bin %> prune --url "https://react.dev/*" --yes',
|
|
28
33
|
},
|
|
34
|
+
{
|
|
35
|
+
description: 'prune entries tagged as deprecated',
|
|
36
|
+
command: '<%= config.bin %> prune --tags deprecated --yes',
|
|
37
|
+
},
|
|
29
38
|
];
|
|
30
39
|
static flags = {
|
|
31
40
|
'older-than': Flags.string({
|
|
32
|
-
description: '
|
|
41
|
+
description: 'content age threshold (fetched_at, else validated_at), e.g. "30d"',
|
|
33
42
|
}),
|
|
34
43
|
inactive: Flags.string({
|
|
35
|
-
description: '
|
|
44
|
+
description: 'idle time threshold (validated_at, else fetched_at), e.g. "14d"',
|
|
36
45
|
}),
|
|
37
46
|
url: Flags.string({
|
|
38
|
-
description:
|
|
47
|
+
description: CLI_FLAG_DESCRIPTIONS.sourceUrlGlob,
|
|
48
|
+
}),
|
|
49
|
+
topic: Flags.string({
|
|
50
|
+
char: 't',
|
|
51
|
+
description: CLI_FLAG_DESCRIPTIONS.filterTopic,
|
|
52
|
+
}),
|
|
53
|
+
tags: Flags.string({
|
|
54
|
+
char: 'g',
|
|
55
|
+
description: CLI_FLAG_DESCRIPTIONS.filterTags,
|
|
56
|
+
multiple: true,
|
|
39
57
|
}),
|
|
40
58
|
'artifact-type': Flags.option({
|
|
41
59
|
// Prune operates on every cached file, so it can target any artifact type — including the
|
|
42
60
|
// `section`/`index` children a page generates (e.g. to clear orphans left after a source is
|
|
43
61
|
// pruned with `--artifact-type source`).
|
|
44
|
-
description:
|
|
62
|
+
description: CLI_FLAG_DESCRIPTIONS.pruneArtifactType,
|
|
45
63
|
options: ARTIFACT_TYPES,
|
|
46
64
|
})(),
|
|
47
65
|
'dry-run': Flags.boolean({
|
|
48
|
-
description: '
|
|
66
|
+
description: 'preview files without deleting',
|
|
49
67
|
default: false,
|
|
50
68
|
}),
|
|
51
69
|
yes: Flags.boolean({
|
|
52
70
|
char: 'y',
|
|
53
|
-
description: 'confirm deletion
|
|
71
|
+
description: 'confirm deletion (required unless --dry-run)',
|
|
54
72
|
default: false,
|
|
55
73
|
}),
|
|
56
74
|
};
|
|
57
75
|
static stdoutIsPrimaryData = true;
|
|
76
|
+
toSuccessJson(data) {
|
|
77
|
+
return enrichPrunePartialEnvelope(this.baseSuccessJson(data), data);
|
|
78
|
+
}
|
|
58
79
|
validatePruneFlags() {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
this.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
// CONFLICTING_FLAGS — read-only mode is the more fundamental constraint being violated.
|
|
74
|
-
if (this.readOnly && this.flags.yes) {
|
|
75
|
-
this.error('--yes cannot be used while read-only mode is active (--read-only/--plan or BONSAI_READ_ONLY/BONSAI_PLAN_MODE): mutations are disabled.', {
|
|
76
|
-
exit: 2,
|
|
77
|
-
code: 'READ_ONLY_MODE',
|
|
78
|
-
suggestions: [`Preview instead: ${this.config.bin} prune --dry-run`],
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
if (this.flags['dry-run'] && this.flags.yes) {
|
|
82
|
-
this.error('--dry-run and --yes are mutually exclusive: --dry-run previews without deleting, --yes confirms deletion. Choose one.', { exit: 2, code: 'CONFLICTING_FLAGS' });
|
|
83
|
-
}
|
|
84
|
-
// Read-only mode implicitly previews, so the usual "pick one" safety check is redundant here.
|
|
85
|
-
if (!this.readOnly && !this.flags['dry-run'] && !this.flags.yes) {
|
|
86
|
-
const olderThanPart = this.flags['older-than']
|
|
87
|
-
? ` --older-than ${this.flags['older-than']}`
|
|
88
|
-
: '';
|
|
89
|
-
const urlPart = this.flags.url ? ` --url "${this.flags.url}"` : '';
|
|
90
|
-
this.error('Safety check: use --yes to confirm pruning, or --dry-run to preview files that would be deleted.', {
|
|
91
|
-
exit: 2,
|
|
92
|
-
code: 'SAFETY_CHECK_REQUIRED',
|
|
93
|
-
suggestions: [
|
|
94
|
-
`Preview first: ${this.config.bin} prune --dry-run${olderThanPart}${urlPart}`,
|
|
95
|
-
],
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
// Validate durations up front: scanCacheDir swallows per-file errors, so a malformed
|
|
99
|
-
// --older-than/--inactive would otherwise be silently ignored and report "0 entries".
|
|
100
|
-
for (const msg of [
|
|
101
|
-
durationFlagError('--older-than', this.flags['older-than']),
|
|
102
|
-
durationFlagError('--inactive', this.flags.inactive),
|
|
103
|
-
]) {
|
|
104
|
-
if (msg)
|
|
105
|
-
this.error(msg, { exit: 2, code: 'INVALID_DURATION' });
|
|
106
|
-
}
|
|
80
|
+
const err = pruneFlagError({
|
|
81
|
+
olderThan: this.flags['older-than'],
|
|
82
|
+
inactive: this.flags.inactive,
|
|
83
|
+
artifactType: this.flags['artifact-type'],
|
|
84
|
+
url: this.flags.url,
|
|
85
|
+
topic: this.flags.topic,
|
|
86
|
+
tags: this.flags.tags,
|
|
87
|
+
dryRun: this.flags['dry-run'],
|
|
88
|
+
yes: this.flags.yes,
|
|
89
|
+
readOnly: this.readOnly,
|
|
90
|
+
bin: this.config.bin,
|
|
91
|
+
});
|
|
92
|
+
if (err)
|
|
93
|
+
this.error(err.message, { exit: 2, code: err.code, suggestions: err.suggestions });
|
|
107
94
|
}
|
|
108
95
|
shouldPrune(meta, currentTime) {
|
|
109
|
-
const fetched = meta.fetched_at ? new Date(meta.fetched_at).getTime() : 0;
|
|
110
|
-
const validated = meta.validated_at ? new Date(meta.validated_at).getTime() : 0;
|
|
111
|
-
const baseTime = Math.max(fetched, validated);
|
|
112
|
-
const ageMs = currentTime.getTime() - baseTime;
|
|
113
96
|
if (this.flags['artifact-type'] && meta.artifact_type !== this.flags['artifact-type']) {
|
|
114
97
|
return false;
|
|
115
98
|
}
|
|
116
99
|
if (this.flags.url && !artifactMatchesUrlFilter(meta, this.flags.url)) {
|
|
117
100
|
return false;
|
|
118
101
|
}
|
|
119
|
-
if (this.flags
|
|
120
|
-
|
|
121
|
-
if (ageMs < olderThanMs) {
|
|
122
|
-
return false;
|
|
123
|
-
}
|
|
102
|
+
if (!matchesTopicFilter(meta, this.flags.topic)) {
|
|
103
|
+
return false;
|
|
124
104
|
}
|
|
125
|
-
if (this.flags.
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
105
|
+
if (!matchesTagsFilter(meta, this.flags.tags)) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
const now = currentTime.getTime();
|
|
109
|
+
// Content age (fetched_at, else validated_at) vs idle time (validated_at, else fetched_at) are
|
|
110
|
+
// deliberately distinct so a recently revalidated but originally-old page can match --older-than
|
|
111
|
+
// without also being treated as inactive.
|
|
112
|
+
if (this.flags['older-than'] &&
|
|
113
|
+
ageMs(meta, now, 'content') < parseTtlToMs(this.flags['older-than'])) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
if (this.flags.inactive && ageMs(meta, now, 'idle') < parseTtlToMs(this.flags.inactive)) {
|
|
117
|
+
return false;
|
|
130
118
|
}
|
|
131
119
|
return true;
|
|
132
120
|
}
|
|
@@ -152,7 +140,12 @@ export default class ResearchPrune extends BaseCommand {
|
|
|
152
140
|
prunedCount++;
|
|
153
141
|
}
|
|
154
142
|
catch (err) {
|
|
155
|
-
|
|
143
|
+
// A failed unlink still flips the command to exit 1 (see the caller's comment), so it must
|
|
144
|
+
// render as an Error, not a Warning — this.error's exit:false form prints the same "Error: …"
|
|
145
|
+
// line and returns instead of throwing, so the remaining candidates still get processed.
|
|
146
|
+
this.error(`Failed to delete cache file ${f.path}: ${err.message}`, {
|
|
147
|
+
exit: false,
|
|
148
|
+
});
|
|
156
149
|
}
|
|
157
150
|
}
|
|
158
151
|
return prunedCount;
|
|
@@ -181,8 +174,12 @@ export default class ResearchPrune extends BaseCommand {
|
|
|
181
174
|
const noun = pluralize(count, 'entry', 'entries');
|
|
182
175
|
this.log(colors.yellow(`[Dry Run] Found ${count} research cache ${noun} that would be pruned:\n`));
|
|
183
176
|
filesToPrune.forEach((f) => {
|
|
184
|
-
|
|
177
|
+
const topicStr = f.topic
|
|
178
|
+
? colors.cyan(sanitizeForTerminal(f.topic))
|
|
179
|
+
: colors.gray(NO_TOPIC_LABEL);
|
|
180
|
+
this.log(`- [${topicStr}] Key: ${colors.bold(f.cacheKey)} (${colors.gray(f.url || 'Imported note')})`);
|
|
185
181
|
});
|
|
182
|
+
this.tip('re-run with --yes to prune these entries.');
|
|
186
183
|
}
|
|
187
184
|
}
|
|
188
185
|
}
|
|
@@ -192,9 +189,16 @@ export default class ResearchPrune extends BaseCommand {
|
|
|
192
189
|
const noun = pluralize(count, 'entry', 'entries');
|
|
193
190
|
this.log(colors.green(`Successfully pruned ${prunedCount} of ${count} research cache ${noun}.`));
|
|
194
191
|
}
|
|
192
|
+
// Partial unlink failure is a runtime outcome agents must see: exit 1 with the counts still
|
|
193
|
+
// in `data` so callers can tell candidates from successes without parsing stderr warnings.
|
|
194
|
+
if (count > 0 && prunedCount < count) {
|
|
195
|
+
process.exitCode = 1;
|
|
196
|
+
}
|
|
195
197
|
}
|
|
196
198
|
return {
|
|
197
199
|
dryRun,
|
|
200
|
+
status: dryRun ? 'would_prune' : 'pruned',
|
|
201
|
+
wouldPruneCount: dryRun ? count : 0,
|
|
198
202
|
prunedCount,
|
|
199
203
|
candidateCount: count,
|
|
200
204
|
files: filesToPrune.map((f) => ({
|
|
@@ -204,3 +208,10 @@ export default class ResearchPrune extends BaseCommand {
|
|
|
204
208
|
};
|
|
205
209
|
}
|
|
206
210
|
}
|
|
211
|
+
/** Age in ms for prune filters. `content` prefers fetched_at; `idle` prefers validated_at. */
|
|
212
|
+
function ageMs(meta, now, kind) {
|
|
213
|
+
const fetched = meta.fetched_at ? new Date(meta.fetched_at).getTime() : 0;
|
|
214
|
+
const validated = meta.validated_at ? new Date(meta.validated_at).getTime() : 0;
|
|
215
|
+
const base = kind === 'content' ? fetched || validated : validated || fetched;
|
|
216
|
+
return now - base;
|
|
217
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BaseCommand } from '../base-command.js';
|
|
2
|
+
/** Report cache hit/miss and freshness without writing. */
|
|
2
3
|
export default class ResearchStatus extends BaseCommand<typeof ResearchStatus> {
|
|
3
4
|
static id: string;
|
|
4
5
|
static summary: string;
|
|
@@ -12,13 +13,13 @@ export default class ResearchStatus extends BaseCommand<typeof ResearchStatus> {
|
|
|
12
13
|
url: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
13
14
|
};
|
|
14
15
|
static flags: {
|
|
15
|
-
tier: import("@oclif/core/interfaces").OptionFlag<"stable" | "standard" | "volatile", import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
+
tier: import("@oclif/core/interfaces").OptionFlag<"stable" | "standard" | "volatile" | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
17
|
ttl: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
18
|
'max-age': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
18
19
|
};
|
|
19
20
|
static stdoutIsPrimaryData: boolean;
|
|
20
|
-
/** Enrich cache-miss envelopes with the same CACHE_MISS code and suggestions as inspect. */
|
|
21
21
|
protected toSuccessJson(data: unknown): Record<string, unknown>;
|
|
22
22
|
run(): Promise<unknown>;
|
|
23
23
|
private checkSingleStatus;
|
|
24
|
+
private logStatusTable;
|
|
24
25
|
}
|