kernelbot 1.0.6 → 1.0.9
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/package.json +1 -1
- package/src/agent.js +185 -79
- package/src/bot.js +12 -1
- package/src/tools/git.js +34 -2
- package/src/utils/config.js +56 -0
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -2,6 +2,7 @@ import Anthropic from '@anthropic-ai/sdk';
|
|
|
2
2
|
import { toolDefinitions, executeTool, checkConfirmation } from './tools/index.js';
|
|
3
3
|
import { getSystemPrompt } from './prompts/system.js';
|
|
4
4
|
import { getLogger } from './utils/logger.js';
|
|
5
|
+
import { getMissingCredential, saveCredential } from './utils/config.js';
|
|
5
6
|
|
|
6
7
|
export class Agent {
|
|
7
8
|
constructor({ config, conversationManager }) {
|
|
@@ -9,69 +10,25 @@ export class Agent {
|
|
|
9
10
|
this.conversationManager = conversationManager;
|
|
10
11
|
this.client = new Anthropic({ apiKey: config.anthropic.api_key });
|
|
11
12
|
this.systemPrompt = getSystemPrompt(config);
|
|
12
|
-
this.
|
|
13
|
+
this._pending = new Map(); // chatId -> pending state
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
async processMessage(chatId, userMessage, user) {
|
|
16
|
+
async processMessage(chatId, userMessage, user, onUpdate) {
|
|
16
17
|
const logger = getLogger();
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
this._onUpdate = onUpdate || null;
|
|
20
|
+
|
|
21
|
+
// Handle pending responses (confirmation or credential)
|
|
22
|
+
const pending = this._pending.get(chatId);
|
|
20
23
|
if (pending) {
|
|
21
|
-
this.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
logger.info(`User confirmed dangerous tool: ${pending.block.name}`);
|
|
27
|
-
const result = await executeTool(pending.block.name, pending.block.input, pending.context);
|
|
28
|
-
|
|
29
|
-
// Resume the agent loop with the tool result
|
|
30
|
-
pending.toolResults.push({
|
|
31
|
-
type: 'tool_result',
|
|
32
|
-
tool_use_id: pending.block.id,
|
|
33
|
-
content: JSON.stringify(result),
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
// Process remaining blocks if any
|
|
37
|
-
for (const block of pending.remainingBlocks) {
|
|
38
|
-
if (block.type !== 'tool_use') continue;
|
|
39
|
-
|
|
40
|
-
const dangerLabel = checkConfirmation(block.name, block.input, this.config);
|
|
41
|
-
if (dangerLabel) {
|
|
42
|
-
// Another dangerous tool — ask again
|
|
43
|
-
this._pendingConfirmation.set(chatId, {
|
|
44
|
-
block,
|
|
45
|
-
context: pending.context,
|
|
46
|
-
toolResults: pending.toolResults,
|
|
47
|
-
remainingBlocks: pending.remainingBlocks.filter((b) => b !== block),
|
|
48
|
-
messages: pending.messages,
|
|
49
|
-
});
|
|
50
|
-
return `⚠️ Next action will **${dangerLabel}**.\n\n\`${block.name}\`: \`${JSON.stringify(block.input)}\`\n\nConfirm? (yes/no)`;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
logger.info(`Tool call: ${block.name}`);
|
|
54
|
-
const r = await executeTool(block.name, block.input, pending.context);
|
|
55
|
-
pending.toolResults.push({
|
|
56
|
-
type: 'tool_result',
|
|
57
|
-
tool_use_id: block.id,
|
|
58
|
-
content: JSON.stringify(r),
|
|
59
|
-
});
|
|
60
|
-
}
|
|
24
|
+
this._pending.delete(chatId);
|
|
25
|
+
|
|
26
|
+
if (pending.type === 'credential') {
|
|
27
|
+
return await this._handleCredentialResponse(chatId, userMessage, user, pending);
|
|
28
|
+
}
|
|
61
29
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return await this._continueLoop(chatId, pending.messages, user);
|
|
65
|
-
} else {
|
|
66
|
-
// User denied
|
|
67
|
-
logger.info(`User denied dangerous tool: ${pending.block.name}`);
|
|
68
|
-
pending.toolResults.push({
|
|
69
|
-
type: 'tool_result',
|
|
70
|
-
tool_use_id: pending.block.id,
|
|
71
|
-
content: JSON.stringify({ error: 'User denied this operation.' }),
|
|
72
|
-
});
|
|
73
|
-
pending.messages.push({ role: 'user', content: pending.toolResults });
|
|
74
|
-
return await this._continueLoop(chatId, pending.messages, user);
|
|
30
|
+
if (pending.type === 'confirmation') {
|
|
31
|
+
return await this._handleConfirmationResponse(chatId, userMessage, user, pending);
|
|
75
32
|
}
|
|
76
33
|
}
|
|
77
34
|
|
|
@@ -86,6 +43,156 @@ export class Agent {
|
|
|
86
43
|
return await this._runLoop(chatId, messages, user, 0, max_tool_depth);
|
|
87
44
|
}
|
|
88
45
|
|
|
46
|
+
_formatToolSummary(name, input) {
|
|
47
|
+
const key = {
|
|
48
|
+
execute_command: 'command',
|
|
49
|
+
read_file: 'path',
|
|
50
|
+
write_file: 'path',
|
|
51
|
+
list_directory: 'path',
|
|
52
|
+
git_clone: 'repo',
|
|
53
|
+
git_checkout: 'branch',
|
|
54
|
+
git_commit: 'message',
|
|
55
|
+
git_push: 'dir',
|
|
56
|
+
git_diff: 'dir',
|
|
57
|
+
github_create_pr: 'title',
|
|
58
|
+
github_create_repo: 'name',
|
|
59
|
+
github_list_prs: 'repo',
|
|
60
|
+
github_get_pr_diff: 'repo',
|
|
61
|
+
github_post_review: 'repo',
|
|
62
|
+
spawn_claude_code: 'prompt',
|
|
63
|
+
kill_process: 'pid',
|
|
64
|
+
docker_exec: 'container',
|
|
65
|
+
docker_logs: 'container',
|
|
66
|
+
docker_compose: 'action',
|
|
67
|
+
curl_url: 'url',
|
|
68
|
+
check_port: 'port',
|
|
69
|
+
}[name];
|
|
70
|
+
const val = key && input[key] ? String(input[key]).slice(0, 120) : JSON.stringify(input).slice(0, 120);
|
|
71
|
+
return `${name}: ${val}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async _sendUpdate(text) {
|
|
75
|
+
if (this._onUpdate) {
|
|
76
|
+
try { await this._onUpdate(text); } catch {}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async _handleCredentialResponse(chatId, userMessage, user, pending) {
|
|
81
|
+
const logger = getLogger();
|
|
82
|
+
const value = userMessage.trim();
|
|
83
|
+
|
|
84
|
+
if (value.toLowerCase() === 'skip' || value.toLowerCase() === 'cancel') {
|
|
85
|
+
logger.info(`User skipped credential: ${pending.credential.envKey}`);
|
|
86
|
+
pending.toolResults.push({
|
|
87
|
+
type: 'tool_result',
|
|
88
|
+
tool_use_id: pending.block.id,
|
|
89
|
+
content: JSON.stringify({ error: `${pending.credential.label} not provided. Operation skipped.` }),
|
|
90
|
+
});
|
|
91
|
+
return await this._resumeAfterPause(chatId, user, pending);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Save the credential
|
|
95
|
+
saveCredential(this.config, pending.credential.envKey, value);
|
|
96
|
+
logger.info(`Saved credential: ${pending.credential.envKey}`);
|
|
97
|
+
|
|
98
|
+
// Now execute the original tool
|
|
99
|
+
const result = await executeTool(pending.block.name, pending.block.input, {
|
|
100
|
+
config: this.config,
|
|
101
|
+
user,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
pending.toolResults.push({
|
|
105
|
+
type: 'tool_result',
|
|
106
|
+
tool_use_id: pending.block.id,
|
|
107
|
+
content: JSON.stringify(result),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
return await this._resumeAfterPause(chatId, user, pending);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async _handleConfirmationResponse(chatId, userMessage, user, pending) {
|
|
114
|
+
const logger = getLogger();
|
|
115
|
+
const lower = userMessage.toLowerCase().trim();
|
|
116
|
+
|
|
117
|
+
if (lower === 'yes' || lower === 'y' || lower === 'confirm') {
|
|
118
|
+
logger.info(`User confirmed dangerous tool: ${pending.block.name}`);
|
|
119
|
+
const result = await executeTool(pending.block.name, pending.block.input, pending.context);
|
|
120
|
+
|
|
121
|
+
pending.toolResults.push({
|
|
122
|
+
type: 'tool_result',
|
|
123
|
+
tool_use_id: pending.block.id,
|
|
124
|
+
content: JSON.stringify(result),
|
|
125
|
+
});
|
|
126
|
+
} else {
|
|
127
|
+
logger.info(`User denied dangerous tool: ${pending.block.name}`);
|
|
128
|
+
pending.toolResults.push({
|
|
129
|
+
type: 'tool_result',
|
|
130
|
+
tool_use_id: pending.block.id,
|
|
131
|
+
content: JSON.stringify({ error: 'User denied this operation.' }),
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return await this._resumeAfterPause(chatId, user, pending);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async _resumeAfterPause(chatId, user, pending) {
|
|
139
|
+
// Process remaining blocks
|
|
140
|
+
for (const block of pending.remainingBlocks) {
|
|
141
|
+
if (block.type !== 'tool_use') continue;
|
|
142
|
+
|
|
143
|
+
const pauseMsg = await this._checkPause(chatId, block, user, pending.toolResults, pending.remainingBlocks.filter((b) => b !== block), pending.messages);
|
|
144
|
+
if (pauseMsg) return pauseMsg;
|
|
145
|
+
|
|
146
|
+
const r = await executeTool(block.name, block.input, { config: this.config, user });
|
|
147
|
+
pending.toolResults.push({
|
|
148
|
+
type: 'tool_result',
|
|
149
|
+
tool_use_id: block.id,
|
|
150
|
+
content: JSON.stringify(r),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
pending.messages.push({ role: 'user', content: pending.toolResults });
|
|
155
|
+
const { max_tool_depth } = this.config.anthropic;
|
|
156
|
+
return await this._runLoop(chatId, pending.messages, user, 0, max_tool_depth);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
_checkPause(chatId, block, user, toolResults, remainingBlocks, messages) {
|
|
160
|
+
const logger = getLogger();
|
|
161
|
+
|
|
162
|
+
// Check missing credentials first
|
|
163
|
+
const missing = getMissingCredential(block.name, this.config);
|
|
164
|
+
if (missing) {
|
|
165
|
+
logger.warn(`Missing credential for ${block.name}: ${missing.envKey}`);
|
|
166
|
+
this._pending.set(chatId, {
|
|
167
|
+
type: 'credential',
|
|
168
|
+
block,
|
|
169
|
+
credential: missing,
|
|
170
|
+
context: { config: this.config, user },
|
|
171
|
+
toolResults,
|
|
172
|
+
remainingBlocks,
|
|
173
|
+
messages,
|
|
174
|
+
});
|
|
175
|
+
return `🔑 **${missing.label}** is required for this action.\n\nPlease send your token now (it will be saved to \`~/.kernelbot/.env\`).\n\nOr reply **skip** to cancel.`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Check dangerous operation confirmation
|
|
179
|
+
const dangerLabel = checkConfirmation(block.name, block.input, this.config);
|
|
180
|
+
if (dangerLabel) {
|
|
181
|
+
logger.warn(`Dangerous tool detected: ${block.name} — ${dangerLabel}`);
|
|
182
|
+
this._pending.set(chatId, {
|
|
183
|
+
type: 'confirmation',
|
|
184
|
+
block,
|
|
185
|
+
context: { config: this.config, user },
|
|
186
|
+
toolResults,
|
|
187
|
+
remainingBlocks,
|
|
188
|
+
messages,
|
|
189
|
+
});
|
|
190
|
+
return `⚠️ This action will **${dangerLabel}**.\n\n\`${block.name}\`: \`${JSON.stringify(block.input)}\`\n\nConfirm? (yes/no)`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
|
|
89
196
|
async _runLoop(chatId, messages, user, startDepth, maxDepth) {
|
|
90
197
|
const logger = getLogger();
|
|
91
198
|
const { model, max_tokens, temperature } = this.config.anthropic;
|
|
@@ -115,30 +222,34 @@ export class Agent {
|
|
|
115
222
|
if (response.stop_reason === 'tool_use') {
|
|
116
223
|
messages.push({ role: 'assistant', content: response.content });
|
|
117
224
|
|
|
225
|
+
// Send Claude's thinking text to the user
|
|
226
|
+
const thinkingBlocks = response.content.filter((b) => b.type === 'text' && b.text.trim());
|
|
227
|
+
if (thinkingBlocks.length > 0) {
|
|
228
|
+
const thinking = thinkingBlocks.map((b) => b.text).join('\n');
|
|
229
|
+
logger.info(`Agent thinking: ${thinking.slice(0, 200)}`);
|
|
230
|
+
await this._sendUpdate(`💭 ${thinking}`);
|
|
231
|
+
}
|
|
232
|
+
|
|
118
233
|
const toolUseBlocks = response.content.filter((b) => b.type === 'tool_use');
|
|
119
234
|
const toolResults = [];
|
|
120
235
|
|
|
121
236
|
for (let i = 0; i < toolUseBlocks.length; i++) {
|
|
122
237
|
const block = toolUseBlocks[i];
|
|
123
238
|
|
|
124
|
-
// Check if
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
remainingBlocks: toolUseBlocks.slice(i + 1),
|
|
135
|
-
messages,
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
return `⚠️ This action will **${dangerLabel}**.\n\n\`${block.name}\`: \`${JSON.stringify(block.input)}\`\n\nConfirm? (yes/no)`;
|
|
139
|
-
}
|
|
239
|
+
// Check if we need to pause (missing cred or dangerous action)
|
|
240
|
+
const pauseMsg = this._checkPause(
|
|
241
|
+
chatId,
|
|
242
|
+
block,
|
|
243
|
+
user,
|
|
244
|
+
toolResults,
|
|
245
|
+
toolUseBlocks.slice(i + 1),
|
|
246
|
+
messages,
|
|
247
|
+
);
|
|
248
|
+
if (pauseMsg) return pauseMsg;
|
|
140
249
|
|
|
141
|
-
|
|
250
|
+
const summary = this._formatToolSummary(block.name, block.input);
|
|
251
|
+
logger.info(`Tool call: ${summary}`);
|
|
252
|
+
await this._sendUpdate(`🔧 \`${summary}\``);
|
|
142
253
|
|
|
143
254
|
const result = await executeTool(block.name, block.input, {
|
|
144
255
|
config: this.config,
|
|
@@ -175,9 +286,4 @@ export class Agent {
|
|
|
175
286
|
this.conversationManager.addMessage(chatId, 'assistant', depthWarning);
|
|
176
287
|
return depthWarning;
|
|
177
288
|
}
|
|
178
|
-
|
|
179
|
-
async _continueLoop(chatId, messages, user) {
|
|
180
|
-
const { max_tool_depth } = this.config.anthropic;
|
|
181
|
-
return await this._runLoop(chatId, messages, user, 0, max_tool_depth);
|
|
182
|
-
}
|
|
183
289
|
}
|
package/src/bot.js
CHANGED
|
@@ -50,10 +50,21 @@ export function startBot(config, agent) {
|
|
|
50
50
|
bot.sendChatAction(chatId, 'typing').catch(() => {});
|
|
51
51
|
|
|
52
52
|
try {
|
|
53
|
+
const onUpdate = async (text) => {
|
|
54
|
+
const parts = splitMessage(text);
|
|
55
|
+
for (const part of parts) {
|
|
56
|
+
try {
|
|
57
|
+
await bot.sendMessage(chatId, part, { parse_mode: 'Markdown' });
|
|
58
|
+
} catch {
|
|
59
|
+
await bot.sendMessage(chatId, part);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
53
64
|
const reply = await agent.processMessage(chatId, msg.text, {
|
|
54
65
|
id: userId,
|
|
55
66
|
username,
|
|
56
|
-
});
|
|
67
|
+
}, onUpdate);
|
|
57
68
|
|
|
58
69
|
clearInterval(typingInterval);
|
|
59
70
|
|
package/src/tools/git.js
CHANGED
|
@@ -9,6 +9,24 @@ function getWorkspaceDir(config) {
|
|
|
9
9
|
return dir;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
function injectToken(url, config) {
|
|
13
|
+
const token = config.github?.token || process.env.GITHUB_TOKEN;
|
|
14
|
+
if (!token) return url;
|
|
15
|
+
|
|
16
|
+
// Inject token into HTTPS GitHub URLs for auth-free push/pull
|
|
17
|
+
try {
|
|
18
|
+
const parsed = new URL(url);
|
|
19
|
+
if (parsed.hostname === 'github.com' && parsed.protocol === 'https:') {
|
|
20
|
+
parsed.username = token;
|
|
21
|
+
parsed.password = 'x-oauth-basic';
|
|
22
|
+
return parsed.toString();
|
|
23
|
+
}
|
|
24
|
+
} catch {
|
|
25
|
+
// Not a parseable URL (e.g. org/repo shorthand before expansion)
|
|
26
|
+
}
|
|
27
|
+
return url;
|
|
28
|
+
}
|
|
29
|
+
|
|
12
30
|
export const definitions = [
|
|
13
31
|
{
|
|
14
32
|
name: 'git_clone',
|
|
@@ -88,12 +106,15 @@ export const handlers = {
|
|
|
88
106
|
url = `https://github.com/${repo}.git`;
|
|
89
107
|
}
|
|
90
108
|
|
|
109
|
+
// Inject GitHub token for authenticated clone (enables push later)
|
|
110
|
+
const authUrl = injectToken(url, context.config);
|
|
111
|
+
|
|
91
112
|
const repoName = dest || repo.split('/').pop().replace('.git', '');
|
|
92
113
|
const targetDir = join(workspaceDir, repoName);
|
|
93
114
|
|
|
94
115
|
try {
|
|
95
116
|
const git = simpleGit();
|
|
96
|
-
await git.clone(
|
|
117
|
+
await git.clone(authUrl, targetDir);
|
|
97
118
|
return { success: true, path: targetDir };
|
|
98
119
|
} catch (err) {
|
|
99
120
|
return { error: err.message };
|
|
@@ -127,10 +148,21 @@ export const handlers = {
|
|
|
127
148
|
}
|
|
128
149
|
},
|
|
129
150
|
|
|
130
|
-
git_push: async (params) => {
|
|
151
|
+
git_push: async (params, context) => {
|
|
131
152
|
const { dir, force = false } = params;
|
|
132
153
|
try {
|
|
133
154
|
const git = simpleGit(dir);
|
|
155
|
+
|
|
156
|
+
// Ensure remote URL has auth token for push
|
|
157
|
+
const remotes = await git.getRemotes(true);
|
|
158
|
+
const origin = remotes.find((r) => r.name === 'origin');
|
|
159
|
+
if (origin) {
|
|
160
|
+
const authUrl = injectToken(origin.refs.push || origin.refs.fetch, context.config);
|
|
161
|
+
if (authUrl !== (origin.refs.push || origin.refs.fetch)) {
|
|
162
|
+
await git.remote(['set-url', 'origin', authUrl]);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
134
166
|
const branch = (await git.branchLocal()).current;
|
|
135
167
|
const options = force ? ['--force'] : [];
|
|
136
168
|
await git.push('origin', branch, options);
|
package/src/utils/config.js
CHANGED
|
@@ -184,3 +184,59 @@ export async function loadConfigInteractive() {
|
|
|
184
184
|
const config = loadConfig();
|
|
185
185
|
return await promptForMissing(config);
|
|
186
186
|
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Save a credential to ~/.kernelbot/.env and update the live config object.
|
|
190
|
+
* Called at runtime when a user provides a missing token via Telegram.
|
|
191
|
+
*/
|
|
192
|
+
export function saveCredential(config, envKey, value) {
|
|
193
|
+
const configDir = getConfigDir();
|
|
194
|
+
mkdirSync(configDir, { recursive: true });
|
|
195
|
+
const envPath = join(configDir, '.env');
|
|
196
|
+
|
|
197
|
+
let content = '';
|
|
198
|
+
if (existsSync(envPath)) {
|
|
199
|
+
content = readFileSync(envPath, 'utf-8').trimEnd() + '\n';
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const regex = new RegExp(`^${envKey}=.*$`, 'm');
|
|
203
|
+
if (regex.test(content)) {
|
|
204
|
+
content = content.replace(regex, `${envKey}=${value}`);
|
|
205
|
+
} else {
|
|
206
|
+
content += `${envKey}=${value}\n`;
|
|
207
|
+
}
|
|
208
|
+
writeFileSync(envPath, content);
|
|
209
|
+
|
|
210
|
+
// Update live config
|
|
211
|
+
switch (envKey) {
|
|
212
|
+
case 'GITHUB_TOKEN':
|
|
213
|
+
if (!config.github) config.github = {};
|
|
214
|
+
config.github.token = value;
|
|
215
|
+
break;
|
|
216
|
+
case 'ANTHROPIC_API_KEY':
|
|
217
|
+
config.anthropic.api_key = value;
|
|
218
|
+
break;
|
|
219
|
+
case 'TELEGRAM_BOT_TOKEN':
|
|
220
|
+
config.telegram.bot_token = value;
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Also set in process.env so tools pick it up
|
|
225
|
+
process.env[envKey] = value;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Check which credentials a tool needs and return the missing one, if any.
|
|
230
|
+
*/
|
|
231
|
+
export function getMissingCredential(toolName, config) {
|
|
232
|
+
const githubTools = ['github_create_pr', 'github_get_pr_diff', 'github_post_review', 'github_create_repo', 'github_list_prs', 'git_clone', 'git_push'];
|
|
233
|
+
|
|
234
|
+
if (githubTools.includes(toolName)) {
|
|
235
|
+
const token = config.github?.token || process.env.GITHUB_TOKEN;
|
|
236
|
+
if (!token) {
|
|
237
|
+
return { envKey: 'GITHUB_TOKEN', label: 'GitHub Personal Access Token' };
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return null;
|
|
242
|
+
}
|