@taurgis/bonsai 3.2.2 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -2
- package/agents/agents/official-docs-researcher.agent.md +102 -0
- package/agents/agents/salesforce-docs-researcher.agent.md +17 -15
- package/agents/hooks/README.md +18 -2
- package/agents/hooks/claude-code/hooks/bonsai-web-fetch.sh +1 -1
- package/agents/hooks/codex/hooks/bonsai-user-prompt-context.sh +1 -1
- package/agents/hooks/codex/hooks/bonsai-web-fetch.sh +1 -1
- package/agents/hooks/cursor/hooks/bonsai-web-fetch.sh +1 -1
- package/agents/hooks/github-copilot/bonsai-web-fetch.sh +1 -1
- package/agents/instructions/salesforce-research.instructions.md +5 -4
- package/agents/instructions/web-research.instructions.md +45 -0
- package/agents/skills/web-research/SKILL.md +139 -0
- package/bin/cli.mjs +7 -4
- package/dist/base-command.d.ts +42 -0
- package/dist/base-command.js +106 -6
- package/dist/commands/config/base.d.ts +2 -7
- package/dist/commands/config/base.js +4 -17
- package/dist/commands/config/get.js +2 -2
- package/dist/commands/config/list.js +2 -2
- package/dist/commands/config/set.js +2 -2
- package/dist/commands/config/unset.js +2 -2
- package/dist/commands/context.d.ts +20 -0
- package/dist/commands/context.js +65 -0
- package/dist/commands/fetch.js +4 -0
- package/dist/commands/inspect.js +11 -2
- package/dist/commands/list.d.ts +23 -11
- package/dist/commands/list.js +110 -121
- package/dist/commands/prune.d.ts +2 -0
- package/dist/commands/prune.js +35 -4
- package/dist/commands/search.d.ts +43 -0
- package/dist/commands/search.js +233 -0
- package/dist/commands/setup.d.ts +27 -0
- package/dist/commands/setup.js +148 -0
- package/dist/commands/status.js +10 -0
- package/dist/commands.d.ts +6 -0
- package/dist/commands.js +6 -0
- package/dist/hooks/command-not-found/suggest.d.ts +1 -1
- package/dist/hooks/command-not-found/suggest.js +12 -2
- package/dist/lib/argv.d.ts +5 -0
- package/dist/lib/argv.js +30 -5
- package/dist/lib/cli-flag-manifest.d.ts +7 -0
- package/dist/lib/cli-flag-manifest.js +7 -0
- package/dist/lib/cli-presentation.d.ts +25 -4
- package/dist/lib/cli-presentation.js +34 -4
- package/dist/lib/cli-result-types.d.ts +84 -1
- package/dist/lib/color.d.ts +2 -0
- package/dist/lib/color.js +6 -0
- package/dist/lib/common-metadata-filter-flags.d.ts +14 -0
- package/dist/lib/common-metadata-filter-flags.js +37 -0
- package/dist/lib/config/index.d.ts +2 -2
- package/dist/lib/config/index.js +2 -2
- package/dist/lib/config/io.d.ts +9 -0
- package/dist/lib/config/io.js +71 -21
- package/dist/lib/config/schema.d.ts +2 -0
- package/dist/lib/config/schema.js +7 -0
- package/dist/lib/envelope.js +11 -4
- package/dist/lib/next-command.d.ts +15 -0
- package/dist/lib/next-command.js +40 -0
- package/dist/lib/prune-flags.d.ts +2 -0
- package/dist/lib/prune-flags.js +18 -6
- package/dist/lib/research/artifact-index.js +7 -1
- package/dist/lib/research/artifact.js +20 -2
- package/dist/lib/research/browser.d.ts +12 -0
- package/dist/lib/research/browser.js +63 -8
- package/dist/lib/research/cli-io.d.ts +7 -0
- package/dist/lib/research/context-dashboard.d.ts +19 -0
- package/dist/lib/research/context-dashboard.js +43 -0
- package/dist/lib/research/fetch-command-service.js +90 -23
- package/dist/lib/research/fetch-result.d.ts +1 -0
- package/dist/lib/research/fetch-result.js +4 -0
- package/dist/lib/research/fetcher.js +3 -1
- package/dist/lib/research/import-command-service.js +22 -1
- package/dist/lib/research/list-row.d.ts +15 -0
- package/dist/lib/research/list-row.js +31 -0
- package/dist/lib/research/metadata-filters.d.ts +39 -0
- package/dist/lib/research/metadata-filters.js +71 -0
- package/dist/lib/research/metadata-flags.d.ts +34 -0
- package/dist/lib/research/metadata-flags.js +45 -0
- package/dist/lib/research/prompt-injection.d.ts +2 -1
- package/dist/lib/research/prompt-injection.js +18 -2
- package/dist/lib/research/resolve-target.d.ts +2 -0
- package/dist/lib/research/resolve-target.js +1 -1
- package/dist/lib/research/schema.d.ts +2 -0
- package/dist/lib/research/schema.js +4 -0
- package/dist/lib/research/search-match.d.ts +39 -0
- package/dist/lib/research/search-match.js +134 -0
- package/dist/lib/research/storage.d.ts +4 -7
- package/dist/lib/research/storage.js +26 -25
- package/dist/lib/scope-flags.d.ts +19 -0
- package/dist/lib/scope-flags.js +27 -0
- package/dist/lib/setup/agents.d.ts +16 -0
- package/dist/lib/setup/agents.js +20 -0
- package/dist/lib/setup/bin-command.d.ts +14 -0
- package/dist/lib/setup/bin-command.js +42 -0
- package/dist/lib/setup/hook-file-io.d.ts +37 -0
- package/dist/lib/setup/hook-file-io.js +80 -0
- package/dist/lib/text.d.ts +20 -8
- package/dist/lib/text.js +32 -9
- package/oclif.manifest.json +406 -13
- package/package.json +2 -1
package/dist/commands/list.js
CHANGED
|
@@ -1,34 +1,49 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
2
3
|
import { BaseCommand } from '../base-command.js';
|
|
3
4
|
import { scanCacheDirs } from '../lib/research/storage.js';
|
|
4
5
|
import { loadStoreRoots } from '../lib/research/store-roots.js';
|
|
5
6
|
import { evaluateFreshness } from '../lib/research/freshness.js';
|
|
6
|
-
import {
|
|
7
|
-
import { NO_TOPIC_LABEL, resultListHeading } from '../lib/text.js';
|
|
7
|
+
import { collapseHomeDir, formatTip, resultListHeading, } from '../lib/text.js';
|
|
8
8
|
import { limitFlag } from '../lib/limit-flag.js';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
9
|
+
import { countByFreshness, toListRow } from '../lib/research/list-row.js';
|
|
10
|
+
import { emptyUrlFilterError } from '../lib/research/url.js';
|
|
11
|
+
import { matchesCommonMetadataFilters, hasActiveMetadataFilters, emptyTopicFilterError, emptyTagsFilterError, } from '../lib/research/metadata-filters.js';
|
|
12
|
+
import { commonMetadataFilterFlags } from '../lib/common-metadata-filter-flags.js';
|
|
13
|
+
import { buildNextLimitCommand } from '../lib/next-command.js';
|
|
14
|
+
import { colors, FRESHNESS_COLOR } from '../lib/color.js';
|
|
15
|
+
import { CLI_FLAG_DESCRIPTIONS, formatResultRowHeader, formatResultRowSourceUrls, formatResultRowTokens, } from '../lib/cli-presentation.js';
|
|
12
16
|
// Listings are ordered newest-first, so the truncation word is "first"; --limit caps at this value.
|
|
13
17
|
const LIST_DEFAULT_MAX_LIMIT = 100;
|
|
18
|
+
// A small default keeps an unfiltered `list` from flooding an agent's context with everything ever
|
|
19
|
+
// cached; `summary.truncated`/`nextCommand` (and the human-mode tip) make raising --limit an
|
|
20
|
+
// explicit, deliberate next step rather than a silent, unbounded dump.
|
|
21
|
+
const LIST_DEFAULT_LIMIT = 10;
|
|
14
22
|
const LIST_LABELS = {
|
|
15
23
|
noun: 'cached research',
|
|
16
24
|
order: 'first',
|
|
17
|
-
maxLimit: LIST_DEFAULT_MAX_LIMIT,
|
|
18
25
|
};
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
26
|
+
/** Project a full row down to the default minimal shape (see {@link ListRowMinimal}). */
|
|
27
|
+
function toMinimalListRow(row) {
|
|
28
|
+
return {
|
|
29
|
+
sourceUrls: row.sourceUrls,
|
|
30
|
+
topic: row.topic,
|
|
31
|
+
freshness: row.freshness,
|
|
32
|
+
tokenEstimate: row.tokenEstimate,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
22
35
|
/** List cached research artifacts by metadata filters. */
|
|
23
36
|
export default class ResearchList extends BaseCommand {
|
|
24
37
|
static id = 'list';
|
|
25
38
|
static summary = 'List cached research artifacts by metadata';
|
|
26
|
-
static description = 'List page-level cached artifacts
|
|
39
|
+
static description = 'List page-level cached artifacts by source URL, topic, freshness, and token estimate. ' +
|
|
40
|
+
'Pass --full for every metadata field (cache key, path, artifact type, tags, capture method, quality notes, timestamps).';
|
|
27
41
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
42
|
+
* Aggregate counts (and an explicit empty/truncation signal) attached to the success envelope so
|
|
43
|
+
* agents get a cache-wide summary without a separate round trip (AXI principles: pre-computed
|
|
44
|
+
* aggregates, definitive empty states). Always set by `run()` before it returns.
|
|
30
45
|
*/
|
|
31
|
-
|
|
46
|
+
jsonSummary = null;
|
|
32
47
|
static examples = [
|
|
33
48
|
{
|
|
34
49
|
description: 'list cached entries',
|
|
@@ -46,66 +61,36 @@ export default class ResearchList extends BaseCommand {
|
|
|
46
61
|
description: 'list entries matching a URL glob',
|
|
47
62
|
command: '<%= config.bin %> list --url "https://react.dev/*"',
|
|
48
63
|
},
|
|
64
|
+
{
|
|
65
|
+
description: 'list every metadata field instead of the minimal default row',
|
|
66
|
+
command: '<%= config.bin %> list --full --json',
|
|
67
|
+
},
|
|
49
68
|
];
|
|
50
69
|
static flags = {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
char: 'g',
|
|
57
|
-
description: CLI_FLAG_DESCRIPTIONS.filterTags,
|
|
58
|
-
multiple: true,
|
|
70
|
+
...commonMetadataFilterFlags(CLI_FLAG_DESCRIPTIONS.listArtifactType),
|
|
71
|
+
limit: limitFlag(LIST_DEFAULT_MAX_LIMIT, LIST_DEFAULT_LIMIT, `result count (max ${LIST_DEFAULT_MAX_LIMIT}, default ${LIST_DEFAULT_LIMIT})`),
|
|
72
|
+
full: Flags.boolean({
|
|
73
|
+
default: false,
|
|
74
|
+
description: CLI_FLAG_DESCRIPTIONS.listFull,
|
|
59
75
|
}),
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}),
|
|
63
|
-
freshness: Flags.option({
|
|
64
|
-
description: 'freshness state',
|
|
65
|
-
options: ['fresh', 'stale_grace', 'stale_expired'],
|
|
66
|
-
})(),
|
|
67
|
-
'artifact-type': Flags.option({
|
|
68
|
-
description: CLI_FLAG_DESCRIPTIONS.listArtifactType,
|
|
69
|
-
options: LISTABLE_ARTIFACT_TYPES,
|
|
70
|
-
})(),
|
|
71
|
-
'capture-method': Flags.option({
|
|
72
|
-
description: 'capture method',
|
|
73
|
-
options: CAPTURE_METHODS,
|
|
74
|
-
})(),
|
|
75
|
-
limit: limitFlag(100, 50, 'result count (default 50, max 100)'),
|
|
76
|
+
// Only ever set by the argv rewrite for a truly bare `bonsai` invocation (see normalizeArgv);
|
|
77
|
+
// hidden because it is not documented or intended for direct use.
|
|
78
|
+
identity: Flags.boolean({ default: false, hidden: true }),
|
|
76
79
|
};
|
|
77
80
|
static stdoutIsPrimaryData = true;
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
81
|
+
/** The shared filter flags as `matchesCommonMetadataFilters`/`hasActiveMetadataFilters` expect them. */
|
|
82
|
+
metadataFilterFlags() {
|
|
83
|
+
return {
|
|
84
|
+
topic: this.flags.topic,
|
|
85
|
+
tags: this.flags.tags,
|
|
86
|
+
url: this.flags.url,
|
|
87
|
+
artifactType: this.flags['artifact-type'],
|
|
88
|
+
captureMethod: this.flags['capture-method'],
|
|
89
|
+
freshness: this.flags.freshness,
|
|
90
|
+
};
|
|
88
91
|
}
|
|
89
92
|
matchesFilters(meta, freshness) {
|
|
90
|
-
|
|
91
|
-
return false;
|
|
92
|
-
}
|
|
93
|
-
if (!this.matchesTags(meta, this.flags.tags)) {
|
|
94
|
-
return false;
|
|
95
|
-
}
|
|
96
|
-
if (this.flags.url && !artifactMatchesUrlFilter(meta, this.flags.url)) {
|
|
97
|
-
return false;
|
|
98
|
-
}
|
|
99
|
-
if (this.flags['artifact-type'] && meta.artifact_type !== this.flags['artifact-type']) {
|
|
100
|
-
return false;
|
|
101
|
-
}
|
|
102
|
-
if (this.flags['capture-method'] && meta.capture_method !== this.flags['capture-method']) {
|
|
103
|
-
return false;
|
|
104
|
-
}
|
|
105
|
-
if (this.flags.freshness && freshness !== this.flags.freshness) {
|
|
106
|
-
return false;
|
|
107
|
-
}
|
|
108
|
-
return true;
|
|
93
|
+
return matchesCommonMetadataFilters(meta, freshness, this.metadataFilterFlags());
|
|
109
94
|
}
|
|
110
95
|
scanCacheDirForList(readRoots, currentTime) {
|
|
111
96
|
// Honor effective read-only: listing must not persist the derived search-index sidecar.
|
|
@@ -116,38 +101,30 @@ export default class ResearchList extends BaseCommand {
|
|
|
116
101
|
// listing (one page yields dozens) and aren't in the documented source/research_note contract.
|
|
117
102
|
// They stay discoverable through `inspect` (which lists a page's sections). `list` answers "what pages/notes do I have?", so keep it page-level. This
|
|
118
103
|
// unconditional guard owns the exclusion (the default no-filter case relies on it);
|
|
119
|
-
//
|
|
104
|
+
// PAGE_LEVEL_ARTIFACT_TYPES just hides `section` from --artifact-type so no one filters for a
|
|
120
105
|
// type list can never return. Keep both in sync if section handling ever changes.
|
|
121
106
|
if (artifact.metadata.artifact_type === 'section')
|
|
122
107
|
return null;
|
|
123
108
|
const freshness = evaluateFreshness(artifact.metadata, currentTime, null);
|
|
124
109
|
if (!this.matchesFilters(artifact.metadata, freshness))
|
|
125
110
|
return null;
|
|
126
|
-
return
|
|
127
|
-
cacheKey: artifact.metadata.cache_key,
|
|
128
|
-
path: filePath,
|
|
129
|
-
artifactType: artifact.metadata.artifact_type,
|
|
130
|
-
sourceUrls: artifact.metadata.source_urls,
|
|
131
|
-
topic: artifact.metadata.topic,
|
|
132
|
-
tags: artifact.metadata.tags,
|
|
133
|
-
freshness,
|
|
134
|
-
captureMethod: artifact.metadata.capture_method,
|
|
135
|
-
tokenEstimate: artifact.metadata.token_estimate,
|
|
136
|
-
qualityNotes: artifact.metadata.quality_notes,
|
|
137
|
-
fetchedAt: artifact.metadata.fetched_at,
|
|
138
|
-
validatedAt: artifact.metadata.validated_at,
|
|
139
|
-
};
|
|
111
|
+
return toListRow(artifact, filePath, freshness);
|
|
140
112
|
}, { persistIndex: !this.readOnly });
|
|
141
113
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
114
|
+
/**
|
|
115
|
+
* Identity header for the AXI "home view" (bare `bonsai`, no args): the tool's own bin path and
|
|
116
|
+
* one-sentence description, shown before the live data so an agent orients on what it's looking
|
|
117
|
+
* at without a separate `--help` round trip. Human mode only — the hidden `--identity` flag is
|
|
118
|
+
* only ever set by the argv rewrite for a bare invocation; it is not documented for direct use.
|
|
119
|
+
*/
|
|
120
|
+
logIdentityHeader() {
|
|
121
|
+
if (!this.flags.identity || this.jsonEnabled())
|
|
122
|
+
return;
|
|
123
|
+
const binPath = collapseHomeDir(process.argv[1] ?? this.config.bin, homedir());
|
|
124
|
+
this.log(`bin: ${binPath}`);
|
|
125
|
+
this.log(`description: ${this.config.pjson.description ?? this.config.bin}\n`);
|
|
149
126
|
}
|
|
150
|
-
logListResults(finalResults, totalMatched) {
|
|
127
|
+
logListResults(finalResults, totalMatched, nextCommand) {
|
|
151
128
|
if (finalResults.length === 0) {
|
|
152
129
|
this.emitEmptyListGuidance();
|
|
153
130
|
return;
|
|
@@ -156,26 +133,22 @@ export default class ResearchList extends BaseCommand {
|
|
|
156
133
|
return;
|
|
157
134
|
this.log(`${resultListHeading(totalMatched, finalResults.length, LIST_LABELS)}\n`);
|
|
158
135
|
finalResults.forEach((res, index) => {
|
|
159
|
-
|
|
160
|
-
const
|
|
161
|
-
this.log(`${index + 1}. [${topicStr}] Key: ${keyStr}`);
|
|
162
|
-
const freshnessColorMap = {
|
|
163
|
-
fresh: colors.green,
|
|
164
|
-
stale_grace: colors.yellow,
|
|
165
|
-
stale_expired: colors.red,
|
|
166
|
-
};
|
|
167
|
-
const freshnessColor = freshnessColorMap[res.freshness];
|
|
168
|
-
const freshnessStr = freshnessColor(res.freshness);
|
|
136
|
+
this.log(formatResultRowHeader(index, res.topic, res.cacheKey));
|
|
137
|
+
const freshnessStr = FRESHNESS_COLOR[res.freshness](res.freshness);
|
|
169
138
|
this.log(` Type: ${colors.bold(res.artifactType)} | Freshness: ${freshnessStr}`);
|
|
170
|
-
this.log(
|
|
171
|
-
this.log(
|
|
139
|
+
this.log(formatResultRowTokens(res.tokenEstimate));
|
|
140
|
+
this.log(formatResultRowSourceUrls(res.sourceUrls));
|
|
172
141
|
});
|
|
142
|
+
this.tipNextCommand(nextCommand);
|
|
173
143
|
}
|
|
174
|
-
/**
|
|
144
|
+
/**
|
|
145
|
+
* Empty-cache / no-match tip (human mode only). Under `--json`/`--toon`, `data: []` is paired with
|
|
146
|
+
* `summary.empty: true` instead of prose — see {@link toSuccessJson}.
|
|
147
|
+
*/
|
|
175
148
|
emitEmptyListGuidance() {
|
|
176
149
|
if (this.jsonEnabled())
|
|
177
150
|
return;
|
|
178
|
-
const filtered = this.
|
|
151
|
+
const filtered = hasActiveMetadataFilters(this.metadataFilterFlags());
|
|
179
152
|
const headline = filtered
|
|
180
153
|
? 'No cached research entries match the given filters.'
|
|
181
154
|
: 'No cached research entries found.';
|
|
@@ -184,12 +157,20 @@ export default class ResearchList extends BaseCommand {
|
|
|
184
157
|
? 'try relaxing filters, or list everything: '
|
|
185
158
|
: 'populate the cache first: ';
|
|
186
159
|
this.log(headline);
|
|
187
|
-
this.log(`\
|
|
160
|
+
this.log(`\n${formatTip(`${tipLead}${colors.cyan(tipCmd)}`)}`);
|
|
161
|
+
// Bare `bonsai` (no args) redirects here (content-first default), so an empty, unfiltered cache
|
|
162
|
+
// is also the first thing a brand-new user sees — point at --help so command discovery isn't lost.
|
|
163
|
+
if (!filtered) {
|
|
164
|
+
this.log(formatTip(`see every command: ${colors.cyan(`${this.config.bin} --help`)}`));
|
|
165
|
+
}
|
|
188
166
|
}
|
|
189
167
|
async run() {
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
this.
|
|
168
|
+
const flagErr = emptyUrlFilterError(this.flags.url) ??
|
|
169
|
+
emptyTopicFilterError(this.flags.topic) ??
|
|
170
|
+
emptyTagsFilterError(this.flags.tags);
|
|
171
|
+
if (flagErr)
|
|
172
|
+
this.error(flagErr, { exit: 2, code: 'INVALID_FLAG_VALUE' });
|
|
173
|
+
this.logIdentityHeader();
|
|
193
174
|
const roots = loadStoreRoots({
|
|
194
175
|
configDir: this.config.configDir,
|
|
195
176
|
cwd: process.cwd(),
|
|
@@ -203,23 +184,31 @@ export default class ResearchList extends BaseCommand {
|
|
|
203
184
|
return timeB - timeA;
|
|
204
185
|
});
|
|
205
186
|
const finalResults = results.slice(0, this.flags.limit);
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
187
|
+
const truncated = results.length > finalResults.length;
|
|
188
|
+
// Suggest exactly enough to see every match, capped at the command's own max — a caller who
|
|
189
|
+
// truncated at the default can raise --limit once and be done, rather than guessing a value.
|
|
190
|
+
const nextCommand = truncated
|
|
191
|
+
? buildNextLimitCommand(this.config.bin, 'list', this.argv, Math.min(results.length, LIST_DEFAULT_MAX_LIMIT))
|
|
192
|
+
: null;
|
|
193
|
+
// Human heading carries truncation inline; --json/--toon get the same counts (plus a freshness
|
|
194
|
+
// breakdown and an explicit `empty` flag) as an always-present envelope `summary` object.
|
|
195
|
+
this.jsonSummary = {
|
|
196
|
+
total: results.length,
|
|
197
|
+
shown: finalResults.length,
|
|
198
|
+
limit: this.flags.limit,
|
|
199
|
+
truncated,
|
|
200
|
+
empty: results.length === 0,
|
|
201
|
+
byFreshness: countByFreshness(results),
|
|
202
|
+
nextCommand,
|
|
203
|
+
};
|
|
204
|
+
this.logListResults(finalResults, results.length, nextCommand);
|
|
205
|
+
return this.flags.full ? finalResults : finalResults.map(toMinimalListRow);
|
|
217
206
|
}
|
|
218
|
-
/** Attach `
|
|
207
|
+
/** Attach the aggregate `summary` computed in `run()` (see {@link ListSummary}). */
|
|
219
208
|
toSuccessJson(data) {
|
|
220
209
|
const envelope = this.baseSuccessJson(data);
|
|
221
|
-
if (!this.
|
|
210
|
+
if (!this.jsonSummary)
|
|
222
211
|
return envelope;
|
|
223
|
-
return { ...envelope,
|
|
212
|
+
return { ...envelope, summary: this.jsonSummary };
|
|
224
213
|
}
|
|
225
214
|
}
|
package/dist/commands/prune.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ export default class ResearchPrune extends BaseCommand<typeof ResearchPrune> {
|
|
|
13
13
|
'older-than': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
14
|
inactive: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
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>;
|
|
16
18
|
'artifact-type': import("@oclif/core/interfaces").OptionFlag<"source" | "research_note" | "index" | "section" | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
19
|
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
18
20
|
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
package/dist/commands/prune.js
CHANGED
|
@@ -7,8 +7,9 @@ import { scanCacheDir } from '../lib/research/storage.js';
|
|
|
7
7
|
import { loadStoreRoots } from '../lib/research/store-roots.js';
|
|
8
8
|
import { parseTtlToMs } from '../lib/research/freshness.js';
|
|
9
9
|
import { ARTIFACT_TYPES } from '../lib/research/schema.js';
|
|
10
|
-
import { NO_TOPIC_LABEL, pluralize } from '../lib/text.js';
|
|
10
|
+
import { NO_TOPIC_LABEL, pluralize, sanitizeForTerminal } from '../lib/text.js';
|
|
11
11
|
import { artifactMatchesUrlFilter } from '../lib/research/url.js';
|
|
12
|
+
import { matchesTopicFilter, matchesTagsFilter } from '../lib/research/metadata-filters.js';
|
|
12
13
|
import { pruneFlagError } from '../lib/prune-flags.js';
|
|
13
14
|
import { colors } from '../lib/color.js';
|
|
14
15
|
import { CLI_FLAG_DESCRIPTIONS } from '../lib/cli-presentation.js';
|
|
@@ -16,7 +17,7 @@ import { CLI_FLAG_DESCRIPTIONS } from '../lib/cli-presentation.js';
|
|
|
16
17
|
export default class ResearchPrune extends BaseCommand {
|
|
17
18
|
static id = 'prune';
|
|
18
19
|
static summary = 'Prune cached research artifacts';
|
|
19
|
-
static description = 'Delete cached artifacts by content age, idle time, URL glob,
|
|
20
|
+
static description = 'Delete cached artifacts by content age, idle time, URL glob, artifact type, topic, or tags.';
|
|
20
21
|
static examples = [
|
|
21
22
|
{
|
|
22
23
|
description: 'preview pruning entries older than 30 days',
|
|
@@ -30,6 +31,10 @@ export default class ResearchPrune extends BaseCommand {
|
|
|
30
31
|
description: 'prune entries matching a URL glob',
|
|
31
32
|
command: '<%= config.bin %> prune --url "https://react.dev/*" --yes',
|
|
32
33
|
},
|
|
34
|
+
{
|
|
35
|
+
description: 'prune entries tagged as deprecated',
|
|
36
|
+
command: '<%= config.bin %> prune --tags deprecated --yes',
|
|
37
|
+
},
|
|
33
38
|
];
|
|
34
39
|
static flags = {
|
|
35
40
|
'older-than': Flags.string({
|
|
@@ -41,6 +46,15 @@ export default class ResearchPrune extends BaseCommand {
|
|
|
41
46
|
url: Flags.string({
|
|
42
47
|
description: CLI_FLAG_DESCRIPTIONS.sourceUrlGlob,
|
|
43
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,
|
|
57
|
+
}),
|
|
44
58
|
'artifact-type': Flags.option({
|
|
45
59
|
// Prune operates on every cached file, so it can target any artifact type — including the
|
|
46
60
|
// `section`/`index` children a page generates (e.g. to clear orphans left after a source is
|
|
@@ -68,6 +82,8 @@ export default class ResearchPrune extends BaseCommand {
|
|
|
68
82
|
inactive: this.flags.inactive,
|
|
69
83
|
artifactType: this.flags['artifact-type'],
|
|
70
84
|
url: this.flags.url,
|
|
85
|
+
topic: this.flags.topic,
|
|
86
|
+
tags: this.flags.tags,
|
|
71
87
|
dryRun: this.flags['dry-run'],
|
|
72
88
|
yes: this.flags.yes,
|
|
73
89
|
readOnly: this.readOnly,
|
|
@@ -83,6 +99,12 @@ export default class ResearchPrune extends BaseCommand {
|
|
|
83
99
|
if (this.flags.url && !artifactMatchesUrlFilter(meta, this.flags.url)) {
|
|
84
100
|
return false;
|
|
85
101
|
}
|
|
102
|
+
if (!matchesTopicFilter(meta, this.flags.topic)) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
if (!matchesTagsFilter(meta, this.flags.tags)) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
86
108
|
const now = currentTime.getTime();
|
|
87
109
|
// Content age (fetched_at, else validated_at) vs idle time (validated_at, else fetched_at) are
|
|
88
110
|
// deliberately distinct so a recently revalidated but originally-old page can match --older-than
|
|
@@ -118,7 +140,12 @@ export default class ResearchPrune extends BaseCommand {
|
|
|
118
140
|
prunedCount++;
|
|
119
141
|
}
|
|
120
142
|
catch (err) {
|
|
121
|
-
|
|
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
|
+
});
|
|
122
149
|
}
|
|
123
150
|
}
|
|
124
151
|
return prunedCount;
|
|
@@ -147,8 +174,12 @@ export default class ResearchPrune extends BaseCommand {
|
|
|
147
174
|
const noun = pluralize(count, 'entry', 'entries');
|
|
148
175
|
this.log(colors.yellow(`[Dry Run] Found ${count} research cache ${noun} that would be pruned:\n`));
|
|
149
176
|
filesToPrune.forEach((f) => {
|
|
150
|
-
|
|
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')})`);
|
|
151
181
|
});
|
|
182
|
+
this.tip('re-run with --yes to prune these entries.');
|
|
152
183
|
}
|
|
153
184
|
}
|
|
154
185
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { BaseCommand } from '../base-command.js';
|
|
2
|
+
import type { SearchRow, SearchRowMinimal } from '../lib/cli-result-types.js';
|
|
3
|
+
/** Search cached research artifacts by tag/content keywords and metadata filters. */
|
|
4
|
+
export default class ResearchSearch extends BaseCommand<typeof ResearchSearch> {
|
|
5
|
+
static id: string;
|
|
6
|
+
static summary: string;
|
|
7
|
+
static description: string;
|
|
8
|
+
/** Aggregate counts (and an explicit empty/truncation signal); see `list.ts`'s equivalent. */
|
|
9
|
+
private jsonSummary;
|
|
10
|
+
static examples: {
|
|
11
|
+
description: string;
|
|
12
|
+
command: string;
|
|
13
|
+
}[];
|
|
14
|
+
static flags: {
|
|
15
|
+
limit: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
+
full: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
17
|
+
topic: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
18
|
+
tags: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
19
|
+
url: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
20
|
+
freshness: import("@oclif/core/interfaces").OptionFlag<"fresh" | "stale_grace" | "stale_expired" | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
21
|
+
'artifact-type': import("@oclif/core/interfaces").OptionFlag<"source" | "research_note" | "index" | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
22
|
+
'capture-method': import("@oclif/core/interfaces").OptionFlag<"static_fetch" | "browser_fallback" | "agent_supplied" | "route_markdown" | "github_source" | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
23
|
+
query: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
24
|
+
'match-any': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
25
|
+
};
|
|
26
|
+
static stdoutIsPrimaryData: boolean;
|
|
27
|
+
/** The shared filter flags as `matchesCommonMetadataFilters`/`hasActiveMetadataFilters` expect them. */
|
|
28
|
+
private metadataFilterFlags;
|
|
29
|
+
private matchesMetadataFilters;
|
|
30
|
+
private scanCacheDirForSearch;
|
|
31
|
+
/** Newest-first (no query) or highest-score-first (query given), both tie-broken by cache key. */
|
|
32
|
+
private sortResults;
|
|
33
|
+
private hasActiveFilters;
|
|
34
|
+
private logSearchResults;
|
|
35
|
+
/**
|
|
36
|
+
* Empty-cache / no-match tip (human mode only). Under `--json`/`--toon`, `data: []` is paired with
|
|
37
|
+
* `summary.empty: true` instead of prose — see {@link toSuccessJson}.
|
|
38
|
+
*/
|
|
39
|
+
private emitEmptySearchGuidance;
|
|
40
|
+
run(): Promise<SearchRow[] | SearchRowMinimal[]>;
|
|
41
|
+
/** Attach the aggregate `summary` computed in `run()` (see {@link SearchSummary}). */
|
|
42
|
+
protected toSuccessJson(data: unknown): Record<string, unknown>;
|
|
43
|
+
}
|