@taurgis/bonsai 4.1.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 +0 -2
- package/bin/cli.mjs +0 -11
- 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/lib/cli-flag-manifest.d.ts +2 -2
- package/dist/lib/cli-flag-manifest.js +15 -7
- package/dist/{commands/config/base.d.ts → lib/config-command.d.ts} +6 -3
- package/dist/{commands/config/base.js → lib/config-command.js} +6 -3
- package/dist/lib/research/fetcher.d.ts +11 -0
- package/dist/lib/research/fetcher.js +26 -0
- package/dist/lib/research/regression.js +6 -1
- 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 +148 -59
- package/package.json +8 -13
package/README.md
CHANGED
|
@@ -316,8 +316,6 @@ When run with `--json`, commands return a stable envelope:
|
|
|
316
316
|
| `NO_COLOR` | Set to any non-empty value to disable all ANSI color output. An empty `NO_COLOR` is treated as unset. |
|
|
317
317
|
| `FORCE_COLOR` | Set to any value other than an empty string, `0`, or `false` to force ANSI color on, even in non-TTY environments. |
|
|
318
318
|
| `TERM=dumb` | Disables ANSI color output when the terminal cannot render escape sequences. |
|
|
319
|
-
| `NO_UPDATE_NOTIFIER` | Suppress the "update available" notification. |
|
|
320
|
-
| `CI` | Set to `1` or `true` to suppress the update notification in CI environments (detected automatically). |
|
|
321
319
|
| `DEBUG` | Enable debug output for specific Bonsai namespaces, e.g. `DEBUG=bonsai:*`. |
|
|
322
320
|
|
|
323
321
|
---
|
package/bin/cli.mjs
CHANGED
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
import { execute } from '@oclif/core';
|
|
4
4
|
import { dirname } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import updateNotifier from 'update-notifier';
|
|
7
|
-
import { createRequire } from 'node:module';
|
|
8
6
|
|
|
9
7
|
import { normalizeArgv } from '../dist/lib/argv.js';
|
|
10
8
|
import { VALUE_TAKING_FLAG_TOKENS, KNOWN_COMMAND_ROOT_TOKENS } from '../dist/lib/cli-flag-manifest.js';
|
|
@@ -12,9 +10,6 @@ import { exitWithPreflight } from '../dist/lib/cli-emit.js';
|
|
|
12
10
|
import { tryUnknownHelpOutput } from '../dist/lib/help-preflight.js';
|
|
13
11
|
import { tryJsonMetaOutput } from '../dist/lib/json-meta.js';
|
|
14
12
|
|
|
15
|
-
const req = createRequire(import.meta.url);
|
|
16
|
-
const pkg = req('../package.json');
|
|
17
|
-
|
|
18
13
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
14
|
|
|
20
15
|
// Normalize argv so the whole oclif pipeline sees one consistent command. oclif's
|
|
@@ -43,12 +38,6 @@ if (jsonMeta) {
|
|
|
43
38
|
exitWithPreflight({ ...jsonMeta, json: true });
|
|
44
39
|
}
|
|
45
40
|
|
|
46
|
-
// Only notify after the earlyExit fast-path so the notifier never fires before
|
|
47
|
-
// a usage-error envelope exits. Also skip under --json/--toon so agent callers never see stderr noise.
|
|
48
|
-
if (!process.argv.includes('--json') && !process.argv.includes('--toon')) {
|
|
49
|
-
updateNotifier({ pkg }).notify();
|
|
50
|
-
}
|
|
51
|
-
|
|
52
41
|
process.argv = [process.argv[0], process.argv[1], ...result.argv];
|
|
53
42
|
|
|
54
43
|
await execute({
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Args } from '@oclif/core';
|
|
2
|
-
import { ConfigCommand, configScopeFlags } from '
|
|
2
|
+
import { ConfigCommand, configScopeFlags } from '../../lib/config-command.js';
|
|
3
3
|
import { formatConfigEntry, resolveConfigEntry, keyValuesHint } from '../../lib/config/index.js';
|
|
4
4
|
export default class ConfigGet extends ConfigCommand {
|
|
5
5
|
static id = 'config get';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ConfigCommand, configScopeFlags } from '
|
|
1
|
+
import { ConfigCommand, configScopeFlags } from '../../lib/config-command.js';
|
|
2
2
|
import { formatConfigEntry, resolveConfigEntries, keyValuesHint } from '../../lib/config/index.js';
|
|
3
3
|
export default class ConfigList extends ConfigCommand {
|
|
4
4
|
static id = 'config list';
|
|
@@ -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 ConfigSet extends ConfigCommand<typeof ConfigSet> {
|
|
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 { 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 {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Every CLI token that consumes a following argv value, from the shipped oclif manifest.
|
|
3
|
+
* Argv normalization stays off the command-class import graph.
|
|
4
4
|
*/
|
|
5
5
|
export declare const VALUE_TAKING_FLAG_TOKENS: ReadonlySet<string>;
|
|
6
6
|
/**
|
|
@@ -1,15 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { valueTakingFlagTokensFromFlagMaps } from './value-flags.js';
|
|
5
|
+
// Startup reads the generated manifest instead of importing every command class. Importing the
|
|
6
|
+
// registry pulls the fetch stack (Readability, Turndown, the browser fallback) into --help.
|
|
7
|
+
function readOclifManifest() {
|
|
8
|
+
const manifestPath = join(dirname(fileURLToPath(import.meta.url)), '../../oclif.manifest.json');
|
|
9
|
+
return JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
10
|
+
}
|
|
11
|
+
const manifest = readOclifManifest();
|
|
4
12
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
13
|
+
* Every CLI token that consumes a following argv value, from the shipped oclif manifest.
|
|
14
|
+
* Argv normalization stays off the command-class import graph.
|
|
7
15
|
*/
|
|
8
|
-
export const VALUE_TAKING_FLAG_TOKENS =
|
|
16
|
+
export const VALUE_TAKING_FLAG_TOKENS = valueTakingFlagTokensFromFlagMaps(Object.values(manifest.commands).map((command) => command.flags));
|
|
9
17
|
/**
|
|
10
18
|
* Root segment of every registered command id (`config:get` → `config`). oclif joins namespaced
|
|
11
19
|
* command ids with `:` regardless of the display-only `topicSeparator` setting, so a token like
|
|
12
20
|
* `config:get` is a real command, not a URL — argv normalization checks this set before assuming
|
|
13
21
|
* `word:` is a URL scheme.
|
|
14
22
|
*/
|
|
15
|
-
export const KNOWN_COMMAND_ROOT_TOKENS = new Set(Object.keys(commands).map((id) => id.split(':')[0]));
|
|
23
|
+
export const KNOWN_COMMAND_ROOT_TOKENS = new Set(Object.keys(manifest.commands).map((id) => id.split(':')[0]));
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
|
-
import { BaseCommand } from '
|
|
3
|
-
import type { ConfigKey, ConfigScope } from '
|
|
4
|
-
import { scopeFlags } from '
|
|
2
|
+
import { BaseCommand } from '../base-command.js';
|
|
3
|
+
import type { ConfigKey, ConfigScope } from './config/index.js';
|
|
4
|
+
import { scopeFlags } from './scope-flags.js';
|
|
5
5
|
/**
|
|
6
6
|
* Shared base for the four `config` subcommands: the mutually-exclusive
|
|
7
7
|
* scope-flag guard, the binary write-scope helper, and known-key validation.
|
|
8
|
+
*
|
|
9
|
+
* Lives outside `src/commands` so oclif's file-based command discovery does not
|
|
10
|
+
* publish it as `config base`.
|
|
8
11
|
*/
|
|
9
12
|
export declare abstract class ConfigCommand<T extends typeof Command> extends BaseCommand<T> {
|
|
10
13
|
protected assertScopeFlagsExclusive(global?: boolean, local?: boolean): void;
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { toConfiguredId } from '@oclif/core';
|
|
2
|
-
import { BaseCommand } from '
|
|
3
|
-
import { isKnownKey, suggestKey, validKeysHint } from '
|
|
4
|
-
import { assertScopeFlagsExclusive, scopeFlags } from '
|
|
2
|
+
import { BaseCommand } from '../base-command.js';
|
|
3
|
+
import { isKnownKey, suggestKey, validKeysHint } from './config/index.js';
|
|
4
|
+
import { assertScopeFlagsExclusive, scopeFlags } from './scope-flags.js';
|
|
5
5
|
/**
|
|
6
6
|
* Shared base for the four `config` subcommands: the mutually-exclusive
|
|
7
7
|
* scope-flag guard, the binary write-scope helper, and known-key validation.
|
|
8
|
+
*
|
|
9
|
+
* Lives outside `src/commands` so oclif's file-based command discovery does not
|
|
10
|
+
* publish it as `config base`.
|
|
8
11
|
*/
|
|
9
12
|
export class ConfigCommand extends BaseCommand {
|
|
10
13
|
assertScopeFlagsExclusive(global, local) {
|
|
@@ -65,3 +65,14 @@ export declare function fetchStaticHtml(url: string, options?: FetchOptions): Pr
|
|
|
65
65
|
* @throws {Error} On unsafe DNS, HTTP failure, or redirect limits.
|
|
66
66
|
*/
|
|
67
67
|
export declare function fetchText(url: string, options?: FetchOptions): Promise<FetchResult>;
|
|
68
|
+
/**
|
|
69
|
+
* Same DNS, redirect, and status checks as {@link fetchText}, but the body is cancelled once the
|
|
70
|
+
* status line is known. Use this to reject HTTP 404 before a browser render without downloading
|
|
71
|
+
* the document.
|
|
72
|
+
*
|
|
73
|
+
* @param url - Absolute URL to probe.
|
|
74
|
+
* @param options - Optional timeout, redirect cap, and headers.
|
|
75
|
+
* @returns The final HTTP status.
|
|
76
|
+
* @throws {Error} On unsafe DNS, HTTP failure, or redirect limits.
|
|
77
|
+
*/
|
|
78
|
+
export declare function fetchStatus(url: string, options?: FetchOptions): Promise<number>;
|
|
@@ -264,3 +264,29 @@ export async function fetchStaticHtml(url, options = {}) {
|
|
|
264
264
|
export async function fetchText(url, options = {}) {
|
|
265
265
|
return fetchWithRedirects(url, options, processTextResponse);
|
|
266
266
|
}
|
|
267
|
+
/**
|
|
268
|
+
* Same DNS, redirect, and status checks as {@link fetchText}, but the body is cancelled once the
|
|
269
|
+
* status line is known. Use this to reject HTTP 404 before a browser render without downloading
|
|
270
|
+
* the document.
|
|
271
|
+
*
|
|
272
|
+
* @param url - Absolute URL to probe.
|
|
273
|
+
* @param options - Optional timeout, redirect cap, and headers.
|
|
274
|
+
* @returns The final HTTP status.
|
|
275
|
+
* @throws {Error} On unsafe DNS, HTTP failure, or redirect limits.
|
|
276
|
+
*/
|
|
277
|
+
export async function fetchStatus(url, options = {}) {
|
|
278
|
+
const result = await fetchWithRedirects(url, options, async (res, _limit, currentUrl) => {
|
|
279
|
+
await res.body?.cancel().catch(() => { });
|
|
280
|
+
assertOk(res);
|
|
281
|
+
return {
|
|
282
|
+
status: res.status,
|
|
283
|
+
contentType: null,
|
|
284
|
+
etag: null,
|
|
285
|
+
lastModified: null,
|
|
286
|
+
finalUrl: currentUrl,
|
|
287
|
+
responseSize: 0,
|
|
288
|
+
content: '',
|
|
289
|
+
};
|
|
290
|
+
});
|
|
291
|
+
return result.status;
|
|
292
|
+
}
|
|
@@ -117,7 +117,12 @@ const LEAKAGE_PATTERNS = [
|
|
|
117
117
|
{ label: 'base64-image', re: /data:image\/[a-z]+;base64,/i },
|
|
118
118
|
{ label: 'skip-to-content', re: /skip to (?:main )?content/i },
|
|
119
119
|
{ label: 'on-this-page', re: /^on this page$/im },
|
|
120
|
-
|
|
120
|
+
// "dark"/"light" or the word "theme" is required. An optional group also matched
|
|
121
|
+
// "switch to mode" after inline code was stripped from "switch to `history` mode".
|
|
122
|
+
{
|
|
123
|
+
label: 'theme-toggle',
|
|
124
|
+
re: /(?:toggle|switch to) (?:the )?(?:dark|light)(?: mode)?|(?:toggle|switch to) (?:the )?theme/i,
|
|
125
|
+
},
|
|
121
126
|
{
|
|
122
127
|
label: 'cookie-consent',
|
|
123
128
|
re: /(?:accept all cookies|we use cookies|cookie (?:policy|preferences|settings))/i,
|
|
@@ -11,4 +11,6 @@ type FlagDef = {
|
|
|
11
11
|
* cannot drift from the real command surface.
|
|
12
12
|
*/
|
|
13
13
|
export declare function valueTakingFlagTokensFromCommands(commands: Record<string, typeof Command>, extraBaseFlags?: Record<string, FlagDef>): Set<string>;
|
|
14
|
+
/** Same token set, read from a generated oclif manifest instead of command classes. */
|
|
15
|
+
export declare function valueTakingFlagTokensFromFlagMaps(flagMaps: Array<Record<string, FlagDef> | undefined>): Set<string>;
|
|
14
16
|
export {};
|
package/dist/lib/value-flags.js
CHANGED
|
@@ -14,10 +14,18 @@ function flagTokens(name, flag) {
|
|
|
14
14
|
* cannot drift from the real command surface.
|
|
15
15
|
*/
|
|
16
16
|
export function valueTakingFlagTokensFromCommands(commands, extraBaseFlags = {}) {
|
|
17
|
+
const maps = Object.values(commands).map((command) => ({
|
|
18
|
+
...extraBaseFlags,
|
|
19
|
+
...command.baseFlags,
|
|
20
|
+
...command.flags,
|
|
21
|
+
}));
|
|
22
|
+
return valueTakingFlagTokensFromFlagMaps(maps);
|
|
23
|
+
}
|
|
24
|
+
/** Same token set, read from a generated oclif manifest instead of command classes. */
|
|
25
|
+
export function valueTakingFlagTokensFromFlagMaps(flagMaps) {
|
|
17
26
|
const tokens = new Set();
|
|
18
|
-
for (const
|
|
19
|
-
const
|
|
20
|
-
for (const [name, flag] of Object.entries(flags)) {
|
|
27
|
+
for (const flags of flagMaps) {
|
|
28
|
+
for (const [name, flag] of Object.entries(flags ?? {})) {
|
|
21
29
|
if (!flag || flag.type === 'boolean')
|
|
22
30
|
continue;
|
|
23
31
|
for (const token of flagTokens(name, flag))
|
|
@@ -3,7 +3,7 @@ import type { SiteFetchResult } from '../types.js';
|
|
|
3
3
|
* Fetches a developer.salesforce.com doc page. Supported articles publish a Markdown twin at the
|
|
4
4
|
* derived `.md` route ("View as Markdown"), which is preferred: one static request instead of a
|
|
5
5
|
* browser render, and it returns the article's source. Pages without a validated twin (API
|
|
6
|
-
* reference, atlas
|
|
6
|
+
* reference, atlas books whose `.md` route is still an HTML shell, landing pages) fall back to the shared Lightning Web Runtime (LWR)
|
|
7
7
|
* shadow-DOM fetcher.
|
|
8
8
|
*
|
|
9
9
|
* @param url - The developer.salesforce.com doc page URL.
|
|
@@ -5,7 +5,9 @@ import { fetchDeveloperRouteMarkdown } from './route-markdown.js';
|
|
|
5
5
|
const CONTENT_SELECTORS = [
|
|
6
6
|
// API-reference pages: doc-amf-reference holds the method/type tables. doc-content-layout only
|
|
7
7
|
// carries the shared deprecation banner and must not win selector priority on those pages.
|
|
8
|
+
// doc-redoc-reference is the Connect REST (B2B Commerce and platform) three-panel host.
|
|
8
9
|
'doc-amf-reference',
|
|
10
|
+
'doc-redoc-reference',
|
|
9
11
|
'doc-content-layout',
|
|
10
12
|
'.markdown-content',
|
|
11
13
|
'main article',
|
|
@@ -16,7 +18,7 @@ const CONTENT_SELECTORS = [
|
|
|
16
18
|
* Fetches a developer.salesforce.com doc page. Supported articles publish a Markdown twin at the
|
|
17
19
|
* derived `.md` route ("View as Markdown"), which is preferred: one static request instead of a
|
|
18
20
|
* browser render, and it returns the article's source. Pages without a validated twin (API
|
|
19
|
-
* reference, atlas
|
|
21
|
+
* reference, atlas books whose `.md` route is still an HTML shell, landing pages) fall back to the shared Lightning Web Runtime (LWR)
|
|
20
22
|
* shadow-DOM fetcher.
|
|
21
23
|
*
|
|
22
24
|
* @param url - The developer.salesforce.com doc page URL.
|
|
@@ -2,8 +2,9 @@ import { fetchText } from '../../lib/research/fetcher.js';
|
|
|
2
2
|
import type { SiteFetchResult } from '../types.js';
|
|
3
3
|
/**
|
|
4
4
|
* Derives the candidate `.md` route for a developer.salesforce.com doc URL, or null when the URL
|
|
5
|
-
* can't serve one (wrong host, not under /docs
|
|
6
|
-
*
|
|
5
|
+
* can't serve one (wrong host, or not under /docs/). Atlas books are included: a shell is
|
|
6
|
+
* rejected later by content-type, and a redirect to Markdown is kept. Query/hash are view
|
|
7
|
+
* state and are dropped.
|
|
7
8
|
*
|
|
8
9
|
* @param url - A developer.salesforce.com doc URL (or any URL; non-matching returns null).
|
|
9
10
|
* @returns The derived `.md` URL, or null when no `.md` route exists for this URL.
|
|
@@ -7,9 +7,10 @@ import { probeMarkdownTwin } from '../markdown-twin.js';
|
|
|
7
7
|
// shadow-DOM render and returns the article's actual source (code fences, admonitions intact).
|
|
8
8
|
//
|
|
9
9
|
// Coverage is rolling out per doc set, so the probe result is only a candidate (T-19/T-24):
|
|
10
|
-
// unsupported articles 404, and atlas.* books answer the .md route with an HTTP 200
|
|
11
|
-
// shell
|
|
12
|
-
//
|
|
10
|
+
// unsupported articles 404, and some atlas.* books still answer the .md route with an HTTP 200
|
|
11
|
+
// HTML SPA shell. Others (REST API, Bulk API's successor) now redirect that same .md route to a
|
|
12
|
+
// real text/markdown article under /docs/platform. Content-type validation rejects the shells;
|
|
13
|
+
// skipping atlas.* up front would miss the books that moved.
|
|
13
14
|
const DEVELOPER_HOST = 'developer.salesforce.com';
|
|
14
15
|
// Salesforce's WAF answers 403 unless the User-Agent LEADS with a recognized HTTP-tool token
|
|
15
16
|
// (curl/wget/python-requests pass; bare product names and browser UAs on a non-browser TLS stack
|
|
@@ -20,8 +21,9 @@ const PROBE_HEADERS = {
|
|
|
20
21
|
};
|
|
21
22
|
/**
|
|
22
23
|
* Derives the candidate `.md` route for a developer.salesforce.com doc URL, or null when the URL
|
|
23
|
-
* can't serve one (wrong host, not under /docs
|
|
24
|
-
*
|
|
24
|
+
* can't serve one (wrong host, or not under /docs/). Atlas books are included: a shell is
|
|
25
|
+
* rejected later by content-type, and a redirect to Markdown is kept. Query/hash are view
|
|
26
|
+
* state and are dropped.
|
|
25
27
|
*
|
|
26
28
|
* @param url - A developer.salesforce.com doc URL (or any URL; non-matching returns null).
|
|
27
29
|
* @returns The derived `.md` URL, or null when no `.md` route exists for this URL.
|
|
@@ -37,7 +39,7 @@ export function deriveMarkdownUrl(url) {
|
|
|
37
39
|
if (parsed.hostname.toLowerCase() !== DEVELOPER_HOST)
|
|
38
40
|
return null;
|
|
39
41
|
let path = parsed.pathname.replace(/\/+$/, '');
|
|
40
|
-
if (!path.startsWith('/docs/')
|
|
42
|
+
if (!path.startsWith('/docs/'))
|
|
41
43
|
return null;
|
|
42
44
|
if (path.endsWith('.md'))
|
|
43
45
|
return `https://${DEVELOPER_HOST}${path}`;
|
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
import type { SiteFetchResult } from './types.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* @param text - Extracted Markdown text to check.
|
|
5
|
-
* @returns True when the text matches a Salesforce error/not-found pattern.
|
|
6
|
-
*/
|
|
7
|
-
export declare function looksLikeSalesforceError(text: string): boolean;
|
|
2
|
+
import { looksLikeSalesforceError } from './salesforce-dom-probe.js';
|
|
3
|
+
export { looksLikeSalesforceError };
|
|
8
4
|
/**
|
|
9
|
-
* Removes known boilerplate
|
|
10
|
-
*
|
|
5
|
+
* Removes known boilerplate from converted Salesforce Markdown: the feedback widget, doc-chrome
|
|
6
|
+
* toolbar labels, the atlas release switcher, and empty icon-anchor lines.
|
|
11
7
|
*
|
|
12
8
|
* @param markdown - Markdown output from the DOM or content API conversion.
|
|
13
|
-
* @returns Markdown with boilerplate
|
|
9
|
+
* @returns Markdown with boilerplate stripped and excess blank lines collapsed.
|
|
14
10
|
*/
|
|
15
11
|
export declare function stripBoilerplate(markdown: string): string;
|
|
16
12
|
/** Parses the Salesforce private content API JSON payload.
|
|
@@ -45,6 +41,21 @@ export interface SalesforceDocOptions {
|
|
|
45
41
|
contentSelectors: string[];
|
|
46
42
|
removeSelectors?: string[];
|
|
47
43
|
}
|
|
44
|
+
/** Article identity for a Salesforce doc URL: Help `id`, otherwise the last path segment.
|
|
45
|
+
* Underscores and hyphens match so a REST rename (`intro_what_is_rest_api` →
|
|
46
|
+
* `intro-what-is-rest-api`) stays the same article.
|
|
47
|
+
*
|
|
48
|
+
* @param url - Requested or post-navigation URL.
|
|
49
|
+
* @returns Normalized article key, or null when the URL has no article identity.
|
|
50
|
+
*/
|
|
51
|
+
export declare function salesforceArticleKey(url: string): string | null;
|
|
52
|
+
/** True when both URLs name the same Salesforce article, or either key can't be read.
|
|
53
|
+
*
|
|
54
|
+
* @param requested - URL the caller asked for.
|
|
55
|
+
* @param finalUrl - URL the browser landed on.
|
|
56
|
+
* @returns False only when both keys exist and differ.
|
|
57
|
+
*/
|
|
58
|
+
export declare function sameSalesforceArticle(requested: string, finalUrl: string): boolean;
|
|
48
59
|
/**
|
|
49
60
|
* Renders a Salesforce Lightning Web Runtime (LWR) doc page, serializes its shadow-DOM content
|
|
50
61
|
* container, and converts it to Markdown. Validates the host before any network access.
|
|
@@ -53,7 +64,8 @@ export interface SalesforceDocOptions {
|
|
|
53
64
|
* @param options - Allowed host, priority content selectors, and optional per-site remove selectors.
|
|
54
65
|
* @returns Extracted Markdown, fetch metadata, and browser-capture provenance.
|
|
55
66
|
* @throws {Error} When the URL is invalid, the host is not the allowed host, DNS is unsafe,
|
|
56
|
-
* Chrome/CDP navigation or evaluation fails, the
|
|
57
|
-
*
|
|
67
|
+
* the document itself is HTTP 404/410, Chrome/CDP navigation or evaluation fails, the page
|
|
68
|
+
* redirects to a different article, the response body exceeds the size limit, or the captured
|
|
69
|
+
* content is too short / looks like an error page.
|
|
58
70
|
*/
|
|
59
71
|
export declare function fetchSalesforceDoc(url: string, { allowedHost, contentSelectors, removeSelectors }: SalesforceDocOptions): Promise<SiteFetchResult>;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { openCdpPage, waitForLoad, ResponseCapture, describeNavigationFailure, } from '../lib/research/browser.js';
|
|
2
|
-
import { checkDnsSafety } from '../lib/research/fetcher.js';
|
|
2
|
+
import { checkDnsSafety, fetchStatus } from '../lib/research/fetcher.js';
|
|
3
3
|
import { htmlToMarkdown } from '../lib/research/markdown.js';
|
|
4
4
|
import { sanitizePromptInjection } from '../lib/research/prompt-injection.js';
|
|
5
5
|
import { contentMetrics, contentRicherThan } from '../lib/research/regression.js';
|
|
6
|
-
import { SALESFORCE_SHADOW_DOM_HELPERS, buildPriorityContainerPickBlock, pollSalesforceContentReady, } from './salesforce-dom-probe.js';
|
|
6
|
+
import { SALESFORCE_SHADOW_DOM_HELPERS, buildPriorityContainerPickBlock, pollSalesforceContentReady, looksLikeSalesforceError, } from './salesforce-dom-probe.js';
|
|
7
|
+
export { looksLikeSalesforceError };
|
|
7
8
|
// Both help.salesforce.com and developer.salesforce.com are LWR sites that render doc content
|
|
8
9
|
// client-side inside web components (shadow DOM). document.documentElement.outerHTML captures
|
|
9
10
|
// only the "Loading…" shell, and Readability prunes the structured result to a single block —
|
|
@@ -19,20 +20,11 @@ const BODY_LIMIT_BYTES = 6 * 1024 * 1024;
|
|
|
19
20
|
/** Grace period after DOM settle for late /docs/get_document_content/ responses on guide pages. */
|
|
20
21
|
const DOCS_API_GRACE_MS = 500;
|
|
21
22
|
const ACCEPT_CONSENT_EXPRESSION = "(() => { const b = document.querySelector('#onetrust-accept-btn-handler'); if (b) b.click(); })()";
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
/
|
|
26
|
-
|
|
27
|
-
];
|
|
28
|
-
/** True when the extracted text is an error/not-found/loading shell rather than real content.
|
|
29
|
-
*
|
|
30
|
-
* @param text - Extracted Markdown text to check.
|
|
31
|
-
* @returns True when the text matches a Salesforce error/not-found pattern.
|
|
32
|
-
*/
|
|
33
|
-
export function looksLikeSalesforceError(text) {
|
|
34
|
-
return ERROR_PATTERNS.some((re) => re.test(text));
|
|
35
|
-
}
|
|
23
|
+
// Salesforce's WAF answers 403 unless the User-Agent leads with a recognized HTTP-tool token.
|
|
24
|
+
// Same token the Markdown probe sends, so a missing-doc check sees the real status.
|
|
25
|
+
const DOC_PROBE_HEADERS = {
|
|
26
|
+
'user-agent': 'curl/8.7.1 bonsai-research-cache (+https://bonsai.rhino-inquisitor.com)',
|
|
27
|
+
};
|
|
36
28
|
// Chrome present on every LWR doc page; stripped before serialization. Per-site noise (e.g.
|
|
37
29
|
// Help's article-feedback widget) is appended via SalesforceDocOptions.removeSelectors.
|
|
38
30
|
const BASE_REMOVE_SELECTORS = [
|
|
@@ -69,25 +61,28 @@ function confidenceFor(length) {
|
|
|
69
61
|
// page. Help wraps it in a removable component (see fetch-page.ts), but Developer renders it
|
|
70
62
|
// as bare divs, so we also strip its lines from the converted Markdown for both sites.
|
|
71
63
|
const BOILERPLATE_LINE = /(did this article solve your issue|let us know so we can improve|share your feedback)/i;
|
|
72
|
-
// Toolbar
|
|
73
|
-
//
|
|
74
|
-
|
|
75
|
-
const MARKDOWN_TOOLBAR_LINE = /^(copy as markdown|view as markdown|copy url to markdown)$/i;
|
|
64
|
+
// Toolbar labels that serialize as their own paragraph: "View as Markdown" on articles that
|
|
65
|
+
// missed the .md probe, and the llms.txt / "Open in ChatGPT" cluster on the newer guide chrome.
|
|
66
|
+
const DOC_CHROME_LINE = /^(copy as markdown|view as markdown|copy url to markdown|view llms\.txt|copy url to llms\.txt|open in chatgpt|open in claude|open in perplexity|this page|all documentation|more actions)$/i;
|
|
76
67
|
// A link with empty anchor text — e.g. an icon-only "home" logo link. It carries no readable
|
|
77
68
|
// content and renders as a bare `[](url)`. (Images `` are intentionally not matched.)
|
|
78
69
|
const EMPTY_LINK_LINE = /^\[\]\([^)]*\)$/;
|
|
70
|
+
// Atlas books render a release switcher as one link per season. Turndown splits the anchor
|
|
71
|
+
// text across lines, so the link is not a single line. Drop the whole link; the article follows.
|
|
72
|
+
const VERSION_PICKER_LINK = /\[\s*(?:Spring|Summer|Winter|Fall)\s+'\d{2}\s+\(API version\s+[\d.]+\)\s*\]\([^)\n]*\)/gi;
|
|
79
73
|
/**
|
|
80
|
-
* Removes known boilerplate
|
|
81
|
-
*
|
|
74
|
+
* Removes known boilerplate from converted Salesforce Markdown: the feedback widget, doc-chrome
|
|
75
|
+
* toolbar labels, the atlas release switcher, and empty icon-anchor lines.
|
|
82
76
|
*
|
|
83
77
|
* @param markdown - Markdown output from the DOM or content API conversion.
|
|
84
|
-
* @returns Markdown with boilerplate
|
|
78
|
+
* @returns Markdown with boilerplate stripped and excess blank lines collapsed.
|
|
85
79
|
*/
|
|
86
80
|
export function stripBoilerplate(markdown) {
|
|
87
81
|
return markdown
|
|
82
|
+
.replace(VERSION_PICKER_LINK, '')
|
|
88
83
|
.split('\n')
|
|
89
84
|
.filter((line) => !BOILERPLATE_LINE.test(line) &&
|
|
90
|
-
!
|
|
85
|
+
!DOC_CHROME_LINE.test(line.trim()) &&
|
|
91
86
|
!EMPTY_LINK_LINE.test(line.trim()))
|
|
92
87
|
.join('\n')
|
|
93
88
|
.replace(/\n{3,}/g, '\n\n')
|
|
@@ -286,6 +281,65 @@ function buildCaptureScript(contentSelectors, extraRemove) {
|
|
|
286
281
|
return { html: serialize(container), title };
|
|
287
282
|
})()`;
|
|
288
283
|
}
|
|
284
|
+
/** Article identity for a Salesforce doc URL: Help `id`, otherwise the last path segment.
|
|
285
|
+
* Underscores and hyphens match so a REST rename (`intro_what_is_rest_api` →
|
|
286
|
+
* `intro-what-is-rest-api`) stays the same article.
|
|
287
|
+
*
|
|
288
|
+
* @param url - Requested or post-navigation URL.
|
|
289
|
+
* @returns Normalized article key, or null when the URL has no article identity.
|
|
290
|
+
*/
|
|
291
|
+
export function salesforceArticleKey(url) {
|
|
292
|
+
let parsed;
|
|
293
|
+
try {
|
|
294
|
+
parsed = new URL(url);
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
const id = parsed.searchParams.get('id');
|
|
300
|
+
const raw = (id ?? parsed.pathname.replace(/\/+$/, '').split('/').pop() ?? '').trim();
|
|
301
|
+
const stripped = raw.replace(/\.(html?|md)$/i, '');
|
|
302
|
+
if (!stripped)
|
|
303
|
+
return null;
|
|
304
|
+
return stripped.toLowerCase().replace(/_/g, '-');
|
|
305
|
+
}
|
|
306
|
+
/** True when both URLs name the same Salesforce article, or either key can't be read.
|
|
307
|
+
*
|
|
308
|
+
* @param requested - URL the caller asked for.
|
|
309
|
+
* @param finalUrl - URL the browser landed on.
|
|
310
|
+
* @returns False only when both keys exist and differ.
|
|
311
|
+
*/
|
|
312
|
+
export function sameSalesforceArticle(requested, finalUrl) {
|
|
313
|
+
const requestedKey = salesforceArticleKey(requested);
|
|
314
|
+
const finalKey = salesforceArticleKey(finalUrl);
|
|
315
|
+
if (!requestedKey || !finalKey)
|
|
316
|
+
return true;
|
|
317
|
+
return requestedKey === finalKey;
|
|
318
|
+
}
|
|
319
|
+
async function assertSalesforceDocumentExists(url) {
|
|
320
|
+
try {
|
|
321
|
+
await fetchStatus(url, { headers: DOC_PROBE_HEADERS, timeoutMs: 8_000 });
|
|
322
|
+
}
|
|
323
|
+
catch (err) {
|
|
324
|
+
const message = err instanceof Error ? err.message : '';
|
|
325
|
+
// Any other failure (WAF 403, timeout) still goes to the browser. Only a real
|
|
326
|
+
// not-found status is enough to skip Chrome.
|
|
327
|
+
if (/\bstatus (404|410)\b/.test(message)) {
|
|
328
|
+
throw new Error(`Salesforce doc not found: ${url}`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
async function assertLandedOnRequestedArticle(page, url) {
|
|
333
|
+
const result = await page.client
|
|
334
|
+
.send('Runtime.evaluate', { expression: 'location.href', returnByValue: true }, page.sessionId)
|
|
335
|
+
.catch(() => null);
|
|
336
|
+
const finalHref = result?.result?.value;
|
|
337
|
+
if (typeof finalHref !== 'string' || !finalHref)
|
|
338
|
+
return;
|
|
339
|
+
if (!sameSalesforceArticle(url, finalHref)) {
|
|
340
|
+
throw new Error(`Salesforce doc ${url} redirected to a different article (${finalHref}).`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
289
343
|
function parseSalesforceDocUrl(url, allowedHost) {
|
|
290
344
|
let parsed;
|
|
291
345
|
try {
|
|
@@ -300,7 +354,10 @@ function parseSalesforceDocUrl(url, allowedHost) {
|
|
|
300
354
|
return parsed;
|
|
301
355
|
}
|
|
302
356
|
function assertReadableSalesforceContent(detailedMarkdown, url) {
|
|
303
|
-
if (
|
|
357
|
+
if (looksLikeSalesforceError(detailedMarkdown)) {
|
|
358
|
+
throw new Error(`Salesforce doc not found: ${url}`);
|
|
359
|
+
}
|
|
360
|
+
if (detailedMarkdown.length < MIN_CONTAINER_CHARS) {
|
|
304
361
|
throw new Error(`Salesforce returned no readable content for ${url} (page may be missing, gated, or still loading).`);
|
|
305
362
|
}
|
|
306
363
|
}
|
|
@@ -351,6 +408,9 @@ async function captureSalesforceAttempt(page, url, captureScript, contentSelecto
|
|
|
351
408
|
}
|
|
352
409
|
let detailedMarkdown = stripBoilerplate(htmlToMarkdown(html));
|
|
353
410
|
let usedDocsApi = false;
|
|
411
|
+
// Retired atlas leaves (apex_qs_hello.htm) stay HTTP 200, then the SPA swaps in the book
|
|
412
|
+
// home. Storing that home under the requested URL would cache the wrong article.
|
|
413
|
+
await assertLandedOnRequestedArticle(page, url);
|
|
354
414
|
if (docsApiHtml) {
|
|
355
415
|
const picked = preferRicherMarkdown(detailedMarkdown, docsApiHtml, title, docsApiTitle);
|
|
356
416
|
detailedMarkdown = picked.markdown;
|
|
@@ -393,12 +453,14 @@ function buildSalesforceFetchResult(url, html, title, detailedMarkdown, qualityN
|
|
|
393
453
|
* @param options - Allowed host, priority content selectors, and optional per-site remove selectors.
|
|
394
454
|
* @returns Extracted Markdown, fetch metadata, and browser-capture provenance.
|
|
395
455
|
* @throws {Error} When the URL is invalid, the host is not the allowed host, DNS is unsafe,
|
|
396
|
-
* Chrome/CDP navigation or evaluation fails, the
|
|
397
|
-
*
|
|
456
|
+
* the document itself is HTTP 404/410, Chrome/CDP navigation or evaluation fails, the page
|
|
457
|
+
* redirects to a different article, the response body exceeds the size limit, or the captured
|
|
458
|
+
* content is too short / looks like an error page.
|
|
398
459
|
*/
|
|
399
460
|
export async function fetchSalesforceDoc(url, { allowedHost, contentSelectors, removeSelectors = [] }) {
|
|
400
461
|
const parsed = parseSalesforceDocUrl(url, allowedHost);
|
|
401
462
|
await checkDnsSafety(parsed.hostname);
|
|
463
|
+
await assertSalesforceDocumentExists(url);
|
|
402
464
|
const captureScript = buildCaptureScript(contentSelectors, removeSelectors);
|
|
403
465
|
const page = await openCdpPage();
|
|
404
466
|
try {
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type { CdpPage } from '../lib/research/browser.js';
|
|
2
2
|
/** Shared in-page helpers for traversing Salesforce Lightning Web Runtime (LWR) open shadow roots. */
|
|
3
3
|
export declare const SALESFORCE_SHADOW_DOM_HELPERS = "\nconst deepElements = (root) => {\n const out = [];\n const visit = (node) => {\n if (node.shadowRoot) visit(node.shadowRoot);\n for (const child of Array.from(node.children || [])) {\n out.push(child);\n visit(child);\n }\n };\n visit(root);\n return out;\n};\n\nconst deepText = (root) => {\n let txt = '';\n const visit = (node) => {\n if (node.nodeType === 3) {\n txt += node.textContent || '';\n } else if (node.nodeType === 1 || node.nodeType === 9 || node.nodeType === 11) {\n const tag = node.tagName ? node.tagName.toLowerCase() : '';\n if (tag !== 'style' && tag !== 'script') {\n if (node.shadowRoot) visit(node.shadowRoot);\n for (const child of Array.from(node.childNodes || [])) visit(child);\n }\n }\n };\n visit(root);\n return txt;\n};\n\nconst textLenFor = (el) =>\n Math.max((el.innerText || '').trim().length, deepText(el).trim().length);\n";
|
|
4
|
+
/** True when extracted text is a Salesforce not-found or error shell rather than an article.
|
|
5
|
+
*
|
|
6
|
+
* @param text - Extracted Markdown or visible page text.
|
|
7
|
+
* @returns True when the text matches a Salesforce error/not-found pattern.
|
|
8
|
+
*/
|
|
9
|
+
export declare function looksLikeSalesforceError(text: string): boolean;
|
|
4
10
|
/**
|
|
5
11
|
* In-page block: pick the highest-priority content container that exists on the page.
|
|
6
12
|
* Does not fall through to lower-priority selectors when a higher-priority host is present
|
|
@@ -33,6 +33,23 @@ const deepText = (root) => {
|
|
|
33
33
|
const textLenFor = (el) =>
|
|
34
34
|
Math.max((el.innerText || '').trim().length, deepText(el).trim().length);
|
|
35
35
|
`;
|
|
36
|
+
// Shells Salesforce renders for a missing article. Shared with the readiness probe so a
|
|
37
|
+
// not-found page ends the wait instead of burning the full content timeout.
|
|
38
|
+
const SALESFORCE_ERROR_PATTERNS = [
|
|
39
|
+
/page\s*(not\s*found|doesn'?t\s*exist|can'?t\s*be\s*found)/i,
|
|
40
|
+
/we\s*couldn'?t\s*find/i,
|
|
41
|
+
/we looked high and low/i,
|
|
42
|
+
/sorry to interrupt/i,
|
|
43
|
+
/head back to the space station/i,
|
|
44
|
+
];
|
|
45
|
+
/** True when extracted text is a Salesforce not-found or error shell rather than an article.
|
|
46
|
+
*
|
|
47
|
+
* @param text - Extracted Markdown or visible page text.
|
|
48
|
+
* @returns True when the text matches a Salesforce error/not-found pattern.
|
|
49
|
+
*/
|
|
50
|
+
export function looksLikeSalesforceError(text) {
|
|
51
|
+
return SALESFORCE_ERROR_PATTERNS.some((re) => re.test(text));
|
|
52
|
+
}
|
|
36
53
|
/**
|
|
37
54
|
* In-page block: pick the highest-priority content container that exists on the page.
|
|
38
55
|
* Does not fall through to lower-priority selectors when a higher-priority host is present
|
|
@@ -64,6 +81,20 @@ export function buildPriorityContainerPickBlock(selectors, minChars, containerVa
|
|
|
64
81
|
}
|
|
65
82
|
}
|
|
66
83
|
${containerVar} = ${containerVar} || document.body || document.documentElement;
|
|
84
|
+
// A Redoc hash (#tag/…/operation/…) names one operation inside the reference.
|
|
85
|
+
// getElementById stops at the shadow boundary, so match the id while walking.
|
|
86
|
+
try {
|
|
87
|
+
const hashId = decodeURIComponent((location.hash || '').replace(/^#/, ''));
|
|
88
|
+
if (hashId) {
|
|
89
|
+
let hashBest = null, hashLen = 0;
|
|
90
|
+
for (const el of deepElements(document)) {
|
|
91
|
+
if (el.id !== hashId) continue;
|
|
92
|
+
const len = textLenFor(el);
|
|
93
|
+
if (len > hashLen) { hashBest = el; hashLen = len; }
|
|
94
|
+
}
|
|
95
|
+
if (hashBest && hashLen >= MIN_CHARS) ${containerVar} = hashBest;
|
|
96
|
+
}
|
|
97
|
+
} catch (e) {}
|
|
67
98
|
`;
|
|
68
99
|
}
|
|
69
100
|
/**
|
|
@@ -75,8 +106,14 @@ export function buildPriorityContainerPickBlock(selectors, minChars, containerVa
|
|
|
75
106
|
* @returns A self-invoking JS expression string that evaluates to `{ has: boolean, len: number }`.
|
|
76
107
|
*/
|
|
77
108
|
export function buildContentReadyProbeExpression(selectors, minChars) {
|
|
109
|
+
const errorSource = SALESFORCE_ERROR_PATTERNS.map((re) => re.source).join('|');
|
|
78
110
|
return `(() => {
|
|
79
111
|
${SALESFORCE_SHADOW_DOM_HELPERS}
|
|
112
|
+
const errorRe = new RegExp(${JSON.stringify(errorSource)}, 'i');
|
|
113
|
+
const pageText = deepText(document.body || document.documentElement);
|
|
114
|
+
if (errorRe.test(pageText)) {
|
|
115
|
+
return { has: true, len: pageText.length, error: true };
|
|
116
|
+
}
|
|
80
117
|
const SELS = ${JSON.stringify(selectors)};
|
|
81
118
|
const MIN = ${minChars};
|
|
82
119
|
let has = false;
|
|
@@ -119,6 +156,9 @@ export async function pollSalesforceContentReady(page, selectors, minChars, time
|
|
|
119
156
|
.send('Runtime.evaluate', { expression, returnByValue: true }, page.sessionId)
|
|
120
157
|
.catch(() => null);
|
|
121
158
|
const value = rawResult?.result?.value;
|
|
159
|
+
// A not-found shell is ready now; waiting for it to "settle" only delays the failure.
|
|
160
|
+
if (value?.error)
|
|
161
|
+
return;
|
|
122
162
|
if (value?.has) {
|
|
123
163
|
const grew = Math.abs((value.len || 0) - previousLength) > Math.max(40, previousLength * 0.02);
|
|
124
164
|
if (!grew && ++stablePolls >= 2)
|
package/oclif.manifest.json
CHANGED
|
@@ -47,7 +47,13 @@
|
|
|
47
47
|
"strict": true,
|
|
48
48
|
"summary": "Show a compact cache dashboard for ambient session context",
|
|
49
49
|
"enableJsonFlag": true,
|
|
50
|
-
"stdoutIsPrimaryData": true
|
|
50
|
+
"stdoutIsPrimaryData": true,
|
|
51
|
+
"isESM": true,
|
|
52
|
+
"relativePath": [
|
|
53
|
+
"dist",
|
|
54
|
+
"commands",
|
|
55
|
+
"context.js"
|
|
56
|
+
]
|
|
51
57
|
},
|
|
52
58
|
"fetch": {
|
|
53
59
|
"aliases": [],
|
|
@@ -199,7 +205,13 @@
|
|
|
199
205
|
"strict": false,
|
|
200
206
|
"summary": "Fetch and cache URL research Markdown",
|
|
201
207
|
"enableJsonFlag": true,
|
|
202
|
-
"stdoutIsPrimaryData": true
|
|
208
|
+
"stdoutIsPrimaryData": true,
|
|
209
|
+
"isESM": true,
|
|
210
|
+
"relativePath": [
|
|
211
|
+
"dist",
|
|
212
|
+
"commands",
|
|
213
|
+
"fetch.js"
|
|
214
|
+
]
|
|
203
215
|
},
|
|
204
216
|
"import": {
|
|
205
217
|
"aliases": [],
|
|
@@ -349,7 +361,13 @@
|
|
|
349
361
|
"strict": true,
|
|
350
362
|
"summary": "Import agent-supplied Markdown into cache",
|
|
351
363
|
"enableJsonFlag": true,
|
|
352
|
-
"stdoutIsPrimaryData": true
|
|
364
|
+
"stdoutIsPrimaryData": true,
|
|
365
|
+
"isESM": true,
|
|
366
|
+
"relativePath": [
|
|
367
|
+
"dist",
|
|
368
|
+
"commands",
|
|
369
|
+
"import.js"
|
|
370
|
+
]
|
|
353
371
|
},
|
|
354
372
|
"inspect": {
|
|
355
373
|
"aliases": [],
|
|
@@ -408,7 +426,13 @@
|
|
|
408
426
|
"strict": false,
|
|
409
427
|
"summary": "Inspect cached research metadata for a URL",
|
|
410
428
|
"enableJsonFlag": true,
|
|
411
|
-
"stdoutIsPrimaryData": true
|
|
429
|
+
"stdoutIsPrimaryData": true,
|
|
430
|
+
"isESM": true,
|
|
431
|
+
"relativePath": [
|
|
432
|
+
"dist",
|
|
433
|
+
"commands",
|
|
434
|
+
"inspect.js"
|
|
435
|
+
]
|
|
412
436
|
},
|
|
413
437
|
"list": {
|
|
414
438
|
"aliases": [],
|
|
@@ -550,7 +574,13 @@
|
|
|
550
574
|
"strict": true,
|
|
551
575
|
"summary": "List cached research artifacts by metadata",
|
|
552
576
|
"enableJsonFlag": true,
|
|
553
|
-
"stdoutIsPrimaryData": true
|
|
577
|
+
"stdoutIsPrimaryData": true,
|
|
578
|
+
"isESM": true,
|
|
579
|
+
"relativePath": [
|
|
580
|
+
"dist",
|
|
581
|
+
"commands",
|
|
582
|
+
"list.js"
|
|
583
|
+
]
|
|
554
584
|
},
|
|
555
585
|
"prune": {
|
|
556
586
|
"aliases": [],
|
|
@@ -670,7 +700,13 @@
|
|
|
670
700
|
"strict": true,
|
|
671
701
|
"summary": "Prune cached research artifacts",
|
|
672
702
|
"enableJsonFlag": true,
|
|
673
|
-
"stdoutIsPrimaryData": true
|
|
703
|
+
"stdoutIsPrimaryData": true,
|
|
704
|
+
"isESM": true,
|
|
705
|
+
"relativePath": [
|
|
706
|
+
"dist",
|
|
707
|
+
"commands",
|
|
708
|
+
"prune.js"
|
|
709
|
+
]
|
|
674
710
|
},
|
|
675
711
|
"search": {
|
|
676
712
|
"aliases": [],
|
|
@@ -816,7 +852,13 @@
|
|
|
816
852
|
"strict": true,
|
|
817
853
|
"summary": "Search cached research artifacts by tags and content",
|
|
818
854
|
"enableJsonFlag": true,
|
|
819
|
-
"stdoutIsPrimaryData": true
|
|
855
|
+
"stdoutIsPrimaryData": true,
|
|
856
|
+
"isESM": true,
|
|
857
|
+
"relativePath": [
|
|
858
|
+
"dist",
|
|
859
|
+
"commands",
|
|
860
|
+
"search.js"
|
|
861
|
+
]
|
|
820
862
|
},
|
|
821
863
|
"setup": {
|
|
822
864
|
"aliases": [],
|
|
@@ -900,7 +942,13 @@
|
|
|
900
942
|
"strict": true,
|
|
901
943
|
"summary": "Install a SessionStart hook that shows live cache context",
|
|
902
944
|
"enableJsonFlag": true,
|
|
903
|
-
"stdoutIsPrimaryData": true
|
|
945
|
+
"stdoutIsPrimaryData": true,
|
|
946
|
+
"isESM": true,
|
|
947
|
+
"relativePath": [
|
|
948
|
+
"dist",
|
|
949
|
+
"commands",
|
|
950
|
+
"setup.js"
|
|
951
|
+
]
|
|
904
952
|
},
|
|
905
953
|
"status": {
|
|
906
954
|
"aliases": [],
|
|
@@ -986,20 +1034,32 @@
|
|
|
986
1034
|
"strict": false,
|
|
987
1035
|
"summary": "Check cache status without fetching or writing",
|
|
988
1036
|
"enableJsonFlag": true,
|
|
989
|
-
"stdoutIsPrimaryData": true
|
|
1037
|
+
"stdoutIsPrimaryData": true,
|
|
1038
|
+
"isESM": true,
|
|
1039
|
+
"relativePath": [
|
|
1040
|
+
"dist",
|
|
1041
|
+
"commands",
|
|
1042
|
+
"status.js"
|
|
1043
|
+
]
|
|
990
1044
|
},
|
|
991
|
-
"config": {
|
|
1045
|
+
"config:get": {
|
|
992
1046
|
"aliases": [],
|
|
993
|
-
"args": {
|
|
994
|
-
|
|
1047
|
+
"args": {
|
|
1048
|
+
"key": {
|
|
1049
|
+
"description": "the configuration key to read",
|
|
1050
|
+
"name": "key",
|
|
1051
|
+
"required": false
|
|
1052
|
+
}
|
|
1053
|
+
},
|
|
1054
|
+
"description": "Print a config value. Without --global/--local, shows the merged effective value, in precedence order: a per-command override flag (e.g. `fetch --storage`) > env var > project file > user file > built-in default.\n\nValid keys: storage (global|project), summary (conservative|balanced|aggressive).",
|
|
995
1055
|
"examples": [
|
|
996
1056
|
{
|
|
997
|
-
"description": "get
|
|
998
|
-
"command": "<%= config.bin %>
|
|
1057
|
+
"description": "get the effective storage mode",
|
|
1058
|
+
"command": "<%= config.bin %> config get storage"
|
|
999
1059
|
},
|
|
1000
1060
|
{
|
|
1001
|
-
"description": "
|
|
1002
|
-
"command": "<%= config.bin %>
|
|
1061
|
+
"description": "read only the project-level value",
|
|
1062
|
+
"command": "<%= config.bin %> config get storage --local"
|
|
1003
1063
|
}
|
|
1004
1064
|
],
|
|
1005
1065
|
"flags": {
|
|
@@ -1024,37 +1084,56 @@
|
|
|
1024
1084
|
"name": "toon",
|
|
1025
1085
|
"allowNo": false,
|
|
1026
1086
|
"type": "boolean"
|
|
1087
|
+
},
|
|
1088
|
+
"global": {
|
|
1089
|
+
"char": "g",
|
|
1090
|
+
"description": "read user-level config only",
|
|
1091
|
+
"name": "global",
|
|
1092
|
+
"allowNo": false,
|
|
1093
|
+
"type": "boolean"
|
|
1094
|
+
},
|
|
1095
|
+
"local": {
|
|
1096
|
+
"aliases": [
|
|
1097
|
+
"project"
|
|
1098
|
+
],
|
|
1099
|
+
"charAliases": [
|
|
1100
|
+
"p"
|
|
1101
|
+
],
|
|
1102
|
+
"description": "read project-level config only",
|
|
1103
|
+
"name": "local",
|
|
1104
|
+
"allowNo": false,
|
|
1105
|
+
"type": "boolean"
|
|
1027
1106
|
}
|
|
1028
1107
|
},
|
|
1029
1108
|
"hasDynamicHelp": false,
|
|
1030
1109
|
"hiddenAliases": [],
|
|
1031
|
-
"id": "config",
|
|
1110
|
+
"id": "config:get",
|
|
1032
1111
|
"pluginAlias": "@taurgis/bonsai",
|
|
1033
1112
|
"pluginName": "@taurgis/bonsai",
|
|
1034
1113
|
"pluginType": "core",
|
|
1035
1114
|
"strict": true,
|
|
1036
|
-
"summary": "
|
|
1037
|
-
"
|
|
1038
|
-
"
|
|
1115
|
+
"summary": "Get a research configuration value",
|
|
1116
|
+
"stdoutIsPrimaryData": true,
|
|
1117
|
+
"isESM": true,
|
|
1118
|
+
"relativePath": [
|
|
1119
|
+
"dist",
|
|
1120
|
+
"commands",
|
|
1121
|
+
"config",
|
|
1122
|
+
"get.js"
|
|
1123
|
+
]
|
|
1039
1124
|
},
|
|
1040
|
-
"config
|
|
1125
|
+
"config": {
|
|
1041
1126
|
"aliases": [],
|
|
1042
|
-
"args": {
|
|
1043
|
-
|
|
1044
|
-
"description": "the configuration key to read",
|
|
1045
|
-
"name": "key",
|
|
1046
|
-
"required": false
|
|
1047
|
-
}
|
|
1048
|
-
},
|
|
1049
|
-
"description": "Print a config value. Without --global/--local, shows the merged effective value, in precedence order: a per-command override flag (e.g. `fetch --storage`) > env var > project file > user file > built-in default.\n\nValid keys: storage (global|project), summary (conservative|balanced|aggressive).",
|
|
1127
|
+
"args": {},
|
|
1128
|
+
"description": "Show config subcommands for reading and writing Bonsai research cache configuration.",
|
|
1050
1129
|
"examples": [
|
|
1051
1130
|
{
|
|
1052
|
-
"description": "get
|
|
1053
|
-
"command": "<%= config.bin %>
|
|
1131
|
+
"description": "get a configuration value",
|
|
1132
|
+
"command": "<%= config.bin %> <%= command.id %> get storage"
|
|
1054
1133
|
},
|
|
1055
1134
|
{
|
|
1056
|
-
"description": "
|
|
1057
|
-
"command": "<%= config.bin %>
|
|
1135
|
+
"description": "set project storage mode",
|
|
1136
|
+
"command": "<%= config.bin %> <%= command.id %> set storage project --local"
|
|
1058
1137
|
}
|
|
1059
1138
|
],
|
|
1060
1139
|
"flags": {
|
|
@@ -1079,36 +1158,25 @@
|
|
|
1079
1158
|
"name": "toon",
|
|
1080
1159
|
"allowNo": false,
|
|
1081
1160
|
"type": "boolean"
|
|
1082
|
-
},
|
|
1083
|
-
"global": {
|
|
1084
|
-
"char": "g",
|
|
1085
|
-
"description": "read user-level config only",
|
|
1086
|
-
"name": "global",
|
|
1087
|
-
"allowNo": false,
|
|
1088
|
-
"type": "boolean"
|
|
1089
|
-
},
|
|
1090
|
-
"local": {
|
|
1091
|
-
"aliases": [
|
|
1092
|
-
"project"
|
|
1093
|
-
],
|
|
1094
|
-
"charAliases": [
|
|
1095
|
-
"p"
|
|
1096
|
-
],
|
|
1097
|
-
"description": "read project-level config only",
|
|
1098
|
-
"name": "local",
|
|
1099
|
-
"allowNo": false,
|
|
1100
|
-
"type": "boolean"
|
|
1101
1161
|
}
|
|
1102
1162
|
},
|
|
1103
1163
|
"hasDynamicHelp": false,
|
|
1104
1164
|
"hiddenAliases": [],
|
|
1105
|
-
"id": "config
|
|
1165
|
+
"id": "config",
|
|
1106
1166
|
"pluginAlias": "@taurgis/bonsai",
|
|
1107
1167
|
"pluginName": "@taurgis/bonsai",
|
|
1108
1168
|
"pluginType": "core",
|
|
1109
1169
|
"strict": true,
|
|
1110
|
-
"summary": "
|
|
1111
|
-
"
|
|
1170
|
+
"summary": "Manage research cache configuration",
|
|
1171
|
+
"enableJsonFlag": true,
|
|
1172
|
+
"stdoutIsPrimaryData": true,
|
|
1173
|
+
"isESM": true,
|
|
1174
|
+
"relativePath": [
|
|
1175
|
+
"dist",
|
|
1176
|
+
"commands",
|
|
1177
|
+
"config",
|
|
1178
|
+
"index.js"
|
|
1179
|
+
]
|
|
1112
1180
|
},
|
|
1113
1181
|
"config:list": {
|
|
1114
1182
|
"aliases": [],
|
|
@@ -1175,7 +1243,14 @@
|
|
|
1175
1243
|
"pluginType": "core",
|
|
1176
1244
|
"strict": true,
|
|
1177
1245
|
"summary": "List research configuration values",
|
|
1178
|
-
"stdoutIsPrimaryData": true
|
|
1246
|
+
"stdoutIsPrimaryData": true,
|
|
1247
|
+
"isESM": true,
|
|
1248
|
+
"relativePath": [
|
|
1249
|
+
"dist",
|
|
1250
|
+
"commands",
|
|
1251
|
+
"config",
|
|
1252
|
+
"list.js"
|
|
1253
|
+
]
|
|
1179
1254
|
},
|
|
1180
1255
|
"config:set": {
|
|
1181
1256
|
"aliases": [],
|
|
@@ -1259,7 +1334,14 @@
|
|
|
1259
1334
|
"pluginType": "core",
|
|
1260
1335
|
"strict": true,
|
|
1261
1336
|
"summary": "Set a research configuration key",
|
|
1262
|
-
"stdoutIsPrimaryData": true
|
|
1337
|
+
"stdoutIsPrimaryData": true,
|
|
1338
|
+
"isESM": true,
|
|
1339
|
+
"relativePath": [
|
|
1340
|
+
"dist",
|
|
1341
|
+
"commands",
|
|
1342
|
+
"config",
|
|
1343
|
+
"set.js"
|
|
1344
|
+
]
|
|
1263
1345
|
},
|
|
1264
1346
|
"config:unset": {
|
|
1265
1347
|
"aliases": [],
|
|
@@ -1338,8 +1420,15 @@
|
|
|
1338
1420
|
"pluginType": "core",
|
|
1339
1421
|
"strict": true,
|
|
1340
1422
|
"summary": "Remove a research configuration key",
|
|
1341
|
-
"stdoutIsPrimaryData": true
|
|
1423
|
+
"stdoutIsPrimaryData": true,
|
|
1424
|
+
"isESM": true,
|
|
1425
|
+
"relativePath": [
|
|
1426
|
+
"dist",
|
|
1427
|
+
"commands",
|
|
1428
|
+
"config",
|
|
1429
|
+
"unset.js"
|
|
1430
|
+
]
|
|
1342
1431
|
}
|
|
1343
1432
|
},
|
|
1344
|
-
"version": "4.1.
|
|
1433
|
+
"version": "4.1.1"
|
|
1345
1434
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taurgis/bonsai",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.1",
|
|
4
4
|
"description": "Bonsai is a standalone local research cache CLI for AI agents",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -35,27 +35,23 @@
|
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@mozilla/readability": "^0.5.0",
|
|
38
|
-
"@oclif/core": "^
|
|
39
|
-
"@oclif/plugin-autocomplete": "^
|
|
38
|
+
"@oclif/core": "^5.1.8",
|
|
39
|
+
"@oclif/plugin-autocomplete": "^4.0.4",
|
|
40
40
|
"@toon-format/toon": "^4.0.0",
|
|
41
|
-
"linkedom": "^0.18.
|
|
41
|
+
"linkedom": "^0.18.13",
|
|
42
42
|
"turndown": "^7.2.0",
|
|
43
|
-
"undici": "^8.
|
|
44
|
-
"update-notifier": "^7.3.1"
|
|
43
|
+
"undici": "^8.12.2"
|
|
45
44
|
},
|
|
46
45
|
"devDependencies": {
|
|
47
46
|
"@changesets/cli": "^2.31.0",
|
|
48
|
-
"@types/jsdom": "^21.1.7",
|
|
49
|
-
"@types/mozilla-readability": "^0.2.1",
|
|
50
47
|
"@types/node": "^22.10.0",
|
|
51
48
|
"@types/turndown": "^5.0.5",
|
|
52
49
|
"@vitest/coverage-istanbul": "^4.0.18",
|
|
53
50
|
"eslint": "^10.7.0",
|
|
54
51
|
"fallow": "^2.102.0",
|
|
55
52
|
"husky": "^9.1.7",
|
|
56
|
-
"jsdom": "^26.0.0",
|
|
57
53
|
"lint-staged": "^16.2.7",
|
|
58
|
-
"oclif": "^
|
|
54
|
+
"oclif": "^6.0.8",
|
|
59
55
|
"prettier": "^3.8.1",
|
|
60
56
|
"typescript": "^5.9.3",
|
|
61
57
|
"typescript-eslint": "^8.64.0",
|
|
@@ -78,9 +74,8 @@
|
|
|
78
74
|
"@oclif/plugin-autocomplete"
|
|
79
75
|
],
|
|
80
76
|
"commands": {
|
|
81
|
-
"strategy": "
|
|
82
|
-
"target": "./dist/commands
|
|
83
|
-
"identifier": "commands"
|
|
77
|
+
"strategy": "pattern",
|
|
78
|
+
"target": "./dist/commands"
|
|
84
79
|
},
|
|
85
80
|
"topicSeparator": " ",
|
|
86
81
|
"hooks": {
|