@kolmo/scout 0.1.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/dist/api/kolmoApiClient.d.ts +2 -0
- package/dist/api/kolmoApiClient.js +25 -0
- package/dist/commands/auth.d.ts +6 -0
- package/dist/commands/auth.js +41 -0
- package/dist/commands/explain.d.ts +2 -0
- package/dist/commands/explain.js +95 -0
- package/dist/commands/scan.d.ts +2 -0
- package/dist/commands/scan.js +151 -0
- package/dist/commands/sync.d.ts +2 -0
- package/dist/commands/sync.js +50 -0
- package/dist/config/authConfig.d.ts +8 -0
- package/dist/config/authConfig.js +26 -0
- package/dist/config/writeKolmoConfig.d.ts +2 -0
- package/dist/config/writeKolmoConfig.js +6 -0
- package/dist/detectors/assetDetector.d.ts +8 -0
- package/dist/detectors/assetDetector.js +7 -0
- package/dist/detectors/fileTypeDetector.d.ts +1 -0
- package/dist/detectors/fileTypeDetector.js +38 -0
- package/dist/detectors/projectTypeDetector.d.ts +4 -0
- package/dist/detectors/projectTypeDetector.js +36 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +65 -0
- package/dist/manifest/buildScoutManifest.d.ts +15 -0
- package/dist/manifest/buildScoutManifest.js +219 -0
- package/dist/profilers/tabularProfiler.d.ts +15 -0
- package/dist/profilers/tabularProfiler.js +131 -0
- package/dist/scanners/contentScanner.d.ts +6 -0
- package/dist/scanners/contentScanner.js +22 -0
- package/dist/scanners/csvScanner.d.ts +5 -0
- package/dist/scanners/csvScanner.js +26 -0
- package/dist/scanners/excelScanner.d.ts +5 -0
- package/dist/scanners/excelScanner.js +25 -0
- package/dist/scanners/fileScanner.d.ts +1 -0
- package/dist/scanners/fileScanner.js +24 -0
- package/dist/scanners/notebookScanner.d.ts +5 -0
- package/dist/scanners/notebookScanner.js +32 -0
- package/dist/scanners/packageScanner.d.ts +4 -0
- package/dist/scanners/packageScanner.js +46 -0
- package/dist/scanners/pythonScanner.d.ts +4 -0
- package/dist/scanners/pythonScanner.js +30 -0
- package/dist/summarizers/repoSummary.d.ts +16 -0
- package/dist/summarizers/repoSummary.js +101 -0
- package/dist/taxonomy/energyTaxonomy.d.ts +27 -0
- package/dist/taxonomy/energyTaxonomy.js +250 -0
- package/dist/types.d.ts +78 -0
- package/dist/types.js +1 -0
- package/dist/workflows/workflowSuggestions.d.ts +2 -0
- package/dist/workflows/workflowSuggestions.js +61 -0
- package/package.json +38 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { getToken } from '../config/authConfig.js';
|
|
2
|
+
export async function sendManifest(manifest, apiUrl, portfolioName) {
|
|
3
|
+
const url = `${apiUrl}/api/scout/manifest`;
|
|
4
|
+
const token = getToken();
|
|
5
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
6
|
+
if (token)
|
|
7
|
+
headers['X-Kolmo-Token'] = token;
|
|
8
|
+
const payload = portfolioName
|
|
9
|
+
? { manifest, portfolio_name: portfolioName }
|
|
10
|
+
: { manifest };
|
|
11
|
+
const response = await fetch(url, {
|
|
12
|
+
method: 'POST',
|
|
13
|
+
headers,
|
|
14
|
+
body: JSON.stringify(payload),
|
|
15
|
+
});
|
|
16
|
+
if (response.status === 401) {
|
|
17
|
+
throw new Error('Not authenticated. Run `kolmo auth --token <your-token>` first.\n' +
|
|
18
|
+
'Generate a token in the Kolmo web app under Settings → Scout → CLI Tokens.');
|
|
19
|
+
}
|
|
20
|
+
if (!response.ok) {
|
|
21
|
+
const text = await response.text();
|
|
22
|
+
throw new Error(`Kolmo API error ${response.status}: ${text}`);
|
|
23
|
+
}
|
|
24
|
+
return response.json();
|
|
25
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { readConfig, writeConfig } from '../config/authConfig.js';
|
|
3
|
+
export async function runAuth(options) {
|
|
4
|
+
const token = options.token.trim();
|
|
5
|
+
if (!token.startsWith('ksc_')) {
|
|
6
|
+
console.error(chalk.red('Invalid token format. Kolmo Scout tokens start with ksc_'));
|
|
7
|
+
console.error(chalk.gray('Generate one at: Settings → Scout → CLI Tokens'));
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
// Validate against backend before saving
|
|
11
|
+
console.log(chalk.gray('Validating token…'));
|
|
12
|
+
try {
|
|
13
|
+
const res = await fetch(`${options.apiUrl}/api/scout/sessions?limit=1`, {
|
|
14
|
+
headers: { 'X-Kolmo-Token': token },
|
|
15
|
+
});
|
|
16
|
+
if (res.status === 401) {
|
|
17
|
+
console.error(chalk.red('Token is invalid or has been revoked.'));
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
if (!res.ok) {
|
|
21
|
+
console.error(chalk.red(`Backend returned ${res.status}. Check --api-url.`));
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
console.error(chalk.red(`Could not reach Kolmo backend at ${options.apiUrl}`));
|
|
27
|
+
console.error(chalk.gray('Check --api-url or that the backend is running.'));
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
const existing = readConfig();
|
|
31
|
+
writeConfig({ ...existing, token, api_url: options.apiUrl });
|
|
32
|
+
console.log(chalk.green('✔ Authenticated'));
|
|
33
|
+
console.log(chalk.bold('Token saved to:'), chalk.cyan('~/.kolmo/config.json'));
|
|
34
|
+
console.log('');
|
|
35
|
+
console.log(chalk.gray('You can now run:'), chalk.bold('kolmo scan'));
|
|
36
|
+
}
|
|
37
|
+
export async function runLogout() {
|
|
38
|
+
const { clearToken } = await import('../config/authConfig.js');
|
|
39
|
+
clearToken();
|
|
40
|
+
console.log(chalk.green('✔ Logged out — token removed from ~/.kolmo/config.json'));
|
|
41
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { existsSync } from 'fs';
|
|
2
|
+
import { readFile } from 'fs/promises';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { buildScoutManifest } from '../manifest/buildScoutManifest.js';
|
|
6
|
+
export async function runExplain(options) {
|
|
7
|
+
try {
|
|
8
|
+
const { manifest, source } = await loadManifest(options.dir);
|
|
9
|
+
if (options.json) {
|
|
10
|
+
console.log(JSON.stringify({ source, manifest, explanations: manifest.explanations }, null, 2));
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
printExplanation(manifest, source);
|
|
14
|
+
}
|
|
15
|
+
catch (err) {
|
|
16
|
+
console.error(chalk.red(`Explain failed: ${String(err)}`));
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
async function loadManifest(dir) {
|
|
21
|
+
const cwd = dir ? path.resolve(dir) : process.cwd();
|
|
22
|
+
const configPath = path.join(cwd, 'kolmo.config.json');
|
|
23
|
+
if (existsSync(configPath)) {
|
|
24
|
+
const content = await readFile(configPath, 'utf-8');
|
|
25
|
+
return { manifest: JSON.parse(content), source: configPath };
|
|
26
|
+
}
|
|
27
|
+
const result = await buildScoutManifest({ dir: cwd });
|
|
28
|
+
return { manifest: result.manifest, source: 'fresh scan (not written)' };
|
|
29
|
+
}
|
|
30
|
+
function printExplanation(manifest, source) {
|
|
31
|
+
const explanations = manifest.explanations;
|
|
32
|
+
console.log(chalk.bold('Kolmo Scout explanation'));
|
|
33
|
+
console.log(chalk.bold('Source:'), chalk.gray(source));
|
|
34
|
+
console.log(chalk.bold('Project:'), manifest.project_name);
|
|
35
|
+
console.log(chalk.bold('Type:'), manifest.project_type);
|
|
36
|
+
console.log(chalk.bold('Summary:'), manifest.repo_summary);
|
|
37
|
+
console.log(chalk.bold('Summary source:'), manifest.summary_source ?? 'unknown');
|
|
38
|
+
console.log('');
|
|
39
|
+
console.log(chalk.bold('Why this project type'));
|
|
40
|
+
for (const reason of explanations?.project_type_reasons ?? ['No project-type explanation available.']) {
|
|
41
|
+
console.log(` ${chalk.cyan('•')} ${reason}`);
|
|
42
|
+
}
|
|
43
|
+
console.log('');
|
|
44
|
+
console.log(chalk.bold('Detected assets'));
|
|
45
|
+
if ((explanations?.asset_evidence ?? []).length === 0) {
|
|
46
|
+
console.log(` ${chalk.gray('none')}`);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
for (const asset of explanations.asset_evidence) {
|
|
50
|
+
const meta = [asset.family, asset.exchange, asset.region, asset.hub].filter(Boolean).join(' / ');
|
|
51
|
+
console.log(` ${chalk.cyan('•')} ${chalk.bold(asset.symbol)} ${chalk.gray(meta)}`);
|
|
52
|
+
for (const evidence of asset.evidence.slice(0, 4)) {
|
|
53
|
+
const file = evidence.file ? `${evidence.file}: ` : '';
|
|
54
|
+
console.log(` ${chalk.gray('-')} ${file}${evidence.source} "${evidence.value}" matched "${evidence.pattern}"`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
console.log('');
|
|
59
|
+
console.log(chalk.bold('File classifications'));
|
|
60
|
+
for (const file of (explanations?.file_type_reasons ?? []).slice(0, 20)) {
|
|
61
|
+
const evidence = file.evidence.length ? chalk.gray(` (${file.evidence.slice(0, 4).join(', ')})`) : '';
|
|
62
|
+
console.log(` ${chalk.cyan('•')} ${file.path}: ${chalk.yellow(file.detected_type)} - ${file.reason}${evidence}`);
|
|
63
|
+
}
|
|
64
|
+
console.log('');
|
|
65
|
+
console.log(chalk.bold('Data quality profile'));
|
|
66
|
+
const profiledFiles = manifest.detected_files.filter((file) => file.metadata?.profile);
|
|
67
|
+
if (profiledFiles.length === 0) {
|
|
68
|
+
console.log(` ${chalk.gray('No tabular profiles available.')}`);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
for (const file of profiledFiles.slice(0, 12)) {
|
|
72
|
+
const profile = file.metadata.profile;
|
|
73
|
+
const bits = [
|
|
74
|
+
profile.frequency_guess ? `freq=${profile.frequency_guess}` : '',
|
|
75
|
+
profile.latest_date ? `latest=${profile.latest_date}` : '',
|
|
76
|
+
profile.is_stale != null ? `stale=${profile.is_stale}` : '',
|
|
77
|
+
profile.missingness_estimate != null ? `missing=${profile.missingness_estimate}` : '',
|
|
78
|
+
Array.isArray(profile.tenor_gaps) && profile.tenor_gaps.length > 0
|
|
79
|
+
? `tenor_gaps=${profile.tenor_gaps.join(',')}`
|
|
80
|
+
: '',
|
|
81
|
+
].filter(Boolean);
|
|
82
|
+
console.log(` ${chalk.cyan('•')} ${file.path}: ${bits.join(' | ') || 'profile captured'}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
console.log('');
|
|
86
|
+
console.log(chalk.bold('Workflow reasons'));
|
|
87
|
+
for (const reason of explanations?.workflow_reasons ?? []) {
|
|
88
|
+
console.log(` ${chalk.cyan('•')} ${reason}`);
|
|
89
|
+
}
|
|
90
|
+
console.log('');
|
|
91
|
+
console.log(chalk.bold('Evidence citations'));
|
|
92
|
+
for (const citation of explanations?.evidence_citations ?? []) {
|
|
93
|
+
console.log(` ${chalk.cyan('•')} ${citation}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { createInterface } from 'readline/promises';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { buildScoutManifest, normalizeSummary } from '../manifest/buildScoutManifest.js';
|
|
5
|
+
import { writeKolmoConfig } from '../config/writeKolmoConfig.js';
|
|
6
|
+
import { sendManifest } from '../api/kolmoApiClient.js';
|
|
7
|
+
export async function runScan(options) {
|
|
8
|
+
const spinner = ora('Scanning project...').start();
|
|
9
|
+
try {
|
|
10
|
+
const { cwd, allFiles, hasEnv, manifest } = await buildScoutManifest({
|
|
11
|
+
dir: options.dir,
|
|
12
|
+
summary: options.summary,
|
|
13
|
+
});
|
|
14
|
+
if (options.verbose && hasEnv) {
|
|
15
|
+
spinner.info('.env detected (contents not read)');
|
|
16
|
+
}
|
|
17
|
+
spinner.succeed(`Scanned ${allFiles.length} files`);
|
|
18
|
+
await maybeReviewRepoSummary(manifest, options);
|
|
19
|
+
await writeKolmoConfig(manifest, cwd);
|
|
20
|
+
printScanSummary(manifest, options);
|
|
21
|
+
if (!options.noUpload) {
|
|
22
|
+
await uploadManifest(manifest, options, cwd);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
console.log(chalk.gray('Upload skipped (--no-upload)'));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
spinner.fail('Scan failed');
|
|
30
|
+
console.error(chalk.red(String(err)));
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async function maybeReviewRepoSummary(manifest, options) {
|
|
35
|
+
console.log('');
|
|
36
|
+
console.log(chalk.bold('Kolmo understanding:'));
|
|
37
|
+
console.log(manifest.repo_summary);
|
|
38
|
+
if (options.summary?.trim() || !shouldReviewSummary(options)) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
console.log(chalk.gray('Press Enter to accept, type a corrected summary, or type "edit" for multi-line input.'));
|
|
42
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
43
|
+
try {
|
|
44
|
+
const answer = (await rl.question(chalk.cyan('Summary override: '))).trim();
|
|
45
|
+
if (!answer)
|
|
46
|
+
return;
|
|
47
|
+
if (answer.toLowerCase() === 'edit') {
|
|
48
|
+
const multiLineSummary = await readMultiLineSummary(rl);
|
|
49
|
+
if (!multiLineSummary)
|
|
50
|
+
return;
|
|
51
|
+
manifest.repo_summary = multiLineSummary;
|
|
52
|
+
manifest.summary_source = 'user_edited';
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
manifest.repo_summary = normalizeSummary(answer);
|
|
56
|
+
manifest.summary_source = 'user_edited';
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
rl.close();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function printScanSummary(manifest, options) {
|
|
63
|
+
console.log('');
|
|
64
|
+
console.log(chalk.bold('Project:'), manifest.project_name);
|
|
65
|
+
console.log(chalk.bold('Type:'), manifest.project_type);
|
|
66
|
+
console.log(chalk.bold('Repo summary:'), manifest.repo_summary);
|
|
67
|
+
if (manifest.detailed_assets.length > 0) {
|
|
68
|
+
const assetLabels = manifest.detailed_assets.map((asset) => asset.inferred ? chalk.yellow(`${asset.symbol}*`) : chalk.green(asset.symbol));
|
|
69
|
+
console.log(chalk.bold('Assets detected:'), assetLabels.join(', '));
|
|
70
|
+
for (const asset of manifest.detailed_assets) {
|
|
71
|
+
if (asset.inferred && asset.inference_note) {
|
|
72
|
+
console.log(chalk.yellow(` * ${asset.inference_note}`));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
console.log(chalk.bold('Assets detected:'), chalk.gray('none'));
|
|
78
|
+
}
|
|
79
|
+
console.log(chalk.bold('Suggested workflows:'), chalk.yellow(`${manifest.suggested_workflows.length} available`));
|
|
80
|
+
console.log(chalk.bold('Evidence citations:'), manifest.explanations.evidence_citations.length
|
|
81
|
+
? chalk.cyan(String(manifest.explanations.evidence_citations.length))
|
|
82
|
+
: chalk.gray('none'));
|
|
83
|
+
console.log(chalk.bold('Config written:'), chalk.cyan('kolmo.config.json'));
|
|
84
|
+
if (options.verbose) {
|
|
85
|
+
console.log('');
|
|
86
|
+
console.log(chalk.bold('Python packages:'), manifest.detected_packages.python.join(', ') || 'none');
|
|
87
|
+
console.log(chalk.bold('Node packages:'), manifest.detected_packages.node.join(', ') || 'none');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function uploadManifest(manifest, options, cwd) {
|
|
91
|
+
console.log('');
|
|
92
|
+
const uploadSpinner = ora('Sending to Kolmo...').start();
|
|
93
|
+
try {
|
|
94
|
+
const result = await sendManifest(manifest, options.apiUrl, options.portfolioName);
|
|
95
|
+
uploadSpinner.succeed('Manifest sent to Kolmo');
|
|
96
|
+
const res = result;
|
|
97
|
+
if (res.session_id) {
|
|
98
|
+
console.log(chalk.bold('Session ID:'), chalk.cyan(String(res.session_id)));
|
|
99
|
+
}
|
|
100
|
+
const llm = res.llm_analysis;
|
|
101
|
+
if (llm?.strategy_type) {
|
|
102
|
+
console.log('');
|
|
103
|
+
console.log(chalk.bold('Strategy analysis:'));
|
|
104
|
+
console.log(` ${chalk.cyan('Type:')} ${chalk.bold(String(llm.strategy_type))}`);
|
|
105
|
+
if (Array.isArray(llm.instruments) && llm.instruments.length > 0) {
|
|
106
|
+
console.log(` ${chalk.cyan('Assets:')} ${llm.instruments.join(', ')}`);
|
|
107
|
+
}
|
|
108
|
+
if (llm.logic_summary) {
|
|
109
|
+
console.log(` ${chalk.cyan('Logic:')} ${chalk.gray(String(llm.logic_summary))}`);
|
|
110
|
+
}
|
|
111
|
+
if (Array.isArray(llm.risk_flags) && llm.risk_flags.length > 0) {
|
|
112
|
+
console.log(` ${chalk.cyan('Risks:')} ${chalk.yellow(llm.risk_flags.join(' · '))}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (Array.isArray(res.suggestions) && res.suggestions.length > 0) {
|
|
116
|
+
console.log(chalk.bold('Kolmo suggestions:'));
|
|
117
|
+
for (const s of res.suggestions) {
|
|
118
|
+
const title = String(s.title ?? '');
|
|
119
|
+
const desc = String(s.description ?? '');
|
|
120
|
+
const conf = s.confidence != null ? chalk.gray(` [${s.confidence}]`) : '';
|
|
121
|
+
console.log(` ${chalk.cyan('•')} ${chalk.bold(title)}${conf}`);
|
|
122
|
+
if (desc)
|
|
123
|
+
console.log(` ${chalk.gray(desc)}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
uploadSpinner.fail('Failed to send manifest to Kolmo');
|
|
129
|
+
if (options.verbose) {
|
|
130
|
+
console.error(chalk.red(String(err)));
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
console.log(chalk.gray('Run with --verbose for error details'));
|
|
134
|
+
}
|
|
135
|
+
process.exitCode = 1;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function shouldReviewSummary(options) {
|
|
139
|
+
return Boolean(!options.yes && process.stdin.isTTY && process.stdout.isTTY);
|
|
140
|
+
}
|
|
141
|
+
async function readMultiLineSummary(rl) {
|
|
142
|
+
const lines = [];
|
|
143
|
+
console.log(chalk.gray('Enter the corrected summary. Finish with a single "." on its own line.'));
|
|
144
|
+
while (true) {
|
|
145
|
+
const line = await rl.question(chalk.cyan('> '));
|
|
146
|
+
if (line.trim() === '.')
|
|
147
|
+
break;
|
|
148
|
+
lines.push(line);
|
|
149
|
+
}
|
|
150
|
+
return normalizeSummary(lines.join(' '));
|
|
151
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { readFile } from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import { sendManifest } from '../api/kolmoApiClient.js';
|
|
6
|
+
export async function runSync(options) {
|
|
7
|
+
const configPath = path.join(process.cwd(), 'kolmo.config.json');
|
|
8
|
+
const spinner = ora('Reading kolmo.config.json...').start();
|
|
9
|
+
try {
|
|
10
|
+
const content = await readFile(configPath, 'utf-8');
|
|
11
|
+
const manifest = JSON.parse(content);
|
|
12
|
+
spinner.succeed('Config loaded');
|
|
13
|
+
console.log(chalk.bold('Project:'), manifest.project_name);
|
|
14
|
+
console.log(chalk.bold('Assets:'), manifest.detected_assets.join(', ') || 'none');
|
|
15
|
+
if (manifest.repo_summary) {
|
|
16
|
+
console.log(chalk.bold('Repo summary:'), manifest.repo_summary);
|
|
17
|
+
}
|
|
18
|
+
const uploadSpinner = ora('Syncing to Kolmo...').start();
|
|
19
|
+
try {
|
|
20
|
+
const result = await sendManifest(manifest, options.apiUrl);
|
|
21
|
+
uploadSpinner.succeed('Manifest synced to Kolmo');
|
|
22
|
+
const res = result;
|
|
23
|
+
if (res.session_id) {
|
|
24
|
+
console.log(chalk.bold('Session ID:'), chalk.cyan(String(res.session_id)));
|
|
25
|
+
}
|
|
26
|
+
if (Array.isArray(res.suggestions) && res.suggestions.length > 0) {
|
|
27
|
+
console.log(chalk.bold('Kolmo suggestions:'));
|
|
28
|
+
for (const s of res.suggestions) {
|
|
29
|
+
const title = String(s.title ?? '');
|
|
30
|
+
const desc = String(s.description ?? '');
|
|
31
|
+
const conf = s.confidence != null ? chalk.gray(` [${s.confidence}]`) : '';
|
|
32
|
+
console.log(` ${chalk.cyan('•')} ${chalk.bold(title)}${conf}`);
|
|
33
|
+
if (desc)
|
|
34
|
+
console.log(` ${chalk.gray(desc)}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
uploadSpinner.fail('Failed to sync manifest');
|
|
40
|
+
console.error(chalk.red(String(err)));
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
spinner.fail('Could not read kolmo.config.json');
|
|
46
|
+
console.error(chalk.red(String(err)));
|
|
47
|
+
console.log(chalk.gray('Run "kolmo scan" first to generate a config.'));
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface KolmoConfig {
|
|
2
|
+
token?: string;
|
|
3
|
+
api_url?: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function readConfig(): KolmoConfig;
|
|
6
|
+
export declare function writeConfig(config: KolmoConfig): void;
|
|
7
|
+
export declare function getToken(): string | undefined;
|
|
8
|
+
export declare function clearToken(): void;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
const CONFIG_DIR = path.join(os.homedir(), '.kolmo');
|
|
5
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
6
|
+
export function readConfig() {
|
|
7
|
+
try {
|
|
8
|
+
const raw = fs.readFileSync(CONFIG_FILE, 'utf8');
|
|
9
|
+
return JSON.parse(raw);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return {};
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function writeConfig(config) {
|
|
16
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
17
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
18
|
+
}
|
|
19
|
+
export function getToken() {
|
|
20
|
+
return readConfig().token;
|
|
21
|
+
}
|
|
22
|
+
export function clearToken() {
|
|
23
|
+
const config = readConfig();
|
|
24
|
+
delete config.token;
|
|
25
|
+
writeConfig(config);
|
|
26
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ScoutAssetInfo } from '../types.js';
|
|
2
|
+
export declare function detectAssets(filenames: string[], columnNames: string[]): string[];
|
|
3
|
+
export declare function detectAssetDetails(input: {
|
|
4
|
+
file?: string;
|
|
5
|
+
filenames?: string[];
|
|
6
|
+
columnNames?: string[];
|
|
7
|
+
textValues?: string[];
|
|
8
|
+
}): ScoutAssetInfo[];
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { detectEnergyAssets } from '../taxonomy/energyTaxonomy.js';
|
|
2
|
+
export function detectAssets(filenames, columnNames) {
|
|
3
|
+
return detectAssetDetails({ filenames, columnNames }).map((asset) => asset.symbol);
|
|
4
|
+
}
|
|
5
|
+
export function detectAssetDetails(input) {
|
|
6
|
+
return detectEnergyAssets(input);
|
|
7
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function detectFileType(filename: string, columns: string[]): string;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
export function detectFileType(filename, columns) {
|
|
3
|
+
const ext = path.extname(filename).toLowerCase();
|
|
4
|
+
const base = path.basename(filename).toLowerCase();
|
|
5
|
+
const cols = columns.map((c) => c.toLowerCase());
|
|
6
|
+
const hasCol = (...names) => names.some((n) => cols.some((c) => c.includes(n)));
|
|
7
|
+
// Notebook
|
|
8
|
+
if (ext === '.ipynb') {
|
|
9
|
+
return 'notebook';
|
|
10
|
+
}
|
|
11
|
+
// Python files
|
|
12
|
+
if (ext === '.py') {
|
|
13
|
+
if (/risk|var|pnl/.test(base))
|
|
14
|
+
return 'risk_model';
|
|
15
|
+
if (/forecast|predict|model/.test(base))
|
|
16
|
+
return 'forecast_model';
|
|
17
|
+
}
|
|
18
|
+
// CSV / Excel data files — classify by column content
|
|
19
|
+
if (hasCol('date', 'ts', 'timestamp') && hasCol('close', 'price', 'settle')) {
|
|
20
|
+
return 'historical_prices';
|
|
21
|
+
}
|
|
22
|
+
if (hasCol('maturity', 'expiry', 'contract') && hasCol('price', 'value')) {
|
|
23
|
+
return 'forward_curve';
|
|
24
|
+
}
|
|
25
|
+
if (hasCol('position', 'volume', 'book', 'notional')) {
|
|
26
|
+
return 'positions';
|
|
27
|
+
}
|
|
28
|
+
if (hasCol('var', 'pnl', 'loss', 'returns') || /var|pnl|risk/.test(base)) {
|
|
29
|
+
return 'risk_results';
|
|
30
|
+
}
|
|
31
|
+
if (hasCol('forecast', 'prediction', 'yhat', 'model_output') || /forecast|predict/.test(base)) {
|
|
32
|
+
return 'forecast_output';
|
|
33
|
+
}
|
|
34
|
+
if (hasCol('headline', 'title', 'url', 'source') && hasCol('published', 'date')) {
|
|
35
|
+
return 'news_events';
|
|
36
|
+
}
|
|
37
|
+
return 'unknown';
|
|
38
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
export function detectProjectType(files, packages) {
|
|
3
|
+
const basenames = files.map((f) => path.basename(f).toLowerCase());
|
|
4
|
+
const exts = files.map((f) => path.extname(f).toLowerCase());
|
|
5
|
+
const hasFile = (name) => basenames.includes(name.toLowerCase());
|
|
6
|
+
const hasExt = (ext) => exts.includes(ext);
|
|
7
|
+
const hasPath = (prefix) => files.some((f) => f.replace(/\\/g, '/').startsWith(prefix));
|
|
8
|
+
// Requirements.txt + any .py → python analytics project
|
|
9
|
+
if (hasFile('requirements.txt') && hasExt('.py')) {
|
|
10
|
+
return 'python_analytics_project';
|
|
11
|
+
}
|
|
12
|
+
// Any notebook
|
|
13
|
+
if (hasExt('.ipynb')) {
|
|
14
|
+
return 'notebook_research_project';
|
|
15
|
+
}
|
|
16
|
+
// Frontend project
|
|
17
|
+
if (hasFile('package.json') &&
|
|
18
|
+
(packages.node.includes('vite') || packages.node.includes('react'))) {
|
|
19
|
+
return 'frontend_project';
|
|
20
|
+
}
|
|
21
|
+
// FastAPI backend
|
|
22
|
+
if (hasFile('main.py') && packages.python.includes('fastapi')) {
|
|
23
|
+
return 'fastapi_backend_project';
|
|
24
|
+
}
|
|
25
|
+
// Supabase project
|
|
26
|
+
if (hasPath('supabase/')) {
|
|
27
|
+
return 'supabase_project';
|
|
28
|
+
}
|
|
29
|
+
// Data folder — >50% csv/xlsx
|
|
30
|
+
const dataExts = ['.csv', '.xlsx', '.xls'];
|
|
31
|
+
const dataFileCount = exts.filter((e) => dataExts.includes(e)).length;
|
|
32
|
+
if (files.length > 0 && dataFileCount / files.length > 0.5) {
|
|
33
|
+
return 'data_folder_project';
|
|
34
|
+
}
|
|
35
|
+
return 'generic_project';
|
|
36
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { program } from 'commander';
|
|
3
|
+
import { runAuth, runLogout } from './commands/auth.js';
|
|
4
|
+
import { runExplain } from './commands/explain.js';
|
|
5
|
+
import { runScan } from './commands/scan.js';
|
|
6
|
+
import { runSync } from './commands/sync.js';
|
|
7
|
+
program
|
|
8
|
+
.name('kolmo')
|
|
9
|
+
.description('Kolmo Scout — scan and connect local energy projects')
|
|
10
|
+
.version('0.1.0');
|
|
11
|
+
program
|
|
12
|
+
.command('auth')
|
|
13
|
+
.description('Authenticate the CLI with your Kolmo account')
|
|
14
|
+
.requiredOption('--token <token>', 'Scout CLI token (starts with ksc_)')
|
|
15
|
+
.option('--api-url <url>', 'Kolmo API URL', process.env['KOLMO_API_URL'] ?? 'http://localhost:8000')
|
|
16
|
+
.action(async (opts) => {
|
|
17
|
+
await runAuth({ token: opts.token, apiUrl: opts.apiUrl });
|
|
18
|
+
});
|
|
19
|
+
program
|
|
20
|
+
.command('logout')
|
|
21
|
+
.description('Remove stored CLI token')
|
|
22
|
+
.action(async () => {
|
|
23
|
+
await runLogout();
|
|
24
|
+
});
|
|
25
|
+
program
|
|
26
|
+
.command('scan [dir]')
|
|
27
|
+
.description('Scan a directory (default: current) and send metadata to Kolmo')
|
|
28
|
+
.option('--api-url <url>', 'Kolmo API URL', process.env['KOLMO_API_URL'] ?? 'http://localhost:8000')
|
|
29
|
+
.option('--no-upload', 'Create config without uploading')
|
|
30
|
+
.option('--no-content', 'Send metadata only — do not read file contents')
|
|
31
|
+
.option('--portfolio <name>', 'Assign scan to a named portfolio')
|
|
32
|
+
.option('--summary <text>', 'Override the generated natural-language repo summary')
|
|
33
|
+
.option('-y, --yes', 'Accept generated summary without interactive review')
|
|
34
|
+
.option('--verbose', 'Verbose output')
|
|
35
|
+
.action(async (dir, opts) => {
|
|
36
|
+
await runScan({
|
|
37
|
+
apiUrl: opts.apiUrl,
|
|
38
|
+
dir,
|
|
39
|
+
noUpload: !opts.upload,
|
|
40
|
+
noContent: !opts.content,
|
|
41
|
+
portfolioName: opts.portfolio,
|
|
42
|
+
summary: opts.summary,
|
|
43
|
+
yes: opts.yes,
|
|
44
|
+
verbose: opts.verbose,
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
program
|
|
48
|
+
.command('sync')
|
|
49
|
+
.description('Re-send existing kolmo.config.json to Kolmo')
|
|
50
|
+
.option('--api-url <url>', 'Kolmo API URL', process.env['KOLMO_API_URL'] ?? 'http://localhost:8000')
|
|
51
|
+
.action(async (opts) => {
|
|
52
|
+
await runSync({ apiUrl: opts.apiUrl });
|
|
53
|
+
});
|
|
54
|
+
program
|
|
55
|
+
.command('explain [dir]')
|
|
56
|
+
.description('Explain why Kolmo classified a project the way it did')
|
|
57
|
+
.option('--json', 'Print machine-readable explanation JSON')
|
|
58
|
+
.action(async (dir, opts) => {
|
|
59
|
+
await runExplain({ dir, json: opts.json });
|
|
60
|
+
});
|
|
61
|
+
// Default to scan if no command given
|
|
62
|
+
if (process.argv.length === 2) {
|
|
63
|
+
process.argv.push('scan');
|
|
64
|
+
}
|
|
65
|
+
program.parse();
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ScoutManifest } from '../types.js';
|
|
2
|
+
export interface BuildScoutManifestOptions {
|
|
3
|
+
dir?: string;
|
|
4
|
+
summary?: string;
|
|
5
|
+
noContent?: boolean;
|
|
6
|
+
portfolioName?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface BuildScoutManifestResult {
|
|
9
|
+
cwd: string;
|
|
10
|
+
allFiles: string[];
|
|
11
|
+
hasEnv: boolean;
|
|
12
|
+
manifest: ScoutManifest;
|
|
13
|
+
}
|
|
14
|
+
export declare function buildScoutManifest(options: BuildScoutManifestOptions): Promise<BuildScoutManifestResult>;
|
|
15
|
+
export declare function normalizeSummary(summary: string): string;
|