kernelbot 1.0.23 → 1.0.25

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.
@@ -0,0 +1,232 @@
1
+ import axios from 'axios';
2
+
3
+ /**
4
+ * Create an axios instance configured for the JIRA REST API.
5
+ * Supports both Atlassian Cloud (*.atlassian.net) and JIRA Server instances.
6
+ *
7
+ * Authentication:
8
+ * - Cloud: email + API token (Basic auth)
9
+ * - Server: username + password/token (Basic auth)
10
+ *
11
+ * Config precedence: config.jira.* → JIRA_* env vars
12
+ */
13
+ function getJiraClient(config) {
14
+ const baseUrl = config.jira?.base_url || process.env.JIRA_BASE_URL;
15
+ const email = config.jira?.email || process.env.JIRA_EMAIL;
16
+ const token = config.jira?.api_token || process.env.JIRA_API_TOKEN;
17
+
18
+ if (!baseUrl) throw new Error('JIRA base URL not configured. Set JIRA_BASE_URL or jira.base_url in config.');
19
+ if (!email) throw new Error('JIRA email/username not configured. Set JIRA_EMAIL or jira.email in config.');
20
+ if (!token) throw new Error('JIRA API token not configured. Set JIRA_API_TOKEN or jira.api_token in config.');
21
+
22
+ const cleanBase = baseUrl.replace(/\/+$/, '');
23
+
24
+ return axios.create({
25
+ baseURL: `${cleanBase}/rest/api/2`,
26
+ auth: { username: email, password: token },
27
+ headers: {
28
+ 'Accept': 'application/json',
29
+ 'Content-Type': 'application/json',
30
+ },
31
+ timeout: 30000,
32
+ });
33
+ }
34
+
35
+ /**
36
+ * Extract structured ticket data from a JIRA issue response.
37
+ */
38
+ function formatIssue(issue) {
39
+ const fields = issue.fields || {};
40
+ return {
41
+ key: issue.key,
42
+ summary: fields.summary || '',
43
+ description: fields.description || '',
44
+ status: fields.status?.name || '',
45
+ assignee: fields.assignee?.displayName || 'Unassigned',
46
+ reporter: fields.reporter?.displayName || '',
47
+ priority: fields.priority?.name || '',
48
+ type: fields.issuetype?.name || '',
49
+ labels: fields.labels || [],
50
+ created: fields.created || '',
51
+ updated: fields.updated || '',
52
+ project: fields.project?.key || '',
53
+ };
54
+ }
55
+
56
+ export const definitions = [
57
+ {
58
+ name: 'jira_get_ticket',
59
+ description: 'Get details of a specific JIRA ticket by its key (e.g. PROJ-123).',
60
+ input_schema: {
61
+ type: 'object',
62
+ properties: {
63
+ ticket_key: {
64
+ type: 'string',
65
+ description: 'The JIRA ticket key (e.g. PROJ-123)',
66
+ },
67
+ },
68
+ required: ['ticket_key'],
69
+ },
70
+ },
71
+ {
72
+ name: 'jira_search_tickets',
73
+ description: 'Search for JIRA tickets using JQL (JIRA Query Language). Example: "project = PROJ AND status = Open".',
74
+ input_schema: {
75
+ type: 'object',
76
+ properties: {
77
+ jql_query: {
78
+ type: 'string',
79
+ description: 'JQL query string',
80
+ },
81
+ max_results: {
82
+ type: 'number',
83
+ description: 'Maximum number of results to return (default 20)',
84
+ default: 20,
85
+ },
86
+ },
87
+ required: ['jql_query'],
88
+ },
89
+ },
90
+ {
91
+ name: 'jira_list_my_tickets',
92
+ description: 'List JIRA tickets assigned to a user. Defaults to the authenticated user.',
93
+ input_schema: {
94
+ type: 'object',
95
+ properties: {
96
+ assignee: {
97
+ type: 'string',
98
+ description: 'Assignee username or "currentUser()" (default)',
99
+ default: 'currentUser()',
100
+ },
101
+ max_results: {
102
+ type: 'number',
103
+ description: 'Maximum number of results to return (default 20)',
104
+ default: 20,
105
+ },
106
+ },
107
+ },
108
+ },
109
+ {
110
+ name: 'jira_get_project_tickets',
111
+ description: 'Get tickets from a specific JIRA project.',
112
+ input_schema: {
113
+ type: 'object',
114
+ properties: {
115
+ project_key: {
116
+ type: 'string',
117
+ description: 'The JIRA project key (e.g. PROJ)',
118
+ },
119
+ max_results: {
120
+ type: 'number',
121
+ description: 'Maximum number of results to return (default 20)',
122
+ default: 20,
123
+ },
124
+ },
125
+ required: ['project_key'],
126
+ },
127
+ },
128
+ ];
129
+
130
+ export const handlers = {
131
+ /**
132
+ * Get details of a specific JIRA ticket.
133
+ * @param {{ ticket_key: string }} params
134
+ * @param {{ config: object }} context
135
+ */
136
+ jira_get_ticket: async (params, context) => {
137
+ try {
138
+ const client = getJiraClient(context.config);
139
+ const { data } = await client.get(`/issue/${params.ticket_key}`);
140
+ return { ticket: formatIssue(data) };
141
+ } catch (err) {
142
+ if (err.response?.status === 404) {
143
+ return { error: `Ticket ${params.ticket_key} not found` };
144
+ }
145
+ return { error: err.response?.data?.errorMessages?.join('; ') || err.message };
146
+ }
147
+ },
148
+
149
+ /**
150
+ * Search for JIRA tickets using JQL.
151
+ * @param {{ jql_query: string, max_results?: number }} params
152
+ * @param {{ config: object }} context
153
+ */
154
+ jira_search_tickets: async (params, context) => {
155
+ try {
156
+ const client = getJiraClient(context.config);
157
+ const maxResults = params.max_results || 20;
158
+
159
+ const { data } = await client.get('/search', {
160
+ params: {
161
+ jql: params.jql_query,
162
+ maxResults,
163
+ fields: 'summary,description,status,assignee,reporter,priority,issuetype,labels,created,updated,project',
164
+ },
165
+ });
166
+
167
+ return {
168
+ total: data.total,
169
+ tickets: (data.issues || []).map(formatIssue),
170
+ };
171
+ } catch (err) {
172
+ return { error: err.response?.data?.errorMessages?.join('; ') || err.message };
173
+ }
174
+ },
175
+
176
+ /**
177
+ * List tickets assigned to a user.
178
+ * @param {{ assignee?: string, max_results?: number }} params
179
+ * @param {{ config: object }} context
180
+ */
181
+ jira_list_my_tickets: async (params, context) => {
182
+ try {
183
+ const client = getJiraClient(context.config);
184
+ const assignee = params.assignee || 'currentUser()';
185
+ const maxResults = params.max_results || 20;
186
+ const jql = `assignee = ${assignee} ORDER BY updated DESC`;
187
+
188
+ const { data } = await client.get('/search', {
189
+ params: {
190
+ jql,
191
+ maxResults,
192
+ fields: 'summary,description,status,assignee,reporter,priority,issuetype,labels,created,updated,project',
193
+ },
194
+ });
195
+
196
+ return {
197
+ total: data.total,
198
+ tickets: (data.issues || []).map(formatIssue),
199
+ };
200
+ } catch (err) {
201
+ return { error: err.response?.data?.errorMessages?.join('; ') || err.message };
202
+ }
203
+ },
204
+
205
+ /**
206
+ * Get tickets from a specific JIRA project.
207
+ * @param {{ project_key: string, max_results?: number }} params
208
+ * @param {{ config: object }} context
209
+ */
210
+ jira_get_project_tickets: async (params, context) => {
211
+ try {
212
+ const client = getJiraClient(context.config);
213
+ const maxResults = params.max_results || 20;
214
+ const jql = `project = ${params.project_key} ORDER BY updated DESC`;
215
+
216
+ const { data } = await client.get('/search', {
217
+ params: {
218
+ jql,
219
+ maxResults,
220
+ fields: 'summary,description,status,assignee,reporter,priority,issuetype,labels,created,updated,project',
221
+ },
222
+ });
223
+
224
+ return {
225
+ total: data.total,
226
+ tickets: (data.issues || []).map(formatIssue),
227
+ };
228
+ } catch (err) {
229
+ return { error: err.response?.data?.errorMessages?.join('; ') || err.message };
230
+ }
231
+ },
232
+ };
@@ -5,13 +5,15 @@ import { createInterface } from 'readline';
5
5
  import yaml from 'js-yaml';
6
6
  import dotenv from 'dotenv';
7
7
  import chalk from 'chalk';
8
+ import { PROVIDERS } from '../providers/models.js';
8
9
 
9
10
  const DEFAULTS = {
10
11
  bot: {
11
12
  name: 'KernelBot',
12
13
  description: 'AI engineering agent with full OS control',
13
14
  },
14
- anthropic: {
15
+ brain: {
16
+ provider: 'anthropic',
15
17
  model: 'claude-sonnet-4-20250514',
16
18
  max_tokens: 8192,
17
19
  temperature: 0.3,
@@ -90,9 +92,126 @@ function ask(rl, question) {
90
92
  return new Promise((res) => rl.question(question, res));
91
93
  }
92
94
 
95
+ /**
96
+ * Migrate legacy `anthropic` config section → `brain` section.
97
+ */
98
+ function migrateAnthropicConfig(config) {
99
+ if (config.anthropic && !config.brain) {
100
+ config.brain = {
101
+ provider: 'anthropic',
102
+ model: config.anthropic.model || DEFAULTS.brain.model,
103
+ max_tokens: config.anthropic.max_tokens || DEFAULTS.brain.max_tokens,
104
+ temperature: config.anthropic.temperature ?? DEFAULTS.brain.temperature,
105
+ max_tool_depth: config.anthropic.max_tool_depth || DEFAULTS.brain.max_tool_depth,
106
+ };
107
+ if (config.anthropic.api_key) {
108
+ config.brain.api_key = config.anthropic.api_key;
109
+ }
110
+ }
111
+ return config;
112
+ }
113
+
114
+ /**
115
+ * Interactive provider → model picker.
116
+ */
117
+ export async function promptProviderSelection(rl) {
118
+ const providerKeys = Object.keys(PROVIDERS);
119
+
120
+ console.log(chalk.bold('\n Select AI provider:\n'));
121
+ providerKeys.forEach((key, i) => {
122
+ console.log(` ${chalk.cyan(`${i + 1}.`)} ${PROVIDERS[key].name}`);
123
+ });
124
+ console.log('');
125
+
126
+ let providerIdx;
127
+ while (true) {
128
+ const input = await ask(rl, chalk.cyan(' Provider (number): '));
129
+ providerIdx = parseInt(input.trim(), 10) - 1;
130
+ if (providerIdx >= 0 && providerIdx < providerKeys.length) break;
131
+ console.log(chalk.dim(' Invalid choice, try again.'));
132
+ }
133
+
134
+ const providerKey = providerKeys[providerIdx];
135
+ const provider = PROVIDERS[providerKey];
136
+
137
+ console.log(chalk.bold(`\n Select model for ${provider.name}:\n`));
138
+ provider.models.forEach((m, i) => {
139
+ console.log(` ${chalk.cyan(`${i + 1}.`)} ${m.label} (${m.id})`);
140
+ });
141
+ console.log('');
142
+
143
+ let modelIdx;
144
+ while (true) {
145
+ const input = await ask(rl, chalk.cyan(' Model (number): '));
146
+ modelIdx = parseInt(input.trim(), 10) - 1;
147
+ if (modelIdx >= 0 && modelIdx < provider.models.length) break;
148
+ console.log(chalk.dim(' Invalid choice, try again.'));
149
+ }
150
+
151
+ const model = provider.models[modelIdx];
152
+ return { providerKey, modelId: model.id };
153
+ }
154
+
155
+ /**
156
+ * Save provider and model to config.yaml.
157
+ */
158
+ export function saveProviderToYaml(providerKey, modelId) {
159
+ const configDir = getConfigDir();
160
+ mkdirSync(configDir, { recursive: true });
161
+ const configPath = join(configDir, 'config.yaml');
162
+
163
+ let existing = {};
164
+ if (existsSync(configPath)) {
165
+ existing = yaml.load(readFileSync(configPath, 'utf-8')) || {};
166
+ }
167
+
168
+ existing.brain = {
169
+ ...(existing.brain || {}),
170
+ provider: providerKey,
171
+ model: modelId,
172
+ };
173
+
174
+ // Remove legacy anthropic section if migrating
175
+ delete existing.anthropic;
176
+
177
+ writeFileSync(configPath, yaml.dump(existing, { lineWidth: -1 }));
178
+ return configPath;
179
+ }
180
+
181
+ /**
182
+ * Full interactive flow: change brain model + optionally enter API key.
183
+ */
184
+ export async function changeBrainModel(config, rl) {
185
+ const { providerKey, modelId } = await promptProviderSelection(rl);
186
+
187
+ const providerDef = PROVIDERS[providerKey];
188
+ const savedPath = saveProviderToYaml(providerKey, modelId);
189
+ console.log(chalk.dim(`\n Saved to ${savedPath}`));
190
+
191
+ // Update live config
192
+ config.brain.provider = providerKey;
193
+ config.brain.model = modelId;
194
+
195
+ // Check if we have the API key for this provider
196
+ const envKey = providerDef.envKey;
197
+ const currentKey = process.env[envKey];
198
+ if (!currentKey) {
199
+ const key = await ask(rl, chalk.cyan(`\n ${providerDef.name} API key (${envKey}): `));
200
+ if (key.trim()) {
201
+ saveCredential(config, envKey, key.trim());
202
+ config.brain.api_key = key.trim();
203
+ console.log(chalk.dim(' Saved.\n'));
204
+ }
205
+ } else {
206
+ config.brain.api_key = currentKey;
207
+ }
208
+
209
+ return config;
210
+ }
211
+
93
212
  async function promptForMissing(config) {
94
213
  const missing = [];
95
- if (!config.anthropic.api_key) missing.push('ANTHROPIC_API_KEY');
214
+ if (!config.brain.api_key) missing.push('brain_api_key');
96
215
  if (!config.telegram.bot_token) missing.push('TELEGRAM_BOT_TOKEN');
97
216
 
98
217
  if (missing.length === 0) return config;
@@ -110,10 +229,19 @@ async function promptForMissing(config) {
110
229
  existingEnv = readFileSync(envPath, 'utf-8');
111
230
  }
112
231
 
113
- if (!mutableConfig.anthropic.api_key) {
114
- const key = await ask(rl, chalk.cyan(' Anthropic API key: '));
115
- mutableConfig.anthropic.api_key = key.trim();
116
- envLines.push(`ANTHROPIC_API_KEY=${key.trim()}`);
232
+ if (!mutableConfig.brain.api_key) {
233
+ // Run provider selection flow
234
+ const { providerKey, modelId } = await promptProviderSelection(rl);
235
+ mutableConfig.brain.provider = providerKey;
236
+ mutableConfig.brain.model = modelId;
237
+ saveProviderToYaml(providerKey, modelId);
238
+
239
+ const providerDef = PROVIDERS[providerKey];
240
+ const envKey = providerDef.envKey;
241
+
242
+ const key = await ask(rl, chalk.cyan(`\n ${providerDef.name} API key: `));
243
+ mutableConfig.brain.api_key = key.trim();
244
+ envLines.push(`${envKey}=${key.trim()}`);
117
245
  }
118
246
 
119
247
  if (!mutableConfig.telegram.bot_token) {
@@ -164,12 +292,21 @@ export function loadConfig() {
164
292
  fileConfig = yaml.load(raw) || {};
165
293
  }
166
294
 
295
+ // Backward compat: migrate anthropic → brain
296
+ migrateAnthropicConfig(fileConfig);
297
+
167
298
  const config = deepMerge(DEFAULTS, fileConfig);
168
299
 
169
- // Overlay env vars for secrets
170
- if (process.env.ANTHROPIC_API_KEY) {
171
- config.anthropic.api_key = process.env.ANTHROPIC_API_KEY;
300
+ // Overlay env vars for brain API key based on provider
301
+ const providerDef = PROVIDERS[config.brain.provider];
302
+ if (providerDef && process.env[providerDef.envKey]) {
303
+ config.brain.api_key = process.env[providerDef.envKey];
304
+ }
305
+ // Legacy fallback: ANTHROPIC_API_KEY for anthropic provider
306
+ if (config.brain.provider === 'anthropic' && !config.brain.api_key && process.env.ANTHROPIC_API_KEY) {
307
+ config.brain.api_key = process.env.ANTHROPIC_API_KEY;
172
308
  }
309
+
173
310
  if (process.env.TELEGRAM_BOT_TOKEN) {
174
311
  config.telegram.bot_token = process.env.TELEGRAM_BOT_TOKEN;
175
312
  }
@@ -177,6 +314,12 @@ export function loadConfig() {
177
314
  if (!config.github) config.github = {};
178
315
  config.github.token = process.env.GITHUB_TOKEN;
179
316
  }
317
+ if (process.env.JIRA_BASE_URL || process.env.JIRA_EMAIL || process.env.JIRA_API_TOKEN) {
318
+ if (!config.jira) config.jira = {};
319
+ if (process.env.JIRA_BASE_URL) config.jira.base_url = process.env.JIRA_BASE_URL;
320
+ if (process.env.JIRA_EMAIL) config.jira.email = process.env.JIRA_EMAIL;
321
+ if (process.env.JIRA_API_TOKEN) config.jira.api_token = process.env.JIRA_API_TOKEN;
322
+ }
180
323
 
181
324
  return config;
182
325
  }
@@ -215,11 +358,32 @@ export function saveCredential(config, envKey, value) {
215
358
  config.github.token = value;
216
359
  break;
217
360
  case 'ANTHROPIC_API_KEY':
218
- config.anthropic.api_key = value;
361
+ if (config.brain.provider === 'anthropic') config.brain.api_key = value;
362
+ break;
363
+ case 'OPENAI_API_KEY':
364
+ if (config.brain.provider === 'openai') config.brain.api_key = value;
365
+ break;
366
+ case 'GOOGLE_API_KEY':
367
+ if (config.brain.provider === 'google') config.brain.api_key = value;
368
+ break;
369
+ case 'GROQ_API_KEY':
370
+ if (config.brain.provider === 'groq') config.brain.api_key = value;
219
371
  break;
220
372
  case 'TELEGRAM_BOT_TOKEN':
221
373
  config.telegram.bot_token = value;
222
374
  break;
375
+ case 'JIRA_BASE_URL':
376
+ if (!config.jira) config.jira = {};
377
+ config.jira.base_url = value;
378
+ break;
379
+ case 'JIRA_EMAIL':
380
+ if (!config.jira) config.jira = {};
381
+ config.jira.email = value;
382
+ break;
383
+ case 'JIRA_API_TOKEN':
384
+ if (!config.jira) config.jira = {};
385
+ config.jira.api_token = value;
386
+ break;
223
387
  }
224
388
 
225
389
  // Also set in process.env so tools pick it up
@@ -239,5 +403,19 @@ export function getMissingCredential(toolName, config) {
239
403
  }
240
404
  }
241
405
 
406
+ const jiraTools = ['jira_get_ticket', 'jira_search_tickets', 'jira_list_my_tickets', 'jira_get_project_tickets'];
407
+
408
+ if (jiraTools.includes(toolName)) {
409
+ if (!config.jira?.base_url && !process.env.JIRA_BASE_URL) {
410
+ return { envKey: 'JIRA_BASE_URL', label: 'JIRA Base URL (e.g. https://yourcompany.atlassian.net)' };
411
+ }
412
+ if (!config.jira?.email && !process.env.JIRA_EMAIL) {
413
+ return { envKey: 'JIRA_EMAIL', label: 'JIRA Email / Username' };
414
+ }
415
+ if (!config.jira?.api_token && !process.env.JIRA_API_TOKEN) {
416
+ return { envKey: 'JIRA_API_TOKEN', label: 'JIRA API Token' };
417
+ }
418
+ }
419
+
242
420
  return null;
243
421
  }
@@ -1,21 +0,0 @@
1
- # Hello World! 🌍
2
-
3
- This is a **test file** created to verify that everything is working properly.
4
-
5
- ## About This File
6
-
7
- This file demonstrates:
8
- - *Basic markdown formatting*
9
- - **Bold text**
10
- - Simple lists
11
-
12
- ## Features Tested
13
-
14
- - ✅ File creation
15
- - ✅ Markdown formatting
16
- - ✅ Emoji support 🚀
17
- - ✅ Basic structure
18
-
19
- ---
20
-
21
- *Created as a test for the KernelBot project!* 🤖
@@ -1,11 +0,0 @@
1
- # Hello World 👋
2
-
3
- Welcome to **newnew-1**! This is a simple hello world file.
4
-
5
- ## Quick Example
6
-
7
- ```python
8
- print("Hello, World!")
9
- ```
10
-
11
- > Keep it simple. Keep it fun.