@taurgis/bonsai 4.0.0 → 4.1.1
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 +24 -3
- package/agents/agents/official-docs-researcher.agent.md +102 -0
- package/agents/agents/salesforce-docs-researcher.agent.md +16 -14
- 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 +0 -11
- package/dist/base-command.d.ts +5 -0
- package/dist/base-command.js +9 -0
- package/dist/commands/config/get.d.ts +1 -1
- package/dist/commands/config/get.js +1 -1
- package/dist/commands/config/list.d.ts +1 -1
- package/dist/commands/config/list.js +1 -1
- package/dist/commands/config/set.d.ts +1 -1
- package/dist/commands/config/set.js +1 -1
- package/dist/commands/config/unset.d.ts +1 -1
- package/dist/commands/config/unset.js +1 -1
- package/dist/commands/context.d.ts +20 -0
- package/dist/commands/context.js +65 -0
- package/dist/commands/list.d.ts +12 -3
- package/dist/commands/list.js +61 -100
- 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.d.ts +6 -0
- package/dist/commands.js +6 -0
- package/dist/lib/argv.js +4 -1
- package/dist/lib/cli-flag-manifest.d.ts +2 -2
- package/dist/lib/cli-flag-manifest.js +15 -7
- package/dist/lib/cli-presentation.d.ts +19 -0
- package/dist/lib/cli-presentation.js +27 -0
- package/dist/lib/cli-result-types.d.ts +59 -0
- 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/{commands/config/base.d.ts → lib/config-command.d.ts} +7 -9
- package/dist/{commands/config/base.js → lib/config-command.js} +9 -20
- package/dist/lib/next-command.d.ts +15 -0
- package/dist/lib/next-command.js +40 -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 +10 -0
- package/dist/lib/research/fetch-result.d.ts +1 -0
- package/dist/lib/research/fetch-result.js +4 -0
- package/dist/lib/research/fetcher.d.ts +11 -0
- package/dist/lib/research/fetcher.js +26 -0
- 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 +22 -1
- package/dist/lib/research/metadata-filters.js +34 -0
- package/dist/lib/research/regression.js +6 -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/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 +6 -0
- package/dist/lib/text.js +11 -0
- package/dist/lib/value-flags.d.ts +2 -0
- package/dist/lib/value-flags.js +11 -3
- package/dist/sites/salesforce-developer/fetch-page.d.ts +1 -1
- package/dist/sites/salesforce-developer/fetch-page.js +3 -1
- package/dist/sites/salesforce-developer/route-markdown.d.ts +3 -2
- package/dist/sites/salesforce-developer/route-markdown.js +8 -6
- package/dist/sites/salesforce-doc-fetch.d.ts +23 -11
- package/dist/sites/salesforce-doc-fetch.js +89 -27
- package/dist/sites/salesforce-dom-probe.d.ts +6 -0
- package/dist/sites/salesforce-dom-probe.js +40 -0
- package/oclif.manifest.json +428 -54
- package/package.json +8 -13
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Args, Flags } from '@oclif/core';
|
|
2
|
-
import { ConfigCommand, configScopeFlags } from '
|
|
2
|
+
import { ConfigCommand, configScopeFlags } from '../../lib/config-command.js';
|
|
3
3
|
import { KEY_META, keyValuesHint } from '../../lib/config/index.js';
|
|
4
4
|
import { persistConfigPatch } from '../../lib/config-persist.js';
|
|
5
5
|
export default class ConfigSet extends ConfigCommand {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ConfigCommand } from '
|
|
1
|
+
import { ConfigCommand } from '../../lib/config-command.js';
|
|
2
2
|
import type { ConfigWriteResult } from '../../lib/cli-result-types.js';
|
|
3
3
|
export default class ConfigUnset extends ConfigCommand<typeof ConfigUnset> {
|
|
4
4
|
static id: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Args, Flags } from '@oclif/core';
|
|
2
|
-
import { ConfigCommand, configScopeFlags } from '
|
|
2
|
+
import { ConfigCommand, configScopeFlags } from '../../lib/config-command.js';
|
|
3
3
|
import { keyValuesHint } from '../../lib/config/index.js';
|
|
4
4
|
import { persistConfigPatch } from '../../lib/config-persist.js';
|
|
5
5
|
export default class ConfigUnset extends ConfigCommand {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { BaseCommand } from '../base-command.js';
|
|
2
|
+
import type { ContextDashboard } from '../lib/cli-result-types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Compact, directory-scoped cache summary (AXI principle 7: ambient session-start context).
|
|
5
|
+
* Plain-text human output is deliberately short — it's meant to be piped verbatim into a
|
|
6
|
+
* SessionStart hook's `additionalContext` by `bonsai setup`, so every line here is also a
|
|
7
|
+
* reasonable thing for an agent to read unprompted at the start of a session.
|
|
8
|
+
*/
|
|
9
|
+
export default class Context extends BaseCommand<typeof Context> {
|
|
10
|
+
static id: string;
|
|
11
|
+
static summary: string;
|
|
12
|
+
static description: string;
|
|
13
|
+
static examples: {
|
|
14
|
+
description: string;
|
|
15
|
+
command: string;
|
|
16
|
+
}[];
|
|
17
|
+
static stdoutIsPrimaryData: boolean;
|
|
18
|
+
private logDashboard;
|
|
19
|
+
run(): Promise<ContextDashboard>;
|
|
20
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { BaseCommand } from '../base-command.js';
|
|
2
|
+
import { loadStoreRoots } from '../lib/research/store-roots.js';
|
|
3
|
+
import { buildContextDashboard } from '../lib/research/context-dashboard.js';
|
|
4
|
+
import { NO_TOPIC_LABEL, formatTip, pluralize, sanitizeForTerminal } from '../lib/text.js';
|
|
5
|
+
import { colors, FRESHNESS_COLOR } from '../lib/color.js';
|
|
6
|
+
/**
|
|
7
|
+
* Compact, directory-scoped cache summary (AXI principle 7: ambient session-start context).
|
|
8
|
+
* Plain-text human output is deliberately short — it's meant to be piped verbatim into a
|
|
9
|
+
* SessionStart hook's `additionalContext` by `bonsai setup`, so every line here is also a
|
|
10
|
+
* reasonable thing for an agent to read unprompted at the start of a session.
|
|
11
|
+
*/
|
|
12
|
+
export default class Context extends BaseCommand {
|
|
13
|
+
static id = 'context';
|
|
14
|
+
static summary = 'Show a compact cache dashboard for ambient session context';
|
|
15
|
+
static description = 'Print a short summary of the current research cache: total entries, a freshness ' +
|
|
16
|
+
'breakdown, and the most recently touched pages. This is what `bonsai setup` wires into a ' +
|
|
17
|
+
"SessionStart hook, so a session starts with the cache's state already visible.";
|
|
18
|
+
static examples = [
|
|
19
|
+
{
|
|
20
|
+
description: 'preview what a SessionStart hook would show',
|
|
21
|
+
command: '<%= config.bin %> context',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
description: 'get the dashboard as JSON',
|
|
25
|
+
command: '<%= config.bin %> context --json',
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
static stdoutIsPrimaryData = true;
|
|
29
|
+
logDashboard(dashboard) {
|
|
30
|
+
if (this.jsonEnabled())
|
|
31
|
+
return;
|
|
32
|
+
if (dashboard.total === 0) {
|
|
33
|
+
this.log(`${this.config.bin} cache: 0 entries found.`);
|
|
34
|
+
this.log(formatTip(`populate the cache: ${colors.cyan(`${this.config.bin} <url>`)}`));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const { fresh, stale_grace: staleGrace, stale_expired: staleExpired } = dashboard.byFreshness;
|
|
38
|
+
this.log(`${this.config.bin} cache: ${dashboard.total} ${pluralize(dashboard.total, 'entry', 'entries')} ` +
|
|
39
|
+
`(${fresh} fresh, ${staleGrace} stale_grace, ${staleExpired} stale_expired)`);
|
|
40
|
+
for (const entry of dashboard.entries) {
|
|
41
|
+
const topicStr = entry.topic
|
|
42
|
+
? colors.cyan(sanitizeForTerminal(entry.topic))
|
|
43
|
+
: colors.gray(NO_TOPIC_LABEL);
|
|
44
|
+
const freshnessStr = FRESHNESS_COLOR[entry.freshness](entry.freshness);
|
|
45
|
+
const url = entry.sourceUrls[0] ?? '';
|
|
46
|
+
this.log(`- [${topicStr}] ${freshnessStr} — ${colors.gray(url)}`);
|
|
47
|
+
}
|
|
48
|
+
if (dashboard.total > dashboard.shown) {
|
|
49
|
+
this.log(formatTip(`see all ${dashboard.total} entries: ${colors.cyan(`${this.config.bin} list`)}`));
|
|
50
|
+
}
|
|
51
|
+
this.log(formatTip(`research a new page: ${colors.cyan(`${this.config.bin} <url>`)}`));
|
|
52
|
+
}
|
|
53
|
+
async run() {
|
|
54
|
+
const roots = loadStoreRoots({
|
|
55
|
+
configDir: this.config.configDir,
|
|
56
|
+
cwd: process.cwd(),
|
|
57
|
+
dataDir: this.config.dataDir,
|
|
58
|
+
});
|
|
59
|
+
const dashboard = buildContextDashboard(roots.readRoots, new Date(), {
|
|
60
|
+
persistIndex: !this.readOnly,
|
|
61
|
+
});
|
|
62
|
+
this.logDashboard(dashboard);
|
|
63
|
+
return dashboard;
|
|
64
|
+
}
|
|
65
|
+
}
|
package/dist/commands/list.d.ts
CHANGED
|
@@ -16,19 +16,28 @@ export default class ResearchList extends BaseCommand<typeof ResearchList> {
|
|
|
16
16
|
command: string;
|
|
17
17
|
}[];
|
|
18
18
|
static flags: {
|
|
19
|
+
limit: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
20
|
+
full: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
21
|
+
identity: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
19
22
|
topic: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
20
23
|
tags: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
21
24
|
url: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
22
25
|
freshness: import("@oclif/core/interfaces").OptionFlag<"fresh" | "stale_grace" | "stale_expired" | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
23
26
|
'artifact-type': import("@oclif/core/interfaces").OptionFlag<"source" | "research_note" | "index" | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
24
27
|
'capture-method': import("@oclif/core/interfaces").OptionFlag<"static_fetch" | "browser_fallback" | "agent_supplied" | "route_markdown" | "github_source" | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
25
|
-
limit: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
26
|
-
full: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
27
28
|
};
|
|
28
29
|
static stdoutIsPrimaryData: boolean;
|
|
30
|
+
/** The shared filter flags as `matchesCommonMetadataFilters`/`hasActiveMetadataFilters` expect them. */
|
|
31
|
+
private metadataFilterFlags;
|
|
29
32
|
private matchesFilters;
|
|
30
33
|
private scanCacheDirForList;
|
|
31
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Identity header for the AXI "home view" (bare `bonsai`, no args): the tool's own bin path and
|
|
36
|
+
* one-sentence description, shown before the live data so an agent orients on what it's looking
|
|
37
|
+
* at without a separate `--help` round trip. Human mode only — the hidden `--identity` flag is
|
|
38
|
+
* only ever set by the argv rewrite for a bare invocation; it is not documented for direct use.
|
|
39
|
+
*/
|
|
40
|
+
private logIdentityHeader;
|
|
32
41
|
private logListResults;
|
|
33
42
|
/**
|
|
34
43
|
* Empty-cache / no-match tip (human mode only). Under `--json`/`--toon`, `data: []` is paired with
|
package/dist/commands/list.js
CHANGED
|
@@ -1,24 +1,28 @@
|
|
|
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, formatTip, resultListHeading, sanitizeForTerminal, } 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 {
|
|
12
|
-
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';
|
|
13
16
|
// Listings are ordered newest-first, so the truncation word is "first"; --limit caps at this value.
|
|
14
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;
|
|
15
22
|
const LIST_LABELS = {
|
|
16
23
|
noun: 'cached research',
|
|
17
24
|
order: 'first',
|
|
18
25
|
};
|
|
19
|
-
// `list` answers "what pages/notes do I have?" and deliberately omits section children (see
|
|
20
|
-
// scanCacheDirForList), so `section` is not an offered filter — every other artifact type can appear.
|
|
21
|
-
const LISTABLE_ARTIFACT_TYPES = ARTIFACT_TYPES.filter((type) => type !== 'section');
|
|
22
26
|
/** Project a full row down to the default minimal shape (see {@link ListRowMinimal}). */
|
|
23
27
|
function toMinimalListRow(row) {
|
|
24
28
|
return {
|
|
@@ -28,13 +32,6 @@ function toMinimalListRow(row) {
|
|
|
28
32
|
tokenEstimate: row.tokenEstimate,
|
|
29
33
|
};
|
|
30
34
|
}
|
|
31
|
-
/** Count matched rows per freshness tier for the envelope's `summary.byFreshness`. */
|
|
32
|
-
function countByFreshness(rows) {
|
|
33
|
-
const counts = { fresh: 0, stale_grace: 0, stale_expired: 0 };
|
|
34
|
-
for (const row of rows)
|
|
35
|
-
counts[row.freshness]++;
|
|
36
|
-
return counts;
|
|
37
|
-
}
|
|
38
35
|
/** List cached research artifacts by metadata filters. */
|
|
39
36
|
export default class ResearchList extends BaseCommand {
|
|
40
37
|
static id = 'list';
|
|
@@ -70,57 +67,30 @@ export default class ResearchList extends BaseCommand {
|
|
|
70
67
|
},
|
|
71
68
|
];
|
|
72
69
|
static flags = {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
description: CLI_FLAG_DESCRIPTIONS.filterTopic,
|
|
76
|
-
}),
|
|
77
|
-
tags: Flags.string({
|
|
78
|
-
char: 'g',
|
|
79
|
-
description: CLI_FLAG_DESCRIPTIONS.filterTags,
|
|
80
|
-
multiple: true,
|
|
81
|
-
}),
|
|
82
|
-
url: Flags.string({
|
|
83
|
-
description: CLI_FLAG_DESCRIPTIONS.sourceUrlGlob,
|
|
84
|
-
}),
|
|
85
|
-
freshness: Flags.option({
|
|
86
|
-
description: 'freshness state',
|
|
87
|
-
options: ['fresh', 'stale_grace', 'stale_expired'],
|
|
88
|
-
})(),
|
|
89
|
-
'artifact-type': Flags.option({
|
|
90
|
-
description: CLI_FLAG_DESCRIPTIONS.listArtifactType,
|
|
91
|
-
options: LISTABLE_ARTIFACT_TYPES,
|
|
92
|
-
})(),
|
|
93
|
-
'capture-method': Flags.option({
|
|
94
|
-
description: 'capture method',
|
|
95
|
-
options: CAPTURE_METHODS,
|
|
96
|
-
})(),
|
|
97
|
-
limit: limitFlag(LIST_DEFAULT_MAX_LIMIT, 50, `result count (max ${LIST_DEFAULT_MAX_LIMIT})`),
|
|
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})`),
|
|
98
72
|
full: Flags.boolean({
|
|
99
73
|
default: false,
|
|
100
74
|
description: CLI_FLAG_DESCRIPTIONS.listFull,
|
|
101
75
|
}),
|
|
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 }),
|
|
102
79
|
};
|
|
103
80
|
static stdoutIsPrimaryData = true;
|
|
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
|
+
};
|
|
91
|
+
}
|
|
104
92
|
matchesFilters(meta, freshness) {
|
|
105
|
-
|
|
106
|
-
return false;
|
|
107
|
-
}
|
|
108
|
-
if (!matchesTagsFilter(meta, this.flags.tags)) {
|
|
109
|
-
return false;
|
|
110
|
-
}
|
|
111
|
-
if (this.flags.url && !artifactMatchesUrlFilter(meta, this.flags.url)) {
|
|
112
|
-
return false;
|
|
113
|
-
}
|
|
114
|
-
if (this.flags['artifact-type'] && meta.artifact_type !== this.flags['artifact-type']) {
|
|
115
|
-
return false;
|
|
116
|
-
}
|
|
117
|
-
if (this.flags['capture-method'] && meta.capture_method !== this.flags['capture-method']) {
|
|
118
|
-
return false;
|
|
119
|
-
}
|
|
120
|
-
if (this.flags.freshness && freshness !== this.flags.freshness) {
|
|
121
|
-
return false;
|
|
122
|
-
}
|
|
123
|
-
return true;
|
|
93
|
+
return matchesCommonMetadataFilters(meta, freshness, this.metadataFilterFlags());
|
|
124
94
|
}
|
|
125
95
|
scanCacheDirForList(readRoots, currentTime) {
|
|
126
96
|
// Honor effective read-only: listing must not persist the derived search-index sidecar.
|
|
@@ -131,38 +101,30 @@ export default class ResearchList extends BaseCommand {
|
|
|
131
101
|
// listing (one page yields dozens) and aren't in the documented source/research_note contract.
|
|
132
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
|
|
133
103
|
// unconditional guard owns the exclusion (the default no-filter case relies on it);
|
|
134
|
-
//
|
|
104
|
+
// PAGE_LEVEL_ARTIFACT_TYPES just hides `section` from --artifact-type so no one filters for a
|
|
135
105
|
// type list can never return. Keep both in sync if section handling ever changes.
|
|
136
106
|
if (artifact.metadata.artifact_type === 'section')
|
|
137
107
|
return null;
|
|
138
108
|
const freshness = evaluateFreshness(artifact.metadata, currentTime, null);
|
|
139
109
|
if (!this.matchesFilters(artifact.metadata, freshness))
|
|
140
110
|
return null;
|
|
141
|
-
return
|
|
142
|
-
cacheKey: artifact.metadata.cache_key,
|
|
143
|
-
path: filePath,
|
|
144
|
-
artifactType: artifact.metadata.artifact_type,
|
|
145
|
-
sourceUrls: artifact.metadata.source_urls,
|
|
146
|
-
topic: artifact.metadata.topic,
|
|
147
|
-
tags: artifact.metadata.tags,
|
|
148
|
-
freshness,
|
|
149
|
-
captureMethod: artifact.metadata.capture_method,
|
|
150
|
-
tokenEstimate: artifact.metadata.token_estimate,
|
|
151
|
-
qualityNotes: artifact.metadata.quality_notes,
|
|
152
|
-
fetchedAt: artifact.metadata.fetched_at,
|
|
153
|
-
validatedAt: artifact.metadata.validated_at,
|
|
154
|
-
};
|
|
111
|
+
return toListRow(artifact, filePath, freshness);
|
|
155
112
|
}, { persistIndex: !this.readOnly });
|
|
156
113
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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`);
|
|
164
126
|
}
|
|
165
|
-
logListResults(finalResults, totalMatched) {
|
|
127
|
+
logListResults(finalResults, totalMatched, nextCommand) {
|
|
166
128
|
if (finalResults.length === 0) {
|
|
167
129
|
this.emitEmptyListGuidance();
|
|
168
130
|
return;
|
|
@@ -171,22 +133,13 @@ export default class ResearchList extends BaseCommand {
|
|
|
171
133
|
return;
|
|
172
134
|
this.log(`${resultListHeading(totalMatched, finalResults.length, LIST_LABELS)}\n`);
|
|
173
135
|
finalResults.forEach((res, index) => {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
: colors.gray(NO_TOPIC_LABEL);
|
|
177
|
-
const keyStr = colors.bold(res.cacheKey);
|
|
178
|
-
this.log(`${index + 1}. [${topicStr}] Key: ${keyStr}`);
|
|
179
|
-
const freshnessColorMap = {
|
|
180
|
-
fresh: colors.green,
|
|
181
|
-
stale_grace: colors.yellow,
|
|
182
|
-
stale_expired: colors.red,
|
|
183
|
-
};
|
|
184
|
-
const freshnessColor = freshnessColorMap[res.freshness];
|
|
185
|
-
const freshnessStr = freshnessColor(res.freshness);
|
|
136
|
+
this.log(formatResultRowHeader(index, res.topic, res.cacheKey));
|
|
137
|
+
const freshnessStr = FRESHNESS_COLOR[res.freshness](res.freshness);
|
|
186
138
|
this.log(` Type: ${colors.bold(res.artifactType)} | Freshness: ${freshnessStr}`);
|
|
187
|
-
this.log(
|
|
188
|
-
this.log(
|
|
139
|
+
this.log(formatResultRowTokens(res.tokenEstimate));
|
|
140
|
+
this.log(formatResultRowSourceUrls(res.sourceUrls));
|
|
189
141
|
});
|
|
142
|
+
this.tipNextCommand(nextCommand);
|
|
190
143
|
}
|
|
191
144
|
/**
|
|
192
145
|
* Empty-cache / no-match tip (human mode only). Under `--json`/`--toon`, `data: []` is paired with
|
|
@@ -195,7 +148,7 @@ export default class ResearchList extends BaseCommand {
|
|
|
195
148
|
emitEmptyListGuidance() {
|
|
196
149
|
if (this.jsonEnabled())
|
|
197
150
|
return;
|
|
198
|
-
const filtered = this.
|
|
151
|
+
const filtered = hasActiveMetadataFilters(this.metadataFilterFlags());
|
|
199
152
|
const headline = filtered
|
|
200
153
|
? 'No cached research entries match the given filters.'
|
|
201
154
|
: 'No cached research entries found.';
|
|
@@ -217,6 +170,7 @@ export default class ResearchList extends BaseCommand {
|
|
|
217
170
|
emptyTagsFilterError(this.flags.tags);
|
|
218
171
|
if (flagErr)
|
|
219
172
|
this.error(flagErr, { exit: 2, code: 'INVALID_FLAG_VALUE' });
|
|
173
|
+
this.logIdentityHeader();
|
|
220
174
|
const roots = loadStoreRoots({
|
|
221
175
|
configDir: this.config.configDir,
|
|
222
176
|
cwd: process.cwd(),
|
|
@@ -230,17 +184,24 @@ export default class ResearchList extends BaseCommand {
|
|
|
230
184
|
return timeB - timeA;
|
|
231
185
|
});
|
|
232
186
|
const finalResults = results.slice(0, this.flags.limit);
|
|
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;
|
|
233
193
|
// Human heading carries truncation inline; --json/--toon get the same counts (plus a freshness
|
|
234
194
|
// breakdown and an explicit `empty` flag) as an always-present envelope `summary` object.
|
|
235
195
|
this.jsonSummary = {
|
|
236
196
|
total: results.length,
|
|
237
197
|
shown: finalResults.length,
|
|
238
198
|
limit: this.flags.limit,
|
|
239
|
-
truncated
|
|
199
|
+
truncated,
|
|
240
200
|
empty: results.length === 0,
|
|
241
201
|
byFreshness: countByFreshness(results),
|
|
202
|
+
nextCommand,
|
|
242
203
|
};
|
|
243
|
-
this.logListResults(finalResults, results.length);
|
|
204
|
+
this.logListResults(finalResults, results.length, nextCommand);
|
|
244
205
|
return this.flags.full ? finalResults : finalResults.map(toMinimalListRow);
|
|
245
206
|
}
|
|
246
207
|
/** Attach the aggregate `summary` computed in `run()` (see {@link ListSummary}). */
|
|
@@ -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
|
+
}
|