thepopebot 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +127 -0
- package/api/index.js +357 -0
- package/bin/cli.js +278 -0
- package/config/index.js +29 -0
- package/config/instrumentation.js +29 -0
- package/docker/Dockerfile +51 -0
- package/docker/entrypoint.sh +100 -0
- package/lib/actions.js +40 -0
- package/lib/claude/conversation.js +76 -0
- package/lib/claude/index.js +142 -0
- package/lib/claude/tools.js +54 -0
- package/lib/cron.js +60 -0
- package/lib/paths.js +30 -0
- package/lib/tools/create-job.js +40 -0
- package/lib/tools/github.js +122 -0
- package/lib/tools/openai.js +35 -0
- package/lib/tools/telegram.js +222 -0
- package/lib/triggers.js +105 -0
- package/lib/utils/render-md.js +39 -0
- package/package.json +57 -0
- package/pi/extensions/env-sanitizer/index.ts +48 -0
- package/pi/extensions/env-sanitizer/package.json +5 -0
- package/pi/skills/llm-secrets/SKILL.md +34 -0
- package/pi/skills/llm-secrets/llm-secrets.js +34 -0
- package/setup/lib/auth.mjs +160 -0
- package/setup/lib/github.mjs +148 -0
- package/setup/lib/prerequisites.mjs +135 -0
- package/setup/lib/prompts.mjs +268 -0
- package/setup/lib/telegram-verify.mjs +66 -0
- package/setup/lib/telegram.mjs +76 -0
- package/setup/package.json +6 -0
- package/setup/setup-telegram.mjs +236 -0
- package/setup/setup.mjs +540 -0
- package/templates/.env.example +38 -0
- package/templates/.github/workflows/auto-merge.yml +117 -0
- package/templates/.github/workflows/docker-build.yml +34 -0
- package/templates/.github/workflows/run-job.yml +40 -0
- package/templates/.github/workflows/update-event-handler.yml +126 -0
- package/templates/.pi/skills/modify-self/SKILL.md +12 -0
- package/templates/CLAUDE.md +52 -0
- package/templates/app/api/[...thepopebot]/route.js +1 -0
- package/templates/app/layout.js +12 -0
- package/templates/app/page.js +8 -0
- package/templates/instrumentation.js +1 -0
- package/templates/next.config.mjs +3 -0
- package/templates/operating_system/AGENT.md +32 -0
- package/templates/operating_system/CHATBOT.md +74 -0
- package/templates/operating_system/CRONS.json +16 -0
- package/templates/operating_system/HEARTBEAT.md +3 -0
- package/templates/operating_system/JOB_SUMMARY.md +36 -0
- package/templates/operating_system/SOUL.md +17 -0
- package/templates/operating_system/TELEGRAM.md +21 -0
- package/templates/operating_system/TRIGGERS.json +18 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { writeFileSync, readFileSync, existsSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
|
|
4
|
+
const ROOT_DIR = process.cwd();
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Validate Anthropic API key by making a minimal test call
|
|
8
|
+
*/
|
|
9
|
+
export async function validateAnthropicKey(key) {
|
|
10
|
+
try {
|
|
11
|
+
const response = await fetch('https://api.anthropic.com/v1/messages', {
|
|
12
|
+
method: 'POST',
|
|
13
|
+
headers: {
|
|
14
|
+
'Content-Type': 'application/json',
|
|
15
|
+
'x-api-key': key,
|
|
16
|
+
'anthropic-version': '2023-06-01',
|
|
17
|
+
},
|
|
18
|
+
body: JSON.stringify({
|
|
19
|
+
model: 'claude-3-haiku-20240307',
|
|
20
|
+
max_tokens: 1,
|
|
21
|
+
messages: [{ role: 'user', content: 'Hi' }],
|
|
22
|
+
}),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
if (response.status === 401) {
|
|
26
|
+
return { valid: false, error: 'Invalid API key' };
|
|
27
|
+
}
|
|
28
|
+
if (response.status === 400) {
|
|
29
|
+
// Bad request but key is valid (e.g., rate limit, model error)
|
|
30
|
+
return { valid: true };
|
|
31
|
+
}
|
|
32
|
+
if (response.ok) {
|
|
33
|
+
return { valid: true };
|
|
34
|
+
}
|
|
35
|
+
return { valid: false, error: `HTTP ${response.status}` };
|
|
36
|
+
} catch (error) {
|
|
37
|
+
return { valid: false, error: error.message };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Build flat secrets JSON for SECRETS GitHub secret.
|
|
43
|
+
* Includes GH_TOKEN + API keys. entrypoint.sh decodes and exports each as an env var.
|
|
44
|
+
*/
|
|
45
|
+
export function buildSecretsJson(pat, keys) {
|
|
46
|
+
const secrets = { GH_TOKEN: pat, ANTHROPIC_API_KEY: keys.anthropic };
|
|
47
|
+
|
|
48
|
+
if (keys.openai) secrets.OPENAI_API_KEY = keys.openai;
|
|
49
|
+
if (keys.groq) secrets.GROQ_API_KEY = keys.groq;
|
|
50
|
+
|
|
51
|
+
return secrets;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Encode secrets to base64 for SECRETS GitHub secret
|
|
56
|
+
*/
|
|
57
|
+
export function encodeSecretsBase64(pat, keys) {
|
|
58
|
+
const secrets = buildSecretsJson(pat, keys);
|
|
59
|
+
return Buffer.from(JSON.stringify(secrets)).toString('base64');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Build LLM secrets JSON for LLM_SECRETS GitHub secret.
|
|
64
|
+
* These credentials are accessible to the LLM (not filtered by env-sanitizer).
|
|
65
|
+
*/
|
|
66
|
+
export function buildLlmSecretsJson(keys) {
|
|
67
|
+
const secrets = {};
|
|
68
|
+
if (keys.brave) secrets.BRAVE_API_KEY = keys.brave;
|
|
69
|
+
return secrets;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Encode LLM secrets to base64 for LLM_SECRETS GitHub secret.
|
|
74
|
+
* Returns null if no LLM secrets are configured.
|
|
75
|
+
*/
|
|
76
|
+
export function encodeLlmSecretsBase64(keys) {
|
|
77
|
+
const secrets = buildLlmSecretsJson(keys);
|
|
78
|
+
if (Object.keys(secrets).length === 0) return null;
|
|
79
|
+
return Buffer.from(JSON.stringify(secrets)).toString('base64');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Write .env file at project root
|
|
84
|
+
*/
|
|
85
|
+
export function writeEnvFile(config) {
|
|
86
|
+
const {
|
|
87
|
+
apiKey,
|
|
88
|
+
githubToken,
|
|
89
|
+
githubOwner,
|
|
90
|
+
githubRepo,
|
|
91
|
+
telegramBotToken,
|
|
92
|
+
telegramWebhookSecret,
|
|
93
|
+
ghWebhookSecret,
|
|
94
|
+
anthropicApiKey,
|
|
95
|
+
openaiApiKey,
|
|
96
|
+
telegramChatId,
|
|
97
|
+
telegramVerification,
|
|
98
|
+
} = config;
|
|
99
|
+
|
|
100
|
+
const envContent = `# thepopebot Configuration
|
|
101
|
+
# Generated by setup wizard
|
|
102
|
+
|
|
103
|
+
# Authentication key for /api/webhook endpoint
|
|
104
|
+
API_KEY=${apiKey}
|
|
105
|
+
|
|
106
|
+
# GitHub Personal Access Token (fine-grained: Actions, Contents, Metadata, Pull requests)
|
|
107
|
+
GH_TOKEN=${githubToken}
|
|
108
|
+
|
|
109
|
+
# Repository info
|
|
110
|
+
GH_OWNER=${githubOwner}
|
|
111
|
+
GH_REPO=${githubRepo}
|
|
112
|
+
|
|
113
|
+
# Telegram bot token from @BotFather
|
|
114
|
+
TELEGRAM_BOT_TOKEN=${telegramBotToken || ''}
|
|
115
|
+
|
|
116
|
+
# Telegram webhook secret (validates requests are from Telegram)
|
|
117
|
+
TELEGRAM_WEBHOOK_SECRET=${telegramWebhookSecret || ''}
|
|
118
|
+
|
|
119
|
+
# Telegram chat ID (restricts bot to this chat only)
|
|
120
|
+
TELEGRAM_CHAT_ID=${telegramChatId || ''}
|
|
121
|
+
|
|
122
|
+
# Telegram verification code (used during setup, can be removed after)
|
|
123
|
+
TELEGRAM_VERIFICATION=${telegramVerification || ''}
|
|
124
|
+
|
|
125
|
+
# Secret for GitHub Actions webhook auth (must match GH_WEBHOOK_SECRET secret)
|
|
126
|
+
GH_WEBHOOK_SECRET=${ghWebhookSecret}
|
|
127
|
+
|
|
128
|
+
# Anthropic API key for Claude chat features
|
|
129
|
+
ANTHROPIC_API_KEY=${anthropicApiKey}
|
|
130
|
+
|
|
131
|
+
# OpenAI API key for Whisper voice transcription (optional)
|
|
132
|
+
OPENAI_API_KEY=${openaiApiKey || ''}
|
|
133
|
+
`;
|
|
134
|
+
|
|
135
|
+
const envPath = join(ROOT_DIR, '.env');
|
|
136
|
+
writeFileSync(envPath, envContent);
|
|
137
|
+
return envPath;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Update a single variable in an existing .env file
|
|
142
|
+
*/
|
|
143
|
+
export function updateEnvVariable(key, value) {
|
|
144
|
+
const envPath = join(ROOT_DIR, '.env');
|
|
145
|
+
if (!existsSync(envPath)) {
|
|
146
|
+
throw new Error('.env file not found. Run npm run setup first.');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let content = readFileSync(envPath, 'utf-8');
|
|
150
|
+
const regex = new RegExp(`^${key}=.*$`, 'm');
|
|
151
|
+
|
|
152
|
+
if (regex.test(content)) {
|
|
153
|
+
content = content.replace(regex, `${key}=${value}`);
|
|
154
|
+
} else {
|
|
155
|
+
content = content.trimEnd() + `\n${key}=${value}\n`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
writeFileSync(envPath, content);
|
|
159
|
+
return envPath;
|
|
160
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { execSync, exec } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
import { randomBytes } from 'crypto';
|
|
4
|
+
|
|
5
|
+
const execAsync = promisify(exec);
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Validate GitHub PAT by making a test API call
|
|
9
|
+
*/
|
|
10
|
+
export async function validatePAT(token) {
|
|
11
|
+
try {
|
|
12
|
+
const response = await fetch('https://api.github.com/user', {
|
|
13
|
+
headers: {
|
|
14
|
+
Authorization: `token ${token}`,
|
|
15
|
+
Accept: 'application/vnd.github.v3+json',
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
if (!response.ok) return { valid: false, error: 'Invalid token' };
|
|
19
|
+
const user = await response.json();
|
|
20
|
+
return { valid: true, user: user.login };
|
|
21
|
+
} catch (error) {
|
|
22
|
+
return { valid: false, error: error.message };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Check PAT scopes/permissions
|
|
28
|
+
* Works with both classic tokens (x-oauth-scopes header) and fine-grained tokens
|
|
29
|
+
*/
|
|
30
|
+
export async function checkPATScopes(token) {
|
|
31
|
+
try {
|
|
32
|
+
const response = await fetch('https://api.github.com/user', {
|
|
33
|
+
headers: {
|
|
34
|
+
Authorization: `token ${token}`,
|
|
35
|
+
Accept: 'application/vnd.github.v3+json',
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
const scopes = response.headers.get('x-oauth-scopes') || '';
|
|
39
|
+
const scopeList = scopes.split(',').map((s) => s.trim()).filter(Boolean);
|
|
40
|
+
|
|
41
|
+
// Classic tokens have x-oauth-scopes header
|
|
42
|
+
if (scopeList.length > 0) {
|
|
43
|
+
return {
|
|
44
|
+
hasRepo: scopeList.includes('repo'),
|
|
45
|
+
hasWorkflow: scopeList.includes('workflow'),
|
|
46
|
+
scopes: scopeList,
|
|
47
|
+
isFineGrained: false,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Fine-grained tokens don't have x-oauth-scopes header
|
|
52
|
+
// We can't check permissions directly, so we assume valid if token works
|
|
53
|
+
return {
|
|
54
|
+
hasRepo: true,
|
|
55
|
+
hasWorkflow: true,
|
|
56
|
+
scopes: [],
|
|
57
|
+
isFineGrained: true,
|
|
58
|
+
};
|
|
59
|
+
} catch {
|
|
60
|
+
return { hasRepo: false, hasWorkflow: false, scopes: [], isFineGrained: false };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Set a GitHub repository secret using gh CLI
|
|
66
|
+
*/
|
|
67
|
+
export async function setSecret(owner, repo, name, value) {
|
|
68
|
+
try {
|
|
69
|
+
// Use stdin to pass the secret value securely
|
|
70
|
+
const { stdout, stderr } = await execAsync(
|
|
71
|
+
`echo "${value.replace(/"/g, '\\"')}" | gh secret set ${name} --repo ${owner}/${repo}`,
|
|
72
|
+
{ encoding: 'utf-8' }
|
|
73
|
+
);
|
|
74
|
+
return { success: true };
|
|
75
|
+
} catch (error) {
|
|
76
|
+
return { success: false, error: error.message };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Set multiple GitHub secrets
|
|
82
|
+
*/
|
|
83
|
+
export async function setSecrets(owner, repo, secrets) {
|
|
84
|
+
const results = {};
|
|
85
|
+
for (const [name, value] of Object.entries(secrets)) {
|
|
86
|
+
results[name] = await setSecret(owner, repo, name, value);
|
|
87
|
+
}
|
|
88
|
+
return results;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* List existing secrets
|
|
93
|
+
*/
|
|
94
|
+
export async function listSecrets(owner, repo) {
|
|
95
|
+
try {
|
|
96
|
+
const { stdout } = await execAsync(`gh secret list --repo ${owner}/${repo}`, {
|
|
97
|
+
encoding: 'utf-8',
|
|
98
|
+
});
|
|
99
|
+
const secrets = stdout
|
|
100
|
+
.trim()
|
|
101
|
+
.split('\n')
|
|
102
|
+
.filter(Boolean)
|
|
103
|
+
.map((line) => line.split('\t')[0]);
|
|
104
|
+
return secrets;
|
|
105
|
+
} catch {
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Set a GitHub repository variable using gh CLI
|
|
112
|
+
*/
|
|
113
|
+
export async function setVariable(owner, repo, name, value) {
|
|
114
|
+
try {
|
|
115
|
+
const { stdout, stderr } = await execAsync(
|
|
116
|
+
`echo "${value.replace(/"/g, '\\"')}" | gh variable set ${name} --repo ${owner}/${repo}`,
|
|
117
|
+
{ encoding: 'utf-8' }
|
|
118
|
+
);
|
|
119
|
+
return { success: true };
|
|
120
|
+
} catch (error) {
|
|
121
|
+
return { success: false, error: error.message };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Set multiple GitHub repository variables
|
|
127
|
+
*/
|
|
128
|
+
export async function setVariables(owner, repo, variables) {
|
|
129
|
+
const results = {};
|
|
130
|
+
for (const [name, value] of Object.entries(variables)) {
|
|
131
|
+
results[name] = await setVariable(owner, repo, name, value);
|
|
132
|
+
}
|
|
133
|
+
return results;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Generate a random webhook secret
|
|
138
|
+
*/
|
|
139
|
+
export function generateWebhookSecret() {
|
|
140
|
+
return randomBytes(32).toString('hex');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Get the GitHub PAT creation URL with pre-selected scopes
|
|
145
|
+
*/
|
|
146
|
+
export function getPATCreationURL() {
|
|
147
|
+
return 'https://github.com/settings/personal-access-tokens/new';
|
|
148
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { execSync, exec } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
|
|
4
|
+
const execAsync = promisify(exec);
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Check if a command exists
|
|
8
|
+
*/
|
|
9
|
+
function commandExists(cmd) {
|
|
10
|
+
try {
|
|
11
|
+
execSync(`which ${cmd}`, { stdio: 'ignore' });
|
|
12
|
+
return true;
|
|
13
|
+
} catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Get Node.js version
|
|
20
|
+
*/
|
|
21
|
+
function getNodeVersion() {
|
|
22
|
+
try {
|
|
23
|
+
const version = execSync('node --version', { encoding: 'utf-8' }).trim();
|
|
24
|
+
return version.replace('v', '');
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Check if gh CLI is authenticated
|
|
32
|
+
*/
|
|
33
|
+
async function isGhAuthenticated() {
|
|
34
|
+
try {
|
|
35
|
+
await execAsync('gh auth status');
|
|
36
|
+
return true;
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get git remote info (owner/repo)
|
|
44
|
+
*/
|
|
45
|
+
function getGitRemoteInfo() {
|
|
46
|
+
try {
|
|
47
|
+
const remote = execSync('git remote get-url origin', { encoding: 'utf-8' }).trim();
|
|
48
|
+
// Handle both HTTPS and SSH formats
|
|
49
|
+
// https://github.com/owner/repo.git
|
|
50
|
+
// git@github.com:owner/repo.git
|
|
51
|
+
const httpsMatch = remote.match(/github\.com\/([^/]+)\/([^/.]+)/);
|
|
52
|
+
const sshMatch = remote.match(/github\.com:([^/]+)\/([^/.]+)/);
|
|
53
|
+
const match = httpsMatch || sshMatch;
|
|
54
|
+
if (match) {
|
|
55
|
+
return { owner: match[1], repo: match[2].replace('.git', '') };
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get package manager (pnpm preferred, npm fallback)
|
|
65
|
+
*/
|
|
66
|
+
function getPackageManager() {
|
|
67
|
+
if (commandExists('pnpm')) return 'pnpm';
|
|
68
|
+
if (commandExists('npm')) return 'npm';
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Check all prerequisites and return status
|
|
74
|
+
*/
|
|
75
|
+
export async function checkPrerequisites() {
|
|
76
|
+
const results = {
|
|
77
|
+
node: { installed: false, version: null, ok: false },
|
|
78
|
+
packageManager: { installed: false, name: null },
|
|
79
|
+
gh: { installed: false, authenticated: false },
|
|
80
|
+
ngrok: { installed: false },
|
|
81
|
+
git: { installed: false, remoteInfo: null },
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// Check Node.js
|
|
85
|
+
const nodeVersion = getNodeVersion();
|
|
86
|
+
if (nodeVersion) {
|
|
87
|
+
results.node.installed = true;
|
|
88
|
+
results.node.version = nodeVersion;
|
|
89
|
+
const [major] = nodeVersion.split('.').map(Number);
|
|
90
|
+
results.node.ok = major >= 18;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Check package manager
|
|
94
|
+
const pm = getPackageManager();
|
|
95
|
+
if (pm) {
|
|
96
|
+
results.packageManager.installed = true;
|
|
97
|
+
results.packageManager.name = pm;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Check gh CLI
|
|
101
|
+
results.gh.installed = commandExists('gh');
|
|
102
|
+
if (results.gh.installed) {
|
|
103
|
+
results.gh.authenticated = await isGhAuthenticated();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Check ngrok
|
|
107
|
+
results.ngrok.installed = commandExists('ngrok');
|
|
108
|
+
|
|
109
|
+
// Check git
|
|
110
|
+
results.git.installed = commandExists('git');
|
|
111
|
+
if (results.git.installed) {
|
|
112
|
+
results.git.remoteInfo = getGitRemoteInfo();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return results;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Install a global npm package
|
|
120
|
+
*/
|
|
121
|
+
export async function installGlobalPackage(packageName) {
|
|
122
|
+
const pm = getPackageManager();
|
|
123
|
+
const cmd = pm === 'pnpm' ? `pnpm add -g ${packageName}` : `npm install -g ${packageName}`;
|
|
124
|
+
await execAsync(cmd);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Run gh auth login
|
|
129
|
+
*/
|
|
130
|
+
export async function runGhAuth() {
|
|
131
|
+
// This needs to be interactive, so we use execSync
|
|
132
|
+
execSync('gh auth login', { stdio: 'inherit' });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export { commandExists, getGitRemoteInfo, getPackageManager };
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import open from 'open';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Mask a secret, showing only last 4 characters
|
|
6
|
+
*/
|
|
7
|
+
export function maskSecret(secret) {
|
|
8
|
+
if (!secret || secret.length < 8) return '****';
|
|
9
|
+
return '****' + secret.slice(-4);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Prompt for GitHub PAT
|
|
14
|
+
*/
|
|
15
|
+
export async function promptForPAT() {
|
|
16
|
+
const { pat } = await inquirer.prompt([
|
|
17
|
+
{
|
|
18
|
+
type: 'password',
|
|
19
|
+
name: 'pat',
|
|
20
|
+
message: 'Paste your GitHub Personal Access Token:',
|
|
21
|
+
mask: '*',
|
|
22
|
+
validate: (input) => {
|
|
23
|
+
if (!input) return 'PAT is required';
|
|
24
|
+
if (!input.startsWith('ghp_') && !input.startsWith('github_pat_')) {
|
|
25
|
+
return 'Invalid PAT format. Should start with ghp_ or github_pat_';
|
|
26
|
+
}
|
|
27
|
+
return true;
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
]);
|
|
31
|
+
return pat;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Prompt for Anthropic API key
|
|
36
|
+
*/
|
|
37
|
+
export async function promptForAnthropicKey() {
|
|
38
|
+
const { key } = await inquirer.prompt([
|
|
39
|
+
{
|
|
40
|
+
type: 'password',
|
|
41
|
+
name: 'key',
|
|
42
|
+
message: 'Enter your Anthropic API key:',
|
|
43
|
+
mask: '*',
|
|
44
|
+
validate: (input) => {
|
|
45
|
+
if (!input) return 'Anthropic API key is required';
|
|
46
|
+
if (!input.startsWith('sk-ant-')) {
|
|
47
|
+
return 'Invalid format. Should start with sk-ant-';
|
|
48
|
+
}
|
|
49
|
+
return true;
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
]);
|
|
53
|
+
return key;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Prompt for optional OpenAI API key
|
|
58
|
+
*/
|
|
59
|
+
export async function promptForOpenAIKey() {
|
|
60
|
+
const { addKey } = await inquirer.prompt([
|
|
61
|
+
{
|
|
62
|
+
type: 'confirm',
|
|
63
|
+
name: 'addKey',
|
|
64
|
+
message: 'Add OpenAI API key? (optional)',
|
|
65
|
+
default: false,
|
|
66
|
+
},
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
if (!addKey) return null;
|
|
70
|
+
|
|
71
|
+
const { openPage } = await inquirer.prompt([
|
|
72
|
+
{
|
|
73
|
+
type: 'confirm',
|
|
74
|
+
name: 'openPage',
|
|
75
|
+
message: 'Open OpenAI API key page in browser?',
|
|
76
|
+
default: true,
|
|
77
|
+
},
|
|
78
|
+
]);
|
|
79
|
+
if (openPage) {
|
|
80
|
+
await open('https://platform.openai.com/settings/organization/api-keys');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const { key } = await inquirer.prompt([
|
|
84
|
+
{
|
|
85
|
+
type: 'password',
|
|
86
|
+
name: 'key',
|
|
87
|
+
message: 'Enter your OpenAI API key:',
|
|
88
|
+
mask: '*',
|
|
89
|
+
validate: (input) => {
|
|
90
|
+
if (!input) return 'Key is required if adding';
|
|
91
|
+
if (!input.startsWith('sk-')) {
|
|
92
|
+
return 'Invalid format. Should start with sk-';
|
|
93
|
+
}
|
|
94
|
+
return true;
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
]);
|
|
98
|
+
return key;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Prompt for optional Groq API key
|
|
103
|
+
*/
|
|
104
|
+
export async function promptForGroqKey() {
|
|
105
|
+
const { addKey } = await inquirer.prompt([
|
|
106
|
+
{
|
|
107
|
+
type: 'confirm',
|
|
108
|
+
name: 'addKey',
|
|
109
|
+
message: 'Add Groq API key? (optional)',
|
|
110
|
+
default: false,
|
|
111
|
+
},
|
|
112
|
+
]);
|
|
113
|
+
|
|
114
|
+
if (!addKey) return null;
|
|
115
|
+
|
|
116
|
+
const { key } = await inquirer.prompt([
|
|
117
|
+
{
|
|
118
|
+
type: 'password',
|
|
119
|
+
name: 'key',
|
|
120
|
+
message: 'Enter your Groq API key:',
|
|
121
|
+
mask: '*',
|
|
122
|
+
validate: (input) => {
|
|
123
|
+
if (!input) return 'Key is required if adding';
|
|
124
|
+
return true;
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
]);
|
|
128
|
+
return key;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Prompt for optional Brave Search API key
|
|
133
|
+
*/
|
|
134
|
+
export async function promptForBraveKey() {
|
|
135
|
+
const { addKey } = await inquirer.prompt([
|
|
136
|
+
{
|
|
137
|
+
type: 'confirm',
|
|
138
|
+
name: 'addKey',
|
|
139
|
+
message: 'Add Brave Search API key? (free tier, greatly improves agent)',
|
|
140
|
+
default: true,
|
|
141
|
+
},
|
|
142
|
+
]);
|
|
143
|
+
|
|
144
|
+
if (!addKey) return null;
|
|
145
|
+
|
|
146
|
+
console.log('\n To get a free Brave Search API key:');
|
|
147
|
+
console.log(' 1. Go to https://api-dashboard.search.brave.com/app/keys');
|
|
148
|
+
console.log(' 2. Click "Get Started"');
|
|
149
|
+
console.log(' 3. Create an account (or sign in)');
|
|
150
|
+
console.log(' 4. Subscribe to the "Free" plan (2,000 queries/month)');
|
|
151
|
+
console.log(' 5. Copy your API key\n');
|
|
152
|
+
|
|
153
|
+
const { openPage } = await inquirer.prompt([
|
|
154
|
+
{
|
|
155
|
+
type: 'confirm',
|
|
156
|
+
name: 'openPage',
|
|
157
|
+
message: 'Open Brave Search API page in browser?',
|
|
158
|
+
default: true,
|
|
159
|
+
},
|
|
160
|
+
]);
|
|
161
|
+
if (openPage) {
|
|
162
|
+
await open('https://api-dashboard.search.brave.com/app/keys');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const { key } = await inquirer.prompt([
|
|
166
|
+
{
|
|
167
|
+
type: 'password',
|
|
168
|
+
name: 'key',
|
|
169
|
+
message: 'Enter your Brave Search API key:',
|
|
170
|
+
mask: '*',
|
|
171
|
+
validate: (input) => {
|
|
172
|
+
if (!input) return 'Key is required if adding';
|
|
173
|
+
return true;
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
]);
|
|
177
|
+
return key;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Prompt for Telegram bot token
|
|
182
|
+
*/
|
|
183
|
+
export async function promptForTelegramToken() {
|
|
184
|
+
const { addTelegram } = await inquirer.prompt([
|
|
185
|
+
{
|
|
186
|
+
type: 'confirm',
|
|
187
|
+
name: 'addTelegram',
|
|
188
|
+
message: 'Set up Telegram bot?',
|
|
189
|
+
default: true,
|
|
190
|
+
},
|
|
191
|
+
]);
|
|
192
|
+
|
|
193
|
+
if (!addTelegram) return null;
|
|
194
|
+
|
|
195
|
+
const { token } = await inquirer.prompt([
|
|
196
|
+
{
|
|
197
|
+
type: 'password',
|
|
198
|
+
name: 'token',
|
|
199
|
+
message: 'Enter your Telegram bot token from @BotFather:',
|
|
200
|
+
mask: '*',
|
|
201
|
+
validate: (input) => {
|
|
202
|
+
if (!input) return 'Token is required';
|
|
203
|
+
if (!/^\d+:[A-Za-z0-9_-]+$/.test(input)) {
|
|
204
|
+
return 'Invalid format. Should be like 123456789:ABC-DEF...';
|
|
205
|
+
}
|
|
206
|
+
return true;
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
]);
|
|
210
|
+
return token;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Generate a Telegram webhook secret
|
|
215
|
+
*/
|
|
216
|
+
export async function generateTelegramWebhookSecret() {
|
|
217
|
+
const { randomBytes } = await import('crypto');
|
|
218
|
+
return randomBytes(32).toString('hex');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Prompt for deployment method
|
|
223
|
+
*/
|
|
224
|
+
export async function promptForDeployMethod() {
|
|
225
|
+
const { method } = await inquirer.prompt([
|
|
226
|
+
{
|
|
227
|
+
type: 'list',
|
|
228
|
+
name: 'method',
|
|
229
|
+
message: 'How would you like to deploy the event handler?',
|
|
230
|
+
choices: [
|
|
231
|
+
{ name: 'Deploy to Vercel via CLI (recommended)', value: 'vercel' },
|
|
232
|
+
{ name: 'Open Vercel Deploy Button in browser', value: 'button' },
|
|
233
|
+
{ name: 'Skip - I\'ll deploy manually later', value: 'skip' },
|
|
234
|
+
],
|
|
235
|
+
},
|
|
236
|
+
]);
|
|
237
|
+
return method;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Prompt for confirmation
|
|
242
|
+
*/
|
|
243
|
+
export async function confirm(message, defaultValue = true) {
|
|
244
|
+
const { confirmed } = await inquirer.prompt([
|
|
245
|
+
{
|
|
246
|
+
type: 'confirm',
|
|
247
|
+
name: 'confirmed',
|
|
248
|
+
message,
|
|
249
|
+
default: defaultValue,
|
|
250
|
+
},
|
|
251
|
+
]);
|
|
252
|
+
return confirmed;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Prompt for text input
|
|
257
|
+
*/
|
|
258
|
+
export async function promptText(message, defaultValue = '') {
|
|
259
|
+
const { value } = await inquirer.prompt([
|
|
260
|
+
{
|
|
261
|
+
type: 'input',
|
|
262
|
+
name: 'value',
|
|
263
|
+
message,
|
|
264
|
+
default: defaultValue,
|
|
265
|
+
},
|
|
266
|
+
]);
|
|
267
|
+
return value;
|
|
268
|
+
}
|