@taurgis/bonsai 3.2.2 → 4.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 +2 -2
- package/agents/agents/salesforce-docs-researcher.agent.md +4 -4
- package/bin/cli.mjs +7 -4
- package/dist/base-command.d.ts +37 -0
- package/dist/base-command.js +97 -6
- package/dist/commands/config/base.js +1 -0
- 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/fetch.js +4 -0
- package/dist/commands/inspect.js +11 -2
- package/dist/commands/list.d.ts +12 -9
- package/dist/commands/list.js +67 -39
- package/dist/commands/prune.d.ts +2 -0
- package/dist/commands/prune.js +35 -4
- package/dist/commands/status.js +10 -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 +27 -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 +6 -4
- package/dist/lib/cli-presentation.js +7 -4
- package/dist/lib/cli-result-types.d.ts +25 -1
- 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/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/fetch-command-service.js +80 -23
- package/dist/lib/research/fetcher.js +3 -1
- package/dist/lib/research/import-command-service.js +22 -1
- package/dist/lib/research/metadata-filters.d.ts +18 -0
- package/dist/lib/research/metadata-filters.js +37 -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/storage.d.ts +4 -7
- package/dist/lib/research/storage.js +26 -25
- package/dist/lib/text.d.ts +14 -8
- package/dist/lib/text.js +21 -9
- package/oclif.manifest.json +120 -12
- package/package.json +2 -1
package/dist/commands/list.js
CHANGED
|
@@ -4,9 +4,10 @@ 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
6
|
import { ARTIFACT_TYPES, CAPTURE_METHODS, } from '../lib/research/schema.js';
|
|
7
|
-
import { NO_TOPIC_LABEL, resultListHeading } from '../lib/text.js';
|
|
7
|
+
import { NO_TOPIC_LABEL, formatTip, resultListHeading, sanitizeForTerminal, } from '../lib/text.js';
|
|
8
8
|
import { limitFlag } from '../lib/limit-flag.js';
|
|
9
9
|
import { artifactMatchesUrlFilter, emptyUrlFilterError } from '../lib/research/url.js';
|
|
10
|
+
import { matchesTopicFilter, matchesTagsFilter, emptyTopicFilterError, emptyTagsFilterError, } from '../lib/research/metadata-filters.js';
|
|
10
11
|
import { colors } from '../lib/color.js';
|
|
11
12
|
import { CLI_FLAG_DESCRIPTIONS } from '../lib/cli-presentation.js';
|
|
12
13
|
// Listings are ordered newest-first, so the truncation word is "first"; --limit caps at this value.
|
|
@@ -14,21 +15,38 @@ const LIST_DEFAULT_MAX_LIMIT = 100;
|
|
|
14
15
|
const LIST_LABELS = {
|
|
15
16
|
noun: 'cached research',
|
|
16
17
|
order: 'first',
|
|
17
|
-
maxLimit: LIST_DEFAULT_MAX_LIMIT,
|
|
18
18
|
};
|
|
19
19
|
// `list` answers "what pages/notes do I have?" and deliberately omits section children (see
|
|
20
20
|
// scanCacheDirForList), so `section` is not an offered filter — every other artifact type can appear.
|
|
21
21
|
const LISTABLE_ARTIFACT_TYPES = ARTIFACT_TYPES.filter((type) => type !== 'section');
|
|
22
|
+
/** Project a full row down to the default minimal shape (see {@link ListRowMinimal}). */
|
|
23
|
+
function toMinimalListRow(row) {
|
|
24
|
+
return {
|
|
25
|
+
sourceUrls: row.sourceUrls,
|
|
26
|
+
topic: row.topic,
|
|
27
|
+
freshness: row.freshness,
|
|
28
|
+
tokenEstimate: row.tokenEstimate,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
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
|
+
}
|
|
22
38
|
/** List cached research artifacts by metadata filters. */
|
|
23
39
|
export default class ResearchList extends BaseCommand {
|
|
24
40
|
static id = 'list';
|
|
25
41
|
static summary = 'List cached research artifacts by metadata';
|
|
26
|
-
static description = 'List page-level cached artifacts
|
|
42
|
+
static description = 'List page-level cached artifacts by source URL, topic, freshness, and token estimate. ' +
|
|
43
|
+
'Pass --full for every metadata field (cache key, path, artifact type, tags, capture method, quality notes, timestamps).';
|
|
27
44
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
45
|
+
* Aggregate counts (and an explicit empty/truncation signal) attached to the success envelope so
|
|
46
|
+
* agents get a cache-wide summary without a separate round trip (AXI principles: pre-computed
|
|
47
|
+
* aggregates, definitive empty states). Always set by `run()` before it returns.
|
|
30
48
|
*/
|
|
31
|
-
|
|
49
|
+
jsonSummary = null;
|
|
32
50
|
static examples = [
|
|
33
51
|
{
|
|
34
52
|
description: 'list cached entries',
|
|
@@ -46,6 +64,10 @@ export default class ResearchList extends BaseCommand {
|
|
|
46
64
|
description: 'list entries matching a URL glob',
|
|
47
65
|
command: '<%= config.bin %> list --url "https://react.dev/*"',
|
|
48
66
|
},
|
|
67
|
+
{
|
|
68
|
+
description: 'list every metadata field instead of the minimal default row',
|
|
69
|
+
command: '<%= config.bin %> list --full --json',
|
|
70
|
+
},
|
|
49
71
|
];
|
|
50
72
|
static flags = {
|
|
51
73
|
topic: Flags.string({
|
|
@@ -72,25 +94,18 @@ export default class ResearchList extends BaseCommand {
|
|
|
72
94
|
description: 'capture method',
|
|
73
95
|
options: CAPTURE_METHODS,
|
|
74
96
|
})(),
|
|
75
|
-
limit: limitFlag(
|
|
97
|
+
limit: limitFlag(LIST_DEFAULT_MAX_LIMIT, 50, `result count (max ${LIST_DEFAULT_MAX_LIMIT})`),
|
|
98
|
+
full: Flags.boolean({
|
|
99
|
+
default: false,
|
|
100
|
+
description: CLI_FLAG_DESCRIPTIONS.listFull,
|
|
101
|
+
}),
|
|
76
102
|
};
|
|
77
103
|
static stdoutIsPrimaryData = true;
|
|
78
|
-
matchesTopic(meta, topic) {
|
|
79
|
-
if (!topic)
|
|
80
|
-
return true;
|
|
81
|
-
return !!meta.topic && meta.topic.trim().toLowerCase() === topic.trim().toLowerCase();
|
|
82
|
-
}
|
|
83
|
-
matchesTags(meta, tags) {
|
|
84
|
-
if (!tags || tags.length === 0)
|
|
85
|
-
return true;
|
|
86
|
-
const metaTagsLower = meta.tags.map((t) => t.toLowerCase());
|
|
87
|
-
return tags.every((t) => metaTagsLower.includes(t.toLowerCase()));
|
|
88
|
-
}
|
|
89
104
|
matchesFilters(meta, freshness) {
|
|
90
|
-
if (!
|
|
105
|
+
if (!matchesTopicFilter(meta, this.flags.topic)) {
|
|
91
106
|
return false;
|
|
92
107
|
}
|
|
93
|
-
if (!
|
|
108
|
+
if (!matchesTagsFilter(meta, this.flags.tags)) {
|
|
94
109
|
return false;
|
|
95
110
|
}
|
|
96
111
|
if (this.flags.url && !artifactMatchesUrlFilter(meta, this.flags.url)) {
|
|
@@ -156,7 +171,9 @@ export default class ResearchList extends BaseCommand {
|
|
|
156
171
|
return;
|
|
157
172
|
this.log(`${resultListHeading(totalMatched, finalResults.length, LIST_LABELS)}\n`);
|
|
158
173
|
finalResults.forEach((res, index) => {
|
|
159
|
-
const topicStr = res.topic
|
|
174
|
+
const topicStr = res.topic
|
|
175
|
+
? colors.cyan(sanitizeForTerminal(res.topic))
|
|
176
|
+
: colors.gray(NO_TOPIC_LABEL);
|
|
160
177
|
const keyStr = colors.bold(res.cacheKey);
|
|
161
178
|
this.log(`${index + 1}. [${topicStr}] Key: ${keyStr}`);
|
|
162
179
|
const freshnessColorMap = {
|
|
@@ -171,7 +188,10 @@ export default class ResearchList extends BaseCommand {
|
|
|
171
188
|
this.log(` Source URLs: ${colors.gray(res.sourceUrls.join(', '))}\n`);
|
|
172
189
|
});
|
|
173
190
|
}
|
|
174
|
-
/**
|
|
191
|
+
/**
|
|
192
|
+
* Empty-cache / no-match tip (human mode only). Under `--json`/`--toon`, `data: []` is paired with
|
|
193
|
+
* `summary.empty: true` instead of prose — see {@link toSuccessJson}.
|
|
194
|
+
*/
|
|
175
195
|
emitEmptyListGuidance() {
|
|
176
196
|
if (this.jsonEnabled())
|
|
177
197
|
return;
|
|
@@ -184,12 +204,19 @@ export default class ResearchList extends BaseCommand {
|
|
|
184
204
|
? 'try relaxing filters, or list everything: '
|
|
185
205
|
: 'populate the cache first: ';
|
|
186
206
|
this.log(headline);
|
|
187
|
-
this.log(`\
|
|
207
|
+
this.log(`\n${formatTip(`${tipLead}${colors.cyan(tipCmd)}`)}`);
|
|
208
|
+
// Bare `bonsai` (no args) redirects here (content-first default), so an empty, unfiltered cache
|
|
209
|
+
// is also the first thing a brand-new user sees — point at --help so command discovery isn't lost.
|
|
210
|
+
if (!filtered) {
|
|
211
|
+
this.log(formatTip(`see every command: ${colors.cyan(`${this.config.bin} --help`)}`));
|
|
212
|
+
}
|
|
188
213
|
}
|
|
189
214
|
async run() {
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
this.
|
|
215
|
+
const flagErr = emptyUrlFilterError(this.flags.url) ??
|
|
216
|
+
emptyTopicFilterError(this.flags.topic) ??
|
|
217
|
+
emptyTagsFilterError(this.flags.tags);
|
|
218
|
+
if (flagErr)
|
|
219
|
+
this.error(flagErr, { exit: 2, code: 'INVALID_FLAG_VALUE' });
|
|
193
220
|
const roots = loadStoreRoots({
|
|
194
221
|
configDir: this.config.configDir,
|
|
195
222
|
cwd: process.cwd(),
|
|
@@ -203,23 +230,24 @@ export default class ResearchList extends BaseCommand {
|
|
|
203
230
|
return timeB - timeA;
|
|
204
231
|
});
|
|
205
232
|
const finalResults = results.slice(0, this.flags.limit);
|
|
206
|
-
// Human heading carries truncation; --json
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
233
|
+
// Human heading carries truncation inline; --json/--toon get the same counts (plus a freshness
|
|
234
|
+
// breakdown and an explicit `empty` flag) as an always-present envelope `summary` object.
|
|
235
|
+
this.jsonSummary = {
|
|
236
|
+
total: results.length,
|
|
237
|
+
shown: finalResults.length,
|
|
238
|
+
limit: this.flags.limit,
|
|
239
|
+
truncated: results.length > finalResults.length,
|
|
240
|
+
empty: results.length === 0,
|
|
241
|
+
byFreshness: countByFreshness(results),
|
|
242
|
+
};
|
|
215
243
|
this.logListResults(finalResults, results.length);
|
|
216
|
-
return finalResults;
|
|
244
|
+
return this.flags.full ? finalResults : finalResults.map(toMinimalListRow);
|
|
217
245
|
}
|
|
218
|
-
/** Attach `
|
|
246
|
+
/** Attach the aggregate `summary` computed in `run()` (see {@link ListSummary}). */
|
|
219
247
|
toSuccessJson(data) {
|
|
220
248
|
const envelope = this.baseSuccessJson(data);
|
|
221
|
-
if (!this.
|
|
249
|
+
if (!this.jsonSummary)
|
|
222
250
|
return envelope;
|
|
223
|
-
return { ...envelope,
|
|
251
|
+
return { ...envelope, summary: this.jsonSummary };
|
|
224
252
|
}
|
|
225
253
|
}
|
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
|
}
|
package/dist/commands/status.js
CHANGED
|
@@ -32,6 +32,10 @@ export default class ResearchStatus extends BaseCommand {
|
|
|
32
32
|
description: 'evaluate status with a specific tier and TTL',
|
|
33
33
|
command: '<%= config.bin %> <%= command.id %> https://example.com/docs --tier volatile --ttl 2h',
|
|
34
34
|
},
|
|
35
|
+
{
|
|
36
|
+
description: 'check multiple URLs in one batch (space-separated)',
|
|
37
|
+
command: '<%= config.bin %> <%= command.id %> https://example.com/docs https://example.com/api',
|
|
38
|
+
},
|
|
35
39
|
];
|
|
36
40
|
static strict = false;
|
|
37
41
|
static args = {
|
|
@@ -83,6 +87,12 @@ export default class ResearchStatus extends BaseCommand {
|
|
|
83
87
|
if (result.status === 'miss') {
|
|
84
88
|
this.warn(cacheMissHint(this.config.bin, normalizedUrl));
|
|
85
89
|
}
|
|
90
|
+
else if (result.action === 'would_revalidate') {
|
|
91
|
+
this.tip(`${this.config.bin} ${normalizedUrl} to revalidate.`);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
this.tip(`${this.config.bin} inspect ${normalizedUrl} for full metadata.`);
|
|
95
|
+
}
|
|
86
96
|
}
|
|
87
97
|
return {
|
|
88
98
|
cacheKey,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Hook, type Interfaces } from '@oclif/core';
|
|
2
|
-
export declare function buildCommandNotFoundDetails(
|
|
2
|
+
export declare function buildCommandNotFoundDetails(rawId: string, argv: string[] | undefined, config: Interfaces.Config): {
|
|
3
3
|
code: 'COMMAND_NOT_FOUND' | 'MISSING_URL_SCHEME' | 'UNEXPECTED_ARGUMENT';
|
|
4
4
|
command: string;
|
|
5
5
|
jsonMode: boolean;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { toConfiguredId } from '@oclif/core';
|
|
2
|
-
import { closestMatch, maxFuzzyDistance } from '../../lib/text.js';
|
|
2
|
+
import { closestMatch, maxFuzzyDistance, sanitizeForTerminal } from '../../lib/text.js';
|
|
3
3
|
import { writeCommandNotFoundJson } from '../../lib/cli-emit.js';
|
|
4
4
|
import { looksLikeSchemelessUrl } from '../../lib/research/url.js';
|
|
5
5
|
/**
|
|
@@ -97,7 +97,17 @@ function missingUrlSchemeDetails(urls, argv, config) {
|
|
|
97
97
|
suggestions: [suggestion],
|
|
98
98
|
};
|
|
99
99
|
}
|
|
100
|
-
export function buildCommandNotFoundDetails(
|
|
100
|
+
export function buildCommandNotFoundDetails(rawId, argv, config) {
|
|
101
|
+
// The attempted id is raw user input echoed straight back into this error's message and
|
|
102
|
+
// suggestions — untrusted per the repo's trust-boundary rules, same reasoning as
|
|
103
|
+
// BaseCommand.error(). Sanitize once, up front, so every derived value below (segments, the
|
|
104
|
+
// display-only "attempted" string, a bare-URL correction) is already safe; a real command/topic id
|
|
105
|
+
// never contains control bytes, so this only affects input that could never have matched one
|
|
106
|
+
// anyway. Gate on jsonMode exactly like BaseCommand.error() does, so `--json` keeps
|
|
107
|
+
// byte-for-byte fidelity — this hook fires before a Command instance exists, so there's no
|
|
108
|
+
// `jsonEnabled()` to call, but the same boolean is cheap to compute directly from argv.
|
|
109
|
+
const jsonMode = isJsonMode(argv);
|
|
110
|
+
const id = jsonMode ? rawId : sanitizeForTerminal(rawId);
|
|
101
111
|
// Match against every loaded command id — including hidden ones that stay invokable
|
|
102
112
|
// (`fetch`, plugin internals). Root `--help` hides them; typo recovery should not.
|
|
103
113
|
const commandIds = config.commandIDs;
|
package/dist/lib/argv.d.ts
CHANGED
|
@@ -16,6 +16,11 @@ export interface NormalizationResult {
|
|
|
16
16
|
export interface ArgvNormalizeOptions {
|
|
17
17
|
/** Tokens that consume the next argv value (`--topic`, `-t`, …). From cli-flag-manifest. */
|
|
18
18
|
valueTakingFlags: ReadonlySet<string>;
|
|
19
|
+
/**
|
|
20
|
+
* Root segment of every registered command id (`config:get` → `config`). From cli-flag-manifest.
|
|
21
|
+
* A `word:` token whose root is in this set is a command (`config:get`), never a URL scheme.
|
|
22
|
+
*/
|
|
23
|
+
knownCommandRoots: ReadonlySet<string>;
|
|
19
24
|
}
|
|
20
25
|
/**
|
|
21
26
|
* Normalize raw process argv for oclif. `valueTakingFlags` must come from the composition root
|
package/dist/lib/argv.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { EXIT_USAGE } from './cli-error-policy.js';
|
|
2
2
|
import { buildCliErrorEnvelope } from './envelope.js';
|
|
3
|
+
import { sanitizeForTerminal } from './text.js';
|
|
3
4
|
/** Shared MISSING_COMMAND copy for flag-only argv preflight. */
|
|
4
5
|
function missingCommandDetails(bin, swallowed) {
|
|
5
6
|
if (swallowed) {
|
|
@@ -26,7 +27,7 @@ function missingCommandDetails(bin, swallowed) {
|
|
|
26
27
|
* it picks a command, so `bonsai --read-only list` would otherwise treat `--read-only` as the
|
|
27
28
|
* command id. Strip and re-append them after the command/URL, same pattern as `--json`.
|
|
28
29
|
*/
|
|
29
|
-
const GLOBAL_BOOLEAN_FLAGS = new Set(['--read-only', '--plan']);
|
|
30
|
+
const GLOBAL_BOOLEAN_FLAGS = new Set(['--read-only', '--plan', '--toon']);
|
|
30
31
|
/**
|
|
31
32
|
* When a value-taking flag is the only "command" and its value looks like a URL, the user almost
|
|
32
33
|
* certainly put the URL where the flag value belongs (e.g. `bonsai --tags https://example.com`).
|
|
@@ -47,7 +48,13 @@ function findSwallowedUrlFlag(argv, valueTakingFlags) {
|
|
|
47
48
|
return null;
|
|
48
49
|
}
|
|
49
50
|
function missingUsageExit(json, argv, valueTakingFlags) {
|
|
50
|
-
const
|
|
51
|
+
const swallowed = findSwallowedUrlFlag(argv, valueTakingFlags);
|
|
52
|
+
// `swallowed.url` is raw user input echoed straight back into the MISSING_COMMAND message and its
|
|
53
|
+
// "pass the URL as the command" suggestion — untrusted per the repo's trust-boundary rules, same
|
|
54
|
+
// reasoning as BaseCommand.error(). This preflight path fires before oclif ever dispatches a
|
|
55
|
+
// command, so there is no `this.jsonEnabled()` to call — but `json` is already known here, so gate
|
|
56
|
+
// on it the same way: sanitize only the human-mode message, keep `--json` byte-for-byte.
|
|
57
|
+
const details = missingCommandDetails('bonsai', swallowed && !json ? { ...swallowed, url: sanitizeForTerminal(swallowed.url) } : swallowed);
|
|
51
58
|
return {
|
|
52
59
|
exitCode: EXIT_USAGE,
|
|
53
60
|
json,
|
|
@@ -86,7 +93,15 @@ function hasCommandToken(argv, valueTakingFlags) {
|
|
|
86
93
|
* (`cli-flag-manifest`) — this module stays free of command registry imports.
|
|
87
94
|
*/
|
|
88
95
|
export function normalizeArgv(rawArgv, options) {
|
|
89
|
-
const { valueTakingFlags } = options;
|
|
96
|
+
const { valueTakingFlags, knownCommandRoots } = options;
|
|
97
|
+
// AXI "content first": a truly bare invocation (`bonsai`, no args/flags at all) shows live cache
|
|
98
|
+
// data instead of oclif's default root help — `bonsai help`, `--help`, and `-h` remain the
|
|
99
|
+
// explicit path to the command reference (all of them are non-empty argv and skip this branch).
|
|
100
|
+
// oclif has no built-in "default command" concept for empty argv (see oclif/oclif#277), so this
|
|
101
|
+
// is a one-line argv rewrite, the same technique the URL-shorthand rewrite below already uses.
|
|
102
|
+
if (rawArgv.length === 0) {
|
|
103
|
+
return { argv: ['list'] };
|
|
104
|
+
}
|
|
90
105
|
// oclif's JSON flag is command-scoped, so `bonsai list --json` works but
|
|
91
106
|
// `bonsai --json list` is otherwise parsed as an unknown command named
|
|
92
107
|
// "--json". Collect every --json, append one copy after the command/URL, and
|
|
@@ -113,8 +128,15 @@ export function normalizeArgv(rawArgv, options) {
|
|
|
113
128
|
// forms like `javascript:` or `data:` so fetch can reject unsupported protocols instead of oclif
|
|
114
129
|
// reporting a misleading "command not found". If the invocation begins with a flag, allow common
|
|
115
130
|
// flag-before-argument usage such as `bonsai --format detailed https://example.com`.
|
|
131
|
+
// Exclude tokens whose part before the first `:` names a real command (`config:get`) — oclif
|
|
132
|
+
// joins namespaced command ids with `:` regardless of the display-only topicSeparator, so those
|
|
133
|
+
// are commands, not URL schemes, and must reach normal command dispatch instead of a misleading
|
|
134
|
+
// "Invalid URL" error.
|
|
116
135
|
const firstNonFlagArgIndex = firstPositionalIndex(core, valueTakingFlags);
|
|
117
|
-
const
|
|
136
|
+
const firstArg = firstNonFlagArgIndex !== -1 ? core[firstNonFlagArgIndex] : undefined;
|
|
137
|
+
const rootFetchShape = firstArg !== undefined &&
|
|
138
|
+
looksLikeUrl(firstArg) &&
|
|
139
|
+
!knownCommandRoots.has(firstArg.split(':')[0]);
|
|
118
140
|
if (rootFetchShape) {
|
|
119
141
|
const url = core[firstNonFlagArgIndex];
|
|
120
142
|
core = [
|
|
@@ -131,7 +153,7 @@ export function normalizeArgv(rawArgv, options) {
|
|
|
131
153
|
core.push('--json');
|
|
132
154
|
// Flag-only argv (`--json`, `--read-only`, `--tags https://…`) never resolves a command.
|
|
133
155
|
// Exit here so command_not_found does not special-case dash-prefixed ids.
|
|
134
|
-
//
|
|
156
|
+
// Truly empty argv already returned above; `--help`/`--version` stay with oclif here.
|
|
135
157
|
if (!helpRequested && core.length > 0 && !hasCommandToken(core, valueTakingFlags)) {
|
|
136
158
|
return {
|
|
137
159
|
argv: jsonMode ? ['--json'] : [],
|
|
@@ -3,3 +3,10 @@
|
|
|
3
3
|
* Built from command class metadata so argv normalization stays free of command imports.
|
|
4
4
|
*/
|
|
5
5
|
export declare const VALUE_TAKING_FLAG_TOKENS: ReadonlySet<string>;
|
|
6
|
+
/**
|
|
7
|
+
* Root segment of every registered command id (`config:get` → `config`). oclif joins namespaced
|
|
8
|
+
* command ids with `:` regardless of the display-only `topicSeparator` setting, so a token like
|
|
9
|
+
* `config:get` is a real command, not a URL — argv normalization checks this set before assuming
|
|
10
|
+
* `word:` is a URL scheme.
|
|
11
|
+
*/
|
|
12
|
+
export declare const KNOWN_COMMAND_ROOT_TOKENS: ReadonlySet<string>;
|
|
@@ -6,3 +6,10 @@ import { valueTakingFlagTokensFromCommands } from './value-flags.js';
|
|
|
6
6
|
* Built from command class metadata so argv normalization stays free of command imports.
|
|
7
7
|
*/
|
|
8
8
|
export const VALUE_TAKING_FLAG_TOKENS = valueTakingFlagTokensFromCommands(commands, BaseCommand.baseFlags);
|
|
9
|
+
/**
|
|
10
|
+
* Root segment of every registered command id (`config:get` → `config`). oclif joins namespaced
|
|
11
|
+
* command ids with `:` regardless of the display-only `topicSeparator` setting, so a token like
|
|
12
|
+
* `config:get` is a real command, not a URL — argv normalization checks this set before assuming
|
|
13
|
+
* `word:` is a URL scheme.
|
|
14
|
+
*/
|
|
15
|
+
export const KNOWN_COMMAND_ROOT_TOKENS = new Set(Object.keys(commands).map((id) => id.split(':')[0]));
|
|
@@ -8,17 +8,19 @@ export declare const CLI_FLAG_DESCRIPTIONS: {
|
|
|
8
8
|
readonly maxAge: "maximum cache age to accept (e.g. \"2h\", \"7d\", \"6m\")";
|
|
9
9
|
readonly fetchStorage: "override where this result is cached (secrets always stored globally)";
|
|
10
10
|
readonly importStorage: "override where this note is cached (secrets always stored globally)";
|
|
11
|
-
readonly fetchTopic: "main research topic for metadata";
|
|
12
|
-
readonly importTopic: "main topic for this research note";
|
|
11
|
+
readonly fetchTopic: "main research topic for metadata (max 200 chars)";
|
|
12
|
+
readonly importTopic: "main topic for this research note (max 200 chars)";
|
|
13
13
|
readonly filterTopic: "exact topic (case-insensitive)";
|
|
14
|
-
readonly fetchTags: "taxonomic tags for this research (can be repeated)";
|
|
15
|
-
readonly importTags: "taxonomic tags (can be repeated)";
|
|
14
|
+
readonly fetchTags: "taxonomic tags for this research (can be repeated, max 100 chars each)";
|
|
15
|
+
readonly importTags: "taxonomic tags (can be repeated, max 100 chars each)";
|
|
16
16
|
readonly filterTags: "tags to require (must match all)";
|
|
17
17
|
readonly format: "output format";
|
|
18
18
|
readonly readOnly: "(alias: --plan) block filesystem writes/deletes; network fetches still run; also honored via BONSAI_READ_ONLY/BONSAI_PLAN_MODE";
|
|
19
|
+
readonly toon: "emit the same envelope as --json, encoded as TOON (fewer tokens); cannot be combined with --json";
|
|
19
20
|
readonly sourceUrlGlob: "source URL glob (case-insensitive, supports * wildcard)";
|
|
20
21
|
readonly listArtifactType: "artifact type; section children are omitted from list - use inspect to see them";
|
|
21
22
|
readonly pruneArtifactType: "artifact type to prune, including section children";
|
|
23
|
+
readonly listFull: "include every metadata field (cache key, path, artifact type, tags, capture method, quality notes, timestamps); default output is a minimal row (source URLs, topic, freshness, token estimate)";
|
|
22
24
|
};
|
|
23
25
|
/** One human-readable label/value pair for CLI stdout. */
|
|
24
26
|
export type HumanField = readonly [label: string, value: string];
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { colors } from './color.js';
|
|
2
|
+
import { MAX_TAG_LENGTH, MAX_TOPIC_LENGTH } from './research/metadata-flags.js';
|
|
2
3
|
const HUMAN_LABEL_WIDTH = 25;
|
|
3
4
|
// Repeated verbatim across several flag descriptions; single-use wording lives inline below.
|
|
4
5
|
const DURATION_EXAMPLES = 'e.g. "2h", "7d", "6m"';
|
|
@@ -13,17 +14,19 @@ export const CLI_FLAG_DESCRIPTIONS = {
|
|
|
13
14
|
maxAge: `maximum cache age to accept (${DURATION_EXAMPLES})`,
|
|
14
15
|
fetchStorage: `override where this result is cached (${SECRET_STORAGE_NOTE})`,
|
|
15
16
|
importStorage: `override where this note is cached (${SECRET_STORAGE_NOTE})`,
|
|
16
|
-
fetchTopic:
|
|
17
|
-
importTopic:
|
|
17
|
+
fetchTopic: `main research topic for metadata (max ${MAX_TOPIC_LENGTH} chars)`,
|
|
18
|
+
importTopic: `main topic for this research note (max ${MAX_TOPIC_LENGTH} chars)`,
|
|
18
19
|
filterTopic: 'exact topic (case-insensitive)',
|
|
19
|
-
fetchTags:
|
|
20
|
-
importTags:
|
|
20
|
+
fetchTags: `taxonomic tags for this research (can be repeated, max ${MAX_TAG_LENGTH} chars each)`,
|
|
21
|
+
importTags: `taxonomic tags (can be repeated, max ${MAX_TAG_LENGTH} chars each)`,
|
|
21
22
|
filterTags: 'tags to require (must match all)',
|
|
22
23
|
format: 'output format',
|
|
23
24
|
readOnly: '(alias: --plan) block filesystem writes/deletes; network fetches still run; also honored via BONSAI_READ_ONLY/BONSAI_PLAN_MODE',
|
|
25
|
+
toon: 'emit the same envelope as --json, encoded as TOON (fewer tokens); cannot be combined with --json',
|
|
24
26
|
sourceUrlGlob: 'source URL glob (case-insensitive, supports * wildcard)',
|
|
25
27
|
listArtifactType: 'artifact type; section children are omitted from list - use inspect to see them',
|
|
26
28
|
pruneArtifactType: 'artifact type to prune, including section children',
|
|
29
|
+
listFull: 'include every metadata field (cache key, path, artifact type, tags, capture method, quality notes, timestamps); default output is a minimal row (source URLs, topic, freshness, token estimate)',
|
|
27
30
|
};
|
|
28
31
|
function formatHumanLabel(label) {
|
|
29
32
|
const text = `${label}:`;
|
|
@@ -33,7 +33,7 @@ export type CacheHitStatus = 'hit' | 'miss' | 'stale' | 'refreshed' | 'revalidat
|
|
|
33
33
|
export type FreshnessState = 'fresh' | 'stale_grace' | 'stale_expired' | 'none';
|
|
34
34
|
/** Freshness values that appear on list rows (an entry always exists). */
|
|
35
35
|
export type ListFreshness = Exclude<FreshnessState, 'none'>;
|
|
36
|
-
/** One row in `list --json` output. */
|
|
36
|
+
/** One row in `list --json` output when `--full` is passed. */
|
|
37
37
|
export interface ListRow {
|
|
38
38
|
cacheKey: string;
|
|
39
39
|
path: string;
|
|
@@ -48,6 +48,30 @@ export interface ListRow {
|
|
|
48
48
|
fetchedAt: string | null;
|
|
49
49
|
validatedAt: string | null;
|
|
50
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Default `list --json`/`--toon` row: the fields an agent needs to judge relevance and act next
|
|
53
|
+
* (fetch/inspect the source, gauge freshness, budget tokens) without the full metadata dump.
|
|
54
|
+
* Pass `--full` for every field on {@link ListRow}.
|
|
55
|
+
*/
|
|
56
|
+
export interface ListRowMinimal {
|
|
57
|
+
sourceUrls: string[];
|
|
58
|
+
topic: string | null;
|
|
59
|
+
freshness: ListFreshness;
|
|
60
|
+
tokenEstimate: TokenEstimate;
|
|
61
|
+
}
|
|
62
|
+
/** Aggregate counts attached to `list`'s envelope so agents skip extra round trips. */
|
|
63
|
+
export interface ListSummary {
|
|
64
|
+
/** Entries matching the given filters, before `--limit` truncation. */
|
|
65
|
+
total: number;
|
|
66
|
+
/** Entries actually returned in `data` (after `--limit`). */
|
|
67
|
+
shown: number;
|
|
68
|
+
limit: number;
|
|
69
|
+
/** True when `total > shown`, i.e. more entries matched than `--limit` allowed through. */
|
|
70
|
+
truncated: boolean;
|
|
71
|
+
/** Explicit signal for `total === 0`, so an empty `data: []` is never ambiguous. */
|
|
72
|
+
empty: boolean;
|
|
73
|
+
byFreshness: Record<ListFreshness, number>;
|
|
74
|
+
}
|
|
51
75
|
/** Section child summary nested under an inspect hit. */
|
|
52
76
|
export interface InspectSectionRow {
|
|
53
77
|
cacheKey: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { type ConfigValues, type ResolvedConfig, type ConfigKey, type StorageMode, type SummaryLevel, type KeyMeta, SCHEMA_VERSION, STORAGE_MODES, SUMMARY_LEVELS, ALL_KEYS, BUILT_IN_DEFAULTS, KEY_META, isKnownKey, suggestKey, validKeysHint, } from './schema.js';
|
|
2
|
-
export { readUserConfig, readProjectConfig, writeUserConfig, writeProjectConfig, USER_CONFIG_FILENAME, PROJECT_CONFIG_FILENAME, } from './io.js';
|
|
1
|
+
export { type ConfigValues, type ResolvedConfig, type ConfigKey, type StorageMode, type SummaryLevel, type KeyMeta, SCHEMA_VERSION, STORAGE_MODES, SUMMARY_LEVELS, ALL_KEYS, BUILT_IN_DEFAULTS, KEY_META, isKnownKey, suggestKey, validKeysHint, keyValuesHint, } from './schema.js';
|
|
2
|
+
export { readUserConfig, readProjectConfig, writeUserConfig, writeProjectConfig, invalidConfigFileWarnings, USER_CONFIG_FILENAME, PROJECT_CONFIG_FILENAME, } from './io.js';
|
|
3
3
|
export { resolveStorageMode, resolveSummaryLevel, resolveReadOnly, parseEnvStorage, parseEnvSummary, parseEnvBoolean, invalidEnvOverrideWarnings, STORAGE_ENV_VAR, SUMMARY_ENV_VAR, READ_ONLY_ENV_VAR, PLAN_MODE_ENV_VAR, type ResolveStorageInput, type ResolveSummaryInput, type ResolveReadOnlyInput, } from './resolve.js';
|
|
4
4
|
import { type ConfigKey, type ConfigValues, type ResolvedConfig, type StorageMode, type SummaryLevel } from './schema.js';
|
|
5
5
|
/**
|
package/dist/lib/config/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { SCHEMA_VERSION, STORAGE_MODES, SUMMARY_LEVELS, ALL_KEYS, BUILT_IN_DEFAULTS, KEY_META, isKnownKey, suggestKey, validKeysHint, } from './schema.js';
|
|
2
|
-
export { readUserConfig, readProjectConfig, writeUserConfig, writeProjectConfig, USER_CONFIG_FILENAME, PROJECT_CONFIG_FILENAME, } from './io.js';
|
|
1
|
+
export { SCHEMA_VERSION, STORAGE_MODES, SUMMARY_LEVELS, ALL_KEYS, BUILT_IN_DEFAULTS, KEY_META, isKnownKey, suggestKey, validKeysHint, keyValuesHint, } from './schema.js';
|
|
2
|
+
export { readUserConfig, readProjectConfig, writeUserConfig, writeProjectConfig, invalidConfigFileWarnings, USER_CONFIG_FILENAME, PROJECT_CONFIG_FILENAME, } from './io.js';
|
|
3
3
|
export { resolveStorageMode, resolveSummaryLevel, resolveReadOnly, parseEnvStorage, parseEnvSummary, parseEnvBoolean, invalidEnvOverrideWarnings, STORAGE_ENV_VAR, SUMMARY_ENV_VAR, READ_ONLY_ENV_VAR, PLAN_MODE_ENV_VAR, } from './resolve.js';
|
|
4
4
|
import { readUserConfig, readProjectConfig } from './io.js';
|
|
5
5
|
import { parseEnvStorage, parseEnvSummary, resolveStorageMode, resolveSummaryLevel, } from './resolve.js';
|
package/dist/lib/config/io.d.ts
CHANGED
|
@@ -4,5 +4,14 @@ export declare const USER_CONFIG_FILENAME = "config.json";
|
|
|
4
4
|
export declare const PROJECT_CONFIG_FILENAME = ".bonsai.json";
|
|
5
5
|
export declare function readUserConfig(configDir: string | undefined): Partial<ConfigValues>;
|
|
6
6
|
export declare function readProjectConfig(cwd: string): Partial<ConfigValues>;
|
|
7
|
+
/**
|
|
8
|
+
* Warnings for the user and project config files, so a corrupted or hand-edited file that would
|
|
9
|
+
* otherwise silently degrade to `{}` (or drop just the offending key) is never a silent surprise —
|
|
10
|
+
* mirrors {@link invalidEnvOverrideWarnings} for env vars. Empty when both files are missing,
|
|
11
|
+
* unreadable, or valid.
|
|
12
|
+
*
|
|
13
|
+
* @param bin - CLI binary name (`this.config.bin`) for the "fix it" suggestion in a warning.
|
|
14
|
+
*/
|
|
15
|
+
export declare function invalidConfigFileWarnings(configDir: string | undefined, cwd: string, bin: string): string[];
|
|
7
16
|
export declare function writeUserConfig(configDir: string, patch: Partial<ConfigValues>): void;
|
|
8
17
|
export declare function writeProjectConfig(cwd: string, patch: Partial<ConfigValues>): void;
|