ownkey 0.2.0 → 0.5.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 +64 -32
- package/dist/ai/factory.d.ts +23 -0
- package/dist/ai/factory.d.ts.map +1 -0
- package/dist/ai/factory.js +96 -0
- package/dist/ai/factory.js.map +1 -0
- package/dist/ai/gemini.d.ts +21 -0
- package/dist/ai/gemini.d.ts.map +1 -0
- package/dist/ai/gemini.js +87 -0
- package/dist/ai/gemini.js.map +1 -0
- package/dist/ai/provider.d.ts +39 -0
- package/dist/ai/provider.d.ts.map +1 -0
- package/dist/ai/provider.js +99 -0
- package/dist/ai/provider.js.map +1 -0
- package/dist/commands/config.js +3 -1
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/suggest.d.ts.map +1 -1
- package/dist/commands/suggest.js +102 -10
- package/dist/commands/suggest.js.map +1 -1
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +5 -2
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/keychain.d.ts.map +1 -1
- package/dist/config/keychain.js +12 -0
- package/dist/config/keychain.js.map +1 -1
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +41 -15
- package/dist/db/client.js.map +1 -1
- package/dist/types/index.d.ts +4 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/package.json +18 -2
- package/.prettierrc +0 -9
- package/src/commands/apply.ts +0 -27
- package/src/commands/config.ts +0 -154
- package/src/commands/scan.ts +0 -114
- package/src/commands/suggest.ts +0 -37
- package/src/config/defaults.ts +0 -80
- package/src/config/keychain.ts +0 -125
- package/src/config/manager.ts +0 -166
- package/src/core/scanner.ts +0 -174
- package/src/db/client.ts +0 -151
- package/src/db/operations.ts +0 -217
- package/src/db/schema.ts +0 -118
- package/src/index.ts +0 -74
- package/src/types/index.ts +0 -198
- package/src/utils/encryption.ts +0 -54
- package/src/utils/logger.ts +0 -111
- package/tsconfig.json +0 -33
- /package/{src → dist}/db/migrations/001_init.sql +0 -0
package/src/commands/config.ts
DELETED
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
import { Command } from 'commander';
|
|
2
|
-
import inquirer from 'inquirer';
|
|
3
|
-
import { configManager } from '../config/manager.js';
|
|
4
|
-
import { logger } from '../utils/logger.js';
|
|
5
|
-
import { AIProvider } from '../types/index.js';
|
|
6
|
-
import chalk from 'chalk';
|
|
7
|
-
|
|
8
|
-
export const configCommand = new Command('config')
|
|
9
|
-
.description('Configure OwnKey CLI (Supabase, AI providers)')
|
|
10
|
-
.option('--supabase-url <url>', 'Supabase project URL')
|
|
11
|
-
.option('--supabase-key <key>', 'Supabase service role key')
|
|
12
|
-
.option('--provider <provider>', 'AI provider (openai, anthropic, ollama)')
|
|
13
|
-
.option('--api-key <key>', 'API key for the provider')
|
|
14
|
-
.option('--model <model>', 'Model name to use')
|
|
15
|
-
.option('--reset', 'Reset configuration to defaults')
|
|
16
|
-
.action(async (options) => {
|
|
17
|
-
try {
|
|
18
|
-
// Handle reset
|
|
19
|
-
if (options.reset) {
|
|
20
|
-
const { confirm } = await inquirer.prompt([
|
|
21
|
-
{
|
|
22
|
-
type: 'confirm',
|
|
23
|
-
name: 'confirm',
|
|
24
|
-
message: 'Are you sure you want to reset configuration?',
|
|
25
|
-
default: false,
|
|
26
|
-
},
|
|
27
|
-
]);
|
|
28
|
-
|
|
29
|
-
if (confirm) {
|
|
30
|
-
await configManager.reset();
|
|
31
|
-
logger.success('Configuration reset successfully');
|
|
32
|
-
}
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// If flags provided, use them
|
|
37
|
-
if (options.supabaseUrl || options.provider) {
|
|
38
|
-
await handleFlagBasedConfig(options);
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Otherwise, run interactive wizard
|
|
43
|
-
await runInteractiveWizard();
|
|
44
|
-
} catch (error) {
|
|
45
|
-
logger.error('Configuration failed', error as Error);
|
|
46
|
-
process.exit(1);
|
|
47
|
-
}
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
async function handleFlagBasedConfig(options: any): Promise<void> {
|
|
51
|
-
// Configure Supabase
|
|
52
|
-
if (options.supabaseUrl && options.supabaseKey) {
|
|
53
|
-
await configManager.setSupabase(options.supabaseUrl, options.supabaseKey);
|
|
54
|
-
logger.success('Supabase configured');
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Configure AI provider
|
|
58
|
-
if (options.provider && options.apiKey) {
|
|
59
|
-
const provider = options.provider as AIProvider;
|
|
60
|
-
await configManager.setProvider(provider, options.apiKey, options.model);
|
|
61
|
-
logger.success(`${provider} provider configured`);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
async function runInteractiveWizard(): Promise<void> {
|
|
66
|
-
console.log(chalk.cyan('\n🔧 OwnKey Configuration Wizard\n'));
|
|
67
|
-
|
|
68
|
-
// Step 1: Supabase configuration
|
|
69
|
-
console.log(chalk.bold('Step 1: Supabase Database (Optional)'));
|
|
70
|
-
console.log(chalk.gray('Supabase stores your analysis history and suggestions.'));
|
|
71
|
-
console.log(chalk.gray('You can skip this and run in local-only mode.\n'));
|
|
72
|
-
|
|
73
|
-
const { configureSupabase } = await inquirer.prompt([
|
|
74
|
-
{
|
|
75
|
-
type: 'confirm',
|
|
76
|
-
name: 'configureSupabase',
|
|
77
|
-
message: 'Configure Supabase database?',
|
|
78
|
-
default: true,
|
|
79
|
-
},
|
|
80
|
-
]);
|
|
81
|
-
|
|
82
|
-
if (configureSupabase) {
|
|
83
|
-
const supabaseAnswers = await inquirer.prompt([
|
|
84
|
-
{
|
|
85
|
-
type: 'input',
|
|
86
|
-
name: 'url',
|
|
87
|
-
message: 'Supabase URL:',
|
|
88
|
-
validate: (input) => {
|
|
89
|
-
if (!input) return 'URL is required';
|
|
90
|
-
if (!input.startsWith('postgresql://')) {
|
|
91
|
-
return 'URL should start with postgresql://';
|
|
92
|
-
}
|
|
93
|
-
return true;
|
|
94
|
-
},
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
type: 'password',
|
|
98
|
-
name: 'key',
|
|
99
|
-
message: 'Supabase Service Role Key:',
|
|
100
|
-
validate: (input) => (input ? true : 'Service role key is required'),
|
|
101
|
-
},
|
|
102
|
-
]);
|
|
103
|
-
|
|
104
|
-
await configManager.setSupabase(supabaseAnswers.url, supabaseAnswers.key);
|
|
105
|
-
logger.success('Supabase configured');
|
|
106
|
-
} else {
|
|
107
|
-
logger.info('Skipping Supabase. You can configure it later.');
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Step 2: AI Provider configuration
|
|
111
|
-
console.log(chalk.bold('\nStep 2: AI Provider'));
|
|
112
|
-
console.log(chalk.gray('Choose your AI provider for code analysis.\n'));
|
|
113
|
-
|
|
114
|
-
const { provider } = await inquirer.prompt([
|
|
115
|
-
{
|
|
116
|
-
type: 'list',
|
|
117
|
-
name: 'provider',
|
|
118
|
-
message: 'Choose AI provider:',
|
|
119
|
-
choices: [
|
|
120
|
-
{ name: 'OpenAI (GPT-4)', value: 'openai' },
|
|
121
|
-
{ name: 'Anthropic (Claude)', value: 'anthropic' },
|
|
122
|
-
{ name: 'Ollama (Local)', value: 'ollama' },
|
|
123
|
-
],
|
|
124
|
-
},
|
|
125
|
-
]);
|
|
126
|
-
|
|
127
|
-
// Get API key (not needed for Ollama)
|
|
128
|
-
if (provider !== 'ollama') {
|
|
129
|
-
const { apiKey } = await inquirer.prompt([
|
|
130
|
-
{
|
|
131
|
-
type: 'password',
|
|
132
|
-
name: 'apiKey',
|
|
133
|
-
message: `${provider === 'openai' ? 'OpenAI' : 'Anthropic'} API Key:`,
|
|
134
|
-
validate: (input) => (input ? true : 'API key is required'),
|
|
135
|
-
},
|
|
136
|
-
]);
|
|
137
|
-
|
|
138
|
-
await configManager.setProvider(provider, apiKey);
|
|
139
|
-
} else {
|
|
140
|
-
// For Ollama, just set it as active provider
|
|
141
|
-
const config = await configManager.load();
|
|
142
|
-
config.activeProvider = 'ollama';
|
|
143
|
-
await configManager.save(config);
|
|
144
|
-
logger.success('Ollama configured (using local endpoint)');
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Step 3: Test connection (optional)
|
|
148
|
-
console.log(chalk.bold('\n✓ Configuration Complete!\n'));
|
|
149
|
-
console.log(chalk.green('You can now use OwnKey to analyze your codebase.\n'));
|
|
150
|
-
console.log(chalk.cyan('Next steps:'));
|
|
151
|
-
console.log(chalk.gray(' 1. ownkey scan <path> - Scan your project'));
|
|
152
|
-
console.log(chalk.gray(' 2. ownkey suggest <path> - Get AI suggestions'));
|
|
153
|
-
console.log(chalk.gray(' 3. ownkey apply <id> - Apply a suggestion\n'));
|
|
154
|
-
}
|
package/src/commands/scan.ts
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import { Command } from 'commander';
|
|
2
|
-
import { resolve } from 'path';
|
|
3
|
-
import { existsSync } from 'fs';
|
|
4
|
-
import { logger } from '../utils/logger.js';
|
|
5
|
-
import { FileScanner } from '../core/scanner.js';
|
|
6
|
-
import { dbOps } from '../db/operations.js';
|
|
7
|
-
import { dbClient } from '../db/client.js';
|
|
8
|
-
import chalk from 'chalk';
|
|
9
|
-
|
|
10
|
-
export const scanCommand = new Command('scan')
|
|
11
|
-
.description('Scan a project directory')
|
|
12
|
-
.argument('[path]', 'Path to scan', '.')
|
|
13
|
-
.option('--max-file-size <size>', 'Maximum file size (e.g., 500kb, 1mb)', '500kb')
|
|
14
|
-
.option('--ignore <patterns...>', 'Additional ignore patterns')
|
|
15
|
-
.action(async (path: string, options) => {
|
|
16
|
-
try {
|
|
17
|
-
const targetPath = resolve(path);
|
|
18
|
-
|
|
19
|
-
// Validate path
|
|
20
|
-
if (!existsSync(targetPath)) {
|
|
21
|
-
logger.error(`Path does not exist: ${targetPath}`);
|
|
22
|
-
process.exit(1);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
logger.info(`Scanning: ${chalk.cyan(targetPath)}`);
|
|
26
|
-
|
|
27
|
-
// Parse max file size
|
|
28
|
-
const maxFileSize = parseFileSize(options.maxFileSize);
|
|
29
|
-
|
|
30
|
-
// Create scanner
|
|
31
|
-
const scanner = new FileScanner(targetPath, maxFileSize);
|
|
32
|
-
|
|
33
|
-
// Scan
|
|
34
|
-
const startTime = Date.now();
|
|
35
|
-
const result = await scanner.scan();
|
|
36
|
-
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
|
|
37
|
-
|
|
38
|
-
// Display results
|
|
39
|
-
console.log(chalk.bold('\n📊 Scan Results:\n'));
|
|
40
|
-
console.log(` Files found: ${chalk.green(result.files.length)}`);
|
|
41
|
-
console.log(` Total size: ${formatBytes(result.totalSize)}`);
|
|
42
|
-
console.log(` Skipped files: ${result.skippedFiles.length}`);
|
|
43
|
-
console.log(` Duration: ${duration}s\n`);
|
|
44
|
-
|
|
45
|
-
if (result.warnings.length > 0) {
|
|
46
|
-
console.log(chalk.yellow(`⚠️ ${result.warnings.length} warnings:`));
|
|
47
|
-
result.warnings.slice(0, 5).forEach((w) => console.log(chalk.gray(` - ${w}`)));
|
|
48
|
-
if (result.warnings.length > 5) {
|
|
49
|
-
console.log(chalk.gray(` ... and ${result.warnings.length - 5} more`));
|
|
50
|
-
}
|
|
51
|
-
console.log();
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Store in database if connected
|
|
55
|
-
if (dbClient.isConnected()) {
|
|
56
|
-
const projectName = targetPath.split(/[/\\]/).pop() || 'unknown';
|
|
57
|
-
let project = await dbOps.getProjectByPath(targetPath);
|
|
58
|
-
|
|
59
|
-
if (!project) {
|
|
60
|
-
project = await dbOps.createProject(projectName, targetPath);
|
|
61
|
-
logger.verbose('Created project record in database');
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (project) {
|
|
65
|
-
const run = await dbOps.createRun(project.id, `scan ${path}`);
|
|
66
|
-
if (run) {
|
|
67
|
-
await dbOps.updateRunStatus(
|
|
68
|
-
run.id,
|
|
69
|
-
'completed',
|
|
70
|
-
`Scanned ${result.files.length} files`
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
logger.success('Scan complete');
|
|
77
|
-
|
|
78
|
-
// Show next steps
|
|
79
|
-
console.log(chalk.cyan('Next step:'));
|
|
80
|
-
console.log(chalk.gray(` ownkey suggest ${path}\n`));
|
|
81
|
-
} catch (error) {
|
|
82
|
-
logger.error('Scan failed', error as Error);
|
|
83
|
-
process.exit(1);
|
|
84
|
-
}
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
function parseFileSize(sizeStr: string): number {
|
|
88
|
-
const match = sizeStr.match(/^(\d+(?:\.\d+)?)(kb|mb|gb)?$/i);
|
|
89
|
-
if (!match) {
|
|
90
|
-
throw new Error(`Invalid file size format: ${sizeStr}`);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const value = parseFloat(match[1]);
|
|
94
|
-
const unit = (match[2] || 'b').toLowerCase();
|
|
95
|
-
|
|
96
|
-
switch (unit) {
|
|
97
|
-
case 'b':
|
|
98
|
-
return value;
|
|
99
|
-
case 'kb':
|
|
100
|
-
return value * 1024;
|
|
101
|
-
case 'mb':
|
|
102
|
-
return value * 1024 * 1024;
|
|
103
|
-
case 'gb':
|
|
104
|
-
return value * 1024 * 1024 * 1024;
|
|
105
|
-
default:
|
|
106
|
-
return value;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function formatBytes(bytes: number): string {
|
|
111
|
-
if (bytes < 1024) return `${bytes} B`;
|
|
112
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
113
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
114
|
-
}
|
package/src/commands/suggest.ts
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { Command } from 'commander';
|
|
2
|
-
import { resolve } from 'path';
|
|
3
|
-
import { existsSync } from 'fs';
|
|
4
|
-
import { logger } from '../utils/logger.js';
|
|
5
|
-
import chalk from 'chalk';
|
|
6
|
-
|
|
7
|
-
export const suggestCommand = new Command('suggest')
|
|
8
|
-
.description('Generate AI-powered suggestions for your code')
|
|
9
|
-
.argument('[path]', 'Path to analyze', '.')
|
|
10
|
-
.option('--type <type>', 'Filter by suggestion type (bug, security, performance, etc.)')
|
|
11
|
-
.option('--severity <severity>', 'Minimum severity (critical, high, medium, low, info)')
|
|
12
|
-
.action(async (path: string) => {
|
|
13
|
-
try {
|
|
14
|
-
const targetPath = resolve(path);
|
|
15
|
-
|
|
16
|
-
if (!existsSync(targetPath)) {
|
|
17
|
-
logger.error(`Path does not exist: ${targetPath}`);
|
|
18
|
-
process.exit(1);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
logger.info(`Analyzing: ${chalk.cyan(targetPath)}`);
|
|
22
|
-
|
|
23
|
-
// TODO: Implement AI analysis
|
|
24
|
-
// For now, show a placeholder message
|
|
25
|
-
console.log(chalk.yellow('\n⚠️ AI provider integration coming in v0.5.0\n'));
|
|
26
|
-
console.log('This command will:');
|
|
27
|
-
console.log(chalk.gray(' 1. Load project context'));
|
|
28
|
-
console.log(chalk.gray(' 2. Send to AI provider for analysis'));
|
|
29
|
-
console.log(chalk.gray(' 3. Display suggestions'));
|
|
30
|
-
console.log(chalk.gray(' 4. Store in database\n'));
|
|
31
|
-
|
|
32
|
-
logger.info('Suggest command placeholder - full implementation coming soon');
|
|
33
|
-
} catch (error) {
|
|
34
|
-
logger.error('Suggest failed', error as Error);
|
|
35
|
-
process.exit(1);
|
|
36
|
-
}
|
|
37
|
-
});
|
package/src/config/defaults.ts
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { Config, LogLevel } from '../types/index.js';
|
|
2
|
-
|
|
3
|
-
export const DEFAULT_CONFIG: Config = {
|
|
4
|
-
providers: {
|
|
5
|
-
openai: {
|
|
6
|
-
model: 'gpt-4-turbo',
|
|
7
|
-
},
|
|
8
|
-
anthropic: {
|
|
9
|
-
model: 'claude-3-sonnet-20240229',
|
|
10
|
-
},
|
|
11
|
-
ollama: {
|
|
12
|
-
endpoint: 'http://localhost:11434',
|
|
13
|
-
model: 'codellama',
|
|
14
|
-
},
|
|
15
|
-
},
|
|
16
|
-
defaults: {
|
|
17
|
-
maxFileSize: '500kb',
|
|
18
|
-
logLevel: 'normal' as LogLevel,
|
|
19
|
-
},
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export const CONFIG_DIR = '.ownkey';
|
|
23
|
-
export const CONFIG_FILE = 'config.json';
|
|
24
|
-
export const KEYS_FILE = 'keys.enc';
|
|
25
|
-
export const LOGS_DIR = 'logs';
|
|
26
|
-
|
|
27
|
-
export const DEFAULT_EXCLUDES = [
|
|
28
|
-
'.git',
|
|
29
|
-
'node_modules',
|
|
30
|
-
'dist',
|
|
31
|
-
'build',
|
|
32
|
-
'target',
|
|
33
|
-
'out',
|
|
34
|
-
'vendor',
|
|
35
|
-
'.cache',
|
|
36
|
-
'.next',
|
|
37
|
-
'.nuxt',
|
|
38
|
-
'coverage',
|
|
39
|
-
'.env',
|
|
40
|
-
'*.lock',
|
|
41
|
-
'*.min.js',
|
|
42
|
-
'*.min.css',
|
|
43
|
-
];
|
|
44
|
-
|
|
45
|
-
export const BINARY_EXTENSIONS = [
|
|
46
|
-
'.jpg',
|
|
47
|
-
'.jpeg',
|
|
48
|
-
'.png',
|
|
49
|
-
'.gif',
|
|
50
|
-
'.bmp',
|
|
51
|
-
'.ico',
|
|
52
|
-
'.svg',
|
|
53
|
-
'.webp',
|
|
54
|
-
'.mp4',
|
|
55
|
-
'.avi',
|
|
56
|
-
'.mov',
|
|
57
|
-
'.mp3',
|
|
58
|
-
'.wav',
|
|
59
|
-
'.zip',
|
|
60
|
-
'.tar',
|
|
61
|
-
'.gz',
|
|
62
|
-
'.rar',
|
|
63
|
-
'.7z',
|
|
64
|
-
'.exe',
|
|
65
|
-
'.dll',
|
|
66
|
-
'.so',
|
|
67
|
-
'.dylib',
|
|
68
|
-
'.pdf',
|
|
69
|
-
'.doc',
|
|
70
|
-
'.docx',
|
|
71
|
-
'.xls',
|
|
72
|
-
'.xlsx',
|
|
73
|
-
'.ppt',
|
|
74
|
-
'.pptx',
|
|
75
|
-
];
|
|
76
|
-
|
|
77
|
-
export const MAX_FILE_SIZE_BYTES = 500 * 1024; // 500KB
|
|
78
|
-
|
|
79
|
-
export const BACKUP_EXTENSION = '.ownkey.bak';
|
|
80
|
-
export const MAX_BACKUPS_PER_FILE = 3;
|
package/src/config/keychain.ts
DELETED
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import { AIProvider } from '../types/index.js';
|
|
2
|
-
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
3
|
-
import { existsSync } from 'fs';
|
|
4
|
-
import { join } from 'path';
|
|
5
|
-
import { homedir } from 'os';
|
|
6
|
-
import { logger } from '../utils/logger.js';
|
|
7
|
-
import { Encryption } from '../utils/encryption.js';
|
|
8
|
-
import { KEYS_FILE, CONFIG_DIR } from './defaults.js';
|
|
9
|
-
|
|
10
|
-
// Try to use keytar, but fallback gracefully if not available
|
|
11
|
-
let keytar: any = null;
|
|
12
|
-
try {
|
|
13
|
-
keytar = await import('keytar');
|
|
14
|
-
} catch (error) {
|
|
15
|
-
logger.verbose('Keytar not available, using encrypted file storage for API keys');
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const SERVICE_NAME = 'ownkey-cli';
|
|
19
|
-
const ENCRYPTION_PASSWORD = 'ownkey-local-encryption-key'; // In production, derive from machine ID
|
|
20
|
-
|
|
21
|
-
export class KeychainManager {
|
|
22
|
-
private keysFilePath: string;
|
|
23
|
-
private useKeychain: boolean;
|
|
24
|
-
|
|
25
|
-
constructor() {
|
|
26
|
-
this.keysFilePath = join(homedir(), CONFIG_DIR, KEYS_FILE);
|
|
27
|
-
this.useKeychain = keytar !== null;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
private async ensureKeysDir(): Promise<void> {
|
|
31
|
-
const dir = join(homedir(), CONFIG_DIR);
|
|
32
|
-
if (!existsSync(dir)) {
|
|
33
|
-
await mkdir(dir, { recursive: true });
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async setApiKey(provider: AIProvider, apiKey: string): Promise<void> {
|
|
38
|
-
const accountName = `${provider}-api-key`;
|
|
39
|
-
|
|
40
|
-
if (this.useKeychain) {
|
|
41
|
-
try {
|
|
42
|
-
await keytar.setPassword(SERVICE_NAME, accountName, apiKey);
|
|
43
|
-
logger.verbose(`Stored ${provider} API key in system keychain`);
|
|
44
|
-
return;
|
|
45
|
-
} catch (error) {
|
|
46
|
-
logger.warn(`Failed to store in keychain, falling back to encrypted file`);
|
|
47
|
-
this.useKeychain = false;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Fallback to encrypted file
|
|
52
|
-
await this.ensureKeysDir();
|
|
53
|
-
const keys = await this.loadKeysFromFile();
|
|
54
|
-
keys[provider] = apiKey;
|
|
55
|
-
await this.saveKeysToFile(keys);
|
|
56
|
-
logger.verbose(`Stored ${provider} API key in encrypted file`);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async getApiKey(provider: AIProvider): Promise<string | null> {
|
|
60
|
-
const accountName = `${provider}-api-key`;
|
|
61
|
-
|
|
62
|
-
if (this.useKeychain) {
|
|
63
|
-
try {
|
|
64
|
-
const key = await keytar.getPassword(SERVICE_NAME, accountName);
|
|
65
|
-
if (key) {
|
|
66
|
-
logger.verbose(`Retrieved ${provider} API key from system keychain`);
|
|
67
|
-
return key;
|
|
68
|
-
}
|
|
69
|
-
} catch (error) {
|
|
70
|
-
logger.warn(`Failed to retrieve from keychain, trying encrypted file`);
|
|
71
|
-
this.useKeychain = false;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Fallback to encrypted file
|
|
76
|
-
const keys = await this.loadKeysFromFile();
|
|
77
|
-
const key = keys[provider] || null;
|
|
78
|
-
if (key) {
|
|
79
|
-
logger.verbose(`Retrieved ${provider} API key from encrypted file`);
|
|
80
|
-
}
|
|
81
|
-
return key;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
async deleteApiKey(provider: AIProvider): Promise<void> {
|
|
85
|
-
const accountName = `${provider}-api-key`;
|
|
86
|
-
|
|
87
|
-
if (this.useKeychain) {
|
|
88
|
-
try {
|
|
89
|
-
await keytar.deletePassword(SERVICE_NAME, accountName);
|
|
90
|
-
logger.verbose(`Deleted ${provider} API key from system keychain`);
|
|
91
|
-
} catch (error) {
|
|
92
|
-
// Ignore errors
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Also delete from file
|
|
97
|
-
const keys = await this.loadKeysFromFile();
|
|
98
|
-
delete keys[provider];
|
|
99
|
-
await this.saveKeysToFile(keys);
|
|
100
|
-
logger.verbose(`Deleted ${provider} API key from encrypted file`);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
private async loadKeysFromFile(): Promise<Record<string, string>> {
|
|
104
|
-
if (!existsSync(this.keysFilePath)) {
|
|
105
|
-
return {};
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
try {
|
|
109
|
-
const encryptedData = await readFile(this.keysFilePath, 'utf-8');
|
|
110
|
-
const decrypted = await Encryption.decrypt(encryptedData, ENCRYPTION_PASSWORD);
|
|
111
|
-
return JSON.parse(decrypted);
|
|
112
|
-
} catch (error) {
|
|
113
|
-
logger.warn('Failed to decrypt keys file, starting fresh');
|
|
114
|
-
return {};
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
private async saveKeysToFile(keys: Record<string, string>): Promise<void> {
|
|
119
|
-
const json = JSON.stringify(keys);
|
|
120
|
-
const encrypted = await Encryption.encrypt(json, ENCRYPTION_PASSWORD);
|
|
121
|
-
await writeFile(this.keysFilePath, encrypted, 'utf-8');
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export const keychainManager = new KeychainManager();
|
package/src/config/manager.ts
DELETED
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
import { Config, ConfigError, AIProvider } from '../types/index.js';
|
|
2
|
-
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
3
|
-
import { existsSync } from 'fs';
|
|
4
|
-
import { join } from 'path';
|
|
5
|
-
import { homedir } from 'os';
|
|
6
|
-
import { logger } from '../utils/logger.js';
|
|
7
|
-
import { keychainManager } from './keychain.js';
|
|
8
|
-
import { DEFAULT_CONFIG, CONFIG_DIR, CONFIG_FILE } from './defaults.js';
|
|
9
|
-
|
|
10
|
-
export class ConfigManager {
|
|
11
|
-
private configPath: string;
|
|
12
|
-
private config: Config | null = null;
|
|
13
|
-
|
|
14
|
-
constructor() {
|
|
15
|
-
this.configPath = join(homedir(), CONFIG_DIR, CONFIG_FILE);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
async ensureConfigDir(): Promise<void> {
|
|
19
|
-
const dir = join(homedir(), CONFIG_DIR);
|
|
20
|
-
if (!existsSync(dir)) {
|
|
21
|
-
await mkdir(dir, { recursive: true });
|
|
22
|
-
logger.verbose(`Created config directory: ${dir}`);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
async exists(): Promise<boolean> {
|
|
27
|
-
return existsSync(this.configPath);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
async load(): Promise<Config> {
|
|
31
|
-
if (this.config) {
|
|
32
|
-
return this.config;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
if (!existsSync(this.configPath)) {
|
|
36
|
-
logger.verbose('No config file found, using defaults');
|
|
37
|
-
this.config = { ...DEFAULT_CONFIG };
|
|
38
|
-
return this.config;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
try {
|
|
42
|
-
const content = await readFile(this.configPath, 'utf-8');
|
|
43
|
-
const loaded = JSON.parse(content);
|
|
44
|
-
|
|
45
|
-
// Merge with defaults to ensure all fields exist
|
|
46
|
-
this.config = {
|
|
47
|
-
...DEFAULT_CONFIG,
|
|
48
|
-
...loaded,
|
|
49
|
-
providers: {
|
|
50
|
-
...DEFAULT_CONFIG.providers,
|
|
51
|
-
...loaded.providers,
|
|
52
|
-
},
|
|
53
|
-
defaults: {
|
|
54
|
-
...DEFAULT_CONFIG.defaults,
|
|
55
|
-
...loaded.defaults,
|
|
56
|
-
},
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
logger.verbose('Loaded configuration from file');
|
|
60
|
-
return this.config!;
|
|
61
|
-
} catch (error) {
|
|
62
|
-
throw new ConfigError('Failed to load configuration file', error);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
async save(config: Config): Promise<void> {
|
|
67
|
-
await this.ensureConfigDir();
|
|
68
|
-
|
|
69
|
-
try {
|
|
70
|
-
const content = JSON.stringify(config, null, 2);
|
|
71
|
-
await writeFile(this.configPath, content, 'utf-8');
|
|
72
|
-
this.config = config;
|
|
73
|
-
logger.verbose('Saved configuration to file');
|
|
74
|
-
} catch (error) {
|
|
75
|
-
throw new ConfigError('Failed to save configuration file', error);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
async update(updates: Partial<Config>): Promise<void> {
|
|
80
|
-
const current = await this.load();
|
|
81
|
-
const updated = {
|
|
82
|
-
...current,
|
|
83
|
-
...updates,
|
|
84
|
-
providers: {
|
|
85
|
-
...current.providers,
|
|
86
|
-
...updates.providers,
|
|
87
|
-
},
|
|
88
|
-
defaults: {
|
|
89
|
-
...current.defaults,
|
|
90
|
-
...updates.defaults,
|
|
91
|
-
},
|
|
92
|
-
};
|
|
93
|
-
await this.save(updated);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
async setSupabase(url: string, serviceRoleKey: string): Promise<void> {
|
|
97
|
-
const config = await this.load();
|
|
98
|
-
config.supabaseUrl = url;
|
|
99
|
-
config.supabaseServiceRoleKey = serviceRoleKey;
|
|
100
|
-
await this.save(config);
|
|
101
|
-
logger.success('Supabase configuration saved');
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
async setProvider(provider: AIProvider, apiKey: string, model?: string): Promise<void> {
|
|
105
|
-
const config = await this.load();
|
|
106
|
-
config.activeProvider = provider;
|
|
107
|
-
|
|
108
|
-
if (model) {
|
|
109
|
-
if (!config.providers[provider]) {
|
|
110
|
-
config.providers[provider] = {};
|
|
111
|
-
}
|
|
112
|
-
config.providers[provider]!.model = model;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
await this.save(config);
|
|
116
|
-
await keychainManager.setApiKey(provider, apiKey);
|
|
117
|
-
logger.success(`${provider} provider configured`);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
async getApiKey(provider?: AIProvider): Promise<string | null> {
|
|
121
|
-
const config = await this.load();
|
|
122
|
-
const targetProvider = provider || config.activeProvider;
|
|
123
|
-
|
|
124
|
-
if (!targetProvider) {
|
|
125
|
-
return null;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return await keychainManager.getApiKey(targetProvider);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
async validate(): Promise<{ valid: boolean; errors: string[] }> {
|
|
132
|
-
const errors: string[] = [];
|
|
133
|
-
|
|
134
|
-
try {
|
|
135
|
-
const config = await this.load();
|
|
136
|
-
|
|
137
|
-
// Check if at least one provider is configured
|
|
138
|
-
if (!config.activeProvider) {
|
|
139
|
-
errors.push('No active AI provider configured. Run: ownkey config');
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Check if API key exists for active provider
|
|
143
|
-
if (config.activeProvider) {
|
|
144
|
-
const apiKey = await this.getApiKey(config.activeProvider);
|
|
145
|
-
if (!apiKey) {
|
|
146
|
-
errors.push(`No API key found for ${config.activeProvider}`);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return { valid: errors.length === 0, errors };
|
|
151
|
-
} catch (error) {
|
|
152
|
-
errors.push('Failed to load configuration');
|
|
153
|
-
return { valid: false, errors };
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
async reset(): Promise<void> {
|
|
158
|
-
this.config = null;
|
|
159
|
-
if (existsSync(this.configPath)) {
|
|
160
|
-
await writeFile(this.configPath, JSON.stringify(DEFAULT_CONFIG, null, 2), 'utf-8');
|
|
161
|
-
logger.success('Configuration reset to defaults');
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
export const configManager = new ConfigManager();
|