apexbot 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.
@@ -0,0 +1,148 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SafetyChecker = void 0;
4
+ const generative_ai_1 = require("@google/generative-ai");
5
+ const eventBus_1 = require("../eventBus");
6
+ /**
7
+ * LLM-based safety checker for market descriptions.
8
+ * Uses Google Gemini API.
9
+ * Analyzes market text for:
10
+ * - Ambiguous resolution criteria
11
+ * - Potential for bad-faith resolution
12
+ * - Misleading question framing
13
+ * - Political/controversial topics with unclear outcomes
14
+ */
15
+ class SafetyChecker {
16
+ client;
17
+ cfg;
18
+ cache = new Map();
19
+ constructor(cfg) {
20
+ this.cfg = {
21
+ model: 'gemini-2.0-flash',
22
+ cacheEnabled: true,
23
+ ...cfg,
24
+ };
25
+ this.client = new generative_ai_1.GoogleGenerativeAI(cfg.googleApiKey);
26
+ }
27
+ async checkMarket(marketId, question, description, creatorId) {
28
+ const cacheKey = `${marketId}:${question.slice(0, 50)}`;
29
+ if (this.cfg.cacheEnabled && this.cache.has(cacheKey)) {
30
+ return this.cache.get(cacheKey);
31
+ }
32
+ const prompt = `You are a prediction market analyst evaluating market quality and safety for automated trading.
33
+
34
+ Analyze this market and identify potential issues:
35
+
36
+ **Question:** ${question}
37
+
38
+ **Description:** ${description || '(no description provided)'}
39
+
40
+ **Creator ID:** ${creatorId || 'unknown'}
41
+
42
+ Evaluate for these risks:
43
+ 1. AMBIGUOUS RESOLUTION: Are the resolution criteria clear and objective?
44
+ 2. BAD FAITH RISK: Could the creator resolve this unfairly?
45
+ 3. MISLEADING FRAMING: Is the question misleading or loaded?
46
+ 4. EXTERNAL DEPENDENCY: Does resolution depend on hard-to-verify information?
47
+ 5. TIME RISK: Is there a clear end date? Could it drag on indefinitely?
48
+
49
+ Respond in this exact JSON format:
50
+ {
51
+ "safe": true/false,
52
+ "score": 0.0-1.0,
53
+ "warnings": ["warning1", "warning2"],
54
+ "reasoning": "brief explanation"
55
+ }
56
+
57
+ Only output the JSON, nothing else.`;
58
+ try {
59
+ const model = this.client.getGenerativeModel({ model: this.cfg.model });
60
+ const result = await model.generateContent(prompt);
61
+ const response = await result.response;
62
+ const text = response.text();
63
+ const parsed = this.parseResponse(text, marketId);
64
+ if (this.cfg.cacheEnabled) {
65
+ this.cache.set(cacheKey, parsed);
66
+ }
67
+ // Emit warning if unsafe
68
+ if (!parsed.safe || parsed.score < 0.5) {
69
+ (0, eventBus_1.emit)('safety:warning', {
70
+ marketId,
71
+ question,
72
+ reason: parsed.warnings.join('; ') || parsed.reasoning,
73
+ score: parsed.score,
74
+ });
75
+ }
76
+ return parsed;
77
+ }
78
+ catch (e) {
79
+ console.error('Safety check failed:', e);
80
+ // Return conservative result on error
81
+ return {
82
+ safe: false,
83
+ score: 0,
84
+ warnings: ['Safety check API error'],
85
+ reasoning: 'Could not complete safety analysis',
86
+ };
87
+ }
88
+ }
89
+ parseResponse(text, marketId) {
90
+ try {
91
+ // Extract JSON from response (handle markdown code blocks)
92
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
93
+ if (!jsonMatch)
94
+ throw new Error('No JSON found');
95
+ const parsed = JSON.parse(jsonMatch[0]);
96
+ return {
97
+ safe: Boolean(parsed.safe),
98
+ score: Math.max(0, Math.min(1, Number(parsed.score) || 0)),
99
+ warnings: Array.isArray(parsed.warnings) ? parsed.warnings : [],
100
+ reasoning: String(parsed.reasoning || ''),
101
+ };
102
+ }
103
+ catch (e) {
104
+ console.error(`Failed to parse safety response for ${marketId}:`, text);
105
+ return {
106
+ safe: false,
107
+ score: 0.3,
108
+ warnings: ['Could not parse safety analysis'],
109
+ reasoning: text.slice(0, 200),
110
+ };
111
+ }
112
+ }
113
+ /**
114
+ * Quick heuristic check (no API call).
115
+ * Returns true if market passes basic sanity checks.
116
+ */
117
+ quickCheck(question, description) {
118
+ const issues = [];
119
+ // Check for common red flags
120
+ if (!description || description.length < 20) {
121
+ issues.push('No or very short description');
122
+ }
123
+ if (question.includes('?') === false) {
124
+ issues.push('Question mark missing - may not be a clear question');
125
+ }
126
+ const redFlags = [
127
+ /i will resolve/i,
128
+ /resolve.*my (discretion|judgment)/i,
129
+ /subjective/i,
130
+ /vibe|feeling|think/i,
131
+ /probably|maybe|might/i,
132
+ ];
133
+ for (const flag of redFlags) {
134
+ if (flag.test(description)) {
135
+ issues.push(`Red flag pattern: ${flag.toString()}`);
136
+ }
137
+ }
138
+ return {
139
+ pass: issues.length === 0,
140
+ issues,
141
+ };
142
+ }
143
+ clearCache() {
144
+ this.cache.clear();
145
+ }
146
+ }
147
+ exports.SafetyChecker = SafetyChecker;
148
+ exports.default = SafetyChecker;
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ /**
3
+ * Session Manager - handles conversation sessions
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.SessionManager = void 0;
7
+ class SessionManager {
8
+ sessions = new Map();
9
+ maxHistoryLength = 50;
10
+ makeId(channel, peer, isGroup, groupId) {
11
+ if (isGroup && groupId) {
12
+ return `${channel}:group:${groupId}`;
13
+ }
14
+ return `${channel}:dm:${peer}`;
15
+ }
16
+ getOrCreate(channel, peer, isGroup, groupId) {
17
+ const id = this.makeId(channel, peer, isGroup, groupId);
18
+ let session = this.sessions.get(id);
19
+ if (!session) {
20
+ session = {
21
+ id,
22
+ channel,
23
+ peer,
24
+ isGroup,
25
+ groupId,
26
+ messages: [],
27
+ messageCount: 0,
28
+ createdAt: Date.now(),
29
+ lastActivity: Date.now(),
30
+ };
31
+ this.sessions.set(id, session);
32
+ console.log(`[Sessions] Created session: ${id}`);
33
+ }
34
+ return session;
35
+ }
36
+ get(id) {
37
+ return this.sessions.get(id);
38
+ }
39
+ addMessage(sessionId, role, content) {
40
+ const session = this.sessions.get(sessionId);
41
+ if (!session)
42
+ return;
43
+ session.messages.push({
44
+ role,
45
+ content,
46
+ timestamp: Date.now(),
47
+ });
48
+ session.messageCount++;
49
+ session.lastActivity = Date.now();
50
+ // Trim history if too long
51
+ if (session.messages.length > this.maxHistoryLength) {
52
+ // Keep system message if present, trim oldest user/assistant messages
53
+ const systemMsgs = session.messages.filter(m => m.role === 'system');
54
+ const otherMsgs = session.messages.filter(m => m.role !== 'system');
55
+ session.messages = [
56
+ ...systemMsgs,
57
+ ...otherMsgs.slice(-this.maxHistoryLength + systemMsgs.length),
58
+ ];
59
+ }
60
+ }
61
+ getHistory(sessionId) {
62
+ const session = this.sessions.get(sessionId);
63
+ return session?.messages || [];
64
+ }
65
+ reset(sessionId) {
66
+ const session = this.sessions.get(sessionId);
67
+ if (session) {
68
+ const systemMsgs = session.messages.filter(m => m.role === 'system');
69
+ session.messages = systemMsgs; // Keep system prompt
70
+ session.messageCount = 0;
71
+ console.log(`[Sessions] Reset session: ${sessionId}`);
72
+ }
73
+ }
74
+ delete(sessionId) {
75
+ this.sessions.delete(sessionId);
76
+ }
77
+ getAll() {
78
+ return Array.from(this.sessions.values());
79
+ }
80
+ count() {
81
+ return this.sessions.size;
82
+ }
83
+ setSystemPrompt(sessionId, prompt) {
84
+ const session = this.sessions.get(sessionId);
85
+ if (session) {
86
+ session.systemPrompt = prompt;
87
+ // Add or update system message
88
+ const systemIdx = session.messages.findIndex(m => m.role === 'system');
89
+ if (systemIdx >= 0) {
90
+ session.messages[systemIdx].content = prompt;
91
+ }
92
+ else {
93
+ session.messages.unshift({ role: 'system', content: prompt, timestamp: Date.now() });
94
+ }
95
+ }
96
+ }
97
+ setModel(sessionId, model) {
98
+ const session = this.sessions.get(sessionId);
99
+ if (session) {
100
+ session.model = model;
101
+ }
102
+ }
103
+ // Cleanup old sessions
104
+ cleanup(maxAgeMs = 24 * 60 * 60 * 1000) {
105
+ const cutoff = Date.now() - maxAgeMs;
106
+ let cleaned = 0;
107
+ for (const [id, session] of this.sessions) {
108
+ if (session.lastActivity < cutoff) {
109
+ this.sessions.delete(id);
110
+ cleaned++;
111
+ }
112
+ }
113
+ if (cleaned > 0) {
114
+ console.log(`[Sessions] Cleaned up ${cleaned} old sessions`);
115
+ }
116
+ return cleaned;
117
+ }
118
+ }
119
+ exports.SessionManager = SessionManager;
120
+ exports.default = SessionManager;
@@ -0,0 +1,151 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ArbitrageDetector = void 0;
4
+ const eventBus_1 = require("../eventBus");
5
+ /**
6
+ * Arbitrage detector for Manifold Markets.
7
+ * Detects:
8
+ * 1. Binary complement arb: market A (YES) + market B (NO on same event) should sum to ~1
9
+ * 2. Multi-outcome arb: multiple choice markets where probabilities don't sum to 1
10
+ * 3. Correlated market arb: related markets with known relationships
11
+ */
12
+ class ArbitrageDetector {
13
+ cfg;
14
+ manifold;
15
+ marketCache = new Map();
16
+ constructor(manifold, cfg) {
17
+ this.manifold = manifold;
18
+ this.cfg = cfg;
19
+ }
20
+ /**
21
+ * Check for binary complement arbitrage.
22
+ * If buying YES on market A at price pA and YES on market B (which is "NOT A") at pB,
23
+ * and pA + pB < 1, there's an arbitrage opportunity.
24
+ */
25
+ checkBinaryComplement(marketA, marketB) {
26
+ const pA = marketA.probability;
27
+ const pB = marketB.probability;
28
+ // If these are complementary events (A and NOT A), probabilities should sum to 1
29
+ // If pA + pB < 1, buy both YES; if pA + pB > 1, buy both NO
30
+ const sum = pA + pB;
31
+ if (sum < 1 - 0.02) {
32
+ // Buy YES on both; guaranteed profit = 1 - sum per $1 total invested
33
+ const profitPerDollar = 1 - sum;
34
+ const expectedProfit = profitPerDollar * 100; // assuming $100 total stake
35
+ if (expectedProfit >= this.cfg.minProfitThreshold) {
36
+ return {
37
+ type: 'binary_complement',
38
+ markets: [marketA.id, marketB.id],
39
+ expectedProfit,
40
+ details: `Buy YES on both: ${marketA.question} (${(pA * 100).toFixed(1)}%) + ${marketB.question} (${(pB * 100).toFixed(1)}%) = ${(sum * 100).toFixed(1)}% < 100%`,
41
+ timestamp: Date.now(),
42
+ };
43
+ }
44
+ }
45
+ if (sum > 1 + 0.02) {
46
+ // Buy NO on both; profit = sum - 1
47
+ const profitPerDollar = sum - 1;
48
+ const expectedProfit = profitPerDollar * 100;
49
+ if (expectedProfit >= this.cfg.minProfitThreshold) {
50
+ return {
51
+ type: 'binary_complement',
52
+ markets: [marketA.id, marketB.id],
53
+ expectedProfit,
54
+ details: `Buy NO on both: ${marketA.question} (${(pA * 100).toFixed(1)}%) + ${marketB.question} (${(pB * 100).toFixed(1)}%) = ${(sum * 100).toFixed(1)}% > 100%`,
55
+ timestamp: Date.now(),
56
+ };
57
+ }
58
+ }
59
+ return null;
60
+ }
61
+ /**
62
+ * Check correlated pairs from config.
63
+ */
64
+ async checkCorrelatedPairs() {
65
+ const opportunities = [];
66
+ for (const pair of this.cfg.correlatedPairs) {
67
+ try {
68
+ const [marketA, marketB] = await Promise.all([
69
+ this.getMarketCached(pair.marketA),
70
+ this.getMarketCached(pair.marketB),
71
+ ]);
72
+ if (!marketA || !marketB)
73
+ continue;
74
+ if (pair.relation === 'sum_to_one') {
75
+ const arb = this.checkBinaryComplement(marketA, marketB);
76
+ if (arb) {
77
+ arb.type = 'correlated';
78
+ opportunities.push(arb);
79
+ }
80
+ }
81
+ else if (pair.relation === 'positive') {
82
+ // Markets should move together; large divergence = opportunity
83
+ const diff = Math.abs(marketA.probability - marketB.probability);
84
+ if (diff > 0.15) { // >15% divergence
85
+ opportunities.push({
86
+ type: 'correlated',
87
+ markets: [marketA.id, marketB.id],
88
+ expectedProfit: diff * 50, // rough estimate
89
+ details: `Correlated markets diverged: ${marketA.question} (${(marketA.probability * 100).toFixed(1)}%) vs ${marketB.question} (${(marketB.probability * 100).toFixed(1)}%)`,
90
+ timestamp: Date.now(),
91
+ });
92
+ }
93
+ }
94
+ else if (pair.relation === 'negative') {
95
+ // Markets should move opposite; if both high or both low = opportunity
96
+ const sum = marketA.probability + marketB.probability;
97
+ if (Math.abs(sum - 1) > 0.15) {
98
+ opportunities.push({
99
+ type: 'correlated',
100
+ markets: [marketA.id, marketB.id],
101
+ expectedProfit: Math.abs(sum - 1) * 50,
102
+ details: `Negative correlation broken: ${marketA.question} (${(marketA.probability * 100).toFixed(1)}%) + ${marketB.question} (${(marketB.probability * 100).toFixed(1)}%) = ${(sum * 100).toFixed(1)}%`,
103
+ timestamp: Date.now(),
104
+ });
105
+ }
106
+ }
107
+ }
108
+ catch (e) {
109
+ console.error(`Error checking pair ${pair.marketA}/${pair.marketB}:`, e);
110
+ }
111
+ }
112
+ return opportunities;
113
+ }
114
+ /**
115
+ * Scan for arbitrage across all markets (expensive; use sparingly).
116
+ */
117
+ async scanAllMarkets(limit = 100) {
118
+ const markets = await this.manifold.getMarkets(limit);
119
+ const opportunities = [];
120
+ // Update cache
121
+ for (const m of markets) {
122
+ this.marketCache.set(m.id, m);
123
+ }
124
+ // Check configured correlated pairs
125
+ const corrArbs = await this.checkCorrelatedPairs();
126
+ opportunities.push(...corrArbs);
127
+ // Emit opportunities
128
+ for (const opp of opportunities) {
129
+ (0, eventBus_1.emit)('arb:opportunity', opp);
130
+ }
131
+ return opportunities;
132
+ }
133
+ async getMarketCached(id) {
134
+ if (this.marketCache.has(id)) {
135
+ return this.marketCache.get(id);
136
+ }
137
+ try {
138
+ const m = await this.manifold.getMarket(id);
139
+ this.marketCache.set(id, m);
140
+ return m;
141
+ }
142
+ catch {
143
+ return null;
144
+ }
145
+ }
146
+ clearCache() {
147
+ this.marketCache.clear();
148
+ }
149
+ }
150
+ exports.ArbitrageDetector = ArbitrageDetector;
151
+ exports.default = ArbitrageDetector;
package/package.json ADDED
@@ -0,0 +1,78 @@
1
+ {
2
+ "name": "apexbot",
3
+ "version": "1.0.0",
4
+ "description": "ApexBot - Your free, private AI assistant. 100% free with Ollama (local AI). Multi-channel: Telegram, Discord, WebChat. Like Clawdbot but open-source!",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "apexbot": "./dist/cli/index.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "scripts",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "preferGlobal": true,
16
+ "scripts": {
17
+ "build": "tsc",
18
+ "start": "ts-node src/cli/index.ts gateway",
19
+ "cli": "ts-node src/cli/index.ts",
20
+ "gateway": "ts-node src/cli/index.ts gateway",
21
+ "onboard": "ts-node src/cli/index.ts onboard",
22
+ "dev": "ts-node-dev --respawn src/cli/index.ts gateway --verbose",
23
+ "typecheck": "tsc --noEmit",
24
+ "lint": "echo 'TODO: add eslint'",
25
+ "test": "echo 'TODO: add tests' && exit 0"
26
+ },
27
+ "keywords": [
28
+ "ai-assistant",
29
+ "telegram-bot",
30
+ "discord-bot",
31
+ "chatbot",
32
+ "ollama",
33
+ "local-llm",
34
+ "gemini",
35
+ "claude",
36
+ "openai",
37
+ "typescript",
38
+ "free",
39
+ "open-source"
40
+ ],
41
+ "author": "ApexBot Team",
42
+ "license": "MIT",
43
+ "homepage": "https://github.com/YOUR_USERNAME/apexbot#readme",
44
+ "bugs": {
45
+ "url": "https://github.com/YOUR_USERNAME/apexbot/issues"
46
+ },
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "git+https://github.com/YOUR_USERNAME/apexbot.git"
50
+ },
51
+ "engines": {
52
+ "node": ">=18.0.0"
53
+ },
54
+ "dependencies": {
55
+ "@anthropic-ai/sdk": "^0.71.2",
56
+ "@google/generative-ai": "^0.24.1",
57
+ "axios": "^1.5.0",
58
+ "boxen": "^5.1.2",
59
+ "chalk": "^4.1.2",
60
+ "commander": "^14.0.2",
61
+ "discord.js": "^14.25.1",
62
+ "dotenv": "^17.2.3",
63
+ "eventemitter3": "^4.0.7",
64
+ "figlet": "^1.10.0",
65
+ "grammy": "^1.39.3",
66
+ "inquirer": "^8.2.7",
67
+ "ora": "^5.4.1",
68
+ "ws": "^8.19.0"
69
+ },
70
+ "devDependencies": {
71
+ "@types/figlet": "^1.7.0",
72
+ "@types/inquirer": "^9.0.9",
73
+ "@types/node": "^20.0.0",
74
+ "@types/ws": "^8.18.1",
75
+ "ts-node": "^10.9.1",
76
+ "typescript": "^5.2.2"
77
+ }
78
+ }
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env pwsh
2
+ # ApexBot Installer for Windows
3
+ # Usage: iwr -useb https://raw.githubusercontent.com/YOUR_USERNAME/apexbot/main/scripts/install.ps1 | iex
4
+
5
+ $ErrorActionPreference = "Stop"
6
+
7
+ # Colors
8
+ function Write-Color($Text, $Color) {
9
+ Write-Host $Text -ForegroundColor $Color
10
+ }
11
+
12
+ # ASCII Art
13
+ $logo = @"
14
+
15
+ ___ ____ _______ ______ ____ ______
16
+ / | / __ \/ ____/ |/ / __ )/ __ \/_ __/
17
+ / /| | / /_/ / __/ | / __ / / / / / /
18
+ / ___ |/ ____/ /___ / / /_/ / /_/ / / /
19
+ /_/ |_/_/ /_____//_/|_/_____/\____/ /_/
20
+
21
+ 🦊 Your Free AI Assistant 🦊
22
+ "@
23
+
24
+ Write-Color $logo "Cyan"
25
+ Write-Host ""
26
+ Write-Color "Welcome to ApexBot Installer!" "Green"
27
+ Write-Host ""
28
+
29
+ # Check Node.js
30
+ Write-Host "Checking prerequisites..." -NoNewline
31
+ if (!(Get-Command node -ErrorAction SilentlyContinue)) {
32
+ Write-Color " FAILED" "Red"
33
+ Write-Host ""
34
+ Write-Color "Node.js is required but not installed." "Yellow"
35
+ Write-Host "Install Node.js from: https://nodejs.org/"
36
+ Write-Host ""
37
+ $install = Read-Host "Would you like to install Node.js via winget? (Y/n)"
38
+ if ($install -ne "n" -and $install -ne "N") {
39
+ winget install OpenJS.NodeJS.LTS
40
+ Write-Color "Please restart your terminal and run this installer again." "Yellow"
41
+ exit 0
42
+ }
43
+ exit 1
44
+ }
45
+ Write-Color " OK" "Green"
46
+
47
+ # Check npm
48
+ if (!(Get-Command npm -ErrorAction SilentlyContinue)) {
49
+ Write-Color "npm is required but not installed." "Red"
50
+ exit 1
51
+ }
52
+
53
+ # Check Ollama (optional but recommended)
54
+ Write-Host "Checking Ollama (recommended for free AI)..." -NoNewline
55
+ $ollamaInstalled = Get-Command ollama -ErrorAction SilentlyContinue
56
+ if ($ollamaInstalled) {
57
+ Write-Color " INSTALLED" "Green"
58
+ } else {
59
+ Write-Color " NOT FOUND" "Yellow"
60
+ Write-Host ""
61
+ Write-Color "Ollama is recommended for 100% free, local AI." "Yellow"
62
+ Write-Host "Install from: https://ollama.com"
63
+ Write-Host ""
64
+ $installOllama = Read-Host "Would you like to install Ollama via winget? (Y/n)"
65
+ if ($installOllama -ne "n" -and $installOllama -ne "N") {
66
+ winget install Ollama.Ollama
67
+ Write-Color "Ollama installed! Pull a model with: ollama pull llama3.2" "Green"
68
+ }
69
+ }
70
+
71
+ Write-Host ""
72
+ Write-Color "Installing ApexBot..." "Cyan"
73
+ Write-Host ""
74
+
75
+ # Install ApexBot globally
76
+ try {
77
+ npm install -g apexbot
78
+ Write-Color "ApexBot installed successfully!" "Green"
79
+ } catch {
80
+ Write-Color "npm global install failed, trying with sudo..." "Yellow"
81
+ npm install -g apexbot --unsafe-perm
82
+ }
83
+
84
+ Write-Host ""
85
+ Write-Color "================================================" "Cyan"
86
+ Write-Host ""
87
+ Write-Color "🎉 Installation Complete!" "Green"
88
+ Write-Host ""
89
+ Write-Host "Next steps:"
90
+ Write-Host " 1. Run: " -NoNewline
91
+ Write-Color "apexbot onboard" "Yellow"
92
+ Write-Host " 2. Follow the setup wizard"
93
+ Write-Host " 3. Start your bot: " -NoNewline
94
+ Write-Color "apexbot daemon start" "Yellow"
95
+ Write-Host ""
96
+
97
+ # Check if Ollama needs setup
98
+ if ($ollamaInstalled) {
99
+ Write-Host "Ollama detected! Make sure to:"
100
+ Write-Host " - Pull a model: " -NoNewline
101
+ Write-Color "ollama pull llama3.2" "Yellow"
102
+ Write-Host " - Start Ollama: " -NoNewline
103
+ Write-Color "ollama serve" "Yellow"
104
+ Write-Host ""
105
+ }
106
+
107
+ Write-Color "Documentation: https://github.com/YOUR_USERNAME/apexbot" "Cyan"
108
+ Write-Host ""
109
+
110
+ # Offer to run onboard
111
+ $runOnboard = Read-Host "Would you like to run the setup wizard now? (Y/n)"
112
+ if ($runOnboard -ne "n" -and $runOnboard -ne "N") {
113
+ apexbot onboard
114
+ }