@taurgis/bonsai 3.1.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 +21 -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 +37 -10
- package/dist/base-command.js +113 -46
- 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 +58 -401
- package/dist/commands/import.d.ts +4 -9
- package/dist/commands/import.js +56 -280
- 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 +56 -65
- 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 +22 -4
- package/dist/lib/config/index.js +59 -4
- package/dist/lib/config/resolve.d.ts +21 -0
- package/dist/lib/config/resolve.js +55 -6
- 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 +11 -1
- package/dist/lib/research/secure-write.js +38 -7
- 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 +192 -87
- 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,120 +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
|
-
if (this.flags['dry-run'] && this.flags.yes) {
|
|
72
|
-
this.error('--dry-run and --yes are mutually exclusive: --dry-run previews without deleting, --yes confirms deletion. Choose one.', { exit: 2, code: 'CONFLICTING_FLAGS' });
|
|
73
|
-
}
|
|
74
|
-
if (!this.flags['dry-run'] && !this.flags.yes) {
|
|
75
|
-
const olderThanPart = this.flags['older-than']
|
|
76
|
-
? ` --older-than ${this.flags['older-than']}`
|
|
77
|
-
: '';
|
|
78
|
-
const urlPart = this.flags.url ? ` --url "${this.flags.url}"` : '';
|
|
79
|
-
this.error('Safety check: use --yes to confirm pruning, or --dry-run to preview files that would be deleted.', {
|
|
80
|
-
exit: 2,
|
|
81
|
-
code: 'SAFETY_CHECK_REQUIRED',
|
|
82
|
-
suggestions: [
|
|
83
|
-
`Preview first: ${this.config.bin} prune --dry-run${olderThanPart}${urlPart}`,
|
|
84
|
-
],
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
// Validate durations up front: scanCacheDir swallows per-file errors, so a malformed
|
|
88
|
-
// --older-than/--inactive would otherwise be silently ignored and report "0 entries".
|
|
89
|
-
for (const msg of [
|
|
90
|
-
durationFlagError('--older-than', this.flags['older-than']),
|
|
91
|
-
durationFlagError('--inactive', this.flags.inactive),
|
|
92
|
-
]) {
|
|
93
|
-
if (msg)
|
|
94
|
-
this.error(msg, { exit: 2, code: 'INVALID_DURATION' });
|
|
95
|
-
}
|
|
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 });
|
|
96
78
|
}
|
|
97
79
|
shouldPrune(meta, currentTime) {
|
|
98
|
-
const fetched = meta.fetched_at ? new Date(meta.fetched_at).getTime() : 0;
|
|
99
|
-
const validated = meta.validated_at ? new Date(meta.validated_at).getTime() : 0;
|
|
100
|
-
const baseTime = Math.max(fetched, validated);
|
|
101
|
-
const ageMs = currentTime.getTime() - baseTime;
|
|
102
80
|
if (this.flags['artifact-type'] && meta.artifact_type !== this.flags['artifact-type']) {
|
|
103
81
|
return false;
|
|
104
82
|
}
|
|
105
83
|
if (this.flags.url && !artifactMatchesUrlFilter(meta, this.flags.url)) {
|
|
106
84
|
return false;
|
|
107
85
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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;
|
|
113
93
|
}
|
|
114
|
-
if (this.flags.inactive) {
|
|
115
|
-
|
|
116
|
-
if (ageMs < inactiveMs) {
|
|
117
|
-
return false;
|
|
118
|
-
}
|
|
94
|
+
if (this.flags.inactive && ageMs(meta, now, 'idle') < parseTtlToMs(this.flags.inactive)) {
|
|
95
|
+
return false;
|
|
119
96
|
}
|
|
120
97
|
return true;
|
|
121
98
|
}
|
|
@@ -155,7 +132,7 @@ export default class ResearchPrune extends BaseCommand {
|
|
|
155
132
|
});
|
|
156
133
|
const currentTime = new Date();
|
|
157
134
|
const filesToPrune = this.findPruneCandidates(roots.readRoots, currentTime);
|
|
158
|
-
const dryRun = this.flags['dry-run'];
|
|
135
|
+
const dryRun = this.effectiveDryRun(this.flags['dry-run']);
|
|
159
136
|
const count = filesToPrune.length;
|
|
160
137
|
// Track deletions actually performed so the JSON envelope and human output agree even when an
|
|
161
138
|
// unlink fails (e.g. a permission error). Reporting candidate count as `prunedCount` would
|
|
@@ -181,9 +158,16 @@ export default class ResearchPrune extends BaseCommand {
|
|
|
181
158
|
const noun = pluralize(count, 'entry', 'entries');
|
|
182
159
|
this.log(colors.green(`Successfully pruned ${prunedCount} of ${count} research cache ${noun}.`));
|
|
183
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
|
+
}
|
|
184
166
|
}
|
|
185
167
|
return {
|
|
186
168
|
dryRun,
|
|
169
|
+
status: dryRun ? 'would_prune' : 'pruned',
|
|
170
|
+
wouldPruneCount: dryRun ? count : 0,
|
|
187
171
|
prunedCount,
|
|
188
172
|
candidateCount: count,
|
|
189
173
|
files: filesToPrune.map((f) => ({
|
|
@@ -193,3 +177,10 @@ export default class ResearchPrune extends BaseCommand {
|
|
|
193
177
|
};
|
|
194
178
|
}
|
|
195
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
|
}
|