@harbinger-ai/harbinger 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/LICENSE +21 -0
- package/README.md +406 -0
- package/agents/README.md +76 -0
- package/agents/_template/CONFIG.yaml +7 -0
- package/agents/_template/HEARTBEAT.md +59 -0
- package/agents/_template/IDENTITY.md +4 -0
- package/agents/_template/SKILLS.md +1 -0
- package/agents/_template/SOUL.md +25 -0
- package/agents/_template/TOOLS.md +3 -0
- package/agents/binary-reverser/CONFIG.yaml +21 -0
- package/agents/binary-reverser/HEARTBEAT.md +65 -0
- package/agents/binary-reverser/IDENTITY.md +1 -0
- package/agents/binary-reverser/SKILLS.md +1 -0
- package/agents/binary-reverser/SOUL.md +23 -0
- package/agents/binary-reverser/TOOLS.md +99 -0
- package/agents/browser-agent/CONFIG.yaml +20 -0
- package/agents/browser-agent/HEARTBEAT.md +79 -0
- package/agents/browser-agent/IDENTITY.md +5 -0
- package/agents/browser-agent/SKILLS.md +86 -0
- package/agents/browser-agent/SOUL.md +23 -0
- package/agents/browser-agent/TOOLS.md +186 -0
- package/agents/cloud-infiltrator/CONFIG.yaml +22 -0
- package/agents/cloud-infiltrator/HEARTBEAT.md +78 -0
- package/agents/cloud-infiltrator/IDENTITY.md +1 -0
- package/agents/cloud-infiltrator/SKILLS.md +1 -0
- package/agents/cloud-infiltrator/SOUL.md +23 -0
- package/agents/cloud-infiltrator/TOOLS.md +68 -0
- package/agents/coding-assistant/CONFIG.yaml +22 -0
- package/agents/coding-assistant/HEARTBEAT.md +57 -0
- package/agents/coding-assistant/IDENTITY.md +5 -0
- package/agents/coding-assistant/SKILLS.md +69 -0
- package/agents/coding-assistant/SOUL.md +60 -0
- package/agents/coding-assistant/TOOLS.md +168 -0
- package/agents/learning-agent/CONFIG.yaml +21 -0
- package/agents/learning-agent/HEARTBEAT.md +63 -0
- package/agents/learning-agent/IDENTITY.md +5 -0
- package/agents/learning-agent/SKILLS.md +86 -0
- package/agents/learning-agent/SOUL.md +77 -0
- package/agents/learning-agent/TOOLS.md +145 -0
- package/agents/maintainer/CONFIG.yaml +31 -0
- package/agents/maintainer/HEARTBEAT.md +28 -0
- package/agents/maintainer/IDENTITY.md +33 -0
- package/agents/maintainer/SKILLS.md +24 -0
- package/agents/maintainer/SOUL.md +61 -0
- package/agents/maintainer/TOOLS.md +29 -0
- package/agents/maintainer/lib/engine.js +279 -0
- package/agents/maintainer/lib/safe-fixer.js +183 -0
- package/agents/morning-brief/CONFIG.yaml +22 -0
- package/agents/morning-brief/HEARTBEAT.md +60 -0
- package/agents/morning-brief/IDENTITY.md +5 -0
- package/agents/morning-brief/SKILLS.md +56 -0
- package/agents/morning-brief/SOUL.md +64 -0
- package/agents/morning-brief/TOOLS.md +112 -0
- package/agents/osint-detective/CONFIG.yaml +24 -0
- package/agents/osint-detective/HEARTBEAT.md +66 -0
- package/agents/osint-detective/IDENTITY.md +1 -0
- package/agents/osint-detective/SKILLS.md +1 -0
- package/agents/osint-detective/SOUL.md +23 -0
- package/agents/osint-detective/TOOLS.md +81 -0
- package/agents/recon-scout/CONFIG.yaml +22 -0
- package/agents/recon-scout/HEARTBEAT.md +79 -0
- package/agents/recon-scout/IDENTITY.md +1 -0
- package/agents/recon-scout/SKILLS.md +1 -0
- package/agents/recon-scout/SOUL.md +23 -0
- package/agents/recon-scout/TOOLS.md +93 -0
- package/agents/report-writer/CONFIG.yaml +21 -0
- package/agents/report-writer/HEARTBEAT.md +63 -0
- package/agents/report-writer/IDENTITY.md +1 -0
- package/agents/report-writer/SKILLS.md +1 -0
- package/agents/report-writer/SOUL.md +23 -0
- package/agents/report-writer/TOOLS.md +69 -0
- package/agents/shared/README.md +13 -0
- package/agents/web-hacker/CONFIG.yaml +24 -0
- package/agents/web-hacker/HEARTBEAT.md +78 -0
- package/agents/web-hacker/IDENTITY.md +1 -0
- package/agents/web-hacker/SKILLS.md +1 -0
- package/agents/web-hacker/SOUL.md +23 -0
- package/agents/web-hacker/TOOLS.md +86 -0
- package/api/CLAUDE.md +19 -0
- package/api/index.js +274 -0
- package/bin/cli.js +620 -0
- package/bin/local.sh +31 -0
- package/bin/postinstall.js +63 -0
- package/config/index.js +24 -0
- package/config/instrumentation.js +93 -0
- package/drizzle/0000_initial.sql +52 -0
- package/drizzle/0001_bounty_and_registry.sql +82 -0
- package/drizzle/0002_sync_columns.sql +7 -0
- package/drizzle/0003_graceful_bloodscream.sql +86 -0
- package/drizzle/meta/0000_snapshot.json +321 -0
- package/drizzle/meta/0003_snapshot.json +878 -0
- package/drizzle/meta/_journal.json +34 -0
- package/drizzle/relations.ts +3 -0
- package/drizzle/schema.ts +145 -0
- package/lib/actions.js +47 -0
- package/lib/agents.js +166 -0
- package/lib/ai/agent.js +96 -0
- package/lib/ai/autonomous-engine.js +261 -0
- package/lib/ai/index.js +359 -0
- package/lib/ai/model-router.js +254 -0
- package/lib/ai/model.js +73 -0
- package/lib/ai/tools.js +84 -0
- package/lib/auth/actions.js +28 -0
- package/lib/auth/config.js +27 -0
- package/lib/auth/edge-config.js +27 -0
- package/lib/auth/index.js +27 -0
- package/lib/auth/middleware.js +53 -0
- package/lib/bounty/actions.js +119 -0
- package/lib/bounty/findings.js +64 -0
- package/lib/bounty/programs.js +34 -0
- package/lib/bounty/sync-targets.js +267 -0
- package/lib/bounty/targets.js +33 -0
- package/lib/channels/base.js +56 -0
- package/lib/channels/index.js +15 -0
- package/lib/channels/telegram.js +148 -0
- package/lib/chat/actions.js +288 -0
- package/lib/chat/api.js +135 -0
- package/lib/chat/components/app-sidebar.js +237 -0
- package/lib/chat/components/app-sidebar.jsx +289 -0
- package/lib/chat/components/chat-header.js +27 -0
- package/lib/chat/components/chat-header.jsx +37 -0
- package/lib/chat/components/chat-input.js +230 -0
- package/lib/chat/components/chat-input.jsx +228 -0
- package/lib/chat/components/chat-nav-context.js +11 -0
- package/lib/chat/components/chat-nav-context.jsx +11 -0
- package/lib/chat/components/chat-page.js +81 -0
- package/lib/chat/components/chat-page.jsx +100 -0
- package/lib/chat/components/chat.js +150 -0
- package/lib/chat/components/chat.jsx +182 -0
- package/lib/chat/components/chats-page.js +302 -0
- package/lib/chat/components/chats-page.jsx +330 -0
- package/lib/chat/components/crons-page.js +172 -0
- package/lib/chat/components/crons-page.jsx +244 -0
- package/lib/chat/components/enhanced-tool-call.js +103 -0
- package/lib/chat/components/enhanced-tool-call.jsx +139 -0
- package/lib/chat/components/findings-page.js +175 -0
- package/lib/chat/components/findings-page.jsx +214 -0
- package/lib/chat/components/greeting.js +22 -0
- package/lib/chat/components/greeting.jsx +26 -0
- package/lib/chat/components/icons.js +777 -0
- package/lib/chat/components/icons.jsx +741 -0
- package/lib/chat/components/index.js +26 -0
- package/lib/chat/components/mcp-page.js +260 -0
- package/lib/chat/components/mcp-page.jsx +355 -0
- package/lib/chat/components/message.js +289 -0
- package/lib/chat/components/message.jsx +315 -0
- package/lib/chat/components/messages.js +66 -0
- package/lib/chat/components/messages.jsx +77 -0
- package/lib/chat/components/notifications-page.js +56 -0
- package/lib/chat/components/notifications-page.jsx +87 -0
- package/lib/chat/components/page-layout.js +21 -0
- package/lib/chat/components/page-layout.jsx +28 -0
- package/lib/chat/components/registry-page.js +222 -0
- package/lib/chat/components/registry-page.jsx +255 -0
- package/lib/chat/components/settings-layout.js +40 -0
- package/lib/chat/components/settings-layout.jsx +54 -0
- package/lib/chat/components/settings-secrets-page.js +216 -0
- package/lib/chat/components/settings-secrets-page.jsx +264 -0
- package/lib/chat/components/sidebar-history-item.js +132 -0
- package/lib/chat/components/sidebar-history-item.jsx +113 -0
- package/lib/chat/components/sidebar-history.js +115 -0
- package/lib/chat/components/sidebar-history.jsx +157 -0
- package/lib/chat/components/sidebar-user-nav.js +63 -0
- package/lib/chat/components/sidebar-user-nav.jsx +73 -0
- package/lib/chat/components/status-bar.js +39 -0
- package/lib/chat/components/status-bar.jsx +51 -0
- package/lib/chat/components/swarm-page.js +157 -0
- package/lib/chat/components/swarm-page.jsx +210 -0
- package/lib/chat/components/targets-page.js +376 -0
- package/lib/chat/components/targets-page.jsx +389 -0
- package/lib/chat/components/tool-call.js +86 -0
- package/lib/chat/components/tool-call.jsx +104 -0
- package/lib/chat/components/tool-panel.js +107 -0
- package/lib/chat/components/tool-panel.jsx +145 -0
- package/lib/chat/components/triggers-page.js +153 -0
- package/lib/chat/components/triggers-page.jsx +221 -0
- package/lib/chat/components/ui/confirm-dialog.js +53 -0
- package/lib/chat/components/ui/confirm-dialog.jsx +57 -0
- package/lib/chat/components/ui/dropdown-menu.js +98 -0
- package/lib/chat/components/ui/dropdown-menu.jsx +116 -0
- package/lib/chat/components/ui/rename-dialog.js +74 -0
- package/lib/chat/components/ui/rename-dialog.jsx +72 -0
- package/lib/chat/components/ui/scroll-area.js +13 -0
- package/lib/chat/components/ui/scroll-area.jsx +17 -0
- package/lib/chat/components/ui/separator.js +21 -0
- package/lib/chat/components/ui/separator.jsx +18 -0
- package/lib/chat/components/ui/sheet.js +75 -0
- package/lib/chat/components/ui/sheet.jsx +95 -0
- package/lib/chat/components/ui/sidebar.js +227 -0
- package/lib/chat/components/ui/sidebar.jsx +245 -0
- package/lib/chat/components/ui/tooltip.js +56 -0
- package/lib/chat/components/ui/tooltip.jsx +66 -0
- package/lib/chat/components/upgrade-dialog.js +151 -0
- package/lib/chat/components/upgrade-dialog.jsx +170 -0
- package/lib/chat/utils.js +11 -0
- package/lib/cron.js +246 -0
- package/lib/db/api-keys.js +163 -0
- package/lib/db/chats.js +145 -0
- package/lib/db/index.js +52 -0
- package/lib/db/notifications.js +99 -0
- package/lib/db/schema.js +145 -0
- package/lib/db/update-check.js +96 -0
- package/lib/db/users.js +89 -0
- package/lib/mcp/actions.js +104 -0
- package/lib/mcp/client.js +79 -0
- package/lib/mcp/handler.js +57 -0
- package/lib/mcp/server.js +165 -0
- package/lib/paths.js +46 -0
- package/lib/registry/actions.js +164 -0
- package/lib/registry/catalog.js +137 -0
- package/lib/registry/tools.js +71 -0
- package/lib/tools/create-job.js +99 -0
- package/lib/tools/github.js +217 -0
- package/lib/tools/openai.js +35 -0
- package/lib/tools/telegram.js +292 -0
- package/lib/triggers.js +118 -0
- package/lib/utils/render-md.js +102 -0
- package/package.json +103 -0
- package/setup/lib/auth.mjs +81 -0
- package/setup/lib/env.mjs +21 -0
- package/setup/lib/fs-utils.mjs +20 -0
- package/setup/lib/github.mjs +149 -0
- package/setup/lib/prerequisites.mjs +155 -0
- package/setup/lib/prompts.mjs +267 -0
- package/setup/lib/providers.mjs +48 -0
- package/setup/lib/sync.mjs +125 -0
- package/setup/lib/targets.mjs +45 -0
- package/setup/lib/telegram-verify.mjs +63 -0
- package/setup/lib/telegram.mjs +76 -0
- package/setup/setup-telegram.mjs +264 -0
- package/setup/setup.mjs +842 -0
- package/templates/.dockerignore +5 -0
- package/templates/.env.example +63 -0
- package/templates/.github/workflows/auto-merge.yml +117 -0
- package/templates/.github/workflows/build-image.yml +36 -0
- package/templates/.github/workflows/notify-job-failed.yml +64 -0
- package/templates/.github/workflows/notify-pr-complete.yml +119 -0
- package/templates/.github/workflows/rebuild-event-handler.yml +121 -0
- package/templates/.github/workflows/run-job.yml +89 -0
- package/templates/.github/workflows/upgrade-event-handler.yml +62 -0
- package/templates/.gitignore.template +45 -0
- package/templates/.pi/extensions/env-sanitizer/index.ts +48 -0
- package/templates/.pi/extensions/env-sanitizer/package.json +5 -0
- package/templates/CLAUDE.md +29 -0
- package/templates/CLAUDE.md.template +307 -0
- package/templates/app/api/[...thepopebot]/route.js +1 -0
- package/templates/app/api/auth/[...nextauth]/route.js +1 -0
- package/templates/app/chat/[chatId]/page.js +8 -0
- package/templates/app/chats/page.js +7 -0
- package/templates/app/components/ascii-logo.jsx +10 -0
- package/templates/app/components/login-form.jsx +92 -0
- package/templates/app/components/setup-form.jsx +82 -0
- package/templates/app/components/theme-provider.jsx +11 -0
- package/templates/app/components/theme-toggle.jsx +38 -0
- package/templates/app/components/ui/button.jsx +21 -0
- package/templates/app/components/ui/card.jsx +23 -0
- package/templates/app/components/ui/input.jsx +10 -0
- package/templates/app/components/ui/label.jsx +10 -0
- package/templates/app/crons/page.js +5 -0
- package/templates/app/findings/page.js +7 -0
- package/templates/app/globals.css +90 -0
- package/templates/app/layout.js +19 -0
- package/templates/app/login/page.js +15 -0
- package/templates/app/notifications/page.js +7 -0
- package/templates/app/page.js +7 -0
- package/templates/app/settings/crons/page.js +5 -0
- package/templates/app/settings/layout.js +7 -0
- package/templates/app/settings/mcp/page.js +5 -0
- package/templates/app/settings/page.js +5 -0
- package/templates/app/settings/secrets/page.js +5 -0
- package/templates/app/settings/triggers/page.js +5 -0
- package/templates/app/stream/chat/route.js +1 -0
- package/templates/app/swarm/page.js +7 -0
- package/templates/app/targets/page.js +7 -0
- package/templates/app/toolbox/page.js +7 -0
- package/templates/app/triggers/page.js +5 -0
- package/templates/config/AGENT.md +34 -0
- package/templates/config/CRONS.json +56 -0
- package/templates/config/EVENT_HANDLER.md +224 -0
- package/templates/config/HEARTBEAT.md +3 -0
- package/templates/config/JOB_SUMMARY.md +130 -0
- package/templates/config/MCP_SERVERS.json +1 -0
- package/templates/config/SKILL_BUILDING_GUIDE.md +90 -0
- package/templates/config/SOUL.md +17 -0
- package/templates/config/TRIGGERS.json +58 -0
- package/templates/docker/event-handler/Dockerfile +20 -0
- package/templates/docker/event-handler/ecosystem.config.cjs +8 -0
- package/templates/docker/job-claude-code/Dockerfile +34 -0
- package/templates/docker/job-claude-code/entrypoint.sh +139 -0
- package/templates/docker/job-pi-coding-agent/Dockerfile +44 -0
- package/templates/docker/job-pi-coding-agent/entrypoint.sh +163 -0
- package/templates/docker-compose.yml +63 -0
- package/templates/instrumentation.js +6 -0
- package/templates/middleware.js +1 -0
- package/templates/next.config.mjs +3 -0
- package/templates/postcss.config.mjs +5 -0
- package/templates/skills/LICENSE +21 -0
- package/templates/skills/README.md +119 -0
- package/templates/skills/brave-search/SKILL.md +79 -0
- package/templates/skills/brave-search/content.js +86 -0
- package/templates/skills/brave-search/package-lock.json +621 -0
- package/templates/skills/brave-search/package.json +14 -0
- package/templates/skills/brave-search/search.js +199 -0
- package/templates/skills/browser-tools/SKILL.md +196 -0
- package/templates/skills/browser-tools/browser-content.js +103 -0
- package/templates/skills/browser-tools/browser-cookies.js +35 -0
- package/templates/skills/browser-tools/browser-eval.js +53 -0
- package/templates/skills/browser-tools/browser-hn-scraper.js +108 -0
- package/templates/skills/browser-tools/browser-nav.js +44 -0
- package/templates/skills/browser-tools/browser-pick.js +162 -0
- package/templates/skills/browser-tools/browser-screenshot.js +34 -0
- package/templates/skills/browser-tools/browser-start.js +87 -0
- package/templates/skills/browser-tools/package-lock.json +2556 -0
- package/templates/skills/browser-tools/package.json +19 -0
- package/templates/skills/llm-secrets/SKILL.md +34 -0
- package/templates/skills/llm-secrets/llm-secrets.js +33 -0
- package/templates/skills/modify-self/SKILL.md +12 -0
package/setup/setup.mjs
ADDED
|
@@ -0,0 +1,842 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import open from 'open';
|
|
8
|
+
import * as clack from '@clack/prompts';
|
|
9
|
+
|
|
10
|
+
import { createDirLink } from './lib/fs-utils.mjs';
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
checkPrerequisites,
|
|
14
|
+
runGhAuth,
|
|
15
|
+
} from './lib/prerequisites.mjs';
|
|
16
|
+
import {
|
|
17
|
+
promptForPAT,
|
|
18
|
+
promptForProvider,
|
|
19
|
+
promptForModel,
|
|
20
|
+
promptForApiKey,
|
|
21
|
+
promptForCustomProvider,
|
|
22
|
+
promptForBraveKey,
|
|
23
|
+
confirm,
|
|
24
|
+
pressEnter,
|
|
25
|
+
maskSecret,
|
|
26
|
+
keepOrReconfigure,
|
|
27
|
+
} from './lib/prompts.mjs';
|
|
28
|
+
import { PROVIDERS } from './lib/providers.mjs';
|
|
29
|
+
import {
|
|
30
|
+
validatePAT,
|
|
31
|
+
checkPATScopes,
|
|
32
|
+
generateWebhookSecret,
|
|
33
|
+
getPATCreationURL,
|
|
34
|
+
setSecret,
|
|
35
|
+
setVariable,
|
|
36
|
+
} from './lib/github.mjs';
|
|
37
|
+
import { writeModelsJson, updateEnvVariable } from './lib/auth.mjs';
|
|
38
|
+
import { loadEnvFile } from './lib/env.mjs';
|
|
39
|
+
import { syncConfig } from './lib/sync.mjs';
|
|
40
|
+
|
|
41
|
+
const logo = `
|
|
42
|
+
_____ _ ____ ____ _
|
|
43
|
+
|_ _| |__ ___| _ \\ ___ _ __ ___| __ ) ___ | |_
|
|
44
|
+
| | | '_ \\ / _ \\ |_) / _ \\| '_ \\ / _ \\ _ \\ / _ \\| __|
|
|
45
|
+
| | | | | | __/ __/ (_) | |_) | __/ |_) | (_) | |_
|
|
46
|
+
|_| |_| |_|\\___|_| \\___/| .__/ \\___|____/ \\___/ \\__|
|
|
47
|
+
|_|
|
|
48
|
+
`;
|
|
49
|
+
|
|
50
|
+
async function main() {
|
|
51
|
+
console.log(chalk.cyan(logo));
|
|
52
|
+
clack.intro('Interactive Setup Wizard');
|
|
53
|
+
|
|
54
|
+
const TOTAL_STEPS = 8;
|
|
55
|
+
let currentStep = 0;
|
|
56
|
+
|
|
57
|
+
// Load existing .env (always exists after init — seed .env has AUTH_SECRET etc.)
|
|
58
|
+
const env = loadEnvFile();
|
|
59
|
+
|
|
60
|
+
if (env) {
|
|
61
|
+
clack.log.info('Existing .env detected — previously configured values can be skipped.');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Flat object collecting all config values for sync
|
|
65
|
+
const collected = {};
|
|
66
|
+
let owner = null;
|
|
67
|
+
let repo = null;
|
|
68
|
+
|
|
69
|
+
// ─── Step 1: Prerequisites ───────────────────────────────────────────
|
|
70
|
+
clack.log.step(`[${++currentStep}/${TOTAL_STEPS}] Checking prerequisites`);
|
|
71
|
+
clack.log.info('Your agent needs a few tools installed on your machine. Let\'s make sure everything is ready.');
|
|
72
|
+
|
|
73
|
+
const s = clack.spinner();
|
|
74
|
+
s.start('Checking system requirements...');
|
|
75
|
+
const prereqs = await checkPrerequisites();
|
|
76
|
+
s.stop('Prerequisites checked');
|
|
77
|
+
|
|
78
|
+
// Node.js
|
|
79
|
+
if (prereqs.node.ok) {
|
|
80
|
+
clack.log.success(`Node.js ${prereqs.node.version}`);
|
|
81
|
+
} else if (prereqs.node.installed) {
|
|
82
|
+
clack.log.error(`Node.js ${prereqs.node.version} (need >= 18)`);
|
|
83
|
+
clack.cancel('Please upgrade Node.js to version 18 or higher.');
|
|
84
|
+
process.exit(1);
|
|
85
|
+
} else {
|
|
86
|
+
clack.log.error('Node.js not found');
|
|
87
|
+
clack.cancel('Please install Node.js 18+: https://nodejs.org');
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Package manager
|
|
92
|
+
if (prereqs.packageManager.installed) {
|
|
93
|
+
clack.log.success(`Package manager: ${prereqs.packageManager.name}`);
|
|
94
|
+
} else {
|
|
95
|
+
clack.log.error('No package manager found (need pnpm or npm)');
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Git
|
|
100
|
+
if (!prereqs.git.installed) {
|
|
101
|
+
clack.log.error('Git not found');
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
clack.log.success('Git installed');
|
|
105
|
+
|
|
106
|
+
// gh CLI
|
|
107
|
+
if (prereqs.gh.installed) {
|
|
108
|
+
if (prereqs.gh.authenticated) {
|
|
109
|
+
clack.log.success('GitHub CLI authenticated');
|
|
110
|
+
} else {
|
|
111
|
+
clack.log.warn('GitHub CLI installed but not authenticated');
|
|
112
|
+
const shouldAuth = await confirm('Run gh auth login now?');
|
|
113
|
+
if (shouldAuth) {
|
|
114
|
+
try {
|
|
115
|
+
runGhAuth();
|
|
116
|
+
clack.log.success('GitHub CLI authenticated');
|
|
117
|
+
} catch {
|
|
118
|
+
clack.log.error('Failed to authenticate gh CLI');
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
clack.log.error('GitHub CLI authentication required');
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
clack.log.error('GitHub CLI (gh) not found');
|
|
128
|
+
const installCmd = process.platform === 'darwin'
|
|
129
|
+
? 'brew install gh'
|
|
130
|
+
: process.platform === 'win32'
|
|
131
|
+
? 'winget install GitHub.cli'
|
|
132
|
+
: 'sudo apt install gh (or see https://github.com/cli/cli#installation)';
|
|
133
|
+
clack.log.info(`Install the GitHub CLI, then re-run setup:\n\n ${installCmd}\n`);
|
|
134
|
+
clack.cancel('Missing prerequisite: gh CLI');
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Initialize git repo if needed
|
|
139
|
+
if (!prereqs.git.initialized) {
|
|
140
|
+
const initSpinner = clack.spinner();
|
|
141
|
+
initSpinner.start('Initializing git repo...');
|
|
142
|
+
execSync('git init', { stdio: 'ignore' });
|
|
143
|
+
initSpinner.stop('Git repo initialized');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (prereqs.git.remoteInfo) {
|
|
147
|
+
owner = prereqs.git.remoteInfo.owner;
|
|
148
|
+
repo = prereqs.git.remoteInfo.repo;
|
|
149
|
+
clack.log.success(`Repository: ${owner}/${repo}`);
|
|
150
|
+
} else {
|
|
151
|
+
clack.log.warn('No GitHub remote detected. We\'ll set one up.');
|
|
152
|
+
|
|
153
|
+
// Stage and commit
|
|
154
|
+
execSync('git add .', { stdio: 'ignore' });
|
|
155
|
+
try {
|
|
156
|
+
execSync('git diff --cached --quiet', { stdio: 'ignore' });
|
|
157
|
+
clack.log.success('Nothing new to commit');
|
|
158
|
+
} catch {
|
|
159
|
+
const commitSpinner = clack.spinner();
|
|
160
|
+
commitSpinner.start('Creating initial commit...');
|
|
161
|
+
execSync('git commit -m "initial commit [skip ci]"', { stdio: 'ignore' });
|
|
162
|
+
commitSpinner.stop('Created initial commit');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Ask for project name
|
|
166
|
+
const dirName = path.basename(process.cwd());
|
|
167
|
+
const projectName = await clack.text({
|
|
168
|
+
message: 'Name your project:',
|
|
169
|
+
initialValue: dirName,
|
|
170
|
+
validate: (input) => {
|
|
171
|
+
if (!input) return 'Name is required';
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
if (clack.isCancel(projectName)) {
|
|
175
|
+
clack.cancel('Setup cancelled.');
|
|
176
|
+
process.exit(0);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
clack.log.info('Create a GitHub repo:');
|
|
180
|
+
clack.log.info(' 1. Create a new private repository');
|
|
181
|
+
clack.log.info(' 2. Do NOT initialize with a README');
|
|
182
|
+
clack.log.info(' 3. Copy the HTTPS URL');
|
|
183
|
+
|
|
184
|
+
const openGitHub = await confirm('Open GitHub repo creation page in browser?');
|
|
185
|
+
if (openGitHub) {
|
|
186
|
+
await open(`https://github.com/new?name=${encodeURIComponent(projectName)}&visibility=private`);
|
|
187
|
+
clack.log.info('Opened in browser (name and private pre-filled).');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Ask for the remote URL and add it
|
|
191
|
+
let remoteAdded = false;
|
|
192
|
+
while (!remoteAdded) {
|
|
193
|
+
const remoteUrl = await clack.text({
|
|
194
|
+
message: 'Paste the HTTPS repository URL:',
|
|
195
|
+
validate: (input) => {
|
|
196
|
+
if (!input) return 'URL is required';
|
|
197
|
+
if (!input.startsWith('https://github.com/')) return 'Must be an HTTPS GitHub URL (https://github.com/...)';
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
if (clack.isCancel(remoteUrl)) {
|
|
201
|
+
clack.cancel('Setup cancelled.');
|
|
202
|
+
process.exit(0);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
const url = remoteUrl.replace(/\/$/, '').replace(/\.git$/, '') + '.git';
|
|
207
|
+
execSync(`git remote add origin "${url}"`, { stdio: 'ignore' });
|
|
208
|
+
remoteAdded = true;
|
|
209
|
+
} catch {
|
|
210
|
+
try {
|
|
211
|
+
const url = remoteUrl.replace(/\/$/, '').replace(/\.git$/, '') + '.git';
|
|
212
|
+
execSync(`git remote set-url origin "${url}"`, { stdio: 'ignore' });
|
|
213
|
+
remoteAdded = true;
|
|
214
|
+
} catch {
|
|
215
|
+
clack.log.error('Failed to set remote. Try again.');
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const { getGitRemoteInfo } = await import('./lib/prerequisites.mjs');
|
|
221
|
+
const remoteInfo = getGitRemoteInfo();
|
|
222
|
+
if (remoteInfo) {
|
|
223
|
+
owner = remoteInfo.owner;
|
|
224
|
+
repo = remoteInfo.repo;
|
|
225
|
+
clack.log.success(`Repository: ${owner}/${repo}`);
|
|
226
|
+
} else {
|
|
227
|
+
clack.log.error('Could not detect repository from remote.');
|
|
228
|
+
process.exit(1);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Add owner/repo to collected
|
|
233
|
+
collected.GH_OWNER = owner;
|
|
234
|
+
collected.GH_REPO = repo;
|
|
235
|
+
|
|
236
|
+
// Track whether we need to push after getting the PAT
|
|
237
|
+
let needsPush = false;
|
|
238
|
+
try {
|
|
239
|
+
execSync('git rev-parse --verify origin/main', { stdio: 'ignore' });
|
|
240
|
+
} catch {
|
|
241
|
+
needsPush = true;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ngrok check (informational only)
|
|
245
|
+
if (prereqs.ngrok.installed) {
|
|
246
|
+
clack.log.success('ngrok installed');
|
|
247
|
+
} else {
|
|
248
|
+
clack.log.warn('ngrok not installed (needed to expose local server)');
|
|
249
|
+
const ngrokInstallCmd = process.platform === 'win32'
|
|
250
|
+
? 'winget install ngrok.ngrok'
|
|
251
|
+
: process.platform === 'darwin'
|
|
252
|
+
? 'brew install ngrok/ngrok/ngrok'
|
|
253
|
+
: 'See https://ngrok.com/download';
|
|
254
|
+
clack.log.info(
|
|
255
|
+
`Install with: ${ngrokInstallCmd}\n` +
|
|
256
|
+
' Sign up for a free account at https://dashboard.ngrok.com/signup\n' +
|
|
257
|
+
' Then run: ngrok config add-authtoken <YOUR_TOKEN>'
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ─── Step 2: GitHub PAT ──────────────────────────────────────────────
|
|
262
|
+
clack.log.step(`[${++currentStep}/${TOTAL_STEPS}] GitHub Personal Access Token`);
|
|
263
|
+
clack.log.info('Your agent needs permission to create branches and pull requests in your GitHub repo. A Personal Access Token (PAT) grants this access.');
|
|
264
|
+
|
|
265
|
+
let pat = null;
|
|
266
|
+
if (await keepOrReconfigure('GitHub PAT', env?.GH_TOKEN ? maskSecret(env.GH_TOKEN) : null)) {
|
|
267
|
+
pat = env.GH_TOKEN;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (!pat) {
|
|
271
|
+
clack.log.info(
|
|
272
|
+
`Create a fine-grained PAT scoped to ${owner}/${repo} only:\n` +
|
|
273
|
+
` Repository access: Only select repositories > ${owner}/${repo}\n` +
|
|
274
|
+
' Actions: Read and write\n' +
|
|
275
|
+
' Administration: Read and write (required for self-hosted runners)\n' +
|
|
276
|
+
' Contents: Read and write\n' +
|
|
277
|
+
' Metadata: Read-only (required, auto-selected)\n' +
|
|
278
|
+
' Pull requests: Read and write\n' +
|
|
279
|
+
' Workflows: Read and write'
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
const openPATPage = await confirm('Open GitHub PAT creation page in browser?');
|
|
283
|
+
if (openPATPage) {
|
|
284
|
+
await open(getPATCreationURL());
|
|
285
|
+
clack.log.info(`Opened in browser. Scope it to ${owner}/${repo} only.`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
let patValid = false;
|
|
289
|
+
while (!patValid) {
|
|
290
|
+
pat = await promptForPAT();
|
|
291
|
+
|
|
292
|
+
const validateSpinner = clack.spinner();
|
|
293
|
+
validateSpinner.start('Validating PAT...');
|
|
294
|
+
const validation = await validatePAT(pat);
|
|
295
|
+
|
|
296
|
+
if (!validation.valid) {
|
|
297
|
+
validateSpinner.stop(`Invalid PAT: ${validation.error}`);
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const scopes = await checkPATScopes(pat);
|
|
302
|
+
if (!scopes.hasRepo || !scopes.hasWorkflow) {
|
|
303
|
+
validateSpinner.stop('PAT missing required scopes');
|
|
304
|
+
clack.log.info(`Found scopes: ${scopes.scopes.join(', ') || 'none'}`);
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (scopes.isFineGrained) {
|
|
309
|
+
validateSpinner.stop(`Fine-grained PAT valid for user: ${validation.user}`);
|
|
310
|
+
} else {
|
|
311
|
+
validateSpinner.stop(`PAT valid for user: ${validation.user}`);
|
|
312
|
+
}
|
|
313
|
+
patValid = true;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
collected.GH_TOKEN = pat;
|
|
318
|
+
|
|
319
|
+
// Push to GitHub now that we have the PAT
|
|
320
|
+
if (needsPush) {
|
|
321
|
+
const remote = execSync('git remote get-url origin', { encoding: 'utf-8' }).trim();
|
|
322
|
+
|
|
323
|
+
let pushed = false;
|
|
324
|
+
while (!pushed) {
|
|
325
|
+
const authedUrl = remote.replace('https://github.com/', `https://x-access-token:${pat}@github.com/`);
|
|
326
|
+
execSync(`git remote set-url origin "${authedUrl}"`, { stdio: 'ignore' });
|
|
327
|
+
|
|
328
|
+
const pushSpinner = clack.spinner();
|
|
329
|
+
pushSpinner.start('Pushing to GitHub...');
|
|
330
|
+
try {
|
|
331
|
+
execSync('git branch -M main', { stdio: 'ignore' });
|
|
332
|
+
execSync('git push -u origin main 2>&1', { encoding: 'utf-8' });
|
|
333
|
+
pushSpinner.stop('Pushed to GitHub');
|
|
334
|
+
pushed = true;
|
|
335
|
+
} catch (err) {
|
|
336
|
+
pushSpinner.stop('Failed to push');
|
|
337
|
+
const output = (err.stdout || '') + (err.stderr || '');
|
|
338
|
+
if (output) clack.log.error(output.trim());
|
|
339
|
+
execSync(`git remote set-url origin "${remote}"`, { stdio: 'ignore' });
|
|
340
|
+
clack.log.info('Your PAT may not have write access to this repository.');
|
|
341
|
+
pat = await promptForPAT();
|
|
342
|
+
collected.GH_TOKEN = pat;
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Reset remote URL back to clean HTTPS (no token embedded)
|
|
347
|
+
execSync(`git remote set-url origin "${remote}"`, { stdio: 'ignore' });
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ─── Step 3: API Keys ────────────────────────────────────────────────
|
|
352
|
+
clack.log.step(`[${++currentStep}/${TOTAL_STEPS}] API Keys`);
|
|
353
|
+
clack.log.info('Your agent uses a large language model (LLM) to think and write code. You\'ll choose a provider and enter an API key from them.');
|
|
354
|
+
|
|
355
|
+
// Step 3a: Chat LLM (event handler)
|
|
356
|
+
let chatProvider = null;
|
|
357
|
+
let chatModel = null;
|
|
358
|
+
let openaiBaseUrl = null;
|
|
359
|
+
|
|
360
|
+
// Agent LLM overrides (only set when user chooses different agent config)
|
|
361
|
+
let agentProvider = null;
|
|
362
|
+
let agentModel = null;
|
|
363
|
+
|
|
364
|
+
// Build display string for existing LLM config
|
|
365
|
+
let llmDisplay = null;
|
|
366
|
+
if (env?.LLM_PROVIDER && env?.LLM_MODEL) {
|
|
367
|
+
const existingEnvKey = env.LLM_PROVIDER === 'custom'
|
|
368
|
+
? 'CUSTOM_API_KEY'
|
|
369
|
+
: PROVIDERS[env.LLM_PROVIDER]?.envKey;
|
|
370
|
+
|
|
371
|
+
if (existingEnvKey) {
|
|
372
|
+
const existingKey = env[existingEnvKey];
|
|
373
|
+
const providerLabel = env.LLM_PROVIDER === 'custom'
|
|
374
|
+
? 'Local (OpenAI Compatible API)'
|
|
375
|
+
: (PROVIDERS[env.LLM_PROVIDER]?.label || env.LLM_PROVIDER);
|
|
376
|
+
llmDisplay = existingKey
|
|
377
|
+
? `${providerLabel} / ${env.LLM_MODEL} (${maskSecret(existingKey)})`
|
|
378
|
+
: `${providerLabel} / ${env.LLM_MODEL}`;
|
|
379
|
+
if ((env.LLM_PROVIDER === 'openai' || env.LLM_PROVIDER === 'custom') && env.OPENAI_BASE_URL) {
|
|
380
|
+
llmDisplay += ` @ ${env.OPENAI_BASE_URL}`;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (llmDisplay && await keepOrReconfigure('LLM', llmDisplay)) {
|
|
386
|
+
// Keep existing LLM config
|
|
387
|
+
chatProvider = env.LLM_PROVIDER;
|
|
388
|
+
chatModel = env.LLM_MODEL;
|
|
389
|
+
const existingEnvKey = chatProvider === 'custom'
|
|
390
|
+
? 'CUSTOM_API_KEY'
|
|
391
|
+
: PROVIDERS[chatProvider].envKey;
|
|
392
|
+
collected.LLM_PROVIDER = chatProvider;
|
|
393
|
+
collected.LLM_MODEL = chatModel;
|
|
394
|
+
collected[existingEnvKey] = env[existingEnvKey] || '';
|
|
395
|
+
if (env.OPENAI_BASE_URL) {
|
|
396
|
+
openaiBaseUrl = env.OPENAI_BASE_URL;
|
|
397
|
+
collected.OPENAI_BASE_URL = openaiBaseUrl;
|
|
398
|
+
}
|
|
399
|
+
} else {
|
|
400
|
+
// Prompt for new LLM config
|
|
401
|
+
clack.log.info('Choose the LLM provider for your bot.');
|
|
402
|
+
|
|
403
|
+
chatProvider = await promptForProvider();
|
|
404
|
+
|
|
405
|
+
if (chatProvider === 'custom') {
|
|
406
|
+
clack.log.info('If the model runs on this machine, use http://host.docker.internal:<port>/v1');
|
|
407
|
+
clack.log.info('instead of localhost (localhost won\'t work from inside Docker)');
|
|
408
|
+
clack.log.info('Ollama example: http://host.docker.internal:11434/v1');
|
|
409
|
+
const custom = await promptForCustomProvider();
|
|
410
|
+
chatModel = custom.model;
|
|
411
|
+
openaiBaseUrl = custom.baseUrl;
|
|
412
|
+
writeModelsJson('custom', {
|
|
413
|
+
baseUrl: custom.baseUrl,
|
|
414
|
+
apiKey: 'CUSTOM_API_KEY',
|
|
415
|
+
api: 'openai-completions',
|
|
416
|
+
models: [custom.model],
|
|
417
|
+
});
|
|
418
|
+
collected.CUSTOM_API_KEY = custom.apiKey || '';
|
|
419
|
+
collected.OPENAI_BASE_URL = openaiBaseUrl;
|
|
420
|
+
clack.log.success(`Custom provider configured: ${custom.model} @ ${custom.baseUrl}`);
|
|
421
|
+
if (custom.apiKey) {
|
|
422
|
+
clack.log.success(`API key added (${maskSecret(custom.apiKey)})`);
|
|
423
|
+
}
|
|
424
|
+
} else {
|
|
425
|
+
const providerConfig = PROVIDERS[chatProvider];
|
|
426
|
+
chatModel = await promptForModel(chatProvider, { defaultModelId: 'claude-sonnet-4-6' });
|
|
427
|
+
const chatApiKey = await promptForApiKey(chatProvider);
|
|
428
|
+
collected[providerConfig.envKey] = chatApiKey;
|
|
429
|
+
|
|
430
|
+
// Non-builtin providers need models.json
|
|
431
|
+
if (!providerConfig.builtin) {
|
|
432
|
+
writeModelsJson(chatProvider, {
|
|
433
|
+
baseUrl: providerConfig.baseUrl,
|
|
434
|
+
apiKey: providerConfig.envKey,
|
|
435
|
+
api: providerConfig.api,
|
|
436
|
+
models: providerConfig.models.map((m) => m.id),
|
|
437
|
+
});
|
|
438
|
+
clack.log.success(`Generated .pi/agent/models.json for ${providerConfig.name}`);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
clack.log.success(`${providerConfig.name} key added (${maskSecret(chatApiKey)})`);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
collected.LLM_PROVIDER = chatProvider;
|
|
445
|
+
collected.LLM_MODEL = chatModel;
|
|
446
|
+
|
|
447
|
+
if (chatProvider === 'custom') {
|
|
448
|
+
collected.RUNS_ON = 'self-hosted';
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Re-run: reconfigure existing OPENAI_BASE_URL if provider was kept
|
|
453
|
+
if ((chatProvider === 'openai' || chatProvider === 'custom') && env?.OPENAI_BASE_URL && !collected.OPENAI_BASE_URL) {
|
|
454
|
+
if (!await keepOrReconfigure('Custom LLM URL', env.OPENAI_BASE_URL)) {
|
|
455
|
+
clack.log.info('If the model runs on this machine, use http://host.docker.internal:<port>/v1');
|
|
456
|
+
clack.log.info('instead of localhost (localhost won\'t work from inside Docker)');
|
|
457
|
+
clack.log.info('Ollama example: http://host.docker.internal:11434/v1');
|
|
458
|
+
const baseUrl = await clack.text({
|
|
459
|
+
message: 'API base URL:',
|
|
460
|
+
validate: (input) => {
|
|
461
|
+
if (!input) return 'URL is required';
|
|
462
|
+
if (!input.startsWith('http://') && !input.startsWith('https://')) {
|
|
463
|
+
return 'URL must start with http:// or https://';
|
|
464
|
+
}
|
|
465
|
+
},
|
|
466
|
+
});
|
|
467
|
+
if (clack.isCancel(baseUrl)) {
|
|
468
|
+
clack.cancel('Setup cancelled.');
|
|
469
|
+
process.exit(0);
|
|
470
|
+
}
|
|
471
|
+
openaiBaseUrl = baseUrl;
|
|
472
|
+
collected.OPENAI_BASE_URL = openaiBaseUrl;
|
|
473
|
+
clack.log.success(`Custom base URL: ${openaiBaseUrl}`);
|
|
474
|
+
} else {
|
|
475
|
+
openaiBaseUrl = env.OPENAI_BASE_URL;
|
|
476
|
+
collected.OPENAI_BASE_URL = openaiBaseUrl;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Step 3b: Separate agent LLM settings
|
|
481
|
+
const useDifferentAgent = await confirm(
|
|
482
|
+
'Would you like agent jobs to use different LLM settings?\n (Required if you want to use a Claude Pro/Max subscription for agent jobs)',
|
|
483
|
+
false
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
if (useDifferentAgent) {
|
|
487
|
+
clack.log.info('Choose the LLM provider for agent jobs.');
|
|
488
|
+
|
|
489
|
+
agentProvider = await promptForProvider();
|
|
490
|
+
|
|
491
|
+
if (agentProvider === 'custom') {
|
|
492
|
+
// Custom/local agent — prompt for model ID directly
|
|
493
|
+
const customModel = await clack.text({
|
|
494
|
+
message: 'Enter agent model ID (e.g., qwen3:8b):',
|
|
495
|
+
validate: (input) => { if (!input) return 'Model ID is required'; },
|
|
496
|
+
});
|
|
497
|
+
if (clack.isCancel(customModel)) { clack.cancel('Setup cancelled.'); process.exit(0); }
|
|
498
|
+
agentModel = customModel;
|
|
499
|
+
collected.RUNS_ON = 'self-hosted';
|
|
500
|
+
} else {
|
|
501
|
+
const agentProviderConfig = PROVIDERS[agentProvider];
|
|
502
|
+
agentModel = await promptForModel(agentProvider);
|
|
503
|
+
|
|
504
|
+
// Collect agent API key if different provider than chat
|
|
505
|
+
if (agentProvider !== chatProvider) {
|
|
506
|
+
const agentApiKey = await promptForApiKey(agentProvider);
|
|
507
|
+
// Set agent API key as a GitHub secret directly — not added to collected
|
|
508
|
+
// to avoid polluting .env with a key the event handler doesn't use
|
|
509
|
+
collected['__agentApiKey'] = { provider: agentProvider, key: agentApiKey, secretName: `AGENT_${agentProviderConfig.envKey}` };
|
|
510
|
+
clack.log.success(`Agent ${agentProviderConfig.name} key added (${maskSecret(agentApiKey)})`);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// OAuth prompt — only when agent provider is Anthropic
|
|
514
|
+
if (agentProviderConfig.oauthSupported) {
|
|
515
|
+
let skipOAuth = false;
|
|
516
|
+
if (env?.CLAUDE_CODE_OAUTH_TOKEN) {
|
|
517
|
+
skipOAuth = await keepOrReconfigure(
|
|
518
|
+
'Claude OAuth Token',
|
|
519
|
+
`${maskSecret(env.CLAUDE_CODE_OAUTH_TOKEN)} (agent backend: ${env.AGENT_BACKEND || 'claude-code'})`
|
|
520
|
+
);
|
|
521
|
+
if (skipOAuth) {
|
|
522
|
+
collected.CLAUDE_CODE_OAUTH_TOKEN = env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
523
|
+
collected.AGENT_BACKEND = env.AGENT_BACKEND || 'claude-code';
|
|
524
|
+
|
|
525
|
+
// OAuth replaces the API key for agent jobs — don't push it to GitHub.
|
|
526
|
+
if (collected.ANTHROPIC_API_KEY) {
|
|
527
|
+
updateEnvVariable('ANTHROPIC_API_KEY', collected.ANTHROPIC_API_KEY);
|
|
528
|
+
delete collected.ANTHROPIC_API_KEY;
|
|
529
|
+
}
|
|
530
|
+
delete collected['__agentApiKey'];
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (!skipOAuth) {
|
|
535
|
+
const hasSub = await confirm('Do you have a Claude Pro or Max subscription?', false);
|
|
536
|
+
|
|
537
|
+
if (hasSub) {
|
|
538
|
+
clack.log.info(
|
|
539
|
+
'You can use your subscription for agent jobs instead of API credits.\n' +
|
|
540
|
+
' This switches your job runner from Pi to Claude Code CLI.\n' +
|
|
541
|
+
' See docs/CLAUDE_CODE_VS_PI.md for details.\n\n' +
|
|
542
|
+
' Your API key will only be saved locally for chat — it won\'t be\n' +
|
|
543
|
+
' pushed to GitHub since agent jobs will use the OAuth token instead.'
|
|
544
|
+
);
|
|
545
|
+
|
|
546
|
+
// Check if claude CLI is installed
|
|
547
|
+
let claudeInstalled = false;
|
|
548
|
+
try {
|
|
549
|
+
execSync('command -v claude', { stdio: 'ignore' });
|
|
550
|
+
claudeInstalled = true;
|
|
551
|
+
} catch {}
|
|
552
|
+
|
|
553
|
+
if (claudeInstalled) {
|
|
554
|
+
clack.log.info(
|
|
555
|
+
'Generate your token by running this in another terminal:\n\n' +
|
|
556
|
+
' claude setup-token\n\n' +
|
|
557
|
+
' This opens your browser to authenticate with your Claude account.\n' +
|
|
558
|
+
' After auth, a 1-year token is printed to your terminal.'
|
|
559
|
+
);
|
|
560
|
+
} else {
|
|
561
|
+
clack.log.info(
|
|
562
|
+
'First, install the Claude Code CLI:\n\n' +
|
|
563
|
+
' npm install -g @anthropic-ai/claude-code\n\n' +
|
|
564
|
+
' Then run:\n\n' +
|
|
565
|
+
' claude setup-token\n\n' +
|
|
566
|
+
' This opens your browser to authenticate with your Claude account.\n' +
|
|
567
|
+
' After auth, a 1-year token is printed to your terminal.'
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
let oauthToken = null;
|
|
572
|
+
while (!oauthToken) {
|
|
573
|
+
const tokenInput = await clack.password({
|
|
574
|
+
message: 'Paste your token here (starts with sk-ant-oat01-):',
|
|
575
|
+
validate: (input) => {
|
|
576
|
+
if (!input) return 'Token is required (or press Ctrl+C to skip)';
|
|
577
|
+
if (!input.startsWith('sk-ant-oat01-')) return 'Token must start with sk-ant-oat01-';
|
|
578
|
+
},
|
|
579
|
+
});
|
|
580
|
+
if (clack.isCancel(tokenInput)) {
|
|
581
|
+
clack.log.info('Skipped OAuth — agent jobs will use Pi with API credits.');
|
|
582
|
+
break;
|
|
583
|
+
}
|
|
584
|
+
oauthToken = tokenInput.trim();
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (oauthToken) {
|
|
588
|
+
collected.CLAUDE_CODE_OAUTH_TOKEN = oauthToken;
|
|
589
|
+
collected.AGENT_BACKEND = 'claude-code';
|
|
590
|
+
|
|
591
|
+
// OAuth replaces the API key for agent jobs — don't push it to GitHub.
|
|
592
|
+
// The key is still needed in .env for the event handler chat, so write it directly.
|
|
593
|
+
if (collected.ANTHROPIC_API_KEY) {
|
|
594
|
+
updateEnvVariable('ANTHROPIC_API_KEY', collected.ANTHROPIC_API_KEY);
|
|
595
|
+
delete collected.ANTHROPIC_API_KEY;
|
|
596
|
+
}
|
|
597
|
+
delete collected['__agentApiKey'];
|
|
598
|
+
|
|
599
|
+
clack.log.success(`Claude OAuth token added (${maskSecret(oauthToken)})`);
|
|
600
|
+
clack.log.info('Agent jobs will use Claude Code CLI with your subscription.');
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Step 3c: Brave Search (optional — not in .env, always ask)
|
|
609
|
+
const braveKey = await promptForBraveKey();
|
|
610
|
+
if (braveKey) {
|
|
611
|
+
collected.BRAVE_API_KEY = braveKey;
|
|
612
|
+
clack.log.success(`Brave Search key added (${maskSecret(braveKey)})`);
|
|
613
|
+
|
|
614
|
+
// Enable brave-search skill symlink
|
|
615
|
+
const braveSymlink = path.join(process.cwd(), 'skills', 'active', 'brave-search');
|
|
616
|
+
if (!fs.existsSync(braveSymlink)) {
|
|
617
|
+
fs.mkdirSync(path.dirname(braveSymlink), { recursive: true });
|
|
618
|
+
createDirLink('../brave-search', braveSymlink);
|
|
619
|
+
clack.log.success('Enabled brave-search skill');
|
|
620
|
+
|
|
621
|
+
// Commit and push the symlink so the Docker agent can use it
|
|
622
|
+
try {
|
|
623
|
+
execSync('git add skills/active/brave-search', { stdio: 'ignore' });
|
|
624
|
+
execSync('git commit -m "enable brave-search skill [no ci]"', { stdio: 'ignore' });
|
|
625
|
+
const remote = execSync('git remote get-url origin', { encoding: 'utf-8' }).trim();
|
|
626
|
+
const authedUrl = remote.replace('https://github.com/', `https://x-access-token:${pat}@github.com/`);
|
|
627
|
+
execSync(`git remote set-url origin "${authedUrl}"`, { stdio: 'ignore' });
|
|
628
|
+
execSync('git push origin main', { stdio: 'ignore' });
|
|
629
|
+
execSync(`git remote set-url origin "${remote}"`, { stdio: 'ignore' });
|
|
630
|
+
clack.log.success('Pushed brave-search skill to GitHub');
|
|
631
|
+
} catch {
|
|
632
|
+
clack.log.warn('Could not push brave-search symlink — you may need to push manually');
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// ─── Step 4: App URL ─────────────────────────────────────────────────
|
|
638
|
+
clack.log.step(`[${++currentStep}/${TOTAL_STEPS}] App URL`);
|
|
639
|
+
clack.log.info('Your agent runs as a web server that receives notifications from GitHub when jobs finish. It needs a public URL to receive those webhooks.');
|
|
640
|
+
|
|
641
|
+
let appUrl = null;
|
|
642
|
+
|
|
643
|
+
if (await keepOrReconfigure('APP_URL', env?.APP_URL || null)) {
|
|
644
|
+
appUrl = env.APP_URL;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
if (!appUrl) {
|
|
648
|
+
clack.log.info(
|
|
649
|
+
'Your app needs a public URL so GitHub can send webhook notifications.\n' +
|
|
650
|
+
' Examples:\n' +
|
|
651
|
+
' ngrok: https://abc123.ngrok.io\n' +
|
|
652
|
+
' VPS: https://mybot.example.com\n' +
|
|
653
|
+
' PaaS: https://mybot.vercel.app'
|
|
654
|
+
);
|
|
655
|
+
|
|
656
|
+
while (!appUrl) {
|
|
657
|
+
const urlInput = await clack.text({
|
|
658
|
+
message: 'Enter your APP_URL (https://...):',
|
|
659
|
+
validate: (input) => {
|
|
660
|
+
if (!input) return 'URL is required';
|
|
661
|
+
if (!input.startsWith('https://')) return 'URL must start with https://';
|
|
662
|
+
},
|
|
663
|
+
});
|
|
664
|
+
if (clack.isCancel(urlInput)) {
|
|
665
|
+
clack.cancel('Setup cancelled.');
|
|
666
|
+
process.exit(0);
|
|
667
|
+
}
|
|
668
|
+
appUrl = urlInput.replace(/\/$/, '');
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
collected.APP_URL = appUrl;
|
|
673
|
+
collected.APP_HOSTNAME = new URL(appUrl).hostname;
|
|
674
|
+
|
|
675
|
+
// Generate GH_WEBHOOK_SECRET if missing
|
|
676
|
+
collected.GH_WEBHOOK_SECRET = env?.GH_WEBHOOK_SECRET || generateWebhookSecret();
|
|
677
|
+
|
|
678
|
+
// ─── Step 5: Sync Config ─────────────────────────────────────────────
|
|
679
|
+
clack.log.step(`[${++currentStep}/${TOTAL_STEPS}] Sync config`);
|
|
680
|
+
|
|
681
|
+
if (!owner || !repo) {
|
|
682
|
+
clack.log.warn('Could not detect repository. Please enter manually.');
|
|
683
|
+
const ownerInput = await clack.text({ message: 'GitHub owner/org:' });
|
|
684
|
+
if (clack.isCancel(ownerInput)) { clack.cancel('Setup cancelled.'); process.exit(0); }
|
|
685
|
+
owner = ownerInput;
|
|
686
|
+
const repoInput = await clack.text({ message: 'Repository name:' });
|
|
687
|
+
if (clack.isCancel(repoInput)) { clack.cancel('Setup cancelled.'); process.exit(0); }
|
|
688
|
+
repo = repoInput;
|
|
689
|
+
collected.GH_OWNER = owner;
|
|
690
|
+
collected.GH_REPO = repo;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// Extract agent API key info before sync (not a real config target)
|
|
694
|
+
const agentApiKeyInfo = collected['__agentApiKey'];
|
|
695
|
+
delete collected['__agentApiKey'];
|
|
696
|
+
|
|
697
|
+
const report = await syncConfig(env, collected, { owner, repo });
|
|
698
|
+
|
|
699
|
+
// If agent uses a different model/provider, overwrite the GitHub variable
|
|
700
|
+
// (.env keeps chatModel for the event handler, GitHub variable gets agentModel for jobs)
|
|
701
|
+
if (agentModel && agentModel !== chatModel) {
|
|
702
|
+
await setVariable(owner, repo, 'LLM_MODEL', agentModel);
|
|
703
|
+
}
|
|
704
|
+
if (agentProvider && agentProvider !== chatProvider) {
|
|
705
|
+
await setVariable(owner, repo, 'LLM_PROVIDER', agentProvider);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// Set agent API key as a separate GitHub secret (not in .env)
|
|
709
|
+
if (agentApiKeyInfo) {
|
|
710
|
+
const s2 = clack.spinner();
|
|
711
|
+
s2.start('Setting agent API key secret...');
|
|
712
|
+
const result = await setSecret(owner, repo, agentApiKeyInfo.secretName, agentApiKeyInfo.key);
|
|
713
|
+
if (result.success) {
|
|
714
|
+
s2.stop(`Agent secret ${agentApiKeyInfo.secretName} set`);
|
|
715
|
+
report.secrets.push(agentApiKeyInfo.secretName);
|
|
716
|
+
} else {
|
|
717
|
+
s2.stop(`Failed to set agent secret: ${result.error}`);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
clack.log.info('Your agent includes a web chat interface at your APP_URL.');
|
|
722
|
+
|
|
723
|
+
// ─── Step 6: Build ──────────────────────────────────────────────────
|
|
724
|
+
clack.log.step(`[${++currentStep}/${TOTAL_STEPS}] Build`);
|
|
725
|
+
|
|
726
|
+
// Helper: run build with retry on failure
|
|
727
|
+
async function runBuildWithRetry() {
|
|
728
|
+
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
729
|
+
try {
|
|
730
|
+
fs.rmSync(path.join(process.cwd(), '.next'), { recursive: true, force: true });
|
|
731
|
+
execSync('npm run build', { stdio: 'inherit' });
|
|
732
|
+
clack.log.success('Build complete');
|
|
733
|
+
return true;
|
|
734
|
+
} catch {
|
|
735
|
+
if (attempt === 1) {
|
|
736
|
+
clack.log.error('Build failed.');
|
|
737
|
+
const retry = await confirm('Retry build?');
|
|
738
|
+
if (!retry) break;
|
|
739
|
+
} else {
|
|
740
|
+
clack.log.error('Build failed again.');
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
clack.log.error(
|
|
745
|
+
'Cannot continue without a successful build.\n' +
|
|
746
|
+
' Fix the error above, then run:\n\n' +
|
|
747
|
+
' npm run build'
|
|
748
|
+
);
|
|
749
|
+
process.exit(1);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
const hasExistingBuild = fs.existsSync(path.join(process.cwd(), '.next'));
|
|
753
|
+
|
|
754
|
+
if (hasExistingBuild) {
|
|
755
|
+
if (await confirm('Existing build found. Rebuild?')) {
|
|
756
|
+
clack.log.info('Building Next.js...');
|
|
757
|
+
await runBuildWithRetry();
|
|
758
|
+
} else {
|
|
759
|
+
clack.log.info('Skipping build');
|
|
760
|
+
}
|
|
761
|
+
} else {
|
|
762
|
+
clack.log.info('Building Next.js...');
|
|
763
|
+
await runBuildWithRetry();
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// ─── Step 7: Start Server ─────────────────────────────────────────────
|
|
767
|
+
clack.log.step(`[${++currentStep}/${TOTAL_STEPS}] Start Server`);
|
|
768
|
+
|
|
769
|
+
let serverRunning = false;
|
|
770
|
+
try {
|
|
771
|
+
await fetch('http://localhost:80/api/ping', {
|
|
772
|
+
method: 'GET',
|
|
773
|
+
signal: AbortSignal.timeout(3000),
|
|
774
|
+
});
|
|
775
|
+
serverRunning = true;
|
|
776
|
+
} catch {
|
|
777
|
+
// Server not reachable
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
if (serverRunning) {
|
|
781
|
+
if (await confirm('Server is already running. Restart?')) {
|
|
782
|
+
clack.log.info('Restarting server...');
|
|
783
|
+
try {
|
|
784
|
+
execSync('docker compose down && docker compose up -d', { stdio: 'inherit' });
|
|
785
|
+
clack.log.success('Server restarted');
|
|
786
|
+
} catch {
|
|
787
|
+
clack.log.warn('Failed to restart. Run manually: docker compose down && docker compose up -d');
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
} else {
|
|
791
|
+
clack.log.info('Starting server...');
|
|
792
|
+
try {
|
|
793
|
+
execSync('docker compose up -d', { stdio: 'inherit' });
|
|
794
|
+
clack.log.success('Server started');
|
|
795
|
+
} catch {
|
|
796
|
+
clack.log.warn('Failed to start. Run manually: docker compose up -d');
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
clack.log.info(`Server starting — visit ${appUrl} (may take 10-20 seconds to load)`);
|
|
801
|
+
|
|
802
|
+
// ─── Step 8: Summary ─────────────────────────────────────────────────
|
|
803
|
+
clack.log.step(`[${++currentStep}/${TOTAL_STEPS}] Setup Complete!`);
|
|
804
|
+
|
|
805
|
+
const chatProviderLabel = chatProvider === 'custom' ? 'Local (OpenAI Compatible API)' : PROVIDERS[chatProvider].label;
|
|
806
|
+
|
|
807
|
+
let summary = '';
|
|
808
|
+
summary += `Repository: ${owner}/${repo}\n`;
|
|
809
|
+
summary += `App URL: ${appUrl}\n`;
|
|
810
|
+
|
|
811
|
+
if (agentProvider || agentModel) {
|
|
812
|
+
const agentProviderLabel = agentProvider
|
|
813
|
+
? (agentProvider === 'custom' ? 'Local (OpenAI Compatible API)' : PROVIDERS[agentProvider].label)
|
|
814
|
+
: chatProviderLabel;
|
|
815
|
+
const agentModelDisplay = agentModel || chatModel;
|
|
816
|
+
summary += `Chat LLM: ${chatProviderLabel} (${chatModel}) [.env]\n`;
|
|
817
|
+
summary += `Agent LLM: ${agentProviderLabel} (${agentModelDisplay}) [GitHub var]\n`;
|
|
818
|
+
} else {
|
|
819
|
+
summary += `LLM: ${chatProviderLabel} (${chatModel})\n`;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
if (collected.AGENT_BACKEND) {
|
|
823
|
+
summary += `Agent Runner: ${collected.AGENT_BACKEND === 'claude-code' ? 'Claude Code CLI (subscription)' : 'Pi Coding Agent (API credits)'}\n`;
|
|
824
|
+
}
|
|
825
|
+
summary += `GitHub PAT: ${maskSecret(pat)}`;
|
|
826
|
+
|
|
827
|
+
clack.note(summary, 'Configuration');
|
|
828
|
+
|
|
829
|
+
if (report.secrets.length > 0) {
|
|
830
|
+
clack.log.info(`GitHub secrets set: ${report.secrets.join(', ')}`);
|
|
831
|
+
}
|
|
832
|
+
if (report.variables.length > 0) {
|
|
833
|
+
clack.log.info(`GitHub variables set: ${report.variables.join(', ')}`);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
clack.outro(`Chat with your agent at ${appUrl}`);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
main().catch((error) => {
|
|
840
|
+
clack.log.error(`Setup failed: ${error.message}`);
|
|
841
|
+
process.exit(1);
|
|
842
|
+
});
|