@taurgis/bonsai 1.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 +239 -0
- package/agents/agents/official-docs-researcher.agent.md +98 -0
- package/agents/instructions/web-research.instructions.md +29 -0
- package/agents/skills/web-research/SKILL.md +126 -0
- package/bin/cli.mjs +22 -0
- package/dist/base-command.d.ts +14 -0
- package/dist/base-command.js +56 -0
- package/dist/commands/config/base.d.ts +23 -0
- package/dist/commands/config/base.js +46 -0
- package/dist/commands/config/get.d.ts +20 -0
- package/dist/commands/config/get.js +48 -0
- package/dist/commands/config/list.d.ts +17 -0
- package/dist/commands/config/list.js +48 -0
- package/dist/commands/config/set.d.ts +31 -0
- package/dist/commands/config/set.js +94 -0
- package/dist/commands/config/unset.d.ts +21 -0
- package/dist/commands/config/unset.js +65 -0
- package/dist/commands/fetch.d.ts +38 -0
- package/dist/commands/fetch.js +311 -0
- package/dist/commands/import.d.ts +38 -0
- package/dist/commands/import.js +313 -0
- package/dist/commands/index.d.ts +35 -0
- package/dist/commands/index.js +306 -0
- package/dist/commands/inspect.d.ts +13 -0
- package/dist/commands/inspect.js +82 -0
- package/dist/commands/list.d.ts +25 -0
- package/dist/commands/list.js +143 -0
- package/dist/commands/prune.d.ts +24 -0
- package/dist/commands/prune.js +141 -0
- package/dist/commands/research/config/base.d.ts +23 -0
- package/dist/commands/research/config/base.js +46 -0
- package/dist/commands/research/config/get.d.ts +20 -0
- package/dist/commands/research/config/get.js +45 -0
- package/dist/commands/research/config/list.d.ts +17 -0
- package/dist/commands/research/config/list.js +42 -0
- package/dist/commands/research/config/set.d.ts +31 -0
- package/dist/commands/research/config/set.js +94 -0
- package/dist/commands/research/config/unset.d.ts +21 -0
- package/dist/commands/research/config/unset.js +59 -0
- package/dist/commands/research/import.d.ts +38 -0
- package/dist/commands/research/import.js +313 -0
- package/dist/commands/research/inspect.d.ts +13 -0
- package/dist/commands/research/inspect.js +86 -0
- package/dist/commands/research/list.d.ts +25 -0
- package/dist/commands/research/list.js +143 -0
- package/dist/commands/research/prune.d.ts +24 -0
- package/dist/commands/research/prune.js +141 -0
- package/dist/commands/research/search.d.ts +32 -0
- package/dist/commands/research/search.js +334 -0
- package/dist/commands/research/status.d.ts +17 -0
- package/dist/commands/research/status.js +108 -0
- package/dist/commands/research.d.ts +36 -0
- package/dist/commands/research.js +304 -0
- package/dist/commands/search.d.ts +32 -0
- package/dist/commands/search.js +334 -0
- package/dist/commands/status.d.ts +17 -0
- package/dist/commands/status.js +95 -0
- package/dist/commands.d.ts +24 -0
- package/dist/commands.js +24 -0
- package/dist/lib/config/index.d.ts +18 -0
- package/dist/lib/config/index.js +33 -0
- package/dist/lib/config/io.d.ts +8 -0
- package/dist/lib/config/io.js +75 -0
- package/dist/lib/config/resolve.d.ts +15 -0
- package/dist/lib/config/resolve.js +20 -0
- package/dist/lib/config/schema.d.ts +28 -0
- package/dist/lib/config/schema.js +32 -0
- package/dist/lib/research/artifact.d.ts +21 -0
- package/dist/lib/research/artifact.js +217 -0
- package/dist/lib/research/browser.d.ts +74 -0
- package/dist/lib/research/browser.js +359 -0
- package/dist/lib/research/cache-key.d.ts +5 -0
- package/dist/lib/research/cache-key.js +10 -0
- package/dist/lib/research/capture.d.ts +29 -0
- package/dist/lib/research/capture.js +181 -0
- package/dist/lib/research/compress.d.ts +5 -0
- package/dist/lib/research/compress.js +17 -0
- package/dist/lib/research/docs/__fixtures__/sphinx-searchindex.d.ts +1 -0
- package/dist/lib/research/docs/__fixtures__/sphinx-searchindex.js +2 -0
- package/dist/lib/research/docs/capabilities.d.ts +43 -0
- package/dist/lib/research/docs/capabilities.js +9 -0
- package/dist/lib/research/docs/clean-dom.d.ts +4 -0
- package/dist/lib/research/docs/clean-dom.js +41 -0
- package/dist/lib/research/docs/code-blocks.d.ts +5 -0
- package/dist/lib/research/docs/code-blocks.js +69 -0
- package/dist/lib/research/docs/detect.d.ts +6 -0
- package/dist/lib/research/docs/detect.js +239 -0
- package/dist/lib/research/docs/machine-readable.d.ts +21 -0
- package/dist/lib/research/docs/machine-readable.js +73 -0
- package/dist/lib/research/docs/markdown-source.d.ts +17 -0
- package/dist/lib/research/docs/markdown-source.js +194 -0
- package/dist/lib/research/docs/quality-gates.d.ts +9 -0
- package/dist/lib/research/docs/quality-gates.js +65 -0
- package/dist/lib/research/docs/remote-search-runner.d.ts +21 -0
- package/dist/lib/research/docs/remote-search-runner.js +54 -0
- package/dist/lib/research/docs/remote-search.d.ts +14 -0
- package/dist/lib/research/docs/remote-search.js +101 -0
- package/dist/lib/research/docs/rsc.d.ts +10 -0
- package/dist/lib/research/docs/rsc.js +30 -0
- package/dist/lib/research/docs/search-index.d.ts +13 -0
- package/dist/lib/research/docs/search-index.js +176 -0
- package/dist/lib/research/docs/section-artifacts.d.ts +12 -0
- package/dist/lib/research/docs/section-artifacts.js +75 -0
- package/dist/lib/research/docs/sections.d.ts +12 -0
- package/dist/lib/research/docs/sections.js +67 -0
- package/dist/lib/research/docs/source-map.d.ts +19 -0
- package/dist/lib/research/docs/source-map.js +87 -0
- package/dist/lib/research/docs/validate.d.ts +15 -0
- package/dist/lib/research/docs/validate.js +63 -0
- package/dist/lib/research/extract.d.ts +11 -0
- package/dist/lib/research/extract.js +83 -0
- package/dist/lib/research/fetcher.d.ts +29 -0
- package/dist/lib/research/fetcher.js +192 -0
- package/dist/lib/research/freshness.d.ts +21 -0
- package/dist/lib/research/freshness.js +82 -0
- package/dist/lib/research/markdown.d.ts +6 -0
- package/dist/lib/research/markdown.js +66 -0
- package/dist/lib/research/regression.d.ts +23 -0
- package/dist/lib/research/regression.js +93 -0
- package/dist/lib/research/resolve-target.d.ts +18 -0
- package/dist/lib/research/resolve-target.js +20 -0
- package/dist/lib/research/revalidate.d.ts +32 -0
- package/dist/lib/research/revalidate.js +152 -0
- package/dist/lib/research/schema.d.ts +47 -0
- package/dist/lib/research/schema.js +1 -0
- package/dist/lib/research/secret-scan.d.ts +5 -0
- package/dist/lib/research/secret-scan.js +37 -0
- package/dist/lib/research/secure-write.d.ts +16 -0
- package/dist/lib/research/secure-write.js +17 -0
- package/dist/lib/research/storage.d.ts +52 -0
- package/dist/lib/research/storage.js +182 -0
- package/dist/lib/research/store-roots.d.ts +29 -0
- package/dist/lib/research/store-roots.js +28 -0
- package/dist/lib/research/token-estimate.d.ts +5 -0
- package/dist/lib/research/token-estimate.js +7 -0
- package/dist/lib/research/url.d.ts +24 -0
- package/dist/lib/research/url.js +121 -0
- package/dist/sites/index.d.ts +4 -0
- package/dist/sites/index.js +28 -0
- package/dist/sites/salesforce/browser-search.d.ts +7 -0
- package/dist/sites/salesforce/browser-search.js +82 -0
- package/dist/sites/salesforce/coveo.d.ts +23 -0
- package/dist/sites/salesforce/coveo.js +161 -0
- package/dist/sites/salesforce/fetch-page.d.ts +6 -0
- package/dist/sites/salesforce/fetch-page.js +42 -0
- package/dist/sites/salesforce/index.d.ts +2 -0
- package/dist/sites/salesforce/index.js +16 -0
- package/dist/sites/salesforce/token.d.ts +26 -0
- package/dist/sites/salesforce/token.js +154 -0
- package/dist/sites/salesforce-developer/fetch-page.d.ts +3 -0
- package/dist/sites/salesforce-developer/fetch-page.js +18 -0
- package/dist/sites/salesforce-developer/index.d.ts +2 -0
- package/dist/sites/salesforce-developer/index.js +12 -0
- package/dist/sites/salesforce-doc-fetch.d.ts +14 -0
- package/dist/sites/salesforce-doc-fetch.js +321 -0
- package/dist/sites/types.d.ts +27 -0
- package/dist/sites/types.js +1 -0
- package/oclif.manifest.json +845 -0
- package/package.json +78 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { BaseCommand } from '../base-command.js';
|
|
2
|
+
export default class ResearchStatus extends BaseCommand<typeof ResearchStatus> {
|
|
3
|
+
static id: string;
|
|
4
|
+
static summary: string;
|
|
5
|
+
static description: string;
|
|
6
|
+
static args: {
|
|
7
|
+
url: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
8
|
+
};
|
|
9
|
+
static flags: {
|
|
10
|
+
tier: import("@oclif/core/interfaces").OptionFlag<"stable" | "standard" | "volatile", import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
ttl: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
'max-age': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
};
|
|
14
|
+
static stdoutIsPrimaryData: boolean;
|
|
15
|
+
init(): Promise<void>;
|
|
16
|
+
execute(): Promise<unknown>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import { BaseCommand } from '../base-command.js';
|
|
3
|
+
import { getArtifactPath } from '../lib/research/storage.js';
|
|
4
|
+
import { evaluateFreshness, checkMaxAgeExpired } from '../lib/research/freshness.js';
|
|
5
|
+
import { resolveResearchTarget } from '../lib/research/resolve-target.js';
|
|
6
|
+
function resolveFreshness(cached, currentTime, ttl, maxAge) {
|
|
7
|
+
const isMaxAgeExpired = maxAge ? checkMaxAgeExpired(cached, currentTime, maxAge) : false;
|
|
8
|
+
return isMaxAgeExpired
|
|
9
|
+
? 'stale_expired'
|
|
10
|
+
: evaluateFreshness(cached.metadata, currentTime, ttl || cached.metadata.ttl);
|
|
11
|
+
}
|
|
12
|
+
function describeCacheStatus(cached, currentTime, ttl, maxAge) {
|
|
13
|
+
if (!cached) {
|
|
14
|
+
return { status: 'miss', freshness: 'stale_expired', action: 'would_fetch' };
|
|
15
|
+
}
|
|
16
|
+
const freshness = resolveFreshness(cached, currentTime, ttl, maxAge);
|
|
17
|
+
return freshness === 'fresh'
|
|
18
|
+
? { status: 'hit', freshness, action: 'would_return_cached' }
|
|
19
|
+
: { status: 'stale', freshness, action: 'would_revalidate' };
|
|
20
|
+
}
|
|
21
|
+
export default class ResearchStatus extends BaseCommand {
|
|
22
|
+
static id = 'status';
|
|
23
|
+
static summary = 'Check the cache status of a URL without fetching or writing.';
|
|
24
|
+
static description = 'Reports whether the URL is cached, its freshness state, and what action (fetch, revalidate, or cached return) the root fetch command would take.';
|
|
25
|
+
static args = {
|
|
26
|
+
url: Args.string({
|
|
27
|
+
required: true,
|
|
28
|
+
description: 'The URL to check.',
|
|
29
|
+
}),
|
|
30
|
+
};
|
|
31
|
+
static flags = {
|
|
32
|
+
tier: Flags.option({
|
|
33
|
+
description: 'Freshness tier policy to evaluate against.',
|
|
34
|
+
options: ['stable', 'standard', 'volatile'],
|
|
35
|
+
default: 'standard',
|
|
36
|
+
})(),
|
|
37
|
+
ttl: Flags.string({
|
|
38
|
+
char: 'l',
|
|
39
|
+
description: 'Custom TTL duration to evaluate against (e.g. "2h", "7d").',
|
|
40
|
+
}),
|
|
41
|
+
'max-age': Flags.string({
|
|
42
|
+
description: 'Maximum age of cache to accept (e.g. "1d", "30d").',
|
|
43
|
+
}),
|
|
44
|
+
};
|
|
45
|
+
static stdoutIsPrimaryData = true;
|
|
46
|
+
async init() {
|
|
47
|
+
await super.init();
|
|
48
|
+
const { args, flags } = await this.parse(ResearchStatus);
|
|
49
|
+
this.args = args;
|
|
50
|
+
this.flags = flags;
|
|
51
|
+
}
|
|
52
|
+
async execute() {
|
|
53
|
+
const { url } = this.args;
|
|
54
|
+
const { ttl, tier, 'max-age': maxAge } = this.flags;
|
|
55
|
+
let target;
|
|
56
|
+
try {
|
|
57
|
+
target = resolveResearchTarget({
|
|
58
|
+
configDir: this.config.configDir,
|
|
59
|
+
cwd: process.cwd(),
|
|
60
|
+
dataDir: this.config.dataDir,
|
|
61
|
+
url,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
this.error(`Invalid URL: ${err.message}`, { exit: 2 });
|
|
66
|
+
}
|
|
67
|
+
const { cacheKey, located, normalizedUrl, roots } = target;
|
|
68
|
+
const cached = located?.artifact ?? null;
|
|
69
|
+
const currentTime = new Date();
|
|
70
|
+
let result;
|
|
71
|
+
try {
|
|
72
|
+
result = describeCacheStatus(cached, currentTime, ttl, maxAge);
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
this.error(`Invalid max-age: ${err.message}`, { exit: 2 });
|
|
76
|
+
}
|
|
77
|
+
// On a hit, report where it actually lives; on a miss, where a fetch would write it.
|
|
78
|
+
const artifactPath = located?.path ?? getArtifactPath(roots.writeRoot, cacheKey);
|
|
79
|
+
if (!this.requestedJson()) {
|
|
80
|
+
this.log(`URL: ${normalizedUrl}`);
|
|
81
|
+
this.log(`Cache Key: ${cacheKey}`);
|
|
82
|
+
this.log(`Cache Path: ${artifactPath}`);
|
|
83
|
+
this.log(`Status: ${result.status}`);
|
|
84
|
+
this.log(`Freshness: ${result.freshness}`);
|
|
85
|
+
this.log(`Action: ${result.action}`);
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
cacheKey,
|
|
89
|
+
cachePath: artifactPath,
|
|
90
|
+
status: result.status,
|
|
91
|
+
freshness: result.freshness,
|
|
92
|
+
action: result.action,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import ConfigGet from './commands/config/get.js';
|
|
2
|
+
import ConfigList from './commands/config/list.js';
|
|
3
|
+
import ConfigSet from './commands/config/set.js';
|
|
4
|
+
import ConfigUnset from './commands/config/unset.js';
|
|
5
|
+
import FetchCommand from './commands/fetch.js';
|
|
6
|
+
import ImportCommand from './commands/import.js';
|
|
7
|
+
import InspectCommand from './commands/inspect.js';
|
|
8
|
+
import ListCommand from './commands/list.js';
|
|
9
|
+
import PruneCommand from './commands/prune.js';
|
|
10
|
+
import SearchCommand from './commands/search.js';
|
|
11
|
+
import StatusCommand from './commands/status.js';
|
|
12
|
+
export declare const commands: {
|
|
13
|
+
fetch: typeof FetchCommand;
|
|
14
|
+
import: typeof ImportCommand;
|
|
15
|
+
inspect: typeof InspectCommand;
|
|
16
|
+
list: typeof ListCommand;
|
|
17
|
+
prune: typeof PruneCommand;
|
|
18
|
+
search: typeof SearchCommand;
|
|
19
|
+
status: typeof StatusCommand;
|
|
20
|
+
'config:get': typeof ConfigGet;
|
|
21
|
+
'config:list': typeof ConfigList;
|
|
22
|
+
'config:set': typeof ConfigSet;
|
|
23
|
+
'config:unset': typeof ConfigUnset;
|
|
24
|
+
};
|
package/dist/commands.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import ConfigGet from './commands/config/get.js';
|
|
2
|
+
import ConfigList from './commands/config/list.js';
|
|
3
|
+
import ConfigSet from './commands/config/set.js';
|
|
4
|
+
import ConfigUnset from './commands/config/unset.js';
|
|
5
|
+
import FetchCommand from './commands/fetch.js';
|
|
6
|
+
import ImportCommand from './commands/import.js';
|
|
7
|
+
import InspectCommand from './commands/inspect.js';
|
|
8
|
+
import ListCommand from './commands/list.js';
|
|
9
|
+
import PruneCommand from './commands/prune.js';
|
|
10
|
+
import SearchCommand from './commands/search.js';
|
|
11
|
+
import StatusCommand from './commands/status.js';
|
|
12
|
+
export const commands = {
|
|
13
|
+
fetch: FetchCommand,
|
|
14
|
+
import: ImportCommand,
|
|
15
|
+
inspect: InspectCommand,
|
|
16
|
+
list: ListCommand,
|
|
17
|
+
prune: PruneCommand,
|
|
18
|
+
search: SearchCommand,
|
|
19
|
+
status: StatusCommand,
|
|
20
|
+
'config:get': ConfigGet,
|
|
21
|
+
'config:list': ConfigList,
|
|
22
|
+
'config:set': ConfigSet,
|
|
23
|
+
'config:unset': ConfigUnset,
|
|
24
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export { type ConfigValues, type ResolvedConfig, type ConfigKey, type StorageMode, type KeyMeta, SCHEMA_VERSION, STORAGE_MODES, 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';
|
|
3
|
+
export { resolveStorageMode, parseEnvStorage, STORAGE_ENV_VAR, type ResolveStorageInput, } from './resolve.js';
|
|
4
|
+
import type { ConfigValues, ResolvedConfig, StorageMode } from './schema.js';
|
|
5
|
+
/**
|
|
6
|
+
* Read both config files once and resolve the effective storage mode.
|
|
7
|
+
* `flag` (a per-invocation `--storage` override) wins over everything else.
|
|
8
|
+
*/
|
|
9
|
+
export declare function loadStorageMode(configDir: string | undefined, cwd: string, flag?: StorageMode, env?: Record<string, string | undefined>): StorageMode;
|
|
10
|
+
/** The resolved value of every config key, factoring file + env layers. Extend as keys are added. */
|
|
11
|
+
export declare function effectiveConfig(configDir: string | undefined, cwd: string): ResolvedConfig;
|
|
12
|
+
export type ConfigScope = 'global' | 'local' | 'effective';
|
|
13
|
+
/**
|
|
14
|
+
* The config values for a requested scope: the user file (`global`), the project file (`local`),
|
|
15
|
+
* or the fully merged result (`effective`). Shared by `config get` and `config list` so the
|
|
16
|
+
* scope-selection lives in one place.
|
|
17
|
+
*/
|
|
18
|
+
export declare function readScopedConfig(scope: ConfigScope, configDir: string | undefined, cwd: string): Partial<ConfigValues>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export { SCHEMA_VERSION, STORAGE_MODES, 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';
|
|
3
|
+
export { resolveStorageMode, parseEnvStorage, STORAGE_ENV_VAR, } from './resolve.js';
|
|
4
|
+
import { readUserConfig, readProjectConfig } from './io.js';
|
|
5
|
+
import { resolveStorageMode } from './resolve.js';
|
|
6
|
+
/**
|
|
7
|
+
* Read both config files once and resolve the effective storage mode.
|
|
8
|
+
* `flag` (a per-invocation `--storage` override) wins over everything else.
|
|
9
|
+
*/
|
|
10
|
+
export function loadStorageMode(configDir, cwd, flag, env = process.env) {
|
|
11
|
+
return resolveStorageMode({
|
|
12
|
+
flag,
|
|
13
|
+
env,
|
|
14
|
+
projectConfig: readProjectConfig(cwd),
|
|
15
|
+
userConfig: readUserConfig(configDir),
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
/** The resolved value of every config key, factoring file + env layers. Extend as keys are added. */
|
|
19
|
+
export function effectiveConfig(configDir, cwd) {
|
|
20
|
+
return { storage: loadStorageMode(configDir, cwd) };
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The config values for a requested scope: the user file (`global`), the project file (`local`),
|
|
24
|
+
* or the fully merged result (`effective`). Shared by `config get` and `config list` so the
|
|
25
|
+
* scope-selection lives in one place.
|
|
26
|
+
*/
|
|
27
|
+
export function readScopedConfig(scope, configDir, cwd) {
|
|
28
|
+
if (scope === 'global')
|
|
29
|
+
return readUserConfig(configDir);
|
|
30
|
+
if (scope === 'local')
|
|
31
|
+
return readProjectConfig(cwd);
|
|
32
|
+
return effectiveConfig(configDir, cwd);
|
|
33
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ConfigValues } from './schema.js';
|
|
2
|
+
/** Global, user-level config lives in the OCLIF config dir; project config sits at the repo root. */
|
|
3
|
+
export declare const USER_CONFIG_FILENAME = "config.json";
|
|
4
|
+
export declare const PROJECT_CONFIG_FILENAME = ".bonsai.json";
|
|
5
|
+
export declare function readUserConfig(configDir: string | undefined): Partial<ConfigValues>;
|
|
6
|
+
export declare function readProjectConfig(cwd: string): Partial<ConfigValues>;
|
|
7
|
+
export declare function writeUserConfig(configDir: string, patch: Partial<ConfigValues>): void;
|
|
8
|
+
export declare function writeProjectConfig(cwd: string, patch: Partial<ConfigValues>): void;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, renameSync, unlinkSync, existsSync, } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { KEY_META, ALL_KEYS } from './schema.js';
|
|
4
|
+
/** Global, user-level config lives in the OCLIF config dir; project config sits at the repo root. */
|
|
5
|
+
export const USER_CONFIG_FILENAME = 'config.json';
|
|
6
|
+
export const PROJECT_CONFIG_FILENAME = '.bonsai.json';
|
|
7
|
+
/** Parse a config file, keeping only known keys with valid values. Invalid input degrades to {}. */
|
|
8
|
+
function parseConfigFile(raw) {
|
|
9
|
+
try {
|
|
10
|
+
const parsed = JSON.parse(raw);
|
|
11
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
|
|
12
|
+
return {};
|
|
13
|
+
const obj = parsed;
|
|
14
|
+
const result = {};
|
|
15
|
+
for (const key of ALL_KEYS) {
|
|
16
|
+
const val = obj[key];
|
|
17
|
+
if (val !== undefined && KEY_META[key].isValid(val)) {
|
|
18
|
+
Object.assign(result, { [key]: val });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function readConfigFile(filePath) {
|
|
28
|
+
if (!existsSync(filePath))
|
|
29
|
+
return {};
|
|
30
|
+
try {
|
|
31
|
+
return parseConfigFile(readFileSync(filePath, 'utf-8'));
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return {};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function readUserConfig(configDir) {
|
|
38
|
+
if (!configDir)
|
|
39
|
+
return {};
|
|
40
|
+
return readConfigFile(join(configDir, USER_CONFIG_FILENAME));
|
|
41
|
+
}
|
|
42
|
+
export function readProjectConfig(cwd) {
|
|
43
|
+
return readConfigFile(join(cwd, PROJECT_CONFIG_FILENAME));
|
|
44
|
+
}
|
|
45
|
+
function writeAtomically(filePath, data) {
|
|
46
|
+
// Unique per call so two rapid writes to the same file (e.g. a programmatic invoker) can't
|
|
47
|
+
// share — and clobber — the same temp before its rename. Mirrors writeArtifact in storage.ts.
|
|
48
|
+
const tempPath = `${filePath}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 7)}.tmp`;
|
|
49
|
+
const content = JSON.stringify(data, null, 2) + '\n';
|
|
50
|
+
try {
|
|
51
|
+
writeFileSync(tempPath, content, 'utf-8');
|
|
52
|
+
renameSync(tempPath, filePath);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
try {
|
|
56
|
+
unlinkSync(tempPath);
|
|
57
|
+
}
|
|
58
|
+
catch { }
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function omitUndefined(obj) {
|
|
63
|
+
return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined));
|
|
64
|
+
}
|
|
65
|
+
export function writeUserConfig(configDir, patch) {
|
|
66
|
+
mkdirSync(configDir, { recursive: true });
|
|
67
|
+
const filePath = join(configDir, USER_CONFIG_FILENAME);
|
|
68
|
+
const merged = omitUndefined({ ...readUserConfig(configDir), ...patch });
|
|
69
|
+
writeAtomically(filePath, { schemaVersion: 1, ...merged });
|
|
70
|
+
}
|
|
71
|
+
export function writeProjectConfig(cwd, patch) {
|
|
72
|
+
const filePath = join(cwd, PROJECT_CONFIG_FILENAME);
|
|
73
|
+
const merged = omitUndefined({ ...readProjectConfig(cwd), ...patch });
|
|
74
|
+
writeAtomically(filePath, { schemaVersion: 1, ...merged });
|
|
75
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ConfigValues, StorageMode } from './schema.js';
|
|
2
|
+
/** Scoped env override, following oclif's `<BIN>_*` env-var convention for this CLI. */
|
|
3
|
+
export declare const STORAGE_ENV_VAR = "BONSAI_STORAGE";
|
|
4
|
+
export declare function parseEnvStorage(env: Record<string, string | undefined>): StorageMode | undefined;
|
|
5
|
+
export interface ResolveStorageInput {
|
|
6
|
+
flag?: StorageMode;
|
|
7
|
+
env: Record<string, string | undefined>;
|
|
8
|
+
projectConfig: Partial<ConfigValues>;
|
|
9
|
+
userConfig: Partial<ConfigValues>;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Resolve the effective storage mode in precedence order:
|
|
13
|
+
* flag > env > project file > user file > built-in default.
|
|
14
|
+
*/
|
|
15
|
+
export declare function resolveStorageMode(input: ResolveStorageInput): StorageMode;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { BUILT_IN_DEFAULTS, STORAGE_MODES } from './schema.js';
|
|
2
|
+
/** Scoped env override, following oclif's `<BIN>_*` env-var convention for this CLI. */
|
|
3
|
+
export const STORAGE_ENV_VAR = 'BONSAI_STORAGE';
|
|
4
|
+
export function parseEnvStorage(env) {
|
|
5
|
+
const raw = env[STORAGE_ENV_VAR]?.trim();
|
|
6
|
+
return STORAGE_MODES.includes(raw ?? '')
|
|
7
|
+
? raw
|
|
8
|
+
: undefined;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Resolve the effective storage mode in precedence order:
|
|
12
|
+
* flag > env > project file > user file > built-in default.
|
|
13
|
+
*/
|
|
14
|
+
export function resolveStorageMode(input) {
|
|
15
|
+
return (input.flag ??
|
|
16
|
+
parseEnvStorage(input.env) ??
|
|
17
|
+
input.projectConfig.storage ??
|
|
18
|
+
input.userConfig.storage ??
|
|
19
|
+
BUILT_IN_DEFAULTS.storage);
|
|
20
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bonsai configuration schema. Mirrors oclif's layered config shape (user file,
|
|
3
|
+
* project file, env, flag), scoped here to the single `storage` key that selects
|
|
4
|
+
* where the research cache lives.
|
|
5
|
+
*/
|
|
6
|
+
export type StorageMode = 'global' | 'project';
|
|
7
|
+
export interface ConfigValues {
|
|
8
|
+
storage?: StorageMode;
|
|
9
|
+
}
|
|
10
|
+
export type ResolvedConfig = Required<ConfigValues>;
|
|
11
|
+
export type ConfigKey = keyof ConfigValues;
|
|
12
|
+
export declare const SCHEMA_VERSION = 1;
|
|
13
|
+
export declare const STORAGE_MODES: readonly StorageMode[];
|
|
14
|
+
export declare const ALL_KEYS: readonly ConfigKey[];
|
|
15
|
+
export declare const BUILT_IN_DEFAULTS: ResolvedConfig;
|
|
16
|
+
export interface KeyMeta {
|
|
17
|
+
description: string;
|
|
18
|
+
/** Complete accepted set for enum keys — powers an actionable "Valid values: …" error. */
|
|
19
|
+
values?: readonly string[];
|
|
20
|
+
parseValue: (raw: string) => unknown;
|
|
21
|
+
isValid: (value: unknown) => boolean;
|
|
22
|
+
format: (value: unknown) => string;
|
|
23
|
+
}
|
|
24
|
+
export declare const KEY_META: Record<ConfigKey, KeyMeta>;
|
|
25
|
+
export declare function isKnownKey(key: string): key is ConfigKey;
|
|
26
|
+
export declare function suggestKey(input: string): ConfigKey | undefined;
|
|
27
|
+
/** Comma-joined valid key list for error messages, kept in one place. */
|
|
28
|
+
export declare function validKeysHint(): string;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bonsai configuration schema. Mirrors oclif's layered config shape (user file,
|
|
3
|
+
* project file, env, flag), scoped here to the single `storage` key that selects
|
|
4
|
+
* where the research cache lives.
|
|
5
|
+
*/
|
|
6
|
+
export const SCHEMA_VERSION = 1;
|
|
7
|
+
export const STORAGE_MODES = ['global', 'project'];
|
|
8
|
+
export const ALL_KEYS = ['storage'];
|
|
9
|
+
export const BUILT_IN_DEFAULTS = {
|
|
10
|
+
// Default to global so existing installs keep using the OCLIF data dir unchanged.
|
|
11
|
+
storage: 'global',
|
|
12
|
+
};
|
|
13
|
+
export const KEY_META = {
|
|
14
|
+
storage: {
|
|
15
|
+
description: `Where research cache is stored (${STORAGE_MODES.join('|')}).`,
|
|
16
|
+
values: STORAGE_MODES,
|
|
17
|
+
parseValue: (raw) => raw.trim(),
|
|
18
|
+
isValid: (v) => STORAGE_MODES.includes(String(v)),
|
|
19
|
+
format: (v) => String(v),
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
export function isKnownKey(key) {
|
|
23
|
+
return ALL_KEYS.includes(key);
|
|
24
|
+
}
|
|
25
|
+
export function suggestKey(input) {
|
|
26
|
+
const lower = input.toLowerCase();
|
|
27
|
+
return ALL_KEYS.find((k) => k.toLowerCase().includes(lower) || lower.includes(k.toLowerCase()));
|
|
28
|
+
}
|
|
29
|
+
/** Comma-joined valid key list for error messages, kept in one place. */
|
|
30
|
+
export function validKeysHint() {
|
|
31
|
+
return ALL_KEYS.join(', ');
|
|
32
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ResearchArtifact, ResearchArtifactMetadata } from './schema.js';
|
|
2
|
+
/**
|
|
3
|
+
* Serializes artifact metadata to standard YAML frontmatter text.
|
|
4
|
+
*/
|
|
5
|
+
export declare function serializeMetadata(meta: ResearchArtifactMetadata): string;
|
|
6
|
+
/**
|
|
7
|
+
* Serializes a full ResearchArtifact to a single Markdown string.
|
|
8
|
+
*/
|
|
9
|
+
export declare function serializeArtifact(artifact: ResearchArtifact): string;
|
|
10
|
+
/**
|
|
11
|
+
* Parses lines of YAML frontmatter into a typed metadata object.
|
|
12
|
+
*/
|
|
13
|
+
export declare function parseMetadata(lines: string[]): ResearchArtifactMetadata;
|
|
14
|
+
/**
|
|
15
|
+
* Extracts a named markdown section (e.g. ## Summary) from body text.
|
|
16
|
+
*/
|
|
17
|
+
export declare function extractSection(body: string, sectionName: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* Parses full Markdown content into a structured ResearchArtifact.
|
|
20
|
+
*/
|
|
21
|
+
export declare function parseArtifact(content: string): ResearchArtifact;
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
function serializeArrayField(lines, name, items) {
|
|
2
|
+
lines.push(`${name}:`);
|
|
3
|
+
for (const item of items) {
|
|
4
|
+
lines.push(` - ${item}`);
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
// A scalar frontmatter line, blanking null/undefined. Using `??` (not `||`) preserves a numeric 0.
|
|
8
|
+
function serializeScalarField(lines, name, value) {
|
|
9
|
+
lines.push(`${name}: ${value ?? ''}`);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Serializes artifact metadata to standard YAML frontmatter text.
|
|
13
|
+
*/
|
|
14
|
+
export function serializeMetadata(meta) {
|
|
15
|
+
const lines = [];
|
|
16
|
+
lines.push('---');
|
|
17
|
+
serializeScalarField(lines, 'schema_version', meta.schema_version);
|
|
18
|
+
serializeScalarField(lines, 'artifact_type', meta.artifact_type);
|
|
19
|
+
serializeScalarField(lines, 'source_url', meta.source_url);
|
|
20
|
+
serializeArrayField(lines, 'source_urls', meta.source_urls);
|
|
21
|
+
serializeScalarField(lines, 'normalized_url', meta.normalized_url);
|
|
22
|
+
serializeScalarField(lines, 'cache_key', meta.cache_key);
|
|
23
|
+
serializeScalarField(lines, 'topic', meta.topic);
|
|
24
|
+
serializeArrayField(lines, 'tags', meta.tags);
|
|
25
|
+
serializeArrayField(lines, 'format_available', meta.format_available);
|
|
26
|
+
serializeScalarField(lines, 'tier', meta.tier);
|
|
27
|
+
serializeScalarField(lines, 'ttl', meta.ttl);
|
|
28
|
+
serializeScalarField(lines, 'fetched_at', meta.fetched_at);
|
|
29
|
+
serializeScalarField(lines, 'validated_at', meta.validated_at);
|
|
30
|
+
serializeScalarField(lines, 'stale_after', meta.stale_after);
|
|
31
|
+
serializeScalarField(lines, 'capture_method', meta.capture_method);
|
|
32
|
+
serializeScalarField(lines, 'extraction_status', meta.extraction_status);
|
|
33
|
+
serializeScalarField(lines, 'extraction_confidence', meta.extraction_confidence);
|
|
34
|
+
serializeArrayField(lines, 'quality_notes', meta.quality_notes);
|
|
35
|
+
serializeScalarField(lines, 'supplied_at', meta.supplied_at);
|
|
36
|
+
serializeScalarField(lines, 'supplied_by', meta.supplied_by);
|
|
37
|
+
serializeScalarField(lines, 'etag', meta.etag);
|
|
38
|
+
serializeScalarField(lines, 'last_modified', meta.last_modified);
|
|
39
|
+
serializeScalarField(lines, 'content_hash', meta.content_hash);
|
|
40
|
+
lines.push('token_estimate:');
|
|
41
|
+
serializeScalarField(lines, ' compressed', meta.token_estimate.compressed);
|
|
42
|
+
serializeScalarField(lines, ' detailed', meta.token_estimate.detailed);
|
|
43
|
+
serializeScalarField(lines, 'status', meta.status);
|
|
44
|
+
serializeScalarField(lines, 'site_module_id', meta.site_module_id);
|
|
45
|
+
serializeScalarField(lines, 'docs_engine', meta.docs_engine);
|
|
46
|
+
serializeScalarField(lines, 'docs_framework', meta.docs_framework);
|
|
47
|
+
serializeScalarField(lines, 'source_doc_url', meta.source_doc_url);
|
|
48
|
+
serializeScalarField(lines, 'search_provider', meta.search_provider);
|
|
49
|
+
serializeScalarField(lines, 'parent_cache_key', meta.parent_cache_key);
|
|
50
|
+
serializeScalarField(lines, 'section_anchor', meta.section_anchor);
|
|
51
|
+
serializeScalarField(lines, 'section_heading_path', meta.section_heading_path);
|
|
52
|
+
lines.push('---');
|
|
53
|
+
return lines.join('\n');
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Serializes a full ResearchArtifact to a single Markdown string.
|
|
57
|
+
*/
|
|
58
|
+
export function serializeArtifact(artifact) {
|
|
59
|
+
const meta = serializeMetadata(artifact.metadata);
|
|
60
|
+
const sections = [
|
|
61
|
+
meta,
|
|
62
|
+
'## Summary',
|
|
63
|
+
artifact.summary.trim(),
|
|
64
|
+
'## Compressed',
|
|
65
|
+
artifact.compressed.trim(),
|
|
66
|
+
'## Detailed',
|
|
67
|
+
artifact.detailed.trim(),
|
|
68
|
+
'## Provenance',
|
|
69
|
+
artifact.provenance.trim(),
|
|
70
|
+
];
|
|
71
|
+
return sections.join('\n\n');
|
|
72
|
+
}
|
|
73
|
+
function handleArrayItem(line, state) {
|
|
74
|
+
if (line.startsWith(' - ')) {
|
|
75
|
+
const val = line.slice(4).trim();
|
|
76
|
+
if (state.currentArrayKey) {
|
|
77
|
+
state.meta[state.currentArrayKey].push(val);
|
|
78
|
+
}
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
function handleNestedProp(line, state) {
|
|
84
|
+
if (line.startsWith(' ')) {
|
|
85
|
+
const trimmed = line.trim();
|
|
86
|
+
const colIdx = trimmed.indexOf(':');
|
|
87
|
+
if (colIdx !== -1 && state.currentObjectKey) {
|
|
88
|
+
const k = trimmed.slice(0, colIdx).trim();
|
|
89
|
+
const v = trimmed.slice(colIdx + 1).trim();
|
|
90
|
+
const numVal = v === '' ? null : Number(v);
|
|
91
|
+
state.meta[state.currentObjectKey][k] = isNaN(numVal) || v === '' ? null : numVal;
|
|
92
|
+
}
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
function initializeEmptyField(key, state) {
|
|
98
|
+
const isArray = ['source_urls', 'tags', 'format_available', 'quality_notes'].includes(key);
|
|
99
|
+
if (isArray) {
|
|
100
|
+
state.currentArrayKey = key;
|
|
101
|
+
state.meta[key] = [];
|
|
102
|
+
}
|
|
103
|
+
else if (key === 'token_estimate') {
|
|
104
|
+
state.currentObjectKey = key;
|
|
105
|
+
state.meta[key] = { compressed: null, detailed: null };
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
state.meta[key] = null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function handleRootProp(line, state) {
|
|
112
|
+
const colIdx = line.indexOf(':');
|
|
113
|
+
if (colIdx === -1)
|
|
114
|
+
return;
|
|
115
|
+
const key = line.slice(0, colIdx).trim();
|
|
116
|
+
const val = line.slice(colIdx + 1).trim();
|
|
117
|
+
state.currentArrayKey = null;
|
|
118
|
+
state.currentObjectKey = null;
|
|
119
|
+
if (val === '') {
|
|
120
|
+
initializeEmptyField(key, state);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
state.meta[key] = key === 'schema_version' ? Number(val) : val;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Parses lines of YAML frontmatter into a typed metadata object.
|
|
128
|
+
*/
|
|
129
|
+
export function parseMetadata(lines) {
|
|
130
|
+
const state = {
|
|
131
|
+
meta: {
|
|
132
|
+
schema_version: 1,
|
|
133
|
+
artifact_type: 'source',
|
|
134
|
+
source_url: '',
|
|
135
|
+
source_urls: [],
|
|
136
|
+
normalized_url: '',
|
|
137
|
+
cache_key: '',
|
|
138
|
+
topic: null,
|
|
139
|
+
tags: [],
|
|
140
|
+
format_available: [],
|
|
141
|
+
tier: 'standard',
|
|
142
|
+
ttl: null,
|
|
143
|
+
fetched_at: null,
|
|
144
|
+
validated_at: null,
|
|
145
|
+
stale_after: null,
|
|
146
|
+
capture_method: null,
|
|
147
|
+
extraction_status: null,
|
|
148
|
+
extraction_confidence: null,
|
|
149
|
+
quality_notes: [],
|
|
150
|
+
supplied_at: null,
|
|
151
|
+
supplied_by: null,
|
|
152
|
+
etag: null,
|
|
153
|
+
last_modified: null,
|
|
154
|
+
content_hash: '',
|
|
155
|
+
token_estimate: { compressed: null, detailed: null },
|
|
156
|
+
status: 'active',
|
|
157
|
+
site_module_id: null,
|
|
158
|
+
docs_engine: null,
|
|
159
|
+
docs_framework: null,
|
|
160
|
+
source_doc_url: null,
|
|
161
|
+
search_provider: null,
|
|
162
|
+
parent_cache_key: null,
|
|
163
|
+
section_anchor: null,
|
|
164
|
+
section_heading_path: null,
|
|
165
|
+
},
|
|
166
|
+
currentArrayKey: null,
|
|
167
|
+
currentObjectKey: null,
|
|
168
|
+
};
|
|
169
|
+
for (let line of lines) {
|
|
170
|
+
line = line.trimEnd();
|
|
171
|
+
if (!line)
|
|
172
|
+
continue;
|
|
173
|
+
if (handleArrayItem(line, state)) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (handleNestedProp(line, state)) {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
handleRootProp(line, state);
|
|
180
|
+
}
|
|
181
|
+
return state.meta;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Extracts a named markdown section (e.g. ## Summary) from body text.
|
|
185
|
+
*/
|
|
186
|
+
export function extractSection(body, sectionName) {
|
|
187
|
+
const header = `## ${sectionName}`;
|
|
188
|
+
const idx = body.indexOf(header);
|
|
189
|
+
if (idx === -1)
|
|
190
|
+
return '';
|
|
191
|
+
const start = idx + header.length;
|
|
192
|
+
const nextHeadingIdx = body.indexOf('\n## ', start);
|
|
193
|
+
const rawText = nextHeadingIdx === -1 ? body.slice(start) : body.slice(start, nextHeadingIdx);
|
|
194
|
+
return rawText.trim();
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Parses full Markdown content into a structured ResearchArtifact.
|
|
198
|
+
*/
|
|
199
|
+
export function parseArtifact(content) {
|
|
200
|
+
const parts = content.split('\n');
|
|
201
|
+
if (parts[0] !== '---') {
|
|
202
|
+
throw new Error('Malformed artifact: Frontmatter missing starting boundary.');
|
|
203
|
+
}
|
|
204
|
+
const closingIdx = parts.indexOf('---', 1);
|
|
205
|
+
if (closingIdx === -1) {
|
|
206
|
+
throw new Error('Malformed artifact: Frontmatter missing ending boundary.');
|
|
207
|
+
}
|
|
208
|
+
const frontmatterLines = parts.slice(1, closingIdx);
|
|
209
|
+
const bodyLines = parts.slice(closingIdx + 1);
|
|
210
|
+
const bodyText = bodyLines.join('\n');
|
|
211
|
+
const metadata = parseMetadata(frontmatterLines);
|
|
212
|
+
const summary = extractSection(bodyText, 'Summary');
|
|
213
|
+
const compressed = extractSection(bodyText, 'Compressed');
|
|
214
|
+
const detailed = extractSection(bodyText, 'Detailed');
|
|
215
|
+
const provenance = extractSection(bodyText, 'Provenance');
|
|
216
|
+
return { metadata, summary, compressed, detailed, provenance };
|
|
217
|
+
}
|