@moxxy/cli 0.0.12 → 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/README.md +278 -112
- package/bin/moxxy +10 -0
- package/package.json +36 -53
- package/src/api-client.js +286 -0
- package/src/cli.js +341 -0
- package/src/commands/agent.js +413 -0
- package/src/commands/auth.js +326 -0
- package/src/commands/channel.js +285 -0
- package/src/commands/doctor.js +261 -0
- package/src/commands/events.js +80 -0
- package/src/commands/gateway.js +428 -0
- package/src/commands/heartbeat.js +145 -0
- package/src/commands/init.js +767 -0
- package/src/commands/mcp.js +278 -0
- package/src/commands/plugin.js +583 -0
- package/src/commands/provider.js +1934 -0
- package/src/commands/skill.js +125 -0
- package/src/commands/template.js +237 -0
- package/src/commands/uninstall.js +196 -0
- package/src/commands/update.js +406 -0
- package/src/commands/vault.js +219 -0
- package/src/help.js +368 -0
- package/src/lib/plugin-registry.js +98 -0
- package/src/platform.js +40 -0
- package/src/sse-client.js +79 -0
- package/src/tui/action-wizards.js +130 -0
- package/src/tui/app.jsx +859 -0
- package/src/tui/components/action-picker.jsx +86 -0
- package/src/tui/components/chat-panel.jsx +120 -0
- package/src/tui/components/footer.jsx +13 -0
- package/src/tui/components/header.jsx +45 -0
- package/src/tui/components/input-area.jsx +384 -0
- package/src/tui/components/messages/ask-message.jsx +13 -0
- package/src/tui/components/messages/assistant-message.jsx +165 -0
- package/src/tui/components/messages/channel-message.jsx +18 -0
- package/src/tui/components/messages/event-message.jsx +22 -0
- package/src/tui/components/messages/hive-status.jsx +34 -0
- package/src/tui/components/messages/skill-message.jsx +31 -0
- package/src/tui/components/messages/system-message.jsx +12 -0
- package/src/tui/components/messages/thinking.jsx +25 -0
- package/src/tui/components/messages/tool-group.jsx +62 -0
- package/src/tui/components/messages/tool-message.jsx +66 -0
- package/src/tui/components/messages/user-message.jsx +12 -0
- package/src/tui/components/model-picker.jsx +138 -0
- package/src/tui/components/multiline-input.jsx +72 -0
- package/src/tui/events-handler.js +730 -0
- package/src/tui/helpers.js +59 -0
- package/src/tui/hooks/use-command-handler.js +451 -0
- package/src/tui/index.jsx +55 -0
- package/src/tui/input-utils.js +26 -0
- package/src/tui/markdown-renderer.js +66 -0
- package/src/tui/mcp-wizard.js +136 -0
- package/src/tui/model-picker.js +174 -0
- package/src/tui/slash-commands.js +26 -0
- package/src/tui/store.js +12 -0
- package/src/tui/theme.js +17 -0
- package/src/ui.js +109 -0
- package/bin/moxxy.js +0 -2
- package/dist/chunk-23LZYKQ6.mjs +0 -1131
- package/dist/chunk-2FZEA3NG.mjs +0 -457
- package/dist/chunk-3KDPLS22.mjs +0 -1131
- package/dist/chunk-3QRJTRBT.mjs +0 -1102
- package/dist/chunk-6DZX6EAA.mjs +0 -37
- package/dist/chunk-A4WRDUNY.mjs +0 -1242
- package/dist/chunk-C46NSEKG.mjs +0 -211
- package/dist/chunk-CAUXONEF.mjs +0 -1131
- package/dist/chunk-CPL5V56X.mjs +0 -1131
- package/dist/chunk-CTBVTTBG.mjs +0 -440
- package/dist/chunk-FHHLXTEZ.mjs +0 -1121
- package/dist/chunk-FXY3GPVA.mjs +0 -1126
- package/dist/chunk-GSNMMI3H.mjs +0 -530
- package/dist/chunk-HHOAOGUS.mjs +0 -1242
- package/dist/chunk-ITBO7BKI.mjs +0 -1243
- package/dist/chunk-J33O35WX.mjs +0 -532
- package/dist/chunk-N5JTPB6U.mjs +0 -820
- package/dist/chunk-NGVL4Q5C.mjs +0 -1102
- package/dist/chunk-Q2OCMNYI.mjs +0 -1131
- package/dist/chunk-QDVRLN6D.mjs +0 -1121
- package/dist/chunk-QO2JONHP.mjs +0 -1131
- package/dist/chunk-RVAPILHA.mjs +0 -1242
- package/dist/chunk-S7YBOV7E.mjs +0 -1131
- package/dist/chunk-SHIG6Y5L.mjs +0 -1074
- package/dist/chunk-SOFST2PV.mjs +0 -1242
- package/dist/chunk-SUNUYS6G.mjs +0 -1243
- package/dist/chunk-TMZWETMH.mjs +0 -1242
- package/dist/chunk-TYD7NMMI.mjs +0 -581
- package/dist/chunk-TYQ3YS42.mjs +0 -1068
- package/dist/chunk-UALWCJ7F.mjs +0 -1131
- package/dist/chunk-UQZKODNW.mjs +0 -1124
- package/dist/chunk-USC6R2ON.mjs +0 -1242
- package/dist/chunk-W32EQCVC.mjs +0 -823
- package/dist/chunk-WMB5ENMC.mjs +0 -1242
- package/dist/chunk-WNHA5JAP.mjs +0 -1242
- package/dist/cli-2AIWTL6F.mjs +0 -8
- package/dist/cli-2QKJ5UUL.mjs +0 -8
- package/dist/cli-4RIS6DQX.mjs +0 -8
- package/dist/cli-5RH4VBBL.mjs +0 -7
- package/dist/cli-7MK4YGOP.mjs +0 -7
- package/dist/cli-B4KH6MZI.mjs +0 -8
- package/dist/cli-CGO2LZ6Z.mjs +0 -8
- package/dist/cli-CVP26EL2.mjs +0 -8
- package/dist/cli-DDRVVNAV.mjs +0 -8
- package/dist/cli-E7U56QVQ.mjs +0 -8
- package/dist/cli-EQNRMLL3.mjs +0 -8
- package/dist/cli-F5RUHHH4.mjs +0 -8
- package/dist/cli-LX6FFSEF.mjs +0 -8
- package/dist/cli-LY74GWKR.mjs +0 -6
- package/dist/cli-MAT3ZJHI.mjs +0 -8
- package/dist/cli-NJXXTQYF.mjs +0 -8
- package/dist/cli-O4ZGFAZG.mjs +0 -8
- package/dist/cli-ORVLI3UQ.mjs +0 -8
- package/dist/cli-PV43ZVKA.mjs +0 -8
- package/dist/cli-REVD6ISM.mjs +0 -8
- package/dist/cli-TBX76KQX.mjs +0 -8
- package/dist/cli-THCGF7SQ.mjs +0 -8
- package/dist/cli-TLX5ENVM.mjs +0 -8
- package/dist/cli-TMNI5ZYE.mjs +0 -8
- package/dist/cli-TNJHCBQA.mjs +0 -6
- package/dist/cli-TUX22CZP.mjs +0 -8
- package/dist/cli-XJVH7EEP.mjs +0 -8
- package/dist/cli-XXOW4VXJ.mjs +0 -8
- package/dist/cli-XZ5RESNB.mjs +0 -6
- package/dist/cli-YCBYZ76Q.mjs +0 -8
- package/dist/cli-ZLMQCU7X.mjs +0 -8
- package/dist/dist-2VGKJRBH.mjs +0 -6820
- package/dist/dist-37BNX4QG.mjs +0 -7081
- package/dist/dist-7LTHRYKA.mjs +0 -11569
- package/dist/dist-7XJPQW5C.mjs +0 -6950
- package/dist/dist-AYMVOW7T.mjs +0 -7123
- package/dist/dist-BHUWCDRS.mjs +0 -7132
- package/dist/dist-FAXRJMEN.mjs +0 -6812
- package/dist/dist-HQGANM3P.mjs +0 -6976
- package/dist/dist-KATLOZQV.mjs +0 -7054
- package/dist/dist-KLSB6YHV.mjs +0 -6964
- package/dist/dist-LKIOZQ42.mjs +0 -17
- package/dist/dist-UYA4RJUH.mjs +0 -2792
- package/dist/dist-ZYHCBILM.mjs +0 -6993
- package/dist/index.d.mts +0 -23
- package/dist/index.d.ts +0 -23
- package/dist/index.js +0 -25531
- package/dist/index.mjs +0 -18
- package/dist/src-APP5P3UD.mjs +0 -1386
- package/dist/src-D5HMDDVE.mjs +0 -1324
- package/dist/src-EK3WD4AU.mjs +0 -1327
- package/dist/src-LSZFLMFN.mjs +0 -1400
- package/dist/src-T77DFTFP.mjs +0 -1407
- package/dist/src-WIOCZRAC.mjs +0 -1397
- package/dist/src-YK6CHCMW.mjs +0 -1400
|
@@ -0,0 +1,767 @@
|
|
|
1
|
+
import { p, handleCancel, withSpinner, showResult } from '../ui.js';
|
|
2
|
+
import { VALID_SCOPES } from './auth.js';
|
|
3
|
+
import { BUILTIN_PROVIDERS, ANTHROPIC_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID, loginAnthropic, loginOpenAiCodex, checkProviderCredentials, resolveBuiltinProviderModels } from './provider.js';
|
|
4
|
+
import { shellExportInstruction, shellProfileName } from '../platform.js';
|
|
5
|
+
import { mkdirSync, existsSync, readFileSync, writeFileSync, chmodSync, copyFileSync, createWriteStream } from 'node:fs';
|
|
6
|
+
import { join, resolve } from 'node:path';
|
|
7
|
+
import { homedir, platform, arch } from 'node:os';
|
|
8
|
+
import { execSync } from 'node:child_process';
|
|
9
|
+
import { pipeline } from 'node:stream/promises';
|
|
10
|
+
|
|
11
|
+
export function getMoxxyHome() {
|
|
12
|
+
return process.env.MOXXY_HOME || join(homedir(), '.moxxy');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Read the auth_mode from ~/.moxxy/config/gateway.yaml.
|
|
17
|
+
* Returns 'token' | 'loopback'.
|
|
18
|
+
* Env var MOXXY_LOOPBACK=true overrides the config file.
|
|
19
|
+
*/
|
|
20
|
+
export function readAuthMode() {
|
|
21
|
+
if (process.env.MOXXY_LOOPBACK === 'true' || process.env.MOXXY_LOOPBACK === '1') {
|
|
22
|
+
return 'loopback';
|
|
23
|
+
}
|
|
24
|
+
const configPath = join(getMoxxyHome(), 'config', 'gateway.yaml');
|
|
25
|
+
try {
|
|
26
|
+
const raw = readFileSync(configPath, 'utf-8');
|
|
27
|
+
const match = raw.match(/^auth_mode:\s*(.+)$/m);
|
|
28
|
+
if (match && match[1].trim() === 'loopback') return 'loopback';
|
|
29
|
+
} catch {
|
|
30
|
+
// config missing or unparseable = default to token
|
|
31
|
+
}
|
|
32
|
+
return 'token';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Reset all tokens by clearing the api_tokens table via sqlite3 CLI.
|
|
37
|
+
* This re-enables the bootstrap path (first token without auth).
|
|
38
|
+
* Returns true if the reset succeeded.
|
|
39
|
+
*/
|
|
40
|
+
export function resetTokens() {
|
|
41
|
+
const dbPath = join(getMoxxyHome(), 'moxxy.db');
|
|
42
|
+
if (!existsSync(dbPath)) return false;
|
|
43
|
+
try {
|
|
44
|
+
execSync(`sqlite3 "${dbPath}" "DELETE FROM api_tokens;"`, { stdio: 'pipe' });
|
|
45
|
+
return true;
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function detectGatewayPlatform() {
|
|
52
|
+
const osMap = { darwin: 'darwin', linux: 'linux' };
|
|
53
|
+
const archMap = { arm64: 'arm64', x64: 'x86_64' };
|
|
54
|
+
const os = osMap[platform()] || platform();
|
|
55
|
+
const cpuArch = archMap[arch()] || arch();
|
|
56
|
+
const binaryName = `moxxy-gateway-${os}-${cpuArch}`;
|
|
57
|
+
return { os, arch: cpuArch, binaryName };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const GITHUB_REPO = process.env.MOXXY_GITHUB_REPO || 'moxxy-ai/moxxy';
|
|
61
|
+
const GITHUB_API = 'https://api.github.com';
|
|
62
|
+
|
|
63
|
+
async function fetchLatestReleaseAssetUrl(binaryName) {
|
|
64
|
+
const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
|
|
65
|
+
const headers = { 'Accept': 'application/vnd.github+json', 'User-Agent': 'moxxy-cli' };
|
|
66
|
+
if (token) headers['Authorization'] = `Bearer ${token}`;
|
|
67
|
+
|
|
68
|
+
const url = `${GITHUB_API}/repos/${GITHUB_REPO}/releases/latest`;
|
|
69
|
+
const resp = await fetch(url, { headers, signal: AbortSignal.timeout(10000) });
|
|
70
|
+
|
|
71
|
+
if (resp.status === 403) {
|
|
72
|
+
const remaining = resp.headers.get('x-ratelimit-remaining');
|
|
73
|
+
if (remaining === '0') {
|
|
74
|
+
throw new Error('GitHub API rate limit exceeded. Set GITHUB_TOKEN env var to increase the limit.');
|
|
75
|
+
}
|
|
76
|
+
throw new Error(`GitHub API returned 403: ${resp.statusText}`);
|
|
77
|
+
}
|
|
78
|
+
if (resp.status === 404) {
|
|
79
|
+
throw new Error('No releases found.');
|
|
80
|
+
}
|
|
81
|
+
if (!resp.ok) {
|
|
82
|
+
throw new Error(`GitHub API error: ${resp.status} ${resp.statusText}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const release = await resp.json();
|
|
86
|
+
const asset = release.assets.find(a => a.name === binaryName);
|
|
87
|
+
if (!asset) {
|
|
88
|
+
const available = release.assets.map(a => a.name).join(', ');
|
|
89
|
+
throw new Error(`No binary for this platform (${binaryName}). Available: ${available}`);
|
|
90
|
+
}
|
|
91
|
+
return { url: asset.browser_download_url, version: release.tag_name };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function installGatewayBinary(moxxyHome) {
|
|
95
|
+
const { binaryName } = detectGatewayPlatform();
|
|
96
|
+
const binDir = join(moxxyHome, 'bin');
|
|
97
|
+
const binName = platform() === 'win32' ? 'moxxy-gateway.exe' : 'moxxy-gateway';
|
|
98
|
+
const binPath = join(binDir, binName);
|
|
99
|
+
|
|
100
|
+
// MOXXY_GATEWAY_URL overrides GitHub releases (for local dev / custom builds)
|
|
101
|
+
const overrideUrl = process.env.MOXXY_GATEWAY_URL;
|
|
102
|
+
|
|
103
|
+
if (existsSync(binPath) && !overrideUrl) {
|
|
104
|
+
p.log.success(`Gateway binary already installed: ${binPath}`);
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
mkdirSync(binDir, { recursive: true });
|
|
109
|
+
const tmpPath = binPath + '.download';
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
let downloadUrl;
|
|
113
|
+
let version;
|
|
114
|
+
|
|
115
|
+
const isLocalPath = overrideUrl && !overrideUrl.startsWith('http://') && !overrideUrl.startsWith('https://');
|
|
116
|
+
|
|
117
|
+
if (isLocalPath) {
|
|
118
|
+
const srcPath = resolve(overrideUrl);
|
|
119
|
+
if (!existsSync(srcPath)) {
|
|
120
|
+
throw new Error(`Local binary not found: ${srcPath}`);
|
|
121
|
+
}
|
|
122
|
+
p.log.info(`Copying local gateway binary: ${srcPath}`);
|
|
123
|
+
copyFileSync(srcPath, binPath);
|
|
124
|
+
chmodSync(binPath, 0o755);
|
|
125
|
+
p.log.success(`Gateway installed: ${binPath}`);
|
|
126
|
+
} else {
|
|
127
|
+
if (overrideUrl) {
|
|
128
|
+
downloadUrl = overrideUrl;
|
|
129
|
+
p.log.info(`Using custom gateway URL: ${overrideUrl}`);
|
|
130
|
+
} else {
|
|
131
|
+
const release = await withSpinner('Fetching latest release...', () =>
|
|
132
|
+
fetchLatestReleaseAssetUrl(binaryName), 'Release found.');
|
|
133
|
+
downloadUrl = release.url;
|
|
134
|
+
version = release.version;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const headers = { 'User-Agent': 'moxxy-cli', 'Accept': 'application/octet-stream' };
|
|
138
|
+
if (!overrideUrl) {
|
|
139
|
+
const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
|
|
140
|
+
if (token) headers['Authorization'] = `Bearer ${token}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
await withSpinner(`Downloading gateway${version ? ` ${version}` : ''} (${binaryName})...`, async () => {
|
|
144
|
+
const resp = await fetch(downloadUrl, { headers, signal: AbortSignal.timeout(120000) });
|
|
145
|
+
if (!resp.ok) {
|
|
146
|
+
throw new Error(`HTTP ${resp.status} ${resp.statusText}`);
|
|
147
|
+
}
|
|
148
|
+
const fileStream = createWriteStream(tmpPath);
|
|
149
|
+
await pipeline(resp.body, fileStream);
|
|
150
|
+
}, 'Gateway downloaded.');
|
|
151
|
+
|
|
152
|
+
const { renameSync } = await import('node:fs');
|
|
153
|
+
renameSync(tmpPath, binPath);
|
|
154
|
+
chmodSync(binPath, 0o755);
|
|
155
|
+
p.log.success(`Gateway installed: ${binPath}`);
|
|
156
|
+
}
|
|
157
|
+
return true;
|
|
158
|
+
} catch (err) {
|
|
159
|
+
// Clean up partial download
|
|
160
|
+
try { const { unlinkSync } = await import('node:fs'); unlinkSync(tmpPath); } catch { /* ignore */ }
|
|
161
|
+
p.log.error(`Failed to download gateway: ${err.message}`);
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export async function runInit(client, args) {
|
|
167
|
+
p.intro('Welcome to Moxxy');
|
|
168
|
+
|
|
169
|
+
// Step 0: Create ~/.moxxy directory structure
|
|
170
|
+
p.note(
|
|
171
|
+
'The ~/.moxxy directory stores agent configs, secrets, and metadata.\n' +
|
|
172
|
+
'It will be created automatically if it doesn\'t exist.',
|
|
173
|
+
'Home Directory'
|
|
174
|
+
);
|
|
175
|
+
const moxxyHome = getMoxxyHome();
|
|
176
|
+
try {
|
|
177
|
+
mkdirSync(join(moxxyHome, 'agents'), { recursive: true });
|
|
178
|
+
mkdirSync(join(moxxyHome, 'config'), { recursive: true });
|
|
179
|
+
p.log.success(`Moxxy home: ${moxxyHome}`);
|
|
180
|
+
} catch (err) {
|
|
181
|
+
p.log.warn(`Could not create ${moxxyHome}: ${err.message}`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Step 1: Install gateway binary
|
|
185
|
+
p.note(
|
|
186
|
+
'The gateway is the backend service that manages agents, routing,\n' +
|
|
187
|
+
'and tool execution. It will be downloaded and installed automatically.',
|
|
188
|
+
'Gateway Installation'
|
|
189
|
+
);
|
|
190
|
+
const gatewayInstalled = await installGatewayBinary(moxxyHome);
|
|
191
|
+
|
|
192
|
+
// Step 1.5: Check/configure API URL
|
|
193
|
+
p.note(
|
|
194
|
+
'The gateway listens on a local port.\n' +
|
|
195
|
+
'The default is http://localhost:3000.',
|
|
196
|
+
'Gateway Connection'
|
|
197
|
+
);
|
|
198
|
+
const useDefault = await p.confirm({
|
|
199
|
+
message: `Use gateway at ${client.baseUrl}?`,
|
|
200
|
+
initialValue: true,
|
|
201
|
+
});
|
|
202
|
+
handleCancel(useDefault);
|
|
203
|
+
|
|
204
|
+
if (!useDefault) {
|
|
205
|
+
const apiUrl = await p.text({
|
|
206
|
+
message: 'Enter gateway URL',
|
|
207
|
+
placeholder: 'http://localhost:3000',
|
|
208
|
+
validate: (val) => {
|
|
209
|
+
try { new URL(val); } catch { return 'Must be a valid URL'; }
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
handleCancel(apiUrl);
|
|
213
|
+
client.baseUrl = apiUrl;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Step 2: Start gateway and check connectivity
|
|
217
|
+
let gatewayReachable = false;
|
|
218
|
+
if (gatewayInstalled) {
|
|
219
|
+
const startIt = await p.confirm({
|
|
220
|
+
message: 'Start the gateway now?',
|
|
221
|
+
initialValue: true,
|
|
222
|
+
});
|
|
223
|
+
handleCancel(startIt);
|
|
224
|
+
|
|
225
|
+
if (startIt) {
|
|
226
|
+
try {
|
|
227
|
+
const { startGateway } = await import('./gateway.js');
|
|
228
|
+
await startGateway();
|
|
229
|
+
gatewayReachable = true;
|
|
230
|
+
} catch (err) {
|
|
231
|
+
p.log.warn(`Could not start gateway: ${err.message}`);
|
|
232
|
+
p.log.info('You can start it later with: moxxy gateway start');
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (!gatewayReachable) {
|
|
238
|
+
try {
|
|
239
|
+
await withSpinner('Checking gateway connection...', async () => {
|
|
240
|
+
const resp = await fetch(`${client.baseUrl}/v1/providers`);
|
|
241
|
+
if (resp) gatewayReachable = true;
|
|
242
|
+
}, 'Gateway is reachable.');
|
|
243
|
+
} catch {
|
|
244
|
+
p.log.warn('Gateway is not reachable. Start it with: moxxy gateway start');
|
|
245
|
+
p.log.info('You can continue setup and connect later.');
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Step 2.5: Auth mode selection
|
|
250
|
+
p.note(
|
|
251
|
+
'Token mode requires an API token for every request (more secure).\n' +
|
|
252
|
+
'Loopback mode skips auth for localhost requests (easier for local dev).',
|
|
253
|
+
'Authentication'
|
|
254
|
+
);
|
|
255
|
+
const authMode = await p.select({
|
|
256
|
+
message: 'Authorization mode?',
|
|
257
|
+
options: [
|
|
258
|
+
{ value: 'token', label: 'Token (default)', hint: 'API tokens required for all requests' },
|
|
259
|
+
{ value: 'loopback', label: 'Loopback', hint: 'no auth needed from localhost' },
|
|
260
|
+
],
|
|
261
|
+
});
|
|
262
|
+
handleCancel(authMode);
|
|
263
|
+
|
|
264
|
+
// Persist auth mode to config
|
|
265
|
+
const configPath = join(moxxyHome, 'config', 'gateway.yaml');
|
|
266
|
+
try {
|
|
267
|
+
writeFileSync(configPath, `auth_mode: ${authMode}\n`);
|
|
268
|
+
p.log.success(`Auth mode set to: ${authMode}`);
|
|
269
|
+
} catch (err) {
|
|
270
|
+
p.log.warn(`Could not write ${configPath}: ${err.message}`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (authMode === 'loopback') {
|
|
274
|
+
p.note(
|
|
275
|
+
'The gateway will accept all requests from localhost without a token.\n' +
|
|
276
|
+
'Non-localhost requests will still require authentication.',
|
|
277
|
+
'Loopback mode'
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Step 3: Token bootstrap (skip if loopback mode)
|
|
282
|
+
if (authMode !== 'loopback') {
|
|
283
|
+
p.note(
|
|
284
|
+
'API tokens authenticate CLI requests to the gateway.\n' +
|
|
285
|
+
'A wildcard (*) token grants full access to all endpoints.',
|
|
286
|
+
'API Token'
|
|
287
|
+
);
|
|
288
|
+
const createToken = await p.confirm({
|
|
289
|
+
message: 'Create an API token?',
|
|
290
|
+
initialValue: true,
|
|
291
|
+
});
|
|
292
|
+
handleCancel(createToken);
|
|
293
|
+
|
|
294
|
+
if (createToken) {
|
|
295
|
+
const scopes = ['*'];
|
|
296
|
+
const ttl = undefined;
|
|
297
|
+
|
|
298
|
+
const payload = { scopes };
|
|
299
|
+
if (ttl) payload.ttl_seconds = ttl;
|
|
300
|
+
|
|
301
|
+
// Try bootstrap (no auth) first, then with existing token
|
|
302
|
+
const savedToken = client.token;
|
|
303
|
+
let result;
|
|
304
|
+
let created = false;
|
|
305
|
+
|
|
306
|
+
// Attempt 1: bootstrap (no auth = works when DB has no tokens)
|
|
307
|
+
client.token = '';
|
|
308
|
+
try {
|
|
309
|
+
result = await withSpinner('Creating token...', () =>
|
|
310
|
+
client.request('/v1/auth/tokens', 'POST', payload), 'Token created.');
|
|
311
|
+
created = true;
|
|
312
|
+
} catch (err) {
|
|
313
|
+
if (err.status !== 401) {
|
|
314
|
+
p.log.error(`Failed to create token: ${err.message}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Attempt 2: use existing MOXXY_TOKEN if bootstrap failed
|
|
319
|
+
if (!created && savedToken) {
|
|
320
|
+
client.token = savedToken;
|
|
321
|
+
try {
|
|
322
|
+
result = await withSpinner('Retrying with existing token...', () =>
|
|
323
|
+
client.request('/v1/auth/tokens', 'POST', payload), 'Token created.');
|
|
324
|
+
created = true;
|
|
325
|
+
} catch (err) {
|
|
326
|
+
if (err.status !== 401) {
|
|
327
|
+
p.log.error(`Failed to create token: ${err.message}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Attempt 3: recovery menu = paste token or reset
|
|
333
|
+
if (!created) {
|
|
334
|
+
p.log.warn('Tokens already exist and your current token is missing or invalid.');
|
|
335
|
+
const recovery = await p.select({
|
|
336
|
+
message: 'How would you like to proceed?',
|
|
337
|
+
options: [
|
|
338
|
+
{ value: 'reset', label: 'Reset tokens', hint: 'clear all existing tokens and create a new one' },
|
|
339
|
+
{ value: 'paste', label: 'Paste a token', hint: 'use an existing valid token' },
|
|
340
|
+
{ value: 'skip', label: 'Skip', hint: 'continue without a token' },
|
|
341
|
+
],
|
|
342
|
+
});
|
|
343
|
+
handleCancel(recovery);
|
|
344
|
+
|
|
345
|
+
if (recovery === 'reset') {
|
|
346
|
+
const confirm = await p.confirm({
|
|
347
|
+
message: 'This will revoke ALL existing tokens. Continue?',
|
|
348
|
+
initialValue: false,
|
|
349
|
+
});
|
|
350
|
+
handleCancel(confirm);
|
|
351
|
+
|
|
352
|
+
if (confirm && resetTokens()) {
|
|
353
|
+
p.log.success('All tokens cleared.');
|
|
354
|
+
client.token = '';
|
|
355
|
+
try {
|
|
356
|
+
result = await withSpinner('Creating token...', () =>
|
|
357
|
+
client.request('/v1/auth/tokens', 'POST', payload), 'Token created.');
|
|
358
|
+
created = true;
|
|
359
|
+
} catch (err) {
|
|
360
|
+
p.log.error(`Failed to create token: ${err.message}`);
|
|
361
|
+
}
|
|
362
|
+
} else if (confirm) {
|
|
363
|
+
p.log.error('Could not reset tokens. Is sqlite3 installed?');
|
|
364
|
+
}
|
|
365
|
+
} else if (recovery === 'paste') {
|
|
366
|
+
const pastedToken = await p.text({
|
|
367
|
+
message: 'Paste a valid API token',
|
|
368
|
+
placeholder: 'mox_...',
|
|
369
|
+
});
|
|
370
|
+
handleCancel(pastedToken);
|
|
371
|
+
if (pastedToken) {
|
|
372
|
+
client.token = pastedToken;
|
|
373
|
+
try {
|
|
374
|
+
result = await withSpinner('Creating token...', () =>
|
|
375
|
+
client.request('/v1/auth/tokens', 'POST', payload), 'Token created.');
|
|
376
|
+
created = true;
|
|
377
|
+
} catch (err) {
|
|
378
|
+
p.log.error(`Failed to create token: ${err.message}`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
} else {
|
|
382
|
+
p.log.info('Skipped. Create a token later with: moxxy auth token create');
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (created) {
|
|
387
|
+
// Use the new token for the rest of the init flow
|
|
388
|
+
client.token = result.token;
|
|
389
|
+
showResult('Your API Token', {
|
|
390
|
+
ID: result.id,
|
|
391
|
+
Token: result.token,
|
|
392
|
+
Scopes: scopes.join(', '),
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
p.note(
|
|
396
|
+
`# Add to ${shellProfileName()}:\n${shellExportInstruction('MOXXY_TOKEN', result.token)}`,
|
|
397
|
+
'Save your token'
|
|
398
|
+
);
|
|
399
|
+
p.log.warn('This token will not be shown again. Save it now.');
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
} // end authMode !== 'loopback'
|
|
403
|
+
|
|
404
|
+
// Step 4: Provider installation (optional)
|
|
405
|
+
let installedProviderId = null;
|
|
406
|
+
|
|
407
|
+
p.note(
|
|
408
|
+
'Providers connect Moxxy to LLM services like Anthropic, OpenAI, or Google.\n' +
|
|
409
|
+
'You need at least one provider installed before creating agents.',
|
|
410
|
+
'Provider Setup'
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
const installProvider = await p.confirm({
|
|
414
|
+
message: 'Install an LLM provider?',
|
|
415
|
+
initialValue: true,
|
|
416
|
+
});
|
|
417
|
+
handleCancel(installProvider);
|
|
418
|
+
|
|
419
|
+
if (installProvider) {
|
|
420
|
+
if (authMode === 'token' && !client.token) {
|
|
421
|
+
p.log.warn('Provider install requires a token in token mode. Skipping.');
|
|
422
|
+
p.log.info('Create a token first, then run: moxxy provider install');
|
|
423
|
+
} else {
|
|
424
|
+
const providerChoice = await p.select({
|
|
425
|
+
message: 'Select a provider to install',
|
|
426
|
+
options: [
|
|
427
|
+
...BUILTIN_PROVIDERS.map(bp => ({
|
|
428
|
+
value: bp.id,
|
|
429
|
+
label: bp.display_name,
|
|
430
|
+
hint: `${bp.models.length} models`,
|
|
431
|
+
})),
|
|
432
|
+
{ value: '__skip__', label: 'Skip', hint: 'install a provider later' },
|
|
433
|
+
],
|
|
434
|
+
});
|
|
435
|
+
handleCancel(providerChoice);
|
|
436
|
+
|
|
437
|
+
if (providerChoice !== '__skip__') {
|
|
438
|
+
const builtin = BUILTIN_PROVIDERS.find(bp => bp.id === providerChoice);
|
|
439
|
+
|
|
440
|
+
// Providers with dedicated login flows (OAuth / validated API key)
|
|
441
|
+
if (builtin.oauth_login || builtin.api_key_login) {
|
|
442
|
+
try {
|
|
443
|
+
const flags = {};
|
|
444
|
+
let result;
|
|
445
|
+
if (providerChoice === ANTHROPIC_PROVIDER_ID) {
|
|
446
|
+
result = await loginAnthropic(client, flags);
|
|
447
|
+
} else if (providerChoice === OPENAI_CODEX_PROVIDER_ID) {
|
|
448
|
+
result = await loginOpenAiCodex(client, flags);
|
|
449
|
+
}
|
|
450
|
+
if (result?.provider_id) {
|
|
451
|
+
installedProviderId = result.provider_id;
|
|
452
|
+
}
|
|
453
|
+
} catch (err) {
|
|
454
|
+
p.log.error(`Failed to install provider: ${err.message}`);
|
|
455
|
+
}
|
|
456
|
+
} else {
|
|
457
|
+
// Generic provider flow (no special login)
|
|
458
|
+
const availableModels = await resolveBuiltinProviderModels(builtin);
|
|
459
|
+
const CUSTOM_MODEL_VALUE = '__custom_model__';
|
|
460
|
+
const selectedModels = handleCancel(await p.multiselect({
|
|
461
|
+
message: 'Select models to install',
|
|
462
|
+
options: [
|
|
463
|
+
...availableModels.map(m => ({
|
|
464
|
+
value: m.model_id,
|
|
465
|
+
label: m.display_name,
|
|
466
|
+
hint: m.model_id,
|
|
467
|
+
})),
|
|
468
|
+
{ value: CUSTOM_MODEL_VALUE, label: 'Custom model ID', hint: 'enter a model ID manually' },
|
|
469
|
+
],
|
|
470
|
+
required: true,
|
|
471
|
+
}));
|
|
472
|
+
|
|
473
|
+
const models = availableModels
|
|
474
|
+
.filter(m => selectedModels.includes(m.model_id))
|
|
475
|
+
.map(m => ({
|
|
476
|
+
...m,
|
|
477
|
+
metadata: m.metadata || (builtin.api_base ? { api_base: builtin.api_base } : {}),
|
|
478
|
+
}));
|
|
479
|
+
|
|
480
|
+
// Handle custom model
|
|
481
|
+
if (selectedModels.includes(CUSTOM_MODEL_VALUE)) {
|
|
482
|
+
const customModelId = handleCancel(await p.text({
|
|
483
|
+
message: 'Custom model ID',
|
|
484
|
+
placeholder: 'e.g. ft:gpt-4o:my-org:custom-suffix',
|
|
485
|
+
validate: (v) => { if (!v.trim()) return 'Required'; },
|
|
486
|
+
}));
|
|
487
|
+
|
|
488
|
+
const customModelName = handleCancel(await p.text({
|
|
489
|
+
message: 'Display name for this model',
|
|
490
|
+
initialValue: customModelId,
|
|
491
|
+
}));
|
|
492
|
+
|
|
493
|
+
models.push({
|
|
494
|
+
model_id: customModelId,
|
|
495
|
+
display_name: customModelName || customModelId,
|
|
496
|
+
metadata: builtin.api_base ? { api_base: builtin.api_base, custom: true } : { custom: true },
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Verify credentials (binary check for CLI providers, API key for others)
|
|
501
|
+
const credOk = await checkProviderCredentials(builtin, client);
|
|
502
|
+
if (!credOk) return;
|
|
503
|
+
|
|
504
|
+
// Install provider
|
|
505
|
+
try {
|
|
506
|
+
await withSpinner(`Installing ${builtin.display_name}...`, () =>
|
|
507
|
+
client.installProvider(builtin.id, builtin.display_name, models),
|
|
508
|
+
`${builtin.display_name} installed.`
|
|
509
|
+
);
|
|
510
|
+
|
|
511
|
+
installedProviderId = builtin.id;
|
|
512
|
+
|
|
513
|
+
const resultInfo = {
|
|
514
|
+
ID: builtin.id,
|
|
515
|
+
Name: builtin.display_name,
|
|
516
|
+
Models: models.map(m => m.model_id).join(', '),
|
|
517
|
+
};
|
|
518
|
+
if (builtin.api_key_env) resultInfo['API Key Env'] = builtin.api_key_env;
|
|
519
|
+
if (builtin.cli_binary) resultInfo['CLI Binary'] = builtin.cli_binary;
|
|
520
|
+
showResult('Provider Installed', resultInfo);
|
|
521
|
+
} catch (err) {
|
|
522
|
+
p.log.error(`Failed to install provider: ${err.message}`);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Step 5: Agent creation (optional)
|
|
530
|
+
p.note(
|
|
531
|
+
'Agents are LLM-powered workers that use tools and skills to complete tasks.\n' +
|
|
532
|
+
'Each agent is bound to a provider and model.',
|
|
533
|
+
'Your First Agent'
|
|
534
|
+
);
|
|
535
|
+
|
|
536
|
+
const createAgent = await p.confirm({
|
|
537
|
+
message: 'Create your first agent?',
|
|
538
|
+
initialValue: true,
|
|
539
|
+
});
|
|
540
|
+
handleCancel(createAgent);
|
|
541
|
+
|
|
542
|
+
if (createAgent) {
|
|
543
|
+
if (authMode === 'token' && !client.token) {
|
|
544
|
+
p.log.warn('Agent creation requires a token in token mode. Skipping.');
|
|
545
|
+
p.log.info('Create a token first, then run: moxxy agent create');
|
|
546
|
+
} else {
|
|
547
|
+
// Check for available providers
|
|
548
|
+
let agentProviderId = installedProviderId;
|
|
549
|
+
|
|
550
|
+
if (!agentProviderId) {
|
|
551
|
+
try {
|
|
552
|
+
const providers = await withSpinner('Fetching providers...', () =>
|
|
553
|
+
client.listProviders(), 'Providers loaded.');
|
|
554
|
+
if (!providers || providers.length === 0) {
|
|
555
|
+
p.log.warn('No providers installed. Install one first with: moxxy provider install');
|
|
556
|
+
agentProviderId = null;
|
|
557
|
+
} else {
|
|
558
|
+
agentProviderId = handleCancel(await p.select({
|
|
559
|
+
message: 'Select a provider',
|
|
560
|
+
options: providers.map(pr => ({
|
|
561
|
+
value: pr.id,
|
|
562
|
+
label: pr.display_name || pr.id,
|
|
563
|
+
})),
|
|
564
|
+
}));
|
|
565
|
+
}
|
|
566
|
+
} catch (err) {
|
|
567
|
+
p.log.warn(`Could not list providers: ${err.message}`);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (agentProviderId) {
|
|
572
|
+
// Agent name
|
|
573
|
+
const agentName = handleCancel(await p.text({
|
|
574
|
+
message: 'Agent name',
|
|
575
|
+
placeholder: 'my-agent',
|
|
576
|
+
validate: (v) => {
|
|
577
|
+
if (!v || v.trim().length === 0) return 'Required';
|
|
578
|
+
if (v.length > 64) return 'Max 64 characters';
|
|
579
|
+
if (!/^[a-z][a-z0-9-]*$/.test(v)) return 'Lowercase alphanumeric and hyphens, must start with a letter';
|
|
580
|
+
},
|
|
581
|
+
}));
|
|
582
|
+
|
|
583
|
+
// Model selection (live from API)
|
|
584
|
+
let agentModelId;
|
|
585
|
+
try {
|
|
586
|
+
const models = await withSpinner('Fetching models...', () =>
|
|
587
|
+
client.listModels(agentProviderId), 'Models loaded.');
|
|
588
|
+
|
|
589
|
+
const CUSTOM_MODEL_VALUE = '__custom_model__';
|
|
590
|
+
const modelOptions = [
|
|
591
|
+
...(models || []).map(m => ({
|
|
592
|
+
value: m.model_id,
|
|
593
|
+
label: m.display_name || m.model_id,
|
|
594
|
+
hint: m.model_id,
|
|
595
|
+
})),
|
|
596
|
+
{ value: CUSTOM_MODEL_VALUE, label: 'Custom model ID', hint: 'enter a model ID manually' },
|
|
597
|
+
];
|
|
598
|
+
|
|
599
|
+
const modelChoice = handleCancel(await p.select({
|
|
600
|
+
message: 'Select a model',
|
|
601
|
+
options: modelOptions,
|
|
602
|
+
}));
|
|
603
|
+
|
|
604
|
+
if (modelChoice === CUSTOM_MODEL_VALUE) {
|
|
605
|
+
agentModelId = handleCancel(await p.text({
|
|
606
|
+
message: 'Custom model ID',
|
|
607
|
+
placeholder: 'e.g. claude-sonnet-4-20250514',
|
|
608
|
+
validate: (v) => { if (!v.trim()) return 'Required'; },
|
|
609
|
+
}));
|
|
610
|
+
} else {
|
|
611
|
+
agentModelId = modelChoice;
|
|
612
|
+
}
|
|
613
|
+
} catch (err) {
|
|
614
|
+
p.log.warn(`Could not fetch models: ${err.message}`);
|
|
615
|
+
agentModelId = handleCancel(await p.text({
|
|
616
|
+
message: 'Model ID',
|
|
617
|
+
placeholder: 'e.g. claude-sonnet-4-20250514',
|
|
618
|
+
validate: (v) => { if (!v.trim()) return 'Required'; },
|
|
619
|
+
}));
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Persona (optional)
|
|
623
|
+
const persona = handleCancel(await p.text({
|
|
624
|
+
message: 'Persona (optional)',
|
|
625
|
+
placeholder: 'e.g. A helpful coding assistant',
|
|
626
|
+
}));
|
|
627
|
+
|
|
628
|
+
// Create the agent
|
|
629
|
+
try {
|
|
630
|
+
const opts = {};
|
|
631
|
+
if (persona && persona.trim()) opts.persona = persona.trim();
|
|
632
|
+
|
|
633
|
+
const agentResult = await withSpinner('Creating agent...', () =>
|
|
634
|
+
client.createAgent(agentProviderId, agentModelId, agentName, opts),
|
|
635
|
+
'Agent created.'
|
|
636
|
+
);
|
|
637
|
+
|
|
638
|
+
showResult('Agent Created', {
|
|
639
|
+
Name: agentResult.name,
|
|
640
|
+
Provider: agentResult.provider_id,
|
|
641
|
+
Model: agentResult.model_id,
|
|
642
|
+
Status: agentResult.status,
|
|
643
|
+
});
|
|
644
|
+
} catch (err) {
|
|
645
|
+
p.log.error(`Failed to create agent: ${err.message}`);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Step 6: Channel setup (optional)
|
|
652
|
+
p.note(
|
|
653
|
+
'Channels enable agent communication via Telegram or Discord.\n' +
|
|
654
|
+
'You can set up channels later with: moxxy channel create',
|
|
655
|
+
'Channels'
|
|
656
|
+
);
|
|
657
|
+
const setupChannel = await p.confirm({
|
|
658
|
+
message: 'Set up a messaging channel (Telegram/Discord)?',
|
|
659
|
+
initialValue: false,
|
|
660
|
+
});
|
|
661
|
+
handleCancel(setupChannel);
|
|
662
|
+
|
|
663
|
+
if (setupChannel) {
|
|
664
|
+
const channelType = await p.select({
|
|
665
|
+
message: 'Channel type',
|
|
666
|
+
options: [
|
|
667
|
+
{ value: 'telegram', label: 'Telegram', hint: 'BotFather bot token required' },
|
|
668
|
+
{ value: 'discord', label: 'Discord', hint: 'coming soon (scaffold)' },
|
|
669
|
+
],
|
|
670
|
+
});
|
|
671
|
+
handleCancel(channelType);
|
|
672
|
+
|
|
673
|
+
if (channelType === 'telegram') {
|
|
674
|
+
p.note(
|
|
675
|
+
'1. Open Telegram and talk to @BotFather\n' +
|
|
676
|
+
'2. Send /newbot and follow the prompts\n' +
|
|
677
|
+
'3. Copy the bot token',
|
|
678
|
+
'Telegram Bot Setup'
|
|
679
|
+
);
|
|
680
|
+
|
|
681
|
+
const botToken = await p.password({
|
|
682
|
+
message: 'Paste your Telegram bot token',
|
|
683
|
+
});
|
|
684
|
+
handleCancel(botToken);
|
|
685
|
+
|
|
686
|
+
const displayName = await p.text({
|
|
687
|
+
message: 'Display name for this channel',
|
|
688
|
+
placeholder: 'My Moxxy Bot',
|
|
689
|
+
});
|
|
690
|
+
handleCancel(displayName);
|
|
691
|
+
|
|
692
|
+
try {
|
|
693
|
+
const result = await withSpinner('Registering Telegram channel...', () =>
|
|
694
|
+
client.request('/v1/channels', 'POST', {
|
|
695
|
+
channel_type: 'telegram',
|
|
696
|
+
display_name: displayName || 'Telegram Bot',
|
|
697
|
+
bot_token: botToken,
|
|
698
|
+
}), 'Channel registered.');
|
|
699
|
+
|
|
700
|
+
showResult('Telegram Channel', { ID: result.id, Status: result.status });
|
|
701
|
+
|
|
702
|
+
// Interactive pairing
|
|
703
|
+
p.note(
|
|
704
|
+
'1. Open your Telegram bot and send /start\n' +
|
|
705
|
+
'2. You will receive a 6-digit pairing code',
|
|
706
|
+
'Pair your chat'
|
|
707
|
+
);
|
|
708
|
+
|
|
709
|
+
const pairCode = await p.text({
|
|
710
|
+
message: 'Enter the 6-digit pairing code',
|
|
711
|
+
placeholder: '123456',
|
|
712
|
+
validate: (v) => {
|
|
713
|
+
if (!v || v.trim().length === 0) return 'Code is required';
|
|
714
|
+
},
|
|
715
|
+
});
|
|
716
|
+
handleCancel(pairCode);
|
|
717
|
+
|
|
718
|
+
// Pick an agent to bind
|
|
719
|
+
let agentId;
|
|
720
|
+
try {
|
|
721
|
+
const agents = await withSpinner('Fetching agents...', () =>
|
|
722
|
+
client.listAgents(), 'Agents loaded.');
|
|
723
|
+
if (!agents || agents.length === 0) {
|
|
724
|
+
p.log.warn('No agents found. Create one first with: moxxy agent create');
|
|
725
|
+
p.log.info(`Pair later with: moxxy channel pair --code ${pairCode} --agent <agent-id>`);
|
|
726
|
+
} else {
|
|
727
|
+
agentId = await p.select({
|
|
728
|
+
message: 'Select agent to bind',
|
|
729
|
+
options: agents.map(a => ({
|
|
730
|
+
value: a.name,
|
|
731
|
+
label: `${a.name} (${a.provider_id}/${a.model_id})`,
|
|
732
|
+
})),
|
|
733
|
+
});
|
|
734
|
+
handleCancel(agentId);
|
|
735
|
+
}
|
|
736
|
+
} catch (err) {
|
|
737
|
+
p.log.warn(`Could not list agents: ${err.message}`);
|
|
738
|
+
p.log.info(`Pair later with: moxxy channel pair --code ${pairCode} --agent <agent-id>`);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
if (agentId) {
|
|
742
|
+
try {
|
|
743
|
+
const pairResult = await withSpinner('Pairing...', () =>
|
|
744
|
+
client.request(`/v1/channels/${result.id}/pair`, 'POST', {
|
|
745
|
+
code: pairCode,
|
|
746
|
+
agent_id: agentId,
|
|
747
|
+
}), 'Paired successfully.');
|
|
748
|
+
showResult('Channel Paired', {
|
|
749
|
+
'Binding ID': pairResult.id,
|
|
750
|
+
Agent: pairResult.agent_id,
|
|
751
|
+
'External Chat': pairResult.external_chat_id,
|
|
752
|
+
});
|
|
753
|
+
} catch (err) {
|
|
754
|
+
p.log.error(`Failed to pair: ${err.message}`);
|
|
755
|
+
p.log.info(`Try again with: moxxy channel pair --code ${pairCode} --agent ${agentId}`);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
} catch (err) {
|
|
759
|
+
p.log.error(`Failed to register channel: ${err.message}`);
|
|
760
|
+
}
|
|
761
|
+
} else {
|
|
762
|
+
p.log.info('Discord channel support is coming soon.');
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
p.outro('Setup complete. Run moxxy to see available commands.');
|
|
767
|
+
}
|