@taurgis/bonsai 1.0.5 → 3.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 +59 -12
- package/agents/agents/official-docs-researcher.agent.md +15 -26
- package/agents/agents/salesforce-docs-researcher.agent.md +17 -35
- package/agents/hooks/README.md +2 -9
- package/agents/hooks/claude-code/hooks/bonsai-web-fetch.sh +1 -1
- package/agents/hooks/codex/hooks/bonsai-user-prompt-context.sh +1 -1
- package/agents/hooks/codex/hooks/bonsai-web-fetch.sh +1 -1
- package/agents/hooks/cursor/hooks/bonsai-web-fetch.sh +1 -1
- package/agents/hooks/github-copilot/bonsai-web-fetch.sh +1 -1
- package/agents/instructions/salesforce-research.instructions.md +7 -6
- package/agents/instructions/web-research.instructions.md +3 -2
- package/agents/skills/web-research/SKILL.md +24 -32
- package/bin/cli.mjs +44 -14
- package/dist/base-command.d.ts +17 -1
- package/dist/base-command.js +70 -9
- 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.d.ts +2 -0
- package/dist/commands/fetch.js +69 -45
- package/dist/commands/import.d.ts +3 -0
- package/dist/commands/import.js +91 -53
- package/dist/commands/inspect.d.ts +8 -0
- package/dist/commands/inspect.js +59 -30
- package/dist/commands/list.d.ts +5 -3
- package/dist/commands/list.js +60 -35
- package/dist/commands/prune.d.ts +2 -1
- package/dist/commands/prune.js +62 -19
- package/dist/commands/status.d.ts +8 -0
- package/dist/commands/status.js +84 -26
- package/dist/commands.d.ts +2 -2
- package/dist/commands.js +2 -2
- package/dist/hooks/command-not-found/suggest.d.ts +7 -1
- package/dist/hooks/command-not-found/suggest.js +100 -46
- package/dist/lib/argv.d.ts +10 -0
- package/dist/lib/argv.js +97 -0
- package/dist/lib/color.d.ts +20 -0
- package/dist/lib/color.js +36 -0
- package/dist/lib/envelope.d.ts +36 -0
- package/dist/lib/envelope.js +82 -0
- package/dist/lib/help-preflight.d.ts +15 -0
- package/dist/lib/help-preflight.js +81 -0
- package/dist/lib/json-meta.d.ts +10 -0
- package/dist/lib/json-meta.js +98 -0
- package/dist/lib/limit-flag.d.ts +1 -0
- package/dist/lib/limit-flag.js +24 -0
- package/dist/lib/research/{search-index.d.ts → artifact-index.d.ts} +5 -5
- package/dist/lib/research/{search-index.js → artifact-index.js} +10 -10
- package/dist/lib/research/browser.d.ts +0 -8
- package/dist/lib/research/browser.js +20 -43
- package/dist/lib/research/regression.d.ts +18 -0
- package/dist/lib/research/regression.js +47 -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/research/storage.d.ts +0 -1
- package/dist/lib/research/storage.js +15 -13
- package/dist/lib/research/url.d.ts +22 -0
- package/dist/lib/research/url.js +63 -1
- package/dist/lib/text.d.ts +5 -1
- package/dist/lib/text.js +11 -1
- package/dist/sites/coveo.d.ts +7 -0
- package/dist/sites/coveo.js +33 -0
- package/dist/sites/index.js +1 -2
- package/dist/sites/salesforce/fetch-page.js +1 -1
- package/dist/sites/salesforce/index.js +3 -8
- package/dist/sites/salesforce-developer/fetch-page.js +3 -1
- package/dist/sites/salesforce-developer/index.js +1 -2
- package/dist/sites/salesforce-doc-fetch.d.ts +11 -0
- package/dist/sites/salesforce-doc-fetch.js +117 -76
- package/dist/sites/salesforce-dom-probe.d.ts +16 -0
- package/dist/sites/salesforce-dom-probe.js +114 -0
- package/dist/sites/types.d.ts +0 -6
- package/oclif.manifest.json +153 -155
- package/package.json +38 -25
- package/dist/commands/index.d.ts +0 -35
- package/dist/commands/index.js +0 -306
- package/dist/commands/research/config/base.d.ts +0 -23
- package/dist/commands/research/config/base.js +0 -46
- package/dist/commands/research/config/get.d.ts +0 -20
- package/dist/commands/research/config/get.js +0 -45
- package/dist/commands/research/config/list.d.ts +0 -17
- package/dist/commands/research/config/list.js +0 -42
- package/dist/commands/research/config/set.d.ts +0 -31
- package/dist/commands/research/config/set.js +0 -94
- package/dist/commands/research/config/unset.d.ts +0 -21
- package/dist/commands/research/config/unset.js +0 -59
- package/dist/commands/research/import.d.ts +0 -38
- package/dist/commands/research/import.js +0 -313
- package/dist/commands/research/inspect.d.ts +0 -13
- package/dist/commands/research/inspect.js +0 -86
- package/dist/commands/research/list.d.ts +0 -25
- package/dist/commands/research/list.js +0 -143
- package/dist/commands/research/prune.d.ts +0 -24
- package/dist/commands/research/prune.js +0 -141
- package/dist/commands/research/search.d.ts +0 -32
- package/dist/commands/research/search.js +0 -334
- package/dist/commands/research/status.d.ts +0 -17
- package/dist/commands/research/status.js +0 -108
- package/dist/commands/research.d.ts +0 -36
- package/dist/commands/research.js +0 -304
- package/dist/commands/search.d.ts +0 -31
- package/dist/commands/search.js +0 -329
- package/dist/lib/research/docs/__fixtures__/sphinx-searchindex.d.ts +0 -1
- package/dist/lib/research/docs/__fixtures__/sphinx-searchindex.js +0 -2
- package/dist/lib/research/docs/discovery-index.d.ts +0 -14
- package/dist/lib/research/docs/discovery-index.js +0 -112
- package/dist/lib/research/docs/remote-search-runner.d.ts +0 -21
- package/dist/lib/research/docs/remote-search-runner.js +0 -114
- package/dist/lib/research/docs/remote-search.d.ts +0 -14
- package/dist/lib/research/docs/remote-search.js +0 -101
- package/dist/lib/research/docs/search-index.d.ts +0 -13
- package/dist/lib/research/docs/search-index.js +0 -176
- package/dist/sites/salesforce/browser-search.d.ts +0 -7
- package/dist/sites/salesforce/browser-search.js +0 -82
- package/dist/sites/salesforce/coveo.d.ts +0 -23
- package/dist/sites/salesforce/coveo.js +0 -161
- package/dist/sites/salesforce/token.d.ts +0 -26
- package/dist/sites/salesforce/token.js +0 -154
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { ConfigCommand } from './base.js';
|
|
2
|
-
export default class ResearchConfigSet extends ConfigCommand<typeof ResearchConfigSet> {
|
|
3
|
-
static id: string;
|
|
4
|
-
static summary: string;
|
|
5
|
-
static description: string;
|
|
6
|
-
static examples: {
|
|
7
|
-
description: string;
|
|
8
|
-
command: string;
|
|
9
|
-
}[];
|
|
10
|
-
static args: {
|
|
11
|
-
key: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
12
|
-
value: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
13
|
-
};
|
|
14
|
-
static flags: {
|
|
15
|
-
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
16
|
-
global: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
17
|
-
local: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
18
|
-
};
|
|
19
|
-
static stdoutIsPrimaryData: boolean;
|
|
20
|
-
init(): Promise<void>;
|
|
21
|
-
execute(): Promise<unknown>;
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Accept the `key=value` convenience form alongside the canonical `key value`.
|
|
25
|
-
* Splits only when no separate value positional was supplied, and on the FIRST `=`
|
|
26
|
-
* so a value may itself contain `=`.
|
|
27
|
-
*/
|
|
28
|
-
export declare function splitInlineKeyValue(rawKey: string | undefined, rawValue: string | undefined): {
|
|
29
|
-
keyArg: string | undefined;
|
|
30
|
-
valueArg: string | undefined;
|
|
31
|
-
};
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { Args, Flags } from '@oclif/core';
|
|
2
|
-
import { ConfigCommand, configScopeFlags } from './base.js';
|
|
3
|
-
import { KEY_META, writeUserConfig, writeProjectConfig } from '../../../lib/config/index.js';
|
|
4
|
-
export default class ResearchConfigSet extends ConfigCommand {
|
|
5
|
-
static id = 'research config set';
|
|
6
|
-
static summary = 'Set a research configuration key.';
|
|
7
|
-
static description = 'Persist a configuration value. Writes user-level config by default; pass --local to write the project-level config (.bonsai.json in cwd).\n\n' +
|
|
8
|
-
'Valid keys: storage. The inline form `<key>=<value>` is also accepted.';
|
|
9
|
-
static examples = [
|
|
10
|
-
{
|
|
11
|
-
description: 'Store research cache inside the current project',
|
|
12
|
-
command: '<%= config.bin %> research config set storage project --local',
|
|
13
|
-
},
|
|
14
|
-
{
|
|
15
|
-
description: 'Set the user-level default using the inline key=value form',
|
|
16
|
-
command: '<%= config.bin %> research config set storage=global',
|
|
17
|
-
},
|
|
18
|
-
];
|
|
19
|
-
static args = {
|
|
20
|
-
key: Args.string({ required: false, description: 'The configuration key to set.' }),
|
|
21
|
-
value: Args.string({ required: false, description: 'The value to assign.' }),
|
|
22
|
-
};
|
|
23
|
-
static flags = {
|
|
24
|
-
...configScopeFlags({
|
|
25
|
-
global: 'Write to user-level config (default).',
|
|
26
|
-
local: 'Write to project-level config (.bonsai.json in cwd).',
|
|
27
|
-
}),
|
|
28
|
-
'dry-run': Flags.boolean({
|
|
29
|
-
description: 'Show what would be written without saving.',
|
|
30
|
-
default: false,
|
|
31
|
-
}),
|
|
32
|
-
};
|
|
33
|
-
static stdoutIsPrimaryData = true;
|
|
34
|
-
async init() {
|
|
35
|
-
await super.init();
|
|
36
|
-
const { args, flags } = await this.parse(ResearchConfigSet);
|
|
37
|
-
this.args = args;
|
|
38
|
-
this.flags = flags;
|
|
39
|
-
}
|
|
40
|
-
async execute() {
|
|
41
|
-
const { keyArg, valueArg } = splitInlineKeyValue(this.args.key, this.args.value);
|
|
42
|
-
if (!keyArg) {
|
|
43
|
-
this.error('Missing required argument: key', { exit: 2 });
|
|
44
|
-
}
|
|
45
|
-
this.assertKnownKey(keyArg);
|
|
46
|
-
this.assertScopeFlagsExclusive(this.flags.global, this.flags.local);
|
|
47
|
-
const meta = KEY_META[keyArg];
|
|
48
|
-
if (valueArg === undefined) {
|
|
49
|
-
this.error(`Missing required argument: value for key "${keyArg}"`, { exit: 2 });
|
|
50
|
-
}
|
|
51
|
-
const parsed = meta.parseValue(valueArg);
|
|
52
|
-
if (!meta.isValid(parsed)) {
|
|
53
|
-
const guidance = meta.values ? `Valid values: ${meta.values.join(', ')}.` : meta.description;
|
|
54
|
-
this.error(`Invalid value "${valueArg}" for "${keyArg}". ${guidance}`, { exit: 2 });
|
|
55
|
-
}
|
|
56
|
-
const scope = this.writeScope(this.flags.local);
|
|
57
|
-
const formatted = meta.format(parsed);
|
|
58
|
-
if (this.flags['dry-run']) {
|
|
59
|
-
if (!this.requestedJson())
|
|
60
|
-
this.log(`[dry-run] Would set ${keyArg} = ${formatted} (${scope})`);
|
|
61
|
-
return { key: keyArg, value: parsed, scope, dryRun: true };
|
|
62
|
-
}
|
|
63
|
-
const patch = { [keyArg]: parsed };
|
|
64
|
-
if (scope === 'project') {
|
|
65
|
-
writeProjectConfig(process.cwd(), patch);
|
|
66
|
-
}
|
|
67
|
-
else {
|
|
68
|
-
const configDir = this.config.configDir;
|
|
69
|
-
if (!configDir) {
|
|
70
|
-
this.error('Could not determine user config directory. Use --local to write project config.', {
|
|
71
|
-
exit: 1,
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
writeUserConfig(configDir, patch);
|
|
75
|
-
}
|
|
76
|
-
if (!this.requestedJson())
|
|
77
|
-
this.log(`Set ${keyArg} = ${formatted} (${scope})`);
|
|
78
|
-
return { key: keyArg, value: parsed, scope, dryRun: false };
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* Accept the `key=value` convenience form alongside the canonical `key value`.
|
|
83
|
-
* Splits only when no separate value positional was supplied, and on the FIRST `=`
|
|
84
|
-
* so a value may itself contain `=`.
|
|
85
|
-
*/
|
|
86
|
-
export function splitInlineKeyValue(rawKey, rawValue) {
|
|
87
|
-
if (rawValue === undefined && rawKey !== undefined) {
|
|
88
|
-
const eq = rawKey.indexOf('=');
|
|
89
|
-
if (eq !== -1) {
|
|
90
|
-
return { keyArg: rawKey.slice(0, eq), valueArg: rawKey.slice(eq + 1) };
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
return { keyArg: rawKey, valueArg: rawValue };
|
|
94
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { ConfigCommand } from './base.js';
|
|
2
|
-
export default class ResearchConfigUnset extends ConfigCommand<typeof ResearchConfigUnset> {
|
|
3
|
-
static id: string;
|
|
4
|
-
static summary: string;
|
|
5
|
-
static description: string;
|
|
6
|
-
static examples: {
|
|
7
|
-
description: string;
|
|
8
|
-
command: string;
|
|
9
|
-
}[];
|
|
10
|
-
static args: {
|
|
11
|
-
key: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
12
|
-
};
|
|
13
|
-
static flags: {
|
|
14
|
-
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
|
-
global: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
16
|
-
local: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
17
|
-
};
|
|
18
|
-
static stdoutIsPrimaryData: boolean;
|
|
19
|
-
init(): Promise<void>;
|
|
20
|
-
execute(): Promise<unknown>;
|
|
21
|
-
}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { Args, Flags } from '@oclif/core';
|
|
2
|
-
import { ConfigCommand, configScopeFlags } from './base.js';
|
|
3
|
-
import { writeUserConfig, writeProjectConfig } from '../../../lib/config/index.js';
|
|
4
|
-
export default class ResearchConfigUnset extends ConfigCommand {
|
|
5
|
-
static id = 'research config unset';
|
|
6
|
-
static summary = 'Remove a research configuration key.';
|
|
7
|
-
static description = 'Delete a key from user-level config (default) or project-level config (--local), restoring the built-in default.\n\nValid keys: storage.';
|
|
8
|
-
static examples = [
|
|
9
|
-
{ description: 'Remove the user-level storage setting', command: '<%= config.bin %> research config unset storage' },
|
|
10
|
-
{
|
|
11
|
-
description: 'Remove the project-level storage setting',
|
|
12
|
-
command: '<%= config.bin %> research config unset storage --local',
|
|
13
|
-
},
|
|
14
|
-
];
|
|
15
|
-
static args = {
|
|
16
|
-
key: Args.string({ required: true, description: 'The configuration key to remove.' }),
|
|
17
|
-
};
|
|
18
|
-
static flags = {
|
|
19
|
-
...configScopeFlags({
|
|
20
|
-
global: 'Remove from user-level config (default).',
|
|
21
|
-
local: 'Remove from project-level config (.bonsai.json in cwd).',
|
|
22
|
-
}),
|
|
23
|
-
'dry-run': Flags.boolean({ description: 'Show what would be removed without saving.', default: false }),
|
|
24
|
-
};
|
|
25
|
-
static stdoutIsPrimaryData = true;
|
|
26
|
-
async init() {
|
|
27
|
-
await super.init();
|
|
28
|
-
const { args, flags } = await this.parse(ResearchConfigUnset);
|
|
29
|
-
this.args = args;
|
|
30
|
-
this.flags = flags;
|
|
31
|
-
}
|
|
32
|
-
async execute() {
|
|
33
|
-
const key = this.args.key;
|
|
34
|
-
this.assertKnownKey(key);
|
|
35
|
-
this.assertScopeFlagsExclusive(this.flags.global, this.flags.local);
|
|
36
|
-
const scope = this.writeScope(this.flags.local);
|
|
37
|
-
if (this.flags['dry-run']) {
|
|
38
|
-
if (!this.requestedJson())
|
|
39
|
-
this.log(`[dry-run] Would unset ${key} (${scope})`);
|
|
40
|
-
return { key, scope, dryRun: true };
|
|
41
|
-
}
|
|
42
|
-
const patch = { [key]: undefined };
|
|
43
|
-
if (scope === 'project') {
|
|
44
|
-
writeProjectConfig(process.cwd(), patch);
|
|
45
|
-
}
|
|
46
|
-
else {
|
|
47
|
-
const configDir = this.config.configDir;
|
|
48
|
-
if (!configDir) {
|
|
49
|
-
this.error('Could not determine user config directory. Use --local to remove project config.', {
|
|
50
|
-
exit: 1,
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
writeUserConfig(configDir, patch);
|
|
54
|
-
}
|
|
55
|
-
if (!this.requestedJson())
|
|
56
|
-
this.log(`Unset ${key} (${scope})`);
|
|
57
|
-
return { key, scope, dryRun: false };
|
|
58
|
-
}
|
|
59
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import * as fs from 'node:fs';
|
|
2
|
-
import { BaseCommand } from '../../base-command.js';
|
|
3
|
-
export default class ResearchImport extends BaseCommand<typeof ResearchImport> {
|
|
4
|
-
static id: string;
|
|
5
|
-
static summary: string;
|
|
6
|
-
static description: string;
|
|
7
|
-
static examples: {
|
|
8
|
-
description: string;
|
|
9
|
-
command: string;
|
|
10
|
-
}[];
|
|
11
|
-
static args: {
|
|
12
|
-
url: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
13
|
-
};
|
|
14
|
-
static flags: {
|
|
15
|
-
stdin: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
16
|
-
file: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
|
-
'input-format': import("@oclif/core/interfaces").OptionFlag<"compressed" | "detailed", import("@oclif/core/interfaces").CustomOptions>;
|
|
18
|
-
topic: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
19
|
-
tags: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
20
|
-
tier: import("@oclif/core/interfaces").OptionFlag<"stable" | "standard" | "volatile", import("@oclif/core/interfaces").CustomOptions>;
|
|
21
|
-
'source-url': import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
22
|
-
ttl: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
23
|
-
storage: import("@oclif/core/interfaces").OptionFlag<"global" | "project" | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
24
|
-
};
|
|
25
|
-
static stdoutIsPrimaryData: boolean;
|
|
26
|
-
init(): Promise<void>;
|
|
27
|
-
private readStdin;
|
|
28
|
-
private getSourceUrls;
|
|
29
|
-
private validateSourceMode;
|
|
30
|
-
protected fsExistsSync(filePath: string): boolean;
|
|
31
|
-
protected fsStatSync(filePath: string): fs.Stats;
|
|
32
|
-
protected fsReadFileSync(filePath: string): string;
|
|
33
|
-
private readAndValidateFile;
|
|
34
|
-
private readAndValidateInput;
|
|
35
|
-
private deriveImportCacheKey;
|
|
36
|
-
private buildImportArtifact;
|
|
37
|
-
execute(): Promise<unknown>;
|
|
38
|
-
}
|
|
@@ -1,313 +0,0 @@
|
|
|
1
|
-
import { Args, Flags } from '@oclif/core';
|
|
2
|
-
import { createHash } from 'node:crypto';
|
|
3
|
-
import * as fs from 'node:fs';
|
|
4
|
-
import * as path from 'node:path';
|
|
5
|
-
import { BaseCommand } from '../../base-command.js';
|
|
6
|
-
import { normalizeUrl } from '../../lib/research/url.js';
|
|
7
|
-
import { deriveCacheKey } from '../../lib/research/cache-key.js';
|
|
8
|
-
import { getArtifactPath } from '../../lib/research/storage.js';
|
|
9
|
-
import { loadStoreRoots } from '../../lib/research/store-roots.js';
|
|
10
|
-
import { writeArtifactSecurely } from '../../lib/research/secure-write.js';
|
|
11
|
-
import { getPolicy } from '../../lib/research/freshness.js';
|
|
12
|
-
import { compressMarkdown } from '../../lib/research/compress.js';
|
|
13
|
-
import { estimateTokens } from '../../lib/research/token-estimate.js';
|
|
14
|
-
export default class ResearchImport extends BaseCommand {
|
|
15
|
-
static id = 'research import';
|
|
16
|
-
static summary = 'Import agent-supplied clean Markdown notes into the local cache.';
|
|
17
|
-
static description = 'Accepts a synthesized Markdown note via stdin and registers it under the single target URL or multiple source URLs.';
|
|
18
|
-
static examples = [
|
|
19
|
-
{
|
|
20
|
-
description: 'Import detailed research for a single URL via stdin',
|
|
21
|
-
command: 'echo "# My Article" | <%= config.bin %> research import https://example.com/docs --stdin',
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
description: 'Import research synthesized from multiple source URLs',
|
|
25
|
-
command: 'echo "# Synthesized" | <%= config.bin %> research import --stdin --topic "React docs" --source-url https://react.dev/a --source-url https://react.dev/b',
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
description: 'Import research from a local Markdown file',
|
|
29
|
-
command: '<%= config.bin %> research import https://example.com/docs --file path/to/notes.md',
|
|
30
|
-
},
|
|
31
|
-
];
|
|
32
|
-
static args = {
|
|
33
|
-
url: Args.string({
|
|
34
|
-
required: false,
|
|
35
|
-
description: 'The single source URL of the page (only for single-source import).',
|
|
36
|
-
}),
|
|
37
|
-
};
|
|
38
|
-
static flags = {
|
|
39
|
-
stdin: Flags.boolean({
|
|
40
|
-
description: 'Read Markdown from stdin.',
|
|
41
|
-
default: false,
|
|
42
|
-
}),
|
|
43
|
-
file: Flags.string({
|
|
44
|
-
description: 'Path to a Markdown file containing research notes to import.',
|
|
45
|
-
}),
|
|
46
|
-
'input-format': Flags.option({
|
|
47
|
-
description: 'Format of the input provided.',
|
|
48
|
-
options: ['compressed', 'detailed'],
|
|
49
|
-
default: 'detailed',
|
|
50
|
-
})(),
|
|
51
|
-
topic: Flags.string({
|
|
52
|
-
char: 't',
|
|
53
|
-
description: 'The main topic for this research note.',
|
|
54
|
-
}),
|
|
55
|
-
tags: Flags.string({
|
|
56
|
-
char: 'g',
|
|
57
|
-
description: 'Taxonomic tags (can be repeated).',
|
|
58
|
-
multiple: true,
|
|
59
|
-
}),
|
|
60
|
-
tier: Flags.option({
|
|
61
|
-
description: 'Freshness tier policy.',
|
|
62
|
-
options: ['stable', 'standard', 'volatile'],
|
|
63
|
-
default: 'standard',
|
|
64
|
-
})(),
|
|
65
|
-
'source-url': Flags.string({
|
|
66
|
-
description: 'Repeated source URLs for multi-source import.',
|
|
67
|
-
multiple: true,
|
|
68
|
-
}),
|
|
69
|
-
ttl: Flags.string({
|
|
70
|
-
char: 'l',
|
|
71
|
-
description: 'Predicted lifespan of the data (e.g. "24h", "7d").',
|
|
72
|
-
}),
|
|
73
|
-
storage: Flags.option({
|
|
74
|
-
description: 'Override where this note is cached (overrides configured default). Secret-bearing notes are always stored globally.',
|
|
75
|
-
options: ['global', 'project'],
|
|
76
|
-
})(),
|
|
77
|
-
};
|
|
78
|
-
static stdoutIsPrimaryData = true;
|
|
79
|
-
async init() {
|
|
80
|
-
await super.init();
|
|
81
|
-
const { args, flags } = await this.parse(ResearchImport);
|
|
82
|
-
this.args = args;
|
|
83
|
-
this.flags = flags;
|
|
84
|
-
}
|
|
85
|
-
async readStdin(limitBytes = 1024 * 1024) {
|
|
86
|
-
return new Promise((resolve, reject) => {
|
|
87
|
-
let data = '';
|
|
88
|
-
let bytesRead = 0;
|
|
89
|
-
process.stdin.setEncoding('utf8');
|
|
90
|
-
const onData = (chunk) => {
|
|
91
|
-
bytesRead += Buffer.byteLength(chunk, 'utf8');
|
|
92
|
-
if (bytesRead > limitBytes) {
|
|
93
|
-
process.stdin.removeListener('data', onData);
|
|
94
|
-
reject(new Error('stdin size limit exceeded (max 1 MiB)'));
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
data += chunk;
|
|
98
|
-
};
|
|
99
|
-
process.stdin.on('data', onData);
|
|
100
|
-
const onEnd = () => {
|
|
101
|
-
process.stdin.removeListener('data', onData);
|
|
102
|
-
resolve(data);
|
|
103
|
-
};
|
|
104
|
-
process.stdin.once('end', onEnd);
|
|
105
|
-
process.stdin.once('error', (err) => {
|
|
106
|
-
process.stdin.removeListener('data', onData);
|
|
107
|
-
process.stdin.removeListener('end', onEnd);
|
|
108
|
-
reject(err);
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
getSourceUrls(hasMulti, multiUrls, singleUrl) {
|
|
113
|
-
const urls = hasMulti ? multiUrls : singleUrl ? [singleUrl] : [];
|
|
114
|
-
const normalized = urls.map((u) => {
|
|
115
|
-
try {
|
|
116
|
-
return normalizeUrl(u);
|
|
117
|
-
}
|
|
118
|
-
catch (err) {
|
|
119
|
-
this.error(`Invalid URL: ${err.message}`, { exit: 2 });
|
|
120
|
-
}
|
|
121
|
-
});
|
|
122
|
-
return hasMulti ? normalized.sort() : normalized;
|
|
123
|
-
}
|
|
124
|
-
validateSourceMode(hasSingle, hasMulti, multiUrls) {
|
|
125
|
-
if (hasSingle && hasMulti) {
|
|
126
|
-
this.error('Cannot specify both positional <url> and --source-url flags.', { exit: 2 });
|
|
127
|
-
}
|
|
128
|
-
if (!hasSingle && !hasMulti) {
|
|
129
|
-
this.error('Must specify either positional <url> (for single-source) or --source-url (for multi-source) import.', { exit: 2 });
|
|
130
|
-
}
|
|
131
|
-
if (hasMulti && !this.flags.topic) {
|
|
132
|
-
this.error('Multi-source import requires the --topic flag.', { exit: 2 });
|
|
133
|
-
}
|
|
134
|
-
if (!this.flags.stdin && !this.flags.file) {
|
|
135
|
-
this.error('Either --stdin or --file <path> must be specified to import content.', {
|
|
136
|
-
exit: 2,
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
if (this.flags.stdin && this.flags.file) {
|
|
140
|
-
this.error('Cannot specify both --stdin and --file <path>. Choose one.', { exit: 2 });
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
fsExistsSync(filePath) {
|
|
144
|
-
return fs.existsSync(filePath);
|
|
145
|
-
}
|
|
146
|
-
fsStatSync(filePath) {
|
|
147
|
-
return fs.statSync(filePath);
|
|
148
|
-
}
|
|
149
|
-
fsReadFileSync(filePath) {
|
|
150
|
-
return fs.readFileSync(filePath, 'utf8');
|
|
151
|
-
}
|
|
152
|
-
readAndValidateFile(filePath) {
|
|
153
|
-
if (!this.fsExistsSync(filePath)) {
|
|
154
|
-
this.error(`File does not exist: ${filePath}`, { exit: 2 });
|
|
155
|
-
}
|
|
156
|
-
const stat = this.fsStatSync(filePath);
|
|
157
|
-
if (!stat.isFile()) {
|
|
158
|
-
this.error(`Path is not a file: ${filePath}`, { exit: 2 });
|
|
159
|
-
}
|
|
160
|
-
if (stat.size > 1024 * 1024) {
|
|
161
|
-
this.error('File size limit exceeded (max 1 MiB).', { exit: 1 });
|
|
162
|
-
}
|
|
163
|
-
const content = this.fsReadFileSync(filePath);
|
|
164
|
-
if (!content.trim()) {
|
|
165
|
-
this.error('Empty file content provided.', { exit: 2 });
|
|
166
|
-
}
|
|
167
|
-
return content;
|
|
168
|
-
}
|
|
169
|
-
async readAndValidateInput() {
|
|
170
|
-
if (this.flags.stdin) {
|
|
171
|
-
try {
|
|
172
|
-
const rawInput = await this.readStdin();
|
|
173
|
-
if (!rawInput.trim()) {
|
|
174
|
-
this.error('Empty stdin content provided.', { exit: 2 });
|
|
175
|
-
}
|
|
176
|
-
return rawInput;
|
|
177
|
-
}
|
|
178
|
-
catch (err) {
|
|
179
|
-
this.error(err.message, { exit: 1 });
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
if (this.flags.file) {
|
|
183
|
-
const filePath = path.resolve(this.flags.file);
|
|
184
|
-
try {
|
|
185
|
-
return this.readAndValidateFile(filePath);
|
|
186
|
-
}
|
|
187
|
-
catch (err) {
|
|
188
|
-
this.error(`Failed to read file: ${err.message}`, { exit: 1 });
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
this.error('No input source specified.', { exit: 2 });
|
|
192
|
-
}
|
|
193
|
-
deriveImportCacheKey(hasSingle, singleNormalizedUrl, sourceUrls) {
|
|
194
|
-
if (hasSingle) {
|
|
195
|
-
return deriveCacheKey(singleNormalizedUrl);
|
|
196
|
-
}
|
|
197
|
-
const normalizedTopic = this.flags.topic.trim().toLowerCase();
|
|
198
|
-
const combinedString = [normalizedTopic, ...sourceUrls].join('|');
|
|
199
|
-
return createHash('sha256').update(combinedString).digest('hex');
|
|
200
|
-
}
|
|
201
|
-
buildImportArtifact(hasSingle, singleNormalizedUrl, sourceUrls, cacheKey, rawInput) {
|
|
202
|
-
const currentTime = new Date();
|
|
203
|
-
const tier = this.flags.tier;
|
|
204
|
-
const ttl = this.flags.ttl || null;
|
|
205
|
-
const { freshWindowMs } = getPolicy(tier, ttl);
|
|
206
|
-
const staleAfterTime = new Date(currentTime.getTime() + freshWindowMs);
|
|
207
|
-
const inputFormat = this.flags['input-format'];
|
|
208
|
-
const detailed = rawInput;
|
|
209
|
-
const compressed = inputFormat === 'detailed' ? compressMarkdown(rawInput) : rawInput;
|
|
210
|
-
const contentHash = createHash('sha256').update(detailed).digest('hex');
|
|
211
|
-
const metadata = {
|
|
212
|
-
schema_version: 1,
|
|
213
|
-
artifact_type: hasSingle ? 'source' : 'research_note',
|
|
214
|
-
source_url: hasSingle ? this.args.url : '',
|
|
215
|
-
source_urls: sourceUrls,
|
|
216
|
-
normalized_url: singleNormalizedUrl,
|
|
217
|
-
cache_key: cacheKey,
|
|
218
|
-
topic: this.flags.topic || null,
|
|
219
|
-
tags: this.flags.tags || [],
|
|
220
|
-
format_available: ['compressed', 'detailed'],
|
|
221
|
-
tier,
|
|
222
|
-
ttl,
|
|
223
|
-
fetched_at: null,
|
|
224
|
-
validated_at: currentTime.toISOString(),
|
|
225
|
-
stale_after: staleAfterTime.toISOString(),
|
|
226
|
-
capture_method: 'agent_supplied',
|
|
227
|
-
extraction_status: 'agent_supplied',
|
|
228
|
-
extraction_confidence: 'high',
|
|
229
|
-
quality_notes: ['agent-supplied research import'],
|
|
230
|
-
supplied_at: currentTime.toISOString(),
|
|
231
|
-
supplied_by: null,
|
|
232
|
-
etag: null,
|
|
233
|
-
last_modified: null,
|
|
234
|
-
content_hash: contentHash,
|
|
235
|
-
token_estimate: {
|
|
236
|
-
compressed: estimateTokens(compressed),
|
|
237
|
-
detailed: estimateTokens(detailed),
|
|
238
|
-
},
|
|
239
|
-
status: 'active',
|
|
240
|
-
site_module_id: null,
|
|
241
|
-
docs_engine: null,
|
|
242
|
-
docs_framework: null,
|
|
243
|
-
source_doc_url: null,
|
|
244
|
-
search_provider: null,
|
|
245
|
-
parent_cache_key: null,
|
|
246
|
-
section_anchor: null,
|
|
247
|
-
section_heading_path: null,
|
|
248
|
-
};
|
|
249
|
-
return {
|
|
250
|
-
metadata,
|
|
251
|
-
summary: `Synthesized research for ${hasSingle ? singleNormalizedUrl : this.flags.topic}`,
|
|
252
|
-
compressed,
|
|
253
|
-
detailed,
|
|
254
|
-
provenance: `Imported via agent-supplied research at ${currentTime.toISOString()}`,
|
|
255
|
-
};
|
|
256
|
-
}
|
|
257
|
-
async execute() {
|
|
258
|
-
const hasSingle = Boolean(this.args.url);
|
|
259
|
-
const multiUrls = this.flags['source-url'] || [];
|
|
260
|
-
const hasMulti = multiUrls.length > 0;
|
|
261
|
-
this.validateSourceMode(hasSingle, hasMulti, multiUrls);
|
|
262
|
-
const rawInput = await this.readAndValidateInput();
|
|
263
|
-
const sourceUrls = this.getSourceUrls(hasMulti, multiUrls, this.args.url);
|
|
264
|
-
const singleNormalizedUrl = hasSingle ? sourceUrls[0] || '' : '';
|
|
265
|
-
const cacheKey = this.deriveImportCacheKey(hasSingle, singleNormalizedUrl, sourceUrls);
|
|
266
|
-
const artifact = this.buildImportArtifact(hasSingle, singleNormalizedUrl, sourceUrls, cacheKey, rawInput);
|
|
267
|
-
const roots = loadStoreRoots({
|
|
268
|
-
configDir: this.config.configDir,
|
|
269
|
-
cwd: process.cwd(),
|
|
270
|
-
dataDir: this.config.dataDir,
|
|
271
|
-
flagOverride: this.flags.storage,
|
|
272
|
-
});
|
|
273
|
-
const writeResult = writeArtifactSecurely(roots, cacheKey, artifact);
|
|
274
|
-
const storagePath = getArtifactPath(writeResult.dataDir, cacheKey);
|
|
275
|
-
if (writeResult.redirected) {
|
|
276
|
-
this.warn(`Detected ${writeResult.secretLabel} in the imported content; stored in the global cache instead of the project to avoid committing secrets.`);
|
|
277
|
-
}
|
|
278
|
-
if (!this.requestedJson()) {
|
|
279
|
-
this.log(`Successfully imported research artifact.`);
|
|
280
|
-
this.log(`Cache Key: ${cacheKey}`);
|
|
281
|
-
this.log(`Storage Path: ${storagePath}`);
|
|
282
|
-
}
|
|
283
|
-
const inputFormat = this.flags['input-format'];
|
|
284
|
-
return {
|
|
285
|
-
schemaVersion: 1,
|
|
286
|
-
command: 'research import',
|
|
287
|
-
cache: {
|
|
288
|
-
key: cacheKey,
|
|
289
|
-
status: 'imported',
|
|
290
|
-
freshness: 'fresh',
|
|
291
|
-
path: storagePath,
|
|
292
|
-
storage: roots.mode,
|
|
293
|
-
redirectedToGlobal: writeResult.redirected,
|
|
294
|
-
},
|
|
295
|
-
source: {
|
|
296
|
-
url: hasSingle ? this.args.url : '',
|
|
297
|
-
normalizedUrl: singleNormalizedUrl,
|
|
298
|
-
captureMethod: 'agent_supplied',
|
|
299
|
-
extractionStatus: 'agent_supplied',
|
|
300
|
-
extractionConfidence: 'high',
|
|
301
|
-
qualityNotes: ['agent-supplied research import'],
|
|
302
|
-
fetchedAt: null,
|
|
303
|
-
validatedAt: artifact.metadata.validated_at,
|
|
304
|
-
staleAfter: artifact.metadata.stale_after,
|
|
305
|
-
},
|
|
306
|
-
format: inputFormat,
|
|
307
|
-
tokenEstimate: inputFormat === 'compressed'
|
|
308
|
-
? artifact.metadata.token_estimate.compressed
|
|
309
|
-
: artifact.metadata.token_estimate.detailed,
|
|
310
|
-
content: inputFormat === 'compressed' ? artifact.compressed : artifact.detailed,
|
|
311
|
-
};
|
|
312
|
-
}
|
|
313
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { BaseCommand } from '../../base-command.js';
|
|
2
|
-
export default class ResearchInspect extends BaseCommand<typeof ResearchInspect> {
|
|
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 stdoutIsPrimaryData: boolean;
|
|
10
|
-
init(): Promise<void>;
|
|
11
|
-
execute(): Promise<unknown>;
|
|
12
|
-
private findSections;
|
|
13
|
-
}
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import { Args } from '@oclif/core';
|
|
2
|
-
import { BaseCommand } from '../../base-command.js';
|
|
3
|
-
import { normalizeUrl } from '../../lib/research/url.js';
|
|
4
|
-
import { deriveCacheKey } from '../../lib/research/cache-key.js';
|
|
5
|
-
import { locateArtifact, scanCacheDirs } from '../../lib/research/storage.js';
|
|
6
|
-
import { loadStoreRoots } from '../../lib/research/store-roots.js';
|
|
7
|
-
export default class ResearchInspect extends BaseCommand {
|
|
8
|
-
static id = 'research inspect';
|
|
9
|
-
static summary = 'Inspect cached research metadata for a URL.';
|
|
10
|
-
static description = 'Inspects local storage and displays full frontmatter metadata for the URL (without printing the content body).';
|
|
11
|
-
static args = {
|
|
12
|
-
url: Args.string({
|
|
13
|
-
required: true,
|
|
14
|
-
description: 'The URL to inspect.',
|
|
15
|
-
}),
|
|
16
|
-
};
|
|
17
|
-
static stdoutIsPrimaryData = true;
|
|
18
|
-
async init() {
|
|
19
|
-
await super.init();
|
|
20
|
-
const { args } = await this.parse(ResearchInspect);
|
|
21
|
-
this.args = args;
|
|
22
|
-
}
|
|
23
|
-
async execute() {
|
|
24
|
-
const { url } = this.args;
|
|
25
|
-
let normalizedUrl;
|
|
26
|
-
let cacheKey;
|
|
27
|
-
try {
|
|
28
|
-
normalizedUrl = normalizeUrl(url);
|
|
29
|
-
cacheKey = deriveCacheKey(normalizedUrl);
|
|
30
|
-
}
|
|
31
|
-
catch (err) {
|
|
32
|
-
this.error(`Invalid URL: ${err.message}`, { exit: 2 });
|
|
33
|
-
}
|
|
34
|
-
const roots = loadStoreRoots({
|
|
35
|
-
configDir: this.config.configDir,
|
|
36
|
-
cwd: process.cwd(),
|
|
37
|
-
dataDir: this.config.dataDir,
|
|
38
|
-
});
|
|
39
|
-
const located = locateArtifact(roots.readRoots, cacheKey);
|
|
40
|
-
if (!located) {
|
|
41
|
-
this.error(`No cached research found for URL: ${normalizedUrl}`, { exit: 1 });
|
|
42
|
-
}
|
|
43
|
-
const cached = located.artifact;
|
|
44
|
-
const artifactPath = located.path;
|
|
45
|
-
const sections = this.findSections(roots.readRoots, cacheKey);
|
|
46
|
-
if (!this.requestedJson()) {
|
|
47
|
-
this.log(`Cache Key: ${cacheKey}`);
|
|
48
|
-
this.log(`Cache Path: ${artifactPath}`);
|
|
49
|
-
this.log(`--- Metadata ---`);
|
|
50
|
-
for (const [key, val] of Object.entries(cached.metadata)) {
|
|
51
|
-
if (typeof val === 'object' && val !== null) {
|
|
52
|
-
this.log(`${key}: ${JSON.stringify(val)}`);
|
|
53
|
-
}
|
|
54
|
-
else {
|
|
55
|
-
this.log(`${key}: ${val}`);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
if (sections.length) {
|
|
59
|
-
this.log(`--- Sections (${sections.length}) ---`);
|
|
60
|
-
for (const s of sections) {
|
|
61
|
-
this.log(`${s.headingPath} [${s.anchor}] (${s.tokenEstimate.detailed} tokens) ${s.cacheKey}`);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return {
|
|
66
|
-
cacheKey,
|
|
67
|
-
cachePath: artifactPath,
|
|
68
|
-
metadata: cached.metadata,
|
|
69
|
-
sections,
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
// Section children link back via parent_cache_key; list the active ones for this page (T-22).
|
|
73
|
-
findSections(readRoots, parentKey) {
|
|
74
|
-
return scanCacheDirs(readRoots, (artifact) => {
|
|
75
|
-
const meta = artifact.metadata;
|
|
76
|
-
if (meta.parent_cache_key !== parentKey || meta.status !== 'active')
|
|
77
|
-
return null;
|
|
78
|
-
return {
|
|
79
|
-
cacheKey: meta.cache_key,
|
|
80
|
-
anchor: meta.section_anchor,
|
|
81
|
-
headingPath: meta.section_heading_path,
|
|
82
|
-
tokenEstimate: meta.token_estimate,
|
|
83
|
-
};
|
|
84
|
-
}).sort((a, b) => (a.headingPath ?? '').localeCompare(b.headingPath ?? ''));
|
|
85
|
-
}
|
|
86
|
-
}
|