@taurgis/bonsai 1.0.5 → 1.0.6
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 +53 -0
- package/agents/agents/official-docs-researcher.agent.md +10 -1
- package/agents/agents/salesforce-docs-researcher.agent.md +10 -1
- package/agents/instructions/salesforce-research.instructions.md +4 -2
- package/agents/instructions/web-research.instructions.md +4 -2
- package/agents/skills/web-research/SKILL.md +8 -3
- package/bin/cli.mjs +30 -13
- package/dist/base-command.d.ts +3 -0
- package/dist/base-command.js +40 -7
- package/dist/commands/config/base.d.ts +2 -0
- package/dist/commands/config/base.js +15 -1
- package/dist/commands/config/get.d.ts +1 -1
- package/dist/commands/config/get.js +4 -5
- package/dist/commands/config/index.d.ts +12 -0
- package/dist/commands/config/index.js +34 -0
- package/dist/commands/config/list.js +2 -2
- package/dist/commands/config/set.js +18 -12
- package/dist/commands/config/unset.d.ts +1 -1
- package/dist/commands/config/unset.js +9 -6
- package/dist/commands/fetch.js +44 -34
- package/dist/commands/import.d.ts +3 -0
- package/dist/commands/import.js +82 -52
- package/dist/commands/inspect.d.ts +4 -0
- package/dist/commands/inspect.js +24 -19
- package/dist/commands/list.d.ts +2 -2
- package/dist/commands/list.js +23 -15
- package/dist/commands/prune.d.ts +1 -1
- package/dist/commands/prune.js +30 -13
- package/dist/commands/search.d.ts +4 -0
- package/dist/commands/search.js +83 -21
- package/dist/commands/status.d.ts +6 -0
- package/dist/commands/status.js +50 -24
- package/dist/commands.d.ts +2 -0
- package/dist/commands.js +2 -0
- package/dist/hooks/command-not-found/suggest.js +37 -8
- package/dist/lib/argv.d.ts +10 -0
- package/dist/lib/argv.js +53 -0
- package/dist/lib/envelope.d.ts +36 -0
- package/dist/lib/envelope.js +76 -0
- package/dist/lib/json-meta.d.ts +10 -0
- package/dist/lib/json-meta.js +98 -0
- package/dist/lib/research/revalidate.js +3 -1
- package/dist/lib/research/schema.d.ts +5 -2
- package/dist/lib/research/schema.js +12 -1
- package/dist/lib/text.d.ts +2 -0
- package/dist/lib/text.js +2 -0
- package/dist/sites/algolia.d.ts +9 -0
- package/dist/sites/algolia.js +26 -0
- package/dist/sites/auto.d.ts +8 -0
- package/dist/sites/auto.js +30 -0
- package/dist/sites/index.js +17 -1
- package/dist/sites/nextjs/index.d.ts +2 -0
- package/dist/sites/nextjs/index.js +52 -0
- package/dist/sites/react/index.d.ts +2 -0
- package/dist/sites/react/index.js +17 -0
- package/dist/sites/tailwind/index.d.ts +2 -0
- package/dist/sites/tailwind/index.js +17 -0
- package/dist/sites/vue/index.d.ts +2 -0
- package/dist/sites/vue/index.js +17 -0
- package/oclif.manifest.json +149 -76
- package/package.json +8 -2
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { toConfiguredId } from '@oclif/core';
|
|
2
2
|
import { closestMatch } from '../../lib/text.js';
|
|
3
|
-
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
|
|
3
|
+
import { buildEnvelope, formatErrorForJson } from '../../lib/envelope.js';
|
|
4
|
+
// Nearest command must be close enough to be useful. A fixed threshold of 3 is too generous for
|
|
5
|
+
// very short inputs (`wat` should not suggest `list`), while still reasonable for longer topic ids.
|
|
6
|
+
function maxSuggestionDistance(input) {
|
|
7
|
+
if (input.length <= 3)
|
|
8
|
+
return 1;
|
|
9
|
+
if (input.length <= 5)
|
|
10
|
+
return 2;
|
|
11
|
+
return 3;
|
|
12
|
+
}
|
|
7
13
|
/**
|
|
8
14
|
* The nearest visible command to a typo, plus how many leading segments of the attempted id name
|
|
9
15
|
* the command. With `topicSeparator: ' '`, oclif folds every leading token of an unknown command
|
|
@@ -14,7 +20,8 @@ const MAX_SUGGESTION_DISTANCE = 3;
|
|
|
14
20
|
*/
|
|
15
21
|
function findSuggestion(segments, visibleIds) {
|
|
16
22
|
for (let n = segments.length; n >= 1; n--) {
|
|
17
|
-
const
|
|
23
|
+
const attempted = segments.slice(0, n).join(':');
|
|
24
|
+
const suggestion = closestMatch(attempted, visibleIds, maxSuggestionDistance(attempted));
|
|
18
25
|
if (suggestion)
|
|
19
26
|
return { suggestion, commandSegments: n };
|
|
20
27
|
}
|
|
@@ -56,6 +63,22 @@ function bareUrlInput(id) {
|
|
|
56
63
|
}
|
|
57
64
|
return DOMAIN_OR_IPV4.test(hostname) ? id : null;
|
|
58
65
|
}
|
|
66
|
+
function isJsonMode(argv) {
|
|
67
|
+
return argv?.includes('--json') ?? false;
|
|
68
|
+
}
|
|
69
|
+
function emitJsonError(command, message, code) {
|
|
70
|
+
const envelope = buildEnvelope({
|
|
71
|
+
command,
|
|
72
|
+
ok: false,
|
|
73
|
+
exitCode: 2,
|
|
74
|
+
stderr: formatErrorForJson({ message, code }),
|
|
75
|
+
data: null,
|
|
76
|
+
code,
|
|
77
|
+
});
|
|
78
|
+
process.exitCode = 2;
|
|
79
|
+
console.log(JSON.stringify(envelope, null, 2));
|
|
80
|
+
return envelope;
|
|
81
|
+
}
|
|
59
82
|
/**
|
|
60
83
|
* Typo-aware "command not found" handler. oclif fires this before printing its own terse error;
|
|
61
84
|
* throwing here (via `this.error`) replaces that default with a more helpful message. A correction
|
|
@@ -70,11 +93,14 @@ const hook = async function (opts) {
|
|
|
70
93
|
// never finds a command for a hostname). The correction is shown, never auto-run (clig.dev).
|
|
71
94
|
const bareUrl = bareUrlInput(opts.id);
|
|
72
95
|
if (bareUrl) {
|
|
73
|
-
|
|
96
|
+
const message = [
|
|
74
97
|
`${bareUrl} is not a ${opts.config.bin} command.`,
|
|
75
98
|
`Run ${opts.config.bin} help for a list of available commands.`,
|
|
76
99
|
`Did you mean ${opts.config.bin} https://${bareUrl}? URLs need an http:// or https:// scheme.`,
|
|
77
|
-
].join('\n')
|
|
100
|
+
].join('\n');
|
|
101
|
+
if (isJsonMode(opts.argv))
|
|
102
|
+
return emitJsonError(bareUrl, message, 'MISSING_URL_SCHEME');
|
|
103
|
+
this.error(message, { exit: 2 });
|
|
78
104
|
}
|
|
79
105
|
const segments = opts.id.split(':');
|
|
80
106
|
const { suggestion, commandSegments } = findSuggestion(segments, visibleIds);
|
|
@@ -88,6 +114,9 @@ const hook = async function (opts) {
|
|
|
88
114
|
lines.push(`Run ${opts.config.bin} help for a list of available commands.`);
|
|
89
115
|
if (suggestion)
|
|
90
116
|
lines.push(`Did you mean ${toConfiguredId(suggestion, opts.config)}?`);
|
|
91
|
-
|
|
117
|
+
const message = lines.join('\n');
|
|
118
|
+
if (isJsonMode(opts.argv))
|
|
119
|
+
return emitJsonError(attempted, message, 'COMMAND_NOT_FOUND');
|
|
120
|
+
this.error(message, { exit: 2 });
|
|
92
121
|
};
|
|
93
122
|
export default hook;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface NormalizationResult {
|
|
2
|
+
/** The normalized argv array to set on process.argv. */
|
|
3
|
+
argv: string[];
|
|
4
|
+
/** Optional metadata indicating the shim should exit early with a JSON envelope. */
|
|
5
|
+
exitWithJson?: {
|
|
6
|
+
exitCode: number;
|
|
7
|
+
envelope: Record<string, unknown>;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export declare function normalizeArgv(rawArgv: string[]): NormalizationResult;
|
package/dist/lib/argv.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { buildEnvelope } from './envelope.js';
|
|
2
|
+
/**
|
|
3
|
+
* Normalizes process.argv (excluding Node executable and script path) so the
|
|
4
|
+
* oclif pipeline sees one consistent command structure.
|
|
5
|
+
*/
|
|
6
|
+
function missingUsageJsonExit() {
|
|
7
|
+
return {
|
|
8
|
+
exitCode: 2,
|
|
9
|
+
envelope: buildEnvelope({
|
|
10
|
+
command: 'bonsai',
|
|
11
|
+
ok: false,
|
|
12
|
+
exitCode: 2,
|
|
13
|
+
stderr: 'Missing URL or command. Run bonsai --help for usage.',
|
|
14
|
+
data: null,
|
|
15
|
+
}),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function normalizeArgv(rawArgv) {
|
|
19
|
+
const onlyJsonFlags = rawArgv.length > 0 && rawArgv.every((arg) => arg === '--json');
|
|
20
|
+
if (onlyJsonFlags) {
|
|
21
|
+
return {
|
|
22
|
+
// bin/cli.mjs exits before applying argv when exitWithJson is set.
|
|
23
|
+
argv: ['--json'],
|
|
24
|
+
exitWithJson: missingUsageJsonExit(),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
// oclif's JSON flag is command-scoped, so `bonsai list --json` works but
|
|
28
|
+
// `bonsai --json list` is otherwise parsed as an unknown command named
|
|
29
|
+
// "--json". Collect every --json, append one copy after the command/URL, and
|
|
30
|
+
// dedupe repeats like `bonsai --json --json list`.
|
|
31
|
+
const jsonMode = rawArgv.includes('--json');
|
|
32
|
+
let tokens = rawArgv.filter((arg) => arg !== '--json');
|
|
33
|
+
// clig.dev: -h and --help must always work at any level.
|
|
34
|
+
tokens = tokens.map((arg) => (arg === '-h' ? '--help' : arg));
|
|
35
|
+
if (tokens[0] === 'help') {
|
|
36
|
+
tokens = [...tokens.slice(1), '--help'];
|
|
37
|
+
}
|
|
38
|
+
const helpRequested = tokens.includes('--help');
|
|
39
|
+
let core = tokens.filter((arg) => arg !== '--help');
|
|
40
|
+
const firstArg = core[0];
|
|
41
|
+
// Treat any first arg that looks like a URL (carries a scheme) as the `fetch` shorthand.
|
|
42
|
+
// Match both `https://…` and scheme-only forms like `javascript:` or `data:` so fetch can
|
|
43
|
+
// reject unsupported protocols instead of oclif reporting a misleading "command not found".
|
|
44
|
+
const looksLikeUrl = firstArg != null && (firstArg.includes('://') || /^[a-z][a-z0-9+.-]*:/i.test(firstArg));
|
|
45
|
+
if (looksLikeUrl) {
|
|
46
|
+
core = ['fetch', ...core];
|
|
47
|
+
}
|
|
48
|
+
if (helpRequested)
|
|
49
|
+
core.push('--help');
|
|
50
|
+
if (jsonMode)
|
|
51
|
+
core.push('--json');
|
|
52
|
+
return { argv: core };
|
|
53
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Fields oclif attaches to CLIError that human pretty-print surfaces but JSON mode must mirror. */
|
|
2
|
+
export interface CliErrorShape {
|
|
3
|
+
message?: string;
|
|
4
|
+
code?: string;
|
|
5
|
+
suggestions?: string[];
|
|
6
|
+
ref?: string;
|
|
7
|
+
}
|
|
8
|
+
/** Map oclif parse-time errors to stable Bonsai codes when the throw site did not set one. */
|
|
9
|
+
export declare function stableErrorCodeFrom(err: unknown): string | undefined;
|
|
10
|
+
/** Strip oclif's generic help suffix from JSON stderr — agents already have structured codes. */
|
|
11
|
+
export declare function normalizeCliErrorMessage(message: string): string;
|
|
12
|
+
export interface EnvelopeParts {
|
|
13
|
+
command: string;
|
|
14
|
+
ok: boolean;
|
|
15
|
+
exitCode: number;
|
|
16
|
+
stderr: string;
|
|
17
|
+
data: unknown;
|
|
18
|
+
/** Stable machine-readable error code when the failure is a CLIError with `code`. */
|
|
19
|
+
code?: string;
|
|
20
|
+
suggestions?: string[];
|
|
21
|
+
ref?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Formats a CLI error for the JSON envelope `stderr` field — same lines as human pretty-print,
|
|
25
|
+
* without ANSI or the leading "Error:" prefix (the message is already the primary line).
|
|
26
|
+
*/
|
|
27
|
+
export declare function formatErrorForJson(err: CliErrorShape): string;
|
|
28
|
+
/**
|
|
29
|
+
* Builds the standard Bonsai CLI JSON envelope structure.
|
|
30
|
+
* This is the single source of truth for the output envelope schema.
|
|
31
|
+
*/
|
|
32
|
+
export declare function buildEnvelope(parts: EnvelopeParts): Record<string, unknown>;
|
|
33
|
+
/**
|
|
34
|
+
* Formats a JSON envelope to a string with indentation.
|
|
35
|
+
*/
|
|
36
|
+
export declare function formatEnvelope(parts: EnvelopeParts): string;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/** Map oclif parse-time errors to stable Bonsai codes when the throw site did not set one. */
|
|
2
|
+
export function stableErrorCodeFrom(err) {
|
|
3
|
+
const e = err;
|
|
4
|
+
if (typeof e?.code === 'string' && e.code)
|
|
5
|
+
return e.code;
|
|
6
|
+
switch (e?.constructor?.name) {
|
|
7
|
+
case 'RequiredArgsError':
|
|
8
|
+
return 'MISSING_ARGUMENT';
|
|
9
|
+
case 'FlagInvalidOptionError':
|
|
10
|
+
case 'ArgInvalidOptionError':
|
|
11
|
+
return 'INVALID_FLAG_VALUE';
|
|
12
|
+
case 'NonExistentFlagsError':
|
|
13
|
+
return 'UNKNOWN_FLAG';
|
|
14
|
+
case 'UnexpectedArgsError':
|
|
15
|
+
return 'UNEXPECTED_ARGUMENT';
|
|
16
|
+
default:
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** Strip oclif's generic help suffix from JSON stderr — agents already have structured codes. */
|
|
21
|
+
export function normalizeCliErrorMessage(message) {
|
|
22
|
+
const suffix = '\nSee more help with --help';
|
|
23
|
+
return message.endsWith(suffix) ? message.slice(0, -suffix.length) : message;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Formats a CLI error for the JSON envelope `stderr` field — same lines as human pretty-print,
|
|
27
|
+
* without ANSI or the leading "Error:" prefix (the message is already the primary line).
|
|
28
|
+
*/
|
|
29
|
+
export function formatErrorForJson(err) {
|
|
30
|
+
const lines = [];
|
|
31
|
+
if (err.message)
|
|
32
|
+
lines.push(err.message);
|
|
33
|
+
if (err.code)
|
|
34
|
+
lines.push(`Code: ${err.code}`);
|
|
35
|
+
if (err.suggestions?.length) {
|
|
36
|
+
if (err.suggestions.length === 1) {
|
|
37
|
+
lines.push(`Try this: ${err.suggestions[0]}`);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
lines.push('Try this:');
|
|
41
|
+
for (const suggestion of err.suggestions)
|
|
42
|
+
lines.push(`* ${suggestion}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (err.ref)
|
|
46
|
+
lines.push(`Reference: ${err.ref}`);
|
|
47
|
+
return lines.join('\n');
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Builds the standard Bonsai CLI JSON envelope structure.
|
|
51
|
+
* This is the single source of truth for the output envelope schema.
|
|
52
|
+
*/
|
|
53
|
+
export function buildEnvelope(parts) {
|
|
54
|
+
const envelope = {
|
|
55
|
+
schemaVersion: 1,
|
|
56
|
+
command: parts.command,
|
|
57
|
+
ok: parts.ok,
|
|
58
|
+
exitCode: parts.exitCode,
|
|
59
|
+
stdout: '',
|
|
60
|
+
stderr: parts.stderr,
|
|
61
|
+
data: parts.data,
|
|
62
|
+
};
|
|
63
|
+
if (parts.code)
|
|
64
|
+
envelope.code = parts.code;
|
|
65
|
+
if (parts.suggestions?.length)
|
|
66
|
+
envelope.suggestions = parts.suggestions;
|
|
67
|
+
if (parts.ref)
|
|
68
|
+
envelope.ref = parts.ref;
|
|
69
|
+
return envelope;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Formats a JSON envelope to a string with indentation.
|
|
73
|
+
*/
|
|
74
|
+
export function formatEnvelope(parts) {
|
|
75
|
+
return JSON.stringify(buildEnvelope(parts), null, 2);
|
|
76
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** True when argv asks for help or version together with --json. */
|
|
2
|
+
export declare function isJsonMetaRequest(argv: readonly string[]): boolean;
|
|
3
|
+
/**
|
|
4
|
+
* When `--json` is combined with `--help` or `--version`, oclif would print human text to stdout.
|
|
5
|
+
* Render the same content inside the standard Bonsai JSON envelope instead.
|
|
6
|
+
*/
|
|
7
|
+
export declare function tryJsonMetaOutput(argv: readonly string[], root: string): Promise<{
|
|
8
|
+
exitCode: number;
|
|
9
|
+
envelope: Record<string, unknown>;
|
|
10
|
+
} | null>;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { Config, Help, toConfiguredId } from '@oclif/core';
|
|
2
|
+
import { buildEnvelope } from './envelope.js';
|
|
3
|
+
/** Strip ANSI color codes so JSON help text stays machine-stable. */
|
|
4
|
+
function stripAnsi(text) {
|
|
5
|
+
// eslint-disable-next-line no-control-regex
|
|
6
|
+
return text.replace(/\x1b\[[0-9;]*m/g, '');
|
|
7
|
+
}
|
|
8
|
+
/** True when argv asks for help or version together with --json. */
|
|
9
|
+
export function isJsonMetaRequest(argv) {
|
|
10
|
+
if (!argv.includes('--json'))
|
|
11
|
+
return false;
|
|
12
|
+
return argv.includes('--help') || argv.includes('--version');
|
|
13
|
+
}
|
|
14
|
+
class CaptureHelp extends Help {
|
|
15
|
+
lines = [];
|
|
16
|
+
log(...args) {
|
|
17
|
+
for (const line of args) {
|
|
18
|
+
// oclif emits section separators as log(''); drop empties so data.help stays compact.
|
|
19
|
+
if (line)
|
|
20
|
+
this.lines.push(line);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function helpSubject(argv) {
|
|
25
|
+
for (const arg of argv) {
|
|
26
|
+
if (arg === '--' || arg === '--help' || arg === 'help')
|
|
27
|
+
continue;
|
|
28
|
+
if (arg.startsWith('-'))
|
|
29
|
+
return;
|
|
30
|
+
return arg;
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
function envelopeCommandId(config, argv) {
|
|
35
|
+
const subject = helpSubject(argv);
|
|
36
|
+
if (!subject)
|
|
37
|
+
return config.bin;
|
|
38
|
+
const command = config.findCommand(subject);
|
|
39
|
+
if (command?.id)
|
|
40
|
+
return toConfiguredId(command.id, config);
|
|
41
|
+
const topic = config.findTopic(subject);
|
|
42
|
+
if (topic)
|
|
43
|
+
return topic.name;
|
|
44
|
+
return subject;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* When `--json` is combined with `--help` or `--version`, oclif would print human text to stdout.
|
|
48
|
+
* Render the same content inside the standard Bonsai JSON envelope instead.
|
|
49
|
+
*/
|
|
50
|
+
export async function tryJsonMetaOutput(argv, root) {
|
|
51
|
+
if (!isJsonMetaRequest(argv))
|
|
52
|
+
return null;
|
|
53
|
+
try {
|
|
54
|
+
const config = await Config.load({ root });
|
|
55
|
+
if (argv.includes('--version')) {
|
|
56
|
+
return {
|
|
57
|
+
exitCode: 0,
|
|
58
|
+
envelope: buildEnvelope({
|
|
59
|
+
command: config.bin,
|
|
60
|
+
ok: true,
|
|
61
|
+
exitCode: 0,
|
|
62
|
+
stderr: '',
|
|
63
|
+
data: {
|
|
64
|
+
version: config.version,
|
|
65
|
+
userAgent: config.userAgent,
|
|
66
|
+
},
|
|
67
|
+
}),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
const help = new CaptureHelp(config, config.pjson.oclif?.helpOptions ?? {});
|
|
71
|
+
await help.showHelp([...argv]);
|
|
72
|
+
const helpText = stripAnsi(help.lines.join('\n').trimEnd());
|
|
73
|
+
return {
|
|
74
|
+
exitCode: 0,
|
|
75
|
+
envelope: buildEnvelope({
|
|
76
|
+
command: envelopeCommandId(config, argv),
|
|
77
|
+
ok: true,
|
|
78
|
+
exitCode: 0,
|
|
79
|
+
stderr: '',
|
|
80
|
+
data: { help: helpText },
|
|
81
|
+
}),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
86
|
+
return {
|
|
87
|
+
exitCode: 1,
|
|
88
|
+
envelope: buildEnvelope({
|
|
89
|
+
command: 'bonsai',
|
|
90
|
+
ok: false,
|
|
91
|
+
exitCode: 1,
|
|
92
|
+
stderr: message,
|
|
93
|
+
data: null,
|
|
94
|
+
code: 'META_RENDER_FAILED',
|
|
95
|
+
}),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -178,7 +178,9 @@ export async function revalidateCache(dataDir, existing, currentTime, options) {
|
|
|
178
178
|
status: 'stale',
|
|
179
179
|
artifact: existing,
|
|
180
180
|
allowed: options.allowStale,
|
|
181
|
-
|
|
181
|
+
// Carry only the underlying cause; the command layer owns the user-facing framing
|
|
182
|
+
// (so the warning reads cleanly instead of stuttering "stale … stale", "failed … failed").
|
|
183
|
+
error: err.message,
|
|
182
184
|
};
|
|
183
185
|
}
|
|
184
186
|
throw new Error(`Revalidation failed and cache has expired beyond the grace period: ${err.message}`);
|
|
@@ -2,10 +2,13 @@ export interface TokenEstimate {
|
|
|
2
2
|
compressed: number | null;
|
|
3
3
|
detailed: number | null;
|
|
4
4
|
}
|
|
5
|
-
export
|
|
5
|
+
export declare const CAPTURE_METHODS: readonly ["static_fetch", "browser_fallback", "agent_supplied", "route_markdown", "github_source"];
|
|
6
|
+
export type CaptureMethod = (typeof CAPTURE_METHODS)[number];
|
|
7
|
+
export declare const ARTIFACT_TYPES: readonly ["source", "research_note", "index", "section"];
|
|
8
|
+
export type ArtifactType = (typeof ARTIFACT_TYPES)[number];
|
|
6
9
|
export interface ResearchArtifactMetadata {
|
|
7
10
|
schema_version: number;
|
|
8
|
-
artifact_type:
|
|
11
|
+
artifact_type: ArtifactType;
|
|
9
12
|
source_url: string;
|
|
10
13
|
source_urls: string[];
|
|
11
14
|
normalized_url: string;
|
|
@@ -1 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
// Single source of truth for the capture-method enum. Command flag `options` lists derive from this
|
|
2
|
+
// so a new method can never drift out of sync with the filters that are supposed to expose it.
|
|
3
|
+
export const CAPTURE_METHODS = [
|
|
4
|
+
'static_fetch',
|
|
5
|
+
'browser_fallback',
|
|
6
|
+
'agent_supplied',
|
|
7
|
+
'route_markdown',
|
|
8
|
+
'github_source',
|
|
9
|
+
];
|
|
10
|
+
// Single source of truth for the artifact-type enum. `index` = navigation/hub or llms.txt site
|
|
11
|
+
// index; `section` = a heading-level child of a page. Command flag `options` derive from this.
|
|
12
|
+
export const ARTIFACT_TYPES = ['source', 'research_note', 'index', 'section'];
|
package/dist/lib/text.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** Display label when an artifact has no topic — not a filterable topic value. */
|
|
2
|
+
export declare const NO_TOPIC_LABEL = "(no topic)";
|
|
1
3
|
/**
|
|
2
4
|
* Pick the singular or plural noun for a count so human-readable output stays grammatical
|
|
3
5
|
* ("1 entry" / "2 entries"). English has irregular plurals, so the plural is passed explicitly
|
package/dist/lib/text.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** Display label when an artifact has no topic — not a filterable topic value. */
|
|
2
|
+
export const NO_TOPIC_LABEL = '(no topic)';
|
|
1
3
|
/**
|
|
2
4
|
* Pick the singular or plural noun for a count so human-readable output stays grammatical
|
|
3
5
|
* ("1 entry" / "2 entries"). English has irregular plurals, so the plural is passed explicitly
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { SiteModule } from './types.js';
|
|
2
|
+
export declare function createAlgoliaSite(id: string, name: string, domains: string[], appId: string, apiKey: string, indexName: string): SiteModule;
|
|
3
|
+
export declare const react: SiteModule;
|
|
4
|
+
export declare const vue: SiteModule;
|
|
5
|
+
export declare const tailwind: SiteModule;
|
|
6
|
+
export declare const jest: SiteModule;
|
|
7
|
+
export declare const cypress: SiteModule;
|
|
8
|
+
export declare const vitest: SiteModule;
|
|
9
|
+
export declare const vitepress: SiteModule;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { algoliaQueryUrl, parseAlgoliaResponse } from '../lib/research/docs/remote-search.js';
|
|
2
|
+
import { postJson } from '../lib/research/fetcher.js';
|
|
3
|
+
export function createAlgoliaSite(id, name, domains, appId, apiKey, indexName) {
|
|
4
|
+
return {
|
|
5
|
+
id,
|
|
6
|
+
name,
|
|
7
|
+
domains,
|
|
8
|
+
search: async (query) => {
|
|
9
|
+
const url = algoliaQueryUrl({ appId, apiKey, indexName });
|
|
10
|
+
const bodyParams = { params: `query=${encodeURIComponent(query)}&hitsPerPage=20` };
|
|
11
|
+
const res = await postJson(url, bodyParams, {
|
|
12
|
+
'Content-Type': 'application/json',
|
|
13
|
+
'X-Algolia-API-Key': apiKey,
|
|
14
|
+
'X-Algolia-Application-Id': appId,
|
|
15
|
+
});
|
|
16
|
+
return parseAlgoliaResponse(res, url);
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export const react = createAlgoliaSite('react', 'React', ['react.dev', 'legacy.reactjs.org'], '1FCF9AYYAT', '1b7ad4e1c89e645e351e59d40544eda1', 'beta-react');
|
|
21
|
+
export const vue = createAlgoliaSite('vue', 'Vue.js', ['vuejs.org'], 'ML0LEBN7FQ', '10e7a8b13e6aec4007343338ab134e05', 'vuejs');
|
|
22
|
+
export const tailwind = createAlgoliaSite('tailwind', 'Tailwind CSS', ['tailwindcss.com'], 'KNPXZI5B0M', '5fc87cef58bb80203d2207578309fab6', 'tailwindcss');
|
|
23
|
+
export const jest = createAlgoliaSite('jest', 'Jest', ['jestjs.io'], 'HP439UUSOL', 'e5e670fd16f8f17caada79d6b0931682', 'jest-v2');
|
|
24
|
+
export const cypress = createAlgoliaSite('cypress', 'Cypress', ['docs.cypress.io', 'cypress.io'], 'R9KDA5FMJB', 'b4af59e23bc2fa05281af7dcf13fcae5', 'cypress_docs');
|
|
25
|
+
export const vitest = createAlgoliaSite('vitest', 'Vitest', ['vitest.dev'], 'ZTF29HGJ69', '9c3ced6fed60d2670bb36ab7e8bed8bc', 'vitest');
|
|
26
|
+
export const vitepress = createAlgoliaSite('vitepress', 'VitePress', ['vitepress.dev'], '8J64VVRP8K', '52f578a92b88ad6abde815aae2b0ad7c', 'vitepress');
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SiteModule } from './types.js';
|
|
2
|
+
export declare function createAutoSite(id: string, name: string, domains: string[], docsUrl: string): SiteModule;
|
|
3
|
+
export declare const angular: SiteModule;
|
|
4
|
+
export declare const redux: SiteModule;
|
|
5
|
+
export declare const vite: SiteModule;
|
|
6
|
+
export declare const fastify: SiteModule;
|
|
7
|
+
export declare const rollup: SiteModule;
|
|
8
|
+
export declare const vueuse: SiteModule;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { runRemoteDocsSearch } from '../lib/research/docs/remote-search-runner.js';
|
|
2
|
+
import { fetchStaticHtml, fetchText, postJson } from '../lib/research/fetcher.js';
|
|
3
|
+
export function createAutoSite(id, name, domains, docsUrl) {
|
|
4
|
+
return {
|
|
5
|
+
id,
|
|
6
|
+
name,
|
|
7
|
+
domains,
|
|
8
|
+
search: async (query) => {
|
|
9
|
+
const deps = {
|
|
10
|
+
fetchStatic: async (url) => {
|
|
11
|
+
const res = await fetchStaticHtml(url);
|
|
12
|
+
return { content: res.content, finalUrl: res.finalUrl };
|
|
13
|
+
},
|
|
14
|
+
fetchText: async (url) => {
|
|
15
|
+
const res = await fetchText(url);
|
|
16
|
+
return { content: res.content, status: res.status };
|
|
17
|
+
},
|
|
18
|
+
postJson: (url, body, headers) => postJson(url, body, headers),
|
|
19
|
+
};
|
|
20
|
+
const { results } = await runRemoteDocsSearch(docsUrl, query, deps);
|
|
21
|
+
return results;
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export const angular = createAutoSite('angular', 'Angular', ['angular.dev'], 'https://angular.dev');
|
|
26
|
+
export const redux = createAutoSite('redux', 'Redux', ['redux.js.org'], 'https://redux.js.org');
|
|
27
|
+
export const vite = createAutoSite('vite', 'Vite', ['vitejs.dev', 'vite.dev'], 'https://vite.dev');
|
|
28
|
+
export const fastify = createAutoSite('fastify', 'Fastify', ['fastify.dev', 'fastify.io'], 'https://fastify.dev');
|
|
29
|
+
export const rollup = createAutoSite('rollup', 'Rollup', ['rollupjs.org'], 'https://rollupjs.org');
|
|
30
|
+
export const vueuse = createAutoSite('vueuse', 'VueUse', ['vueuse.org'], 'https://vueuse.org');
|
package/dist/sites/index.js
CHANGED
|
@@ -1,11 +1,27 @@
|
|
|
1
1
|
import { salesforce } from './salesforce/index.js';
|
|
2
2
|
import { salesforceDeveloper } from './salesforce-developer/index.js';
|
|
3
|
+
import { nextjs } from './nextjs/index.js';
|
|
4
|
+
import { react, vue, tailwind, jest, cypress, vitest, vitepress } from './algolia.js';
|
|
5
|
+
import { angular, redux, vite, fastify, rollup, vueuse } from './auto.js';
|
|
3
6
|
// The known documentation sites. A plain constant, not a registry: these ship
|
|
4
7
|
// with the CLI, so there is nothing to register at runtime. Sites with custom
|
|
5
8
|
// fetch/search behavior live in their own module (e.g. ./salesforce). Salesforce Help and
|
|
6
9
|
// Salesforce Developer are separate modules — different domains, different extraction.
|
|
7
10
|
export const SITES = [
|
|
8
|
-
|
|
11
|
+
react,
|
|
12
|
+
vue,
|
|
13
|
+
tailwind,
|
|
14
|
+
nextjs,
|
|
15
|
+
jest,
|
|
16
|
+
cypress,
|
|
17
|
+
vitest,
|
|
18
|
+
vitepress,
|
|
19
|
+
angular,
|
|
20
|
+
redux,
|
|
21
|
+
vite,
|
|
22
|
+
fastify,
|
|
23
|
+
rollup,
|
|
24
|
+
vueuse,
|
|
9
25
|
salesforce,
|
|
10
26
|
salesforceDeveloper,
|
|
11
27
|
// TanStack docs embed a GitHub "edit this page" link, so capture resolves the page's source
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { postJson } from '../../lib/research/fetcher.js';
|
|
2
|
+
export const nextjs = {
|
|
3
|
+
id: 'nextjs',
|
|
4
|
+
name: 'Next.js',
|
|
5
|
+
domains: ['nextjs.org'],
|
|
6
|
+
search: async (query) => {
|
|
7
|
+
const url = 'https://nextjs.org/api/search';
|
|
8
|
+
const bodyStr = await postJson(url, { query, version: 'stable', routerType: 'app' }, {});
|
|
9
|
+
let data;
|
|
10
|
+
try {
|
|
11
|
+
// The Next.js API returns a JSON array sometimes or a JSON object with 'hits'
|
|
12
|
+
const parsed = JSON.parse(bodyStr);
|
|
13
|
+
if (Array.isArray(parsed)) {
|
|
14
|
+
data = parsed;
|
|
15
|
+
}
|
|
16
|
+
else if (parsed && Array.isArray(parsed.hits)) {
|
|
17
|
+
data = parsed.hits;
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
throw new Error('Unexpected response format from Next.js search');
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
throw new Error(`Failed to parse Next.js search response: ${err.message}`);
|
|
25
|
+
}
|
|
26
|
+
const queryLower = query.toLowerCase();
|
|
27
|
+
return data.map((item) => {
|
|
28
|
+
const content = item.content || '';
|
|
29
|
+
let snippet = content;
|
|
30
|
+
const idx = content.toLowerCase().indexOf(queryLower);
|
|
31
|
+
if (idx !== -1) {
|
|
32
|
+
const start = Math.max(0, idx - 40);
|
|
33
|
+
const end = Math.min(content.length, idx + 110);
|
|
34
|
+
snippet = content.slice(start, end);
|
|
35
|
+
if (start > 0)
|
|
36
|
+
snippet = '...' + snippet;
|
|
37
|
+
if (end < content.length)
|
|
38
|
+
snippet = snippet + '...';
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
snippet = content.slice(0, 150);
|
|
42
|
+
}
|
|
43
|
+
// Clean up whitespace and newlines so it looks like a snippet and not a raw script
|
|
44
|
+
snippet = snippet.replace(/\s+/g, ' ').trim();
|
|
45
|
+
return {
|
|
46
|
+
url: `https://nextjs.org${item.path}${item.anchor ? `#${item.anchor}` : ''}`,
|
|
47
|
+
title: item.title ? `${item.pageTitle} - ${item.title}` : item.pageTitle || 'Untitled',
|
|
48
|
+
snippet,
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
},
|
|
52
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { algoliaQueryUrl, parseAlgoliaResponse } from '../../lib/research/docs/remote-search.js';
|
|
2
|
+
import { postJson } from '../../lib/research/fetcher.js';
|
|
3
|
+
export const react = {
|
|
4
|
+
id: 'react',
|
|
5
|
+
name: 'React',
|
|
6
|
+
domains: ['react.dev', 'legacy.reactjs.org'],
|
|
7
|
+
search: async (query) => {
|
|
8
|
+
const config = {
|
|
9
|
+
appId: '1FCF9AYYAT',
|
|
10
|
+
apiKey: '1b7ad4e1c89e645e351e59d40544eda1',
|
|
11
|
+
indexName: 'beta-react'
|
|
12
|
+
};
|
|
13
|
+
const url = algoliaQueryUrl(config);
|
|
14
|
+
const body = await postJson(url, { params: `query=${encodeURIComponent(query)}` }, { 'X-Algolia-API-Key': config.apiKey, 'X-Algolia-Application-Id': config.appId });
|
|
15
|
+
return parseAlgoliaResponse(body, url);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { algoliaQueryUrl, parseAlgoliaResponse } from '../../lib/research/docs/remote-search.js';
|
|
2
|
+
import { postJson } from '../../lib/research/fetcher.js';
|
|
3
|
+
export const tailwind = {
|
|
4
|
+
id: 'tailwind',
|
|
5
|
+
name: 'Tailwind CSS',
|
|
6
|
+
domains: ['tailwindcss.com'],
|
|
7
|
+
search: async (query) => {
|
|
8
|
+
const config = {
|
|
9
|
+
appId: 'KNPXZI5B0M',
|
|
10
|
+
apiKey: '5fc87cef58bb80203d2207578309fab6',
|
|
11
|
+
indexName: 'tailwindcss'
|
|
12
|
+
};
|
|
13
|
+
const url = algoliaQueryUrl(config);
|
|
14
|
+
const body = await postJson(url, { params: `query=${encodeURIComponent(query)}` }, { 'X-Algolia-API-Key': config.apiKey, 'X-Algolia-Application-Id': config.appId });
|
|
15
|
+
return parseAlgoliaResponse(body, url);
|
|
16
|
+
}
|
|
17
|
+
};
|