@taurgis/bonsai 3.2.0 → 3.2.2
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 -4
- package/agents/agents/salesforce-docs-researcher.agent.md +14 -1
- package/agents/instructions/salesforce-research.instructions.md +2 -1
- package/bin/cli.mjs +9 -17
- package/dist/base-command.d.ts +22 -12
- package/dist/base-command.js +86 -49
- package/dist/commands/config/base.js +11 -2
- package/dist/commands/config/get.js +8 -12
- package/dist/commands/config/index.js +2 -2
- package/dist/commands/config/list.js +10 -16
- package/dist/commands/config/set.d.ts +2 -1
- package/dist/commands/config/set.js +34 -27
- package/dist/commands/config/unset.d.ts +2 -1
- package/dist/commands/config/unset.js +25 -24
- package/dist/commands/fetch.d.ts +7 -15
- package/dist/commands/fetch.js +57 -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 +103 -52
- package/dist/commands/list.d.ts +13 -1
- package/dist/commands/list.js +79 -32
- package/dist/commands/prune.d.ts +4 -1
- package/dist/commands/prune.js +55 -75
- package/dist/commands/status.d.ts +3 -2
- package/dist/commands/status.js +52 -79
- package/dist/hooks/command-not-found/suggest.d.ts +1 -0
- package/dist/hooks/command-not-found/suggest.js +66 -39
- package/dist/lib/argv.d.ts +24 -3
- package/dist/lib/argv.js +124 -53
- 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 +5 -0
- package/dist/lib/cli-flag-manifest.js +8 -0
- package/dist/lib/cli-presentation.d.ts +39 -0
- package/dist/lib/cli-presentation.js +50 -0
- package/dist/lib/cli-result-types.d.ts +88 -0
- package/dist/lib/cli-result-types.js +2 -0
- package/dist/lib/config/index.d.ts +21 -3
- package/dist/lib/config/index.js +58 -3
- 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 +147 -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 +28 -0
- package/dist/lib/prune-flags.js +74 -0
- package/dist/lib/research/artifact-index.d.ts +11 -2
- package/dist/lib/research/artifact-index.js +7 -4
- package/dist/lib/research/browser.d.ts +41 -2
- package/dist/lib/research/browser.js +139 -50
- 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 +33 -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 +310 -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 +58 -30
- 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 +381 -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/persist-artifact.d.ts +23 -0
- package/dist/lib/research/persist-artifact.js +27 -0
- package/dist/lib/research/prompt-injection.d.ts +8 -0
- package/dist/lib/research/prompt-injection.js +42 -13
- package/dist/lib/research/proxy.d.ts +14 -8
- package/dist/lib/research/proxy.js +14 -10
- 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 +5 -1
- package/dist/lib/research/storage.js +8 -3
- 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 +7 -2
- package/dist/lib/text.js +13 -2
- 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 +104 -98
- package/package.json +19 -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,15 @@
|
|
|
1
1
|
import { BaseCommand } from '../base-command.js';
|
|
2
|
+
import type { ListRow } 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
|
+
* When `--json` results were capped by `--limit`, attached to the success envelope so agents can
|
|
10
|
+
* detect truncation without scraping prose (#91 / AUDIT_71_FINAL #73 deferred).
|
|
11
|
+
*/
|
|
12
|
+
private jsonTruncation;
|
|
6
13
|
static examples: {
|
|
7
14
|
description: string;
|
|
8
15
|
command: string;
|
|
@@ -21,6 +28,11 @@ export default class ResearchList extends BaseCommand<typeof ResearchList> {
|
|
|
21
28
|
private matchesTags;
|
|
22
29
|
private matchesFilters;
|
|
23
30
|
private scanCacheDirForList;
|
|
31
|
+
private hasActiveFilters;
|
|
24
32
|
private logListResults;
|
|
25
|
-
|
|
33
|
+
/** Empty-cache / no-match tip (human mode only; --json returns `data: []` with no messaging). */
|
|
34
|
+
private emitEmptyListGuidance;
|
|
35
|
+
run(): Promise<ListRow[]>;
|
|
36
|
+
/** Attach `truncation` when the returned `data` array was capped by `--limit`. */
|
|
37
|
+
protected toSuccessJson(data: unknown): Record<string, unknown>;
|
|
26
38
|
}
|
package/dist/commands/list.js
CHANGED
|
@@ -3,64 +3,76 @@ 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, resultListHeading } 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
10
|
import { colors } from '../lib/color.js';
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
import { CLI_FLAG_DESCRIPTIONS } from '../lib/cli-presentation.js';
|
|
12
|
+
// Listings are ordered newest-first, so the truncation word is "first"; --limit caps at this value.
|
|
13
|
+
const LIST_DEFAULT_MAX_LIMIT = 100;
|
|
14
|
+
const LIST_LABELS = {
|
|
15
|
+
noun: 'cached research',
|
|
16
|
+
order: 'first',
|
|
17
|
+
maxLimit: LIST_DEFAULT_MAX_LIMIT,
|
|
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
|
+
/** List cached research artifacts by metadata filters. */
|
|
16
23
|
export default class ResearchList extends BaseCommand {
|
|
17
24
|
static id = 'list';
|
|
18
|
-
static summary = 'List cached research artifacts by metadata
|
|
19
|
-
static description = '
|
|
25
|
+
static summary = 'List cached research artifacts by metadata';
|
|
26
|
+
static description = 'List page-level cached artifacts with source URLs, topic, tags, freshness, capture method, and token estimates.';
|
|
27
|
+
/**
|
|
28
|
+
* When `--json` results were capped by `--limit`, attached to the success envelope so agents can
|
|
29
|
+
* detect truncation without scraping prose (#91 / AUDIT_71_FINAL #73 deferred).
|
|
30
|
+
*/
|
|
31
|
+
jsonTruncation = null;
|
|
20
32
|
static examples = [
|
|
21
33
|
{
|
|
22
|
-
description: 'list
|
|
34
|
+
description: 'list cached entries',
|
|
23
35
|
command: '<%= config.bin %> list',
|
|
24
36
|
},
|
|
25
37
|
{
|
|
26
|
-
description: 'list
|
|
38
|
+
description: 'list entries for a topic as JSON',
|
|
27
39
|
command: '<%= config.bin %> list --topic "React Suspense" --json',
|
|
28
40
|
},
|
|
29
41
|
{
|
|
30
|
-
description: 'list
|
|
42
|
+
description: 'list fresh entries matching tags',
|
|
31
43
|
command: '<%= config.bin %> list --freshness fresh --tags node --tags url',
|
|
32
44
|
},
|
|
33
45
|
{
|
|
34
|
-
description: 'list entries matching a
|
|
46
|
+
description: 'list entries matching a URL glob',
|
|
35
47
|
command: '<%= config.bin %> list --url "https://react.dev/*"',
|
|
36
48
|
},
|
|
37
49
|
];
|
|
38
50
|
static flags = {
|
|
39
51
|
topic: Flags.string({
|
|
40
52
|
char: 't',
|
|
41
|
-
description:
|
|
53
|
+
description: CLI_FLAG_DESCRIPTIONS.filterTopic,
|
|
42
54
|
}),
|
|
43
55
|
tags: Flags.string({
|
|
44
56
|
char: 'g',
|
|
45
|
-
description:
|
|
57
|
+
description: CLI_FLAG_DESCRIPTIONS.filterTags,
|
|
46
58
|
multiple: true,
|
|
47
59
|
}),
|
|
48
60
|
url: Flags.string({
|
|
49
|
-
description:
|
|
61
|
+
description: CLI_FLAG_DESCRIPTIONS.sourceUrlGlob,
|
|
50
62
|
}),
|
|
51
63
|
freshness: Flags.option({
|
|
52
|
-
description: '
|
|
64
|
+
description: 'freshness state',
|
|
53
65
|
options: ['fresh', 'stale_grace', 'stale_expired'],
|
|
54
66
|
})(),
|
|
55
67
|
'artifact-type': Flags.option({
|
|
56
|
-
description:
|
|
68
|
+
description: CLI_FLAG_DESCRIPTIONS.listArtifactType,
|
|
57
69
|
options: LISTABLE_ARTIFACT_TYPES,
|
|
58
70
|
})(),
|
|
59
71
|
'capture-method': Flags.option({
|
|
60
|
-
description: '
|
|
72
|
+
description: 'capture method',
|
|
61
73
|
options: CAPTURE_METHODS,
|
|
62
74
|
})(),
|
|
63
|
-
limit: limitFlag(100, 50, '
|
|
75
|
+
limit: limitFlag(100, 50, 'result count (default 50, max 100)'),
|
|
64
76
|
};
|
|
65
77
|
static stdoutIsPrimaryData = true;
|
|
66
78
|
matchesTopic(meta, topic) {
|
|
@@ -71,7 +83,7 @@ export default class ResearchList extends BaseCommand {
|
|
|
71
83
|
matchesTags(meta, tags) {
|
|
72
84
|
if (!tags || tags.length === 0)
|
|
73
85
|
return true;
|
|
74
|
-
const metaTagsLower =
|
|
86
|
+
const metaTagsLower = meta.tags.map((t) => t.toLowerCase());
|
|
75
87
|
return tags.every((t) => metaTagsLower.includes(t.toLowerCase()));
|
|
76
88
|
}
|
|
77
89
|
matchesFilters(meta, freshness) {
|
|
@@ -96,6 +108,7 @@ export default class ResearchList extends BaseCommand {
|
|
|
96
108
|
return true;
|
|
97
109
|
}
|
|
98
110
|
scanCacheDirForList(readRoots, currentTime) {
|
|
111
|
+
// Honor effective read-only: listing must not persist the derived search-index sidecar.
|
|
99
112
|
return scanCacheDirs(readRoots, (artifact, filePath) => {
|
|
100
113
|
if (artifact.metadata.status !== 'active')
|
|
101
114
|
return null;
|
|
@@ -124,16 +137,23 @@ export default class ResearchList extends BaseCommand {
|
|
|
124
137
|
fetchedAt: artifact.metadata.fetched_at,
|
|
125
138
|
validatedAt: artifact.metadata.validated_at,
|
|
126
139
|
};
|
|
127
|
-
});
|
|
140
|
+
}, { persistIndex: !this.readOnly });
|
|
141
|
+
}
|
|
142
|
+
hasActiveFilters() {
|
|
143
|
+
return Boolean(this.flags.topic ||
|
|
144
|
+
(this.flags.tags && this.flags.tags.length > 0) ||
|
|
145
|
+
this.flags.freshness ||
|
|
146
|
+
this.flags['artifact-type'] ||
|
|
147
|
+
this.flags['capture-method'] ||
|
|
148
|
+
this.flags.url);
|
|
128
149
|
}
|
|
129
150
|
logListResults(finalResults, totalMatched) {
|
|
130
|
-
if (this.jsonEnabled())
|
|
131
|
-
return;
|
|
132
151
|
if (finalResults.length === 0) {
|
|
133
|
-
this.
|
|
134
|
-
this.log(`\nTip: populate the cache first: ${colors.cyan(this.config.bin + ' <url>')}`);
|
|
152
|
+
this.emitEmptyListGuidance();
|
|
135
153
|
return;
|
|
136
154
|
}
|
|
155
|
+
if (this.jsonEnabled())
|
|
156
|
+
return;
|
|
137
157
|
this.log(`${resultListHeading(totalMatched, finalResults.length, LIST_LABELS)}\n`);
|
|
138
158
|
finalResults.forEach((res, index) => {
|
|
139
159
|
const topicStr = res.topic ? colors.cyan(res.topic) : colors.gray(NO_TOPIC_LABEL);
|
|
@@ -144,14 +164,32 @@ export default class ResearchList extends BaseCommand {
|
|
|
144
164
|
stale_grace: colors.yellow,
|
|
145
165
|
stale_expired: colors.red,
|
|
146
166
|
};
|
|
147
|
-
const freshnessColor = freshnessColorMap[res.freshness]
|
|
167
|
+
const freshnessColor = freshnessColorMap[res.freshness];
|
|
148
168
|
const freshnessStr = freshnessColor(res.freshness);
|
|
149
169
|
this.log(` Type: ${colors.bold(res.artifactType)} | Freshness: ${freshnessStr}`);
|
|
150
170
|
this.log(` Tokens: compressed=${colors.bold(String(res.tokenEstimate?.compressed || 0))}, detailed=${colors.bold(String(res.tokenEstimate?.detailed || 0))}`);
|
|
151
171
|
this.log(` Source URLs: ${colors.gray(res.sourceUrls.join(', '))}\n`);
|
|
152
172
|
});
|
|
153
173
|
}
|
|
174
|
+
/** Empty-cache / no-match tip (human mode only; --json returns `data: []` with no messaging). */
|
|
175
|
+
emitEmptyListGuidance() {
|
|
176
|
+
if (this.jsonEnabled())
|
|
177
|
+
return;
|
|
178
|
+
const filtered = this.hasActiveFilters();
|
|
179
|
+
const headline = filtered
|
|
180
|
+
? 'No cached research entries match the given filters.'
|
|
181
|
+
: 'No cached research entries found.';
|
|
182
|
+
const tipCmd = filtered ? `${this.config.bin} list` : `${this.config.bin} <url>`;
|
|
183
|
+
const tipLead = filtered
|
|
184
|
+
? 'try relaxing filters, or list everything: '
|
|
185
|
+
: 'populate the cache first: ';
|
|
186
|
+
this.log(headline);
|
|
187
|
+
this.log(`\nTip: ${tipLead}${colors.cyan(tipCmd)}`);
|
|
188
|
+
}
|
|
154
189
|
async run() {
|
|
190
|
+
const urlErr = emptyUrlFilterError(this.flags.url);
|
|
191
|
+
if (urlErr)
|
|
192
|
+
this.error(urlErr, { exit: 2, code: 'INVALID_FLAG_VALUE' });
|
|
155
193
|
const roots = loadStoreRoots({
|
|
156
194
|
configDir: this.config.configDir,
|
|
157
195
|
cwd: process.cwd(),
|
|
@@ -159,20 +197,29 @@ export default class ResearchList extends BaseCommand {
|
|
|
159
197
|
});
|
|
160
198
|
const currentTime = new Date();
|
|
161
199
|
const results = this.scanCacheDirForList(roots.readRoots, currentTime);
|
|
162
|
-
// Sort by validated_at or fetched_at descending (most recent first)
|
|
163
200
|
results.sort((a, b) => {
|
|
164
201
|
const timeA = new Date(a.validatedAt || a.fetchedAt || 0).getTime();
|
|
165
202
|
const timeB = new Date(b.validatedAt || b.fetchedAt || 0).getTime();
|
|
166
203
|
return timeB - timeA;
|
|
167
204
|
});
|
|
168
205
|
const finalResults = results.slice(0, this.flags.limit);
|
|
169
|
-
//
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
206
|
+
// Human heading carries truncation; --json gets an envelope `truncation` object when capped.
|
|
207
|
+
this.jsonTruncation =
|
|
208
|
+
results.length > finalResults.length
|
|
209
|
+
? {
|
|
210
|
+
totalMatched: results.length,
|
|
211
|
+
shown: finalResults.length,
|
|
212
|
+
limit: this.flags.limit,
|
|
213
|
+
}
|
|
214
|
+
: null;
|
|
175
215
|
this.logListResults(finalResults, results.length);
|
|
176
216
|
return finalResults;
|
|
177
217
|
}
|
|
218
|
+
/** Attach `truncation` when the returned `data` array was capped by `--limit`. */
|
|
219
|
+
toSuccessJson(data) {
|
|
220
|
+
const envelope = this.baseSuccessJson(data);
|
|
221
|
+
if (!this.jsonTruncation)
|
|
222
|
+
return envelope;
|
|
223
|
+
return { ...envelope, truncation: this.jsonTruncation };
|
|
224
|
+
}
|
|
178
225
|
}
|
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;
|
|
@@ -16,9 +18,10 @@ export default class ResearchPrune extends BaseCommand<typeof ResearchPrune> {
|
|
|
16
18
|
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
17
19
|
};
|
|
18
20
|
static stdoutIsPrimaryData: boolean;
|
|
21
|
+
protected toSuccessJson(data: unknown): Record<string, unknown>;
|
|
19
22
|
private validatePruneFlags;
|
|
20
23
|
private shouldPrune;
|
|
21
24
|
private findPruneCandidates;
|
|
22
25
|
private deletePruneCandidates;
|
|
23
|
-
run(): Promise<
|
|
26
|
+
run(): Promise<PruneWriteResult>;
|
|
24
27
|
}
|
package/dist/commands/prune.js
CHANGED
|
@@ -2,131 +2,97 @@ 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
10
|
import { NO_TOPIC_LABEL, pluralize } from '../lib/text.js';
|
|
10
11
|
import { artifactMatchesUrlFilter } from '../lib/research/url.js';
|
|
12
|
+
import { pruneFlagError } from '../lib/prune-flags.js';
|
|
11
13
|
import { colors } from '../lib/color.js';
|
|
14
|
+
import { CLI_FLAG_DESCRIPTIONS } from '../lib/cli-presentation.js';
|
|
15
|
+
/** Prune cached artifacts by age, idle time, or filters. */
|
|
12
16
|
export default class ResearchPrune extends BaseCommand {
|
|
13
17
|
static id = 'prune';
|
|
14
|
-
static summary = '
|
|
15
|
-
static description = '
|
|
18
|
+
static summary = 'Prune cached research artifacts';
|
|
19
|
+
static description = 'Delete cached artifacts by content age, idle time, URL glob, or artifact type.';
|
|
16
20
|
static examples = [
|
|
17
21
|
{
|
|
18
|
-
description: '
|
|
19
|
-
command: '<%= config.bin %> prune --older-than
|
|
22
|
+
description: 'preview pruning entries older than 30 days',
|
|
23
|
+
command: '<%= config.bin %> prune --older-than 30d --dry-run',
|
|
20
24
|
},
|
|
21
25
|
{
|
|
22
|
-
description: '
|
|
26
|
+
description: 'prune source artifacts older than 30 days',
|
|
23
27
|
command: '<%= config.bin %> prune --older-than 30d --artifact-type source --yes',
|
|
24
28
|
},
|
|
25
29
|
{
|
|
26
|
-
description: 'prune entries matching a URL glob
|
|
30
|
+
description: 'prune entries matching a URL glob',
|
|
27
31
|
command: '<%= config.bin %> prune --url "https://react.dev/*" --yes',
|
|
28
32
|
},
|
|
29
33
|
];
|
|
30
34
|
static flags = {
|
|
31
35
|
'older-than': Flags.string({
|
|
32
|
-
description: '
|
|
36
|
+
description: 'content age threshold (fetched_at, else validated_at), e.g. "30d"',
|
|
33
37
|
}),
|
|
34
38
|
inactive: Flags.string({
|
|
35
|
-
description: '
|
|
39
|
+
description: 'idle time threshold (validated_at, else fetched_at), e.g. "14d"',
|
|
36
40
|
}),
|
|
37
41
|
url: Flags.string({
|
|
38
|
-
description:
|
|
42
|
+
description: CLI_FLAG_DESCRIPTIONS.sourceUrlGlob,
|
|
39
43
|
}),
|
|
40
44
|
'artifact-type': Flags.option({
|
|
41
45
|
// Prune operates on every cached file, so it can target any artifact type — including the
|
|
42
46
|
// `section`/`index` children a page generates (e.g. to clear orphans left after a source is
|
|
43
47
|
// pruned with `--artifact-type source`).
|
|
44
|
-
description:
|
|
48
|
+
description: CLI_FLAG_DESCRIPTIONS.pruneArtifactType,
|
|
45
49
|
options: ARTIFACT_TYPES,
|
|
46
50
|
})(),
|
|
47
51
|
'dry-run': Flags.boolean({
|
|
48
|
-
description: '
|
|
52
|
+
description: 'preview files without deleting',
|
|
49
53
|
default: false,
|
|
50
54
|
}),
|
|
51
55
|
yes: Flags.boolean({
|
|
52
56
|
char: 'y',
|
|
53
|
-
description: 'confirm deletion
|
|
57
|
+
description: 'confirm deletion (required unless --dry-run)',
|
|
54
58
|
default: false,
|
|
55
59
|
}),
|
|
56
60
|
};
|
|
57
61
|
static stdoutIsPrimaryData = true;
|
|
62
|
+
toSuccessJson(data) {
|
|
63
|
+
return enrichPrunePartialEnvelope(this.baseSuccessJson(data), data);
|
|
64
|
+
}
|
|
58
65
|
validatePruneFlags() {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
this.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
// Checked before the generic --dry-run/--yes conflict below so a combination like
|
|
72
|
-
// `--dry-run --yes --read-only` reports the more specific READ_ONLY_MODE code, not
|
|
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
|
-
}
|
|
66
|
+
const err = pruneFlagError({
|
|
67
|
+
olderThan: this.flags['older-than'],
|
|
68
|
+
inactive: this.flags.inactive,
|
|
69
|
+
artifactType: this.flags['artifact-type'],
|
|
70
|
+
url: this.flags.url,
|
|
71
|
+
dryRun: this.flags['dry-run'],
|
|
72
|
+
yes: this.flags.yes,
|
|
73
|
+
readOnly: this.readOnly,
|
|
74
|
+
bin: this.config.bin,
|
|
75
|
+
});
|
|
76
|
+
if (err)
|
|
77
|
+
this.error(err.message, { exit: 2, code: err.code, suggestions: err.suggestions });
|
|
107
78
|
}
|
|
108
79
|
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
80
|
if (this.flags['artifact-type'] && meta.artifact_type !== this.flags['artifact-type']) {
|
|
114
81
|
return false;
|
|
115
82
|
}
|
|
116
83
|
if (this.flags.url && !artifactMatchesUrlFilter(meta, this.flags.url)) {
|
|
117
84
|
return false;
|
|
118
85
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
86
|
+
const now = currentTime.getTime();
|
|
87
|
+
// Content age (fetched_at, else validated_at) vs idle time (validated_at, else fetched_at) are
|
|
88
|
+
// deliberately distinct so a recently revalidated but originally-old page can match --older-than
|
|
89
|
+
// without also being treated as inactive.
|
|
90
|
+
if (this.flags['older-than'] &&
|
|
91
|
+
ageMs(meta, now, 'content') < parseTtlToMs(this.flags['older-than'])) {
|
|
92
|
+
return false;
|
|
124
93
|
}
|
|
125
|
-
if (this.flags.inactive) {
|
|
126
|
-
|
|
127
|
-
if (ageMs < inactiveMs) {
|
|
128
|
-
return false;
|
|
129
|
-
}
|
|
94
|
+
if (this.flags.inactive && ageMs(meta, now, 'idle') < parseTtlToMs(this.flags.inactive)) {
|
|
95
|
+
return false;
|
|
130
96
|
}
|
|
131
97
|
return true;
|
|
132
98
|
}
|
|
@@ -192,9 +158,16 @@ export default class ResearchPrune extends BaseCommand {
|
|
|
192
158
|
const noun = pluralize(count, 'entry', 'entries');
|
|
193
159
|
this.log(colors.green(`Successfully pruned ${prunedCount} of ${count} research cache ${noun}.`));
|
|
194
160
|
}
|
|
161
|
+
// Partial unlink failure is a runtime outcome agents must see: exit 1 with the counts still
|
|
162
|
+
// in `data` so callers can tell candidates from successes without parsing stderr warnings.
|
|
163
|
+
if (count > 0 && prunedCount < count) {
|
|
164
|
+
process.exitCode = 1;
|
|
165
|
+
}
|
|
195
166
|
}
|
|
196
167
|
return {
|
|
197
168
|
dryRun,
|
|
169
|
+
status: dryRun ? 'would_prune' : 'pruned',
|
|
170
|
+
wouldPruneCount: dryRun ? count : 0,
|
|
198
171
|
prunedCount,
|
|
199
172
|
candidateCount: count,
|
|
200
173
|
files: filesToPrune.map((f) => ({
|
|
@@ -204,3 +177,10 @@ export default class ResearchPrune extends BaseCommand {
|
|
|
204
177
|
};
|
|
205
178
|
}
|
|
206
179
|
}
|
|
180
|
+
/** Age in ms for prune filters. `content` prefers fetched_at; `idle` prefers validated_at. */
|
|
181
|
+
function ageMs(meta, now, kind) {
|
|
182
|
+
const fetched = meta.fetched_at ? new Date(meta.fetched_at).getTime() : 0;
|
|
183
|
+
const validated = meta.validated_at ? new Date(meta.validated_at).getTime() : 0;
|
|
184
|
+
const base = kind === 'content' ? fetched || validated : validated || fetched;
|
|
185
|
+
return now - base;
|
|
186
|
+
}
|
|
@@ -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
|
}
|