@taurgis/bonsai 1.0.5 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -12
- package/agents/agents/official-docs-researcher.agent.md +15 -26
- package/agents/agents/salesforce-docs-researcher.agent.md +17 -35
- package/agents/hooks/README.md +2 -9
- 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 +7 -6
- package/agents/instructions/web-research.instructions.md +3 -2
- package/agents/skills/web-research/SKILL.md +24 -32
- package/bin/cli.mjs +44 -14
- package/dist/base-command.d.ts +17 -1
- package/dist/base-command.js +70 -9
- package/dist/commands/config/base.d.ts +2 -0
- package/dist/commands/config/base.js +15 -1
- package/dist/commands/config/get.d.ts +1 -1
- package/dist/commands/config/get.js +4 -5
- package/dist/commands/config/index.d.ts +12 -0
- package/dist/commands/config/index.js +34 -0
- package/dist/commands/config/list.js +2 -2
- package/dist/commands/config/set.js +18 -12
- package/dist/commands/config/unset.d.ts +1 -1
- package/dist/commands/config/unset.js +9 -6
- package/dist/commands/fetch.d.ts +2 -0
- package/dist/commands/fetch.js +69 -45
- package/dist/commands/import.d.ts +3 -0
- package/dist/commands/import.js +91 -53
- package/dist/commands/inspect.d.ts +8 -0
- package/dist/commands/inspect.js +59 -30
- package/dist/commands/list.d.ts +5 -3
- package/dist/commands/list.js +60 -35
- package/dist/commands/prune.d.ts +2 -1
- package/dist/commands/prune.js +62 -19
- package/dist/commands/status.d.ts +8 -0
- package/dist/commands/status.js +84 -26
- package/dist/commands.d.ts +2 -2
- package/dist/commands.js +2 -2
- package/dist/hooks/command-not-found/suggest.d.ts +7 -1
- package/dist/hooks/command-not-found/suggest.js +100 -46
- package/dist/lib/argv.d.ts +10 -0
- package/dist/lib/argv.js +97 -0
- package/dist/lib/color.d.ts +20 -0
- package/dist/lib/color.js +36 -0
- package/dist/lib/envelope.d.ts +36 -0
- package/dist/lib/envelope.js +82 -0
- package/dist/lib/help-preflight.d.ts +15 -0
- package/dist/lib/help-preflight.js +81 -0
- package/dist/lib/json-meta.d.ts +10 -0
- package/dist/lib/json-meta.js +98 -0
- package/dist/lib/limit-flag.d.ts +1 -0
- package/dist/lib/limit-flag.js +24 -0
- package/dist/lib/research/{search-index.d.ts → artifact-index.d.ts} +5 -5
- package/dist/lib/research/{search-index.js → artifact-index.js} +10 -10
- package/dist/lib/research/browser.d.ts +0 -8
- package/dist/lib/research/browser.js +20 -43
- package/dist/lib/research/regression.d.ts +18 -0
- package/dist/lib/research/regression.js +47 -0
- package/dist/lib/research/revalidate.js +3 -1
- package/dist/lib/research/schema.d.ts +5 -2
- package/dist/lib/research/schema.js +12 -1
- package/dist/lib/research/storage.d.ts +0 -1
- package/dist/lib/research/storage.js +15 -13
- package/dist/lib/research/url.d.ts +22 -0
- package/dist/lib/research/url.js +63 -1
- package/dist/lib/text.d.ts +5 -1
- package/dist/lib/text.js +11 -1
- package/dist/sites/coveo.d.ts +7 -0
- package/dist/sites/coveo.js +33 -0
- package/dist/sites/index.js +1 -2
- package/dist/sites/salesforce/fetch-page.js +1 -1
- package/dist/sites/salesforce/index.js +3 -8
- package/dist/sites/salesforce-developer/fetch-page.js +3 -1
- package/dist/sites/salesforce-developer/index.js +1 -2
- package/dist/sites/salesforce-doc-fetch.d.ts +11 -0
- package/dist/sites/salesforce-doc-fetch.js +117 -76
- package/dist/sites/salesforce-dom-probe.d.ts +16 -0
- package/dist/sites/salesforce-dom-probe.js +114 -0
- package/dist/sites/types.d.ts +0 -6
- package/oclif.manifest.json +153 -155
- package/package.json +38 -25
- package/dist/commands/index.d.ts +0 -35
- package/dist/commands/index.js +0 -306
- package/dist/commands/research/config/base.d.ts +0 -23
- package/dist/commands/research/config/base.js +0 -46
- package/dist/commands/research/config/get.d.ts +0 -20
- package/dist/commands/research/config/get.js +0 -45
- package/dist/commands/research/config/list.d.ts +0 -17
- package/dist/commands/research/config/list.js +0 -42
- package/dist/commands/research/config/set.d.ts +0 -31
- package/dist/commands/research/config/set.js +0 -94
- package/dist/commands/research/config/unset.d.ts +0 -21
- package/dist/commands/research/config/unset.js +0 -59
- package/dist/commands/research/import.d.ts +0 -38
- package/dist/commands/research/import.js +0 -313
- package/dist/commands/research/inspect.d.ts +0 -13
- package/dist/commands/research/inspect.js +0 -86
- package/dist/commands/research/list.d.ts +0 -25
- package/dist/commands/research/list.js +0 -143
- package/dist/commands/research/prune.d.ts +0 -24
- package/dist/commands/research/prune.js +0 -141
- package/dist/commands/research/search.d.ts +0 -32
- package/dist/commands/research/search.js +0 -334
- package/dist/commands/research/status.d.ts +0 -17
- package/dist/commands/research/status.js +0 -108
- package/dist/commands/research.d.ts +0 -36
- package/dist/commands/research.js +0 -304
- package/dist/commands/search.d.ts +0 -31
- package/dist/commands/search.js +0 -329
- package/dist/lib/research/docs/__fixtures__/sphinx-searchindex.d.ts +0 -1
- package/dist/lib/research/docs/__fixtures__/sphinx-searchindex.js +0 -2
- package/dist/lib/research/docs/discovery-index.d.ts +0 -14
- package/dist/lib/research/docs/discovery-index.js +0 -112
- package/dist/lib/research/docs/remote-search-runner.d.ts +0 -21
- package/dist/lib/research/docs/remote-search-runner.js +0 -114
- package/dist/lib/research/docs/remote-search.d.ts +0 -14
- package/dist/lib/research/docs/remote-search.js +0 -101
- package/dist/lib/research/docs/search-index.d.ts +0 -13
- package/dist/lib/research/docs/search-index.js +0 -176
- package/dist/sites/salesforce/browser-search.d.ts +0 -7
- package/dist/sites/salesforce/browser-search.js +0 -82
- package/dist/sites/salesforce/coveo.d.ts +0 -23
- package/dist/sites/salesforce/coveo.js +0 -161
- package/dist/sites/salesforce/token.d.ts +0 -26
- package/dist/sites/salesforce/token.js +0 -154
package/dist/commands/list.d.ts
CHANGED
|
@@ -10,13 +10,15 @@ export default class ResearchList extends BaseCommand<typeof ResearchList> {
|
|
|
10
10
|
static flags: {
|
|
11
11
|
topic: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
12
|
tags: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
url: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
14
|
freshness: import("@oclif/core/interfaces").OptionFlag<"fresh" | "stale_grace" | "stale_expired" | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
-
'artifact-type': import("@oclif/core/interfaces").OptionFlag<"source" | "research_note" | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
-
'capture-method': import("@oclif/core/interfaces").OptionFlag<"static_fetch" | "browser_fallback" | "agent_supplied" | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
'artifact-type': import("@oclif/core/interfaces").OptionFlag<"source" | "research_note" | "index" | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
+
'capture-method': import("@oclif/core/interfaces").OptionFlag<"static_fetch" | "browser_fallback" | "agent_supplied" | "route_markdown" | "github_source" | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
17
|
limit: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
18
|
};
|
|
18
19
|
static stdoutIsPrimaryData: boolean;
|
|
19
|
-
private
|
|
20
|
+
private matchesTopic;
|
|
21
|
+
private matchesTags;
|
|
20
22
|
private matchesFilters;
|
|
21
23
|
private scanCacheDirForList;
|
|
22
24
|
private logListResults;
|
package/dist/commands/list.js
CHANGED
|
@@ -3,72 +3,86 @@ 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 {
|
|
6
|
+
import { ARTIFACT_TYPES, CAPTURE_METHODS } from '../lib/research/schema.js';
|
|
7
|
+
import { NO_TOPIC_LABEL, resultListHeading, truncationNotice, } from '../lib/text.js';
|
|
8
|
+
import { limitFlag } from '../lib/limit-flag.js';
|
|
9
|
+
import { artifactMatchesUrlFilter } from '../lib/research/url.js';
|
|
10
|
+
import { colors } from '../lib/color.js';
|
|
7
11
|
// Listings are ordered newest-first, so the truncation word is "first"; --limit caps at 100.
|
|
8
12
|
const LIST_LABELS = { noun: 'cached research', order: 'first', maxLimit: 100 };
|
|
13
|
+
// `list` answers "what pages/notes do I have?" and deliberately omits section children (see
|
|
14
|
+
// scanCacheDirForList), so `section` is not an offered filter — every other artifact type can appear.
|
|
15
|
+
const LISTABLE_ARTIFACT_TYPES = ARTIFACT_TYPES.filter((type) => type !== 'section');
|
|
9
16
|
export default class ResearchList extends BaseCommand {
|
|
10
17
|
static id = 'list';
|
|
11
18
|
static summary = 'List cached research artifacts by metadata filters.';
|
|
12
19
|
static description = 'Lists cached research artifacts, including metadata details like path, source count, freshness, token estimates, and quality metrics without printing full content.';
|
|
13
20
|
static examples = [
|
|
14
21
|
{
|
|
15
|
-
description: '
|
|
22
|
+
description: 'list all cached entries',
|
|
16
23
|
command: '<%= config.bin %> list',
|
|
17
24
|
},
|
|
18
25
|
{
|
|
19
|
-
description: '
|
|
26
|
+
description: 'list cached entries for a specific topic with JSON output',
|
|
20
27
|
command: '<%= config.bin %> list --topic "React Suspense" --json',
|
|
21
28
|
},
|
|
22
29
|
{
|
|
23
|
-
description: '
|
|
30
|
+
description: 'list only fresh entries filtered by tags',
|
|
24
31
|
command: '<%= config.bin %> list --freshness fresh --tags node --tags url',
|
|
25
32
|
},
|
|
33
|
+
{
|
|
34
|
+
description: 'list entries matching a specific URL glob',
|
|
35
|
+
command: '<%= config.bin %> list --url "https://react.dev/*"',
|
|
36
|
+
},
|
|
26
37
|
];
|
|
27
38
|
static flags = {
|
|
28
39
|
topic: Flags.string({
|
|
29
40
|
char: 't',
|
|
30
|
-
description: '
|
|
41
|
+
description: 'filter by exact topic (case-insensitive)',
|
|
31
42
|
}),
|
|
32
43
|
tags: Flags.string({
|
|
33
44
|
char: 'g',
|
|
34
|
-
description: '
|
|
45
|
+
description: 'filter by tags (must match all tags specified)',
|
|
35
46
|
multiple: true,
|
|
36
47
|
}),
|
|
48
|
+
url: Flags.string({
|
|
49
|
+
description: 'filter by source URL glob pattern (case-insensitive, supports * wildcard)',
|
|
50
|
+
}),
|
|
37
51
|
freshness: Flags.option({
|
|
38
|
-
description: '
|
|
52
|
+
description: 'filter by freshness state',
|
|
39
53
|
options: ['fresh', 'stale_grace', 'stale_expired'],
|
|
40
54
|
})(),
|
|
41
55
|
'artifact-type': Flags.option({
|
|
42
|
-
description: '
|
|
43
|
-
options:
|
|
56
|
+
description: 'filter by artifact type',
|
|
57
|
+
options: LISTABLE_ARTIFACT_TYPES,
|
|
44
58
|
})(),
|
|
45
59
|
'capture-method': Flags.option({
|
|
46
|
-
description: '
|
|
47
|
-
options:
|
|
60
|
+
description: 'filter by capture method',
|
|
61
|
+
options: CAPTURE_METHODS,
|
|
48
62
|
})(),
|
|
49
|
-
limit:
|
|
50
|
-
description: 'Maximum number of results to return (default 50, max 100).',
|
|
51
|
-
default: 50,
|
|
52
|
-
}),
|
|
63
|
+
limit: limitFlag(100, 50, 'maximum number of results to return (default 50, max 100)'),
|
|
53
64
|
};
|
|
54
65
|
static stdoutIsPrimaryData = true;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
66
|
+
matchesTopic(meta, topic) {
|
|
67
|
+
if (!topic)
|
|
68
|
+
return true;
|
|
69
|
+
return !!meta.topic && meta.topic.trim().toLowerCase() === topic.trim().toLowerCase();
|
|
70
|
+
}
|
|
71
|
+
matchesTags(meta, tags) {
|
|
72
|
+
if (!tags || tags.length === 0)
|
|
73
|
+
return true;
|
|
74
|
+
const metaTagsLower = (meta.tags || []).map((t) => t.toLowerCase());
|
|
75
|
+
return tags.every((t) => metaTagsLower.includes(t.toLowerCase()));
|
|
60
76
|
}
|
|
61
77
|
matchesFilters(meta, freshness) {
|
|
62
|
-
if (this.flags.topic
|
|
63
|
-
(!meta.topic || meta.topic.trim().toLowerCase() !== this.flags.topic.trim().toLowerCase())) {
|
|
78
|
+
if (!this.matchesTopic(meta, this.flags.topic)) {
|
|
64
79
|
return false;
|
|
65
80
|
}
|
|
66
|
-
if (this.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
81
|
+
if (!this.matchesTags(meta, this.flags.tags)) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
if (this.flags.url && !artifactMatchesUrlFilter(meta, this.flags.url)) {
|
|
85
|
+
return false;
|
|
72
86
|
}
|
|
73
87
|
if (this.flags['artifact-type'] && meta.artifact_type !== this.flags['artifact-type']) {
|
|
74
88
|
return false;
|
|
@@ -87,8 +101,10 @@ export default class ResearchList extends BaseCommand {
|
|
|
87
101
|
return null;
|
|
88
102
|
// Section children are sub-chunks of a page, not artifacts a user "has" — they would flood the
|
|
89
103
|
// listing (one page yields dozens) and aren't in the documented source/research_note contract.
|
|
90
|
-
// They stay discoverable through `
|
|
91
|
-
//
|
|
104
|
+
// 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
|
|
105
|
+
// unconditional guard owns the exclusion (the default no-filter case relies on it);
|
|
106
|
+
// LISTABLE_ARTIFACT_TYPES just hides `section` from --artifact-type so no one filters for a
|
|
107
|
+
// type list can never return. Keep both in sync if section handling ever changes.
|
|
92
108
|
if (artifact.metadata.artifact_type === 'section')
|
|
93
109
|
return null;
|
|
94
110
|
const freshness = evaluateFreshness(artifact.metadata, currentTime, null);
|
|
@@ -115,18 +131,27 @@ export default class ResearchList extends BaseCommand {
|
|
|
115
131
|
return;
|
|
116
132
|
if (finalResults.length === 0) {
|
|
117
133
|
this.log('No cached research entries found matching filters.');
|
|
134
|
+
this.log(`\nTip: populate the cache first: ${colors.cyan(this.config.bin + ' <url>')}`);
|
|
118
135
|
return;
|
|
119
136
|
}
|
|
120
137
|
this.log(`${resultListHeading(totalMatched, finalResults.length, LIST_LABELS)}\n`);
|
|
121
138
|
finalResults.forEach((res, index) => {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
this.log(
|
|
125
|
-
|
|
139
|
+
const topicStr = res.topic ? colors.cyan(res.topic) : colors.gray(NO_TOPIC_LABEL);
|
|
140
|
+
const keyStr = colors.bold(res.cacheKey);
|
|
141
|
+
this.log(`${index + 1}. [${topicStr}] Key: ${keyStr}`);
|
|
142
|
+
const freshnessColorMap = {
|
|
143
|
+
fresh: colors.green,
|
|
144
|
+
stale_grace: colors.yellow,
|
|
145
|
+
stale_expired: colors.red,
|
|
146
|
+
};
|
|
147
|
+
const freshnessColor = freshnessColorMap[res.freshness] || ((t) => t);
|
|
148
|
+
const freshnessStr = freshnessColor(res.freshness);
|
|
149
|
+
this.log(` Type: ${colors.bold(res.artifactType)} | Freshness: ${freshnessStr}`);
|
|
150
|
+
this.log(` Tokens: compressed=${colors.bold(String(res.tokenEstimate?.compressed || 0))}, detailed=${colors.bold(String(res.tokenEstimate?.detailed || 0))}`);
|
|
151
|
+
this.log(` Source URLs: ${colors.gray(res.sourceUrls.join(', '))}\n`);
|
|
126
152
|
});
|
|
127
153
|
}
|
|
128
154
|
async run() {
|
|
129
|
-
this.validateListFlags();
|
|
130
155
|
const roots = loadStoreRoots({
|
|
131
156
|
configDir: this.config.configDir,
|
|
132
157
|
cwd: process.cwd(),
|
package/dist/commands/prune.d.ts
CHANGED
|
@@ -10,7 +10,8 @@ export default class ResearchPrune extends BaseCommand<typeof ResearchPrune> {
|
|
|
10
10
|
static flags: {
|
|
11
11
|
'older-than': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
12
|
inactive: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
-
|
|
13
|
+
url: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
'artifact-type': import("@oclif/core/interfaces").OptionFlag<"source" | "research_note" | "index" | "section" | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
15
|
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
16
|
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
16
17
|
};
|
package/dist/commands/prune.js
CHANGED
|
@@ -5,49 +5,84 @@ import { BaseCommand } from '../base-command.js';
|
|
|
5
5
|
import { scanCacheDir } from '../lib/research/storage.js';
|
|
6
6
|
import { loadStoreRoots } from '../lib/research/store-roots.js';
|
|
7
7
|
import { parseTtlToMs, durationFlagError } from '../lib/research/freshness.js';
|
|
8
|
-
import {
|
|
8
|
+
import { ARTIFACT_TYPES } from '../lib/research/schema.js';
|
|
9
|
+
import { NO_TOPIC_LABEL, pluralize } from '../lib/text.js';
|
|
10
|
+
import { artifactMatchesUrlFilter } from '../lib/research/url.js';
|
|
11
|
+
import { colors } from '../lib/color.js';
|
|
9
12
|
export default class ResearchPrune extends BaseCommand {
|
|
10
13
|
static id = 'prune';
|
|
11
14
|
static summary = 'Clean up old or inactive research cache entries.';
|
|
12
15
|
static description = 'Prunes cached research entries based on age, inactivity, or artifact type to free up disk space.';
|
|
13
16
|
static examples = [
|
|
14
17
|
{
|
|
15
|
-
description: '
|
|
18
|
+
description: 'perform a dry run of pruning entries older than 90 days',
|
|
16
19
|
command: '<%= config.bin %> prune --older-than 90d --dry-run',
|
|
17
20
|
},
|
|
18
21
|
{
|
|
19
|
-
description: '
|
|
22
|
+
description: 'actually prune entries older than 30 days that are source scrapes',
|
|
20
23
|
command: '<%= config.bin %> prune --older-than 30d --artifact-type source --yes',
|
|
21
24
|
},
|
|
25
|
+
{
|
|
26
|
+
description: 'prune entries matching a URL glob pattern',
|
|
27
|
+
command: '<%= config.bin %> prune --url "https://react.dev/*" --yes',
|
|
28
|
+
},
|
|
22
29
|
];
|
|
23
30
|
static flags = {
|
|
24
31
|
'older-than': Flags.string({
|
|
25
|
-
description: '
|
|
32
|
+
description: 'prune entries older than duration (e.g. "30d", "90d")',
|
|
26
33
|
}),
|
|
27
34
|
inactive: Flags.string({
|
|
28
|
-
description: '
|
|
35
|
+
description: 'prune entries inactive (unvalidated/unfetched) for duration (e.g. "14d", "30d")',
|
|
36
|
+
}),
|
|
37
|
+
url: Flags.string({
|
|
38
|
+
description: 'filter pruning to source URL glob pattern (case-insensitive, supports * wildcard)',
|
|
29
39
|
}),
|
|
30
40
|
'artifact-type': Flags.option({
|
|
31
|
-
|
|
32
|
-
|
|
41
|
+
// Prune operates on every cached file, so it can target any artifact type — including the
|
|
42
|
+
// `section`/`index` children a page generates (e.g. to clear orphans left after a source is
|
|
43
|
+
// pruned with `--artifact-type source`).
|
|
44
|
+
description: 'filter pruning to specific artifact type',
|
|
45
|
+
options: ARTIFACT_TYPES,
|
|
33
46
|
})(),
|
|
34
47
|
'dry-run': Flags.boolean({
|
|
35
|
-
description: '
|
|
48
|
+
description: 'list files that would be deleted without actually deleting them',
|
|
36
49
|
default: false,
|
|
37
50
|
}),
|
|
38
51
|
yes: Flags.boolean({
|
|
39
52
|
char: 'y',
|
|
40
|
-
description: '
|
|
53
|
+
description: 'confirm deletion and prune matched entries (required unless --dry-run)',
|
|
41
54
|
default: false,
|
|
42
55
|
}),
|
|
43
56
|
};
|
|
44
57
|
static stdoutIsPrimaryData = true;
|
|
45
58
|
validatePruneFlags() {
|
|
46
|
-
if (!this.flags['older-than'] &&
|
|
47
|
-
this.
|
|
59
|
+
if (!this.flags['older-than'] &&
|
|
60
|
+
!this.flags.inactive &&
|
|
61
|
+
!this.flags['artifact-type'] &&
|
|
62
|
+
!this.flags.url) {
|
|
63
|
+
this.error('Must specify at least one pruning filter: --older-than, --inactive, --artifact-type, or --url.', {
|
|
64
|
+
exit: 2,
|
|
65
|
+
code: 'MISSING_FILTER',
|
|
66
|
+
suggestions: [
|
|
67
|
+
`Preview age-based pruning: ${this.config.bin} prune --older-than 90d --dry-run`,
|
|
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' });
|
|
48
73
|
}
|
|
49
74
|
if (!this.flags['dry-run'] && !this.flags.yes) {
|
|
50
|
-
|
|
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
|
+
});
|
|
51
86
|
}
|
|
52
87
|
// Validate durations up front: scanCacheDir swallows per-file errors, so a malformed
|
|
53
88
|
// --older-than/--inactive would otherwise be silently ignored and report "0 entries".
|
|
@@ -56,7 +91,7 @@ export default class ResearchPrune extends BaseCommand {
|
|
|
56
91
|
durationFlagError('--inactive', this.flags.inactive),
|
|
57
92
|
]) {
|
|
58
93
|
if (msg)
|
|
59
|
-
this.error(msg, { exit: 2 });
|
|
94
|
+
this.error(msg, { exit: 2, code: 'INVALID_DURATION' });
|
|
60
95
|
}
|
|
61
96
|
}
|
|
62
97
|
shouldPrune(meta, currentTime) {
|
|
@@ -67,6 +102,9 @@ export default class ResearchPrune extends BaseCommand {
|
|
|
67
102
|
if (this.flags['artifact-type'] && meta.artifact_type !== this.flags['artifact-type']) {
|
|
68
103
|
return false;
|
|
69
104
|
}
|
|
105
|
+
if (this.flags.url && !artifactMatchesUrlFilter(meta, this.flags.url)) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
70
108
|
if (this.flags['older-than']) {
|
|
71
109
|
const olderThanMs = parseTtlToMs(this.flags['older-than']);
|
|
72
110
|
if (ageMs < olderThanMs) {
|
|
@@ -125,18 +163,23 @@ export default class ResearchPrune extends BaseCommand {
|
|
|
125
163
|
let prunedCount = 0;
|
|
126
164
|
if (dryRun) {
|
|
127
165
|
if (!this.jsonEnabled()) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
166
|
+
if (count === 0) {
|
|
167
|
+
this.log(colors.yellow('[Dry Run] No research cache entries match the given filters.'));
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
const noun = pluralize(count, 'entry', 'entries');
|
|
171
|
+
this.log(colors.yellow(`[Dry Run] Found ${count} research cache ${noun} that would be pruned:\n`));
|
|
172
|
+
filesToPrune.forEach((f) => {
|
|
173
|
+
this.log(`- [${f.topic ? colors.cyan(f.topic) : colors.gray(NO_TOPIC_LABEL)}] Key: ${colors.bold(f.cacheKey)} (${colors.gray(f.url || 'Imported note')})`);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
133
176
|
}
|
|
134
177
|
}
|
|
135
178
|
else {
|
|
136
179
|
prunedCount = this.deletePruneCandidates(filesToPrune);
|
|
137
180
|
if (!this.jsonEnabled()) {
|
|
138
181
|
const noun = pluralize(count, 'entry', 'entries');
|
|
139
|
-
this.log(`Successfully pruned ${prunedCount} of ${count} research cache ${noun}.`);
|
|
182
|
+
this.log(colors.green(`Successfully pruned ${prunedCount} of ${count} research cache ${noun}.`));
|
|
140
183
|
}
|
|
141
184
|
}
|
|
142
185
|
return {
|
|
@@ -3,6 +3,11 @@ export default class ResearchStatus extends BaseCommand<typeof ResearchStatus> {
|
|
|
3
3
|
static id: string;
|
|
4
4
|
static summary: string;
|
|
5
5
|
static description: string;
|
|
6
|
+
static examples: {
|
|
7
|
+
description: string;
|
|
8
|
+
command: string;
|
|
9
|
+
}[];
|
|
10
|
+
static strict: boolean;
|
|
6
11
|
static args: {
|
|
7
12
|
url: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
8
13
|
};
|
|
@@ -12,5 +17,8 @@ export default class ResearchStatus extends BaseCommand<typeof ResearchStatus> {
|
|
|
12
17
|
'max-age': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
18
|
};
|
|
14
19
|
static stdoutIsPrimaryData: boolean;
|
|
20
|
+
/** Enrich cache-miss envelopes with the same CACHE_MISS code and suggestions as inspect. */
|
|
21
|
+
protected toSuccessJson(data: unknown): Record<string, unknown>;
|
|
15
22
|
run(): Promise<unknown>;
|
|
23
|
+
private checkSingleStatus;
|
|
16
24
|
}
|
package/dist/commands/status.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Args, Flags } from '@oclif/core';
|
|
2
2
|
import { BaseCommand } from '../base-command.js';
|
|
3
|
+
import { formatErrorForJson } from '../lib/envelope.js';
|
|
3
4
|
import { getArtifactPath } from '../lib/research/storage.js';
|
|
4
5
|
import { evaluateFreshness, checkMaxAgeExpired, durationFlagError, } from '../lib/research/freshness.js';
|
|
5
|
-
import {
|
|
6
|
+
import { colors } from '../lib/color.js';
|
|
6
7
|
function resolveFreshness(cached, currentTime, ttl, maxAge) {
|
|
7
8
|
const isMaxAgeExpired = maxAge ? checkMaxAgeExpired(cached, currentTime, maxAge) : false;
|
|
8
9
|
return isMaxAgeExpired
|
|
@@ -22,66 +23,123 @@ export default class ResearchStatus extends BaseCommand {
|
|
|
22
23
|
static id = 'status';
|
|
23
24
|
static summary = 'Check the cache status of a URL without fetching or writing.';
|
|
24
25
|
static description = 'Reports whether the URL is cached, its freshness state, and what action (fetch, revalidate, or cached return) the root fetch command would take.';
|
|
26
|
+
static examples = [
|
|
27
|
+
{
|
|
28
|
+
description: 'check if a URL is already in the cache',
|
|
29
|
+
command: '<%= config.bin %> <%= command.id %> https://example.com/docs',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
description: 'check status against a specific freshness tier and TTL',
|
|
33
|
+
command: '<%= config.bin %> <%= command.id %> https://example.com/docs --tier volatile --ttl 2h',
|
|
34
|
+
},
|
|
35
|
+
];
|
|
36
|
+
static strict = false;
|
|
25
37
|
static args = {
|
|
26
38
|
url: Args.string({
|
|
27
39
|
required: true,
|
|
28
|
-
description: '
|
|
40
|
+
description: 'the URL to check',
|
|
29
41
|
}),
|
|
30
42
|
};
|
|
31
43
|
static flags = {
|
|
32
44
|
tier: Flags.option({
|
|
33
|
-
description: '
|
|
45
|
+
description: 'freshness tier policy to evaluate against',
|
|
34
46
|
options: ['stable', 'standard', 'volatile'],
|
|
35
47
|
default: 'standard',
|
|
36
48
|
})(),
|
|
37
49
|
ttl: Flags.string({
|
|
38
50
|
char: 'l',
|
|
39
|
-
description: '
|
|
51
|
+
description: 'custom TTL duration to evaluate against (e.g. "2h", "7d")',
|
|
40
52
|
}),
|
|
41
53
|
'max-age': Flags.string({
|
|
42
|
-
description: '
|
|
54
|
+
description: 'maximum age of cache to accept (e.g. "1d", "30d")',
|
|
43
55
|
}),
|
|
44
56
|
};
|
|
45
57
|
static stdoutIsPrimaryData = true;
|
|
58
|
+
/** Enrich cache-miss envelopes with the same CACHE_MISS code and suggestions as inspect. */
|
|
59
|
+
toSuccessJson(data) {
|
|
60
|
+
const envelope = super.toSuccessJson(data);
|
|
61
|
+
const list = Array.isArray(data) ? data : [data];
|
|
62
|
+
const misses = list.filter((d) => d && d.status === 'miss');
|
|
63
|
+
if (misses.length === 0)
|
|
64
|
+
return envelope;
|
|
65
|
+
const firstMiss = misses[0];
|
|
66
|
+
const normalizedUrl = firstMiss.normalizedUrl ?? '';
|
|
67
|
+
const suggestions = misses.map((m) => `Fetch and cache it first: ${this.config.bin} ${m.normalizedUrl}`);
|
|
68
|
+
const stderr = formatErrorForJson({
|
|
69
|
+
message: `Cache miss for ${normalizedUrl}` +
|
|
70
|
+
(misses.length > 1 ? ` and ${misses.length - 1} other URLs` : ''),
|
|
71
|
+
code: 'CACHE_MISS',
|
|
72
|
+
suggestions,
|
|
73
|
+
});
|
|
74
|
+
return { ...envelope, ok: false, exitCode: 1, stderr, code: 'CACHE_MISS', suggestions };
|
|
75
|
+
}
|
|
46
76
|
async run() {
|
|
47
|
-
const
|
|
77
|
+
const urls = this.parsedArgv;
|
|
48
78
|
const { ttl, tier, 'max-age': maxAge } = this.flags;
|
|
49
79
|
// Validate the duration flags up front so a malformed value reports the exact flag that is
|
|
50
80
|
// wrong, rather than later surfacing as a misattributed parse failure.
|
|
51
81
|
for (const msg of [durationFlagError('--ttl', ttl), durationFlagError('--max-age', maxAge)]) {
|
|
52
82
|
if (msg)
|
|
53
|
-
this.error(msg, { exit: 2 });
|
|
83
|
+
this.error(msg, { exit: 2, code: 'INVALID_DURATION' });
|
|
54
84
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
85
|
+
const results = [];
|
|
86
|
+
let hasMiss = false;
|
|
87
|
+
const currentTime = new Date();
|
|
88
|
+
for (const url of urls) {
|
|
89
|
+
const res = this.checkSingleStatus(url, currentTime, ttl, maxAge, urls.length > 1);
|
|
90
|
+
if (res.status === 'miss') {
|
|
91
|
+
hasMiss = true;
|
|
92
|
+
}
|
|
93
|
+
results.push(res);
|
|
63
94
|
}
|
|
64
|
-
|
|
65
|
-
|
|
95
|
+
if (hasMiss) {
|
|
96
|
+
process.exitCode = 1;
|
|
66
97
|
}
|
|
98
|
+
return urls.length === 1 ? results[0] : results;
|
|
99
|
+
}
|
|
100
|
+
checkSingleStatus(url, currentTime, ttl, maxAge, showSeparator) {
|
|
101
|
+
const target = this.resolveResearchTargetOrFail(url);
|
|
67
102
|
const { cacheKey, located, normalizedUrl, roots } = target;
|
|
68
103
|
const cached = located?.artifact ?? null;
|
|
69
|
-
const currentTime = new Date();
|
|
70
|
-
// Both duration flags were validated above, so freshness resolution cannot throw on a parse error.
|
|
71
104
|
const result = describeCacheStatus(cached, currentTime, ttl, maxAge);
|
|
72
|
-
// On a hit, report where it actually lives; on a miss, where a fetch would write it.
|
|
73
105
|
const artifactPath = located?.path ?? getArtifactPath(roots.writeRoot, cacheKey);
|
|
74
106
|
if (!this.jsonEnabled()) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
107
|
+
const statusColorMap = {
|
|
108
|
+
hit: colors.green,
|
|
109
|
+
stale: colors.yellow,
|
|
110
|
+
miss: colors.red,
|
|
111
|
+
};
|
|
112
|
+
const freshnessColorMap = {
|
|
113
|
+
fresh: colors.green,
|
|
114
|
+
stale_grace: colors.yellow,
|
|
115
|
+
stale_expired: colors.red,
|
|
116
|
+
none: colors.gray,
|
|
117
|
+
};
|
|
118
|
+
const actionColorMap = {
|
|
119
|
+
would_return_cached: colors.green,
|
|
120
|
+
would_revalidate: colors.yellow,
|
|
121
|
+
would_fetch: colors.red,
|
|
122
|
+
};
|
|
123
|
+
const statusColor = statusColorMap[result.status] || ((t) => t);
|
|
124
|
+
const freshnessColor = freshnessColorMap[result.freshness] || ((t) => t);
|
|
125
|
+
const actionColor = actionColorMap[result.action] || ((t) => t);
|
|
126
|
+
this.log(`${colors.cyan('URL:'.padEnd(25))} ${colors.bold(normalizedUrl)}`);
|
|
127
|
+
this.log(`${colors.cyan('Cache Key:'.padEnd(25))} ${colors.bold(cacheKey)}`);
|
|
128
|
+
this.log(`${colors.cyan('Cache Path:'.padEnd(25))} ${colors.gray(artifactPath)}`);
|
|
129
|
+
this.log(`${colors.cyan('Status:'.padEnd(25))} ${statusColor(result.status)}`);
|
|
130
|
+
this.log(`${colors.cyan('Freshness:'.padEnd(25))} ${freshnessColor(result.freshness)}`);
|
|
131
|
+
this.log(`${colors.cyan('Action:'.padEnd(25))} ${actionColor(result.action)}`);
|
|
132
|
+
if (showSeparator) {
|
|
133
|
+
this.log('-'.repeat(40));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (result.status === 'miss' && !this.jsonEnabled()) {
|
|
137
|
+
this.warn(`Cache miss — run: ${this.config.bin} ${normalizedUrl}`);
|
|
81
138
|
}
|
|
82
139
|
return {
|
|
83
140
|
cacheKey,
|
|
84
141
|
cachePath: artifactPath,
|
|
142
|
+
normalizedUrl,
|
|
85
143
|
status: result.status,
|
|
86
144
|
freshness: result.freshness,
|
|
87
145
|
action: result.action,
|
package/dist/commands.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import ConfigGet from './commands/config/get.js';
|
|
2
|
+
import ConfigIndex from './commands/config/index.js';
|
|
2
3
|
import ConfigList from './commands/config/list.js';
|
|
3
4
|
import ConfigSet from './commands/config/set.js';
|
|
4
5
|
import ConfigUnset from './commands/config/unset.js';
|
|
@@ -7,7 +8,6 @@ import ImportCommand from './commands/import.js';
|
|
|
7
8
|
import InspectCommand from './commands/inspect.js';
|
|
8
9
|
import ListCommand from './commands/list.js';
|
|
9
10
|
import PruneCommand from './commands/prune.js';
|
|
10
|
-
import SearchCommand from './commands/search.js';
|
|
11
11
|
import StatusCommand from './commands/status.js';
|
|
12
12
|
export declare const commands: {
|
|
13
13
|
fetch: typeof FetchCommand;
|
|
@@ -15,8 +15,8 @@ export declare const commands: {
|
|
|
15
15
|
inspect: typeof InspectCommand;
|
|
16
16
|
list: typeof ListCommand;
|
|
17
17
|
prune: typeof PruneCommand;
|
|
18
|
-
search: typeof SearchCommand;
|
|
19
18
|
status: typeof StatusCommand;
|
|
19
|
+
config: typeof ConfigIndex;
|
|
20
20
|
'config:get': typeof ConfigGet;
|
|
21
21
|
'config:list': typeof ConfigList;
|
|
22
22
|
'config:set': typeof ConfigSet;
|
package/dist/commands.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import ConfigGet from './commands/config/get.js';
|
|
2
|
+
import ConfigIndex from './commands/config/index.js';
|
|
2
3
|
import ConfigList from './commands/config/list.js';
|
|
3
4
|
import ConfigSet from './commands/config/set.js';
|
|
4
5
|
import ConfigUnset from './commands/config/unset.js';
|
|
@@ -7,7 +8,6 @@ import ImportCommand from './commands/import.js';
|
|
|
7
8
|
import InspectCommand from './commands/inspect.js';
|
|
8
9
|
import ListCommand from './commands/list.js';
|
|
9
10
|
import PruneCommand from './commands/prune.js';
|
|
10
|
-
import SearchCommand from './commands/search.js';
|
|
11
11
|
import StatusCommand from './commands/status.js';
|
|
12
12
|
export const commands = {
|
|
13
13
|
fetch: FetchCommand,
|
|
@@ -15,8 +15,8 @@ export const commands = {
|
|
|
15
15
|
inspect: InspectCommand,
|
|
16
16
|
list: ListCommand,
|
|
17
17
|
prune: PruneCommand,
|
|
18
|
-
search: SearchCommand,
|
|
19
18
|
status: StatusCommand,
|
|
19
|
+
config: ConfigIndex,
|
|
20
20
|
'config:get': ConfigGet,
|
|
21
21
|
'config:list': ConfigList,
|
|
22
22
|
'config:set': ConfigSet,
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import { type Hook } from '@oclif/core';
|
|
1
|
+
import { type Hook, type Interfaces } from '@oclif/core';
|
|
2
|
+
export declare function buildCommandNotFoundDetails(id: string, argv: string[] | undefined, config: Interfaces.Config): {
|
|
3
|
+
code: 'COMMAND_NOT_FOUND' | 'MISSING_URL_SCHEME' | 'UNEXPECTED_ARGUMENT';
|
|
4
|
+
command: string;
|
|
5
|
+
jsonMode: boolean;
|
|
6
|
+
message: string;
|
|
7
|
+
};
|
|
2
8
|
/**
|
|
3
9
|
* Typo-aware "command not found" handler. oclif fires this before printing its own terse error;
|
|
4
10
|
* throwing here (via `this.error`) replaces that default with a more helpful message. A correction
|