@moxxy/cli 0.1.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moxxy/cli",
3
- "version": "0.1.0",
3
+ "version": "1.0.0",
4
4
  "description": "CLI for the Moxxy agentic framework — manage agents, skills, plugins, channels, and vaults from the terminal",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/cli.js CHANGED
@@ -16,6 +16,7 @@ import { runDoctor } from './commands/doctor.js';
16
16
  import { runUpdate } from './commands/update.js';
17
17
  import { runUninstall } from './commands/uninstall.js';
18
18
  import { runPlugin } from './commands/plugin.js';
19
+ import { runSettings } from './commands/settings.js';
19
20
  import { COMMAND_HELP, showHelp } from './help.js';
20
21
  import chalk from 'chalk';
21
22
  import { createInterface, cursorTo, clearScreenDown } from 'node:readline';
@@ -87,6 +88,9 @@ Usage:
87
88
  moxxy tui [--agent <id>] Full-screen chat interface
88
89
  moxxy chat [--agent <id>] Alias for tui
89
90
  moxxy events tail [--agent <id>] [--run <id>] [--json]
91
+ moxxy settings network-mode [safe|unsafe] Get or set network mode
92
+ moxxy settings get [--key <k>] [--json] View settings
93
+ moxxy settings set --key <k> --value <v> Set a setting
90
94
  moxxy doctor Diagnose installation
91
95
  moxxy update [--check] [--force] [--json] Check for and install updates
92
96
  moxxy update --rollback Restore previous gateway version
@@ -168,6 +172,9 @@ async function routeCommand(client, command, rest) {
168
172
  case 'plugin':
169
173
  await runPlugin(client, rest);
170
174
  break;
175
+ case 'settings':
176
+ await runSettings(client, rest);
177
+ break;
171
178
  case 'tui':
172
179
  case 'chat': {
173
180
  const { startTui } = await import('./tui/index.jsx');
@@ -217,7 +224,7 @@ async function main() {
217
224
  security: { label: 'Security', hint: 'auth tokens & secrets' },
218
225
  integrations: { label: 'Integrations', hint: 'providers, channels, MCP, plugins' },
219
226
  tools: { label: 'Tools', hint: 'events stream' },
220
- system: { label: 'System', hint: 'update & uninstall' },
227
+ system: { label: 'System', hint: 'settings, update & uninstall' },
221
228
  };
222
229
 
223
230
  const SUBMENUS = {
@@ -246,6 +253,7 @@ async function main() {
246
253
  { value: 'events', label: 'Events', hint: 'stream live events' },
247
254
  ],
248
255
  system: [
256
+ { value: 'settings', label: 'Settings', hint: 'network mode & global config' },
249
257
  { value: 'update', label: 'Update', hint: 'check for and install updates' },
250
258
  { value: 'uninstall', label: 'Uninstall', hint: 'remove all Moxxy data' },
251
259
  ],
@@ -763,5 +763,192 @@ export async function runInit(client, args) {
763
763
  }
764
764
  }
765
765
 
766
+ // Step 7: Browser rendering (optional)
767
+ p.note(
768
+ 'Browser rendering enables agents to load JavaScript-heavy websites\n' +
769
+ 'using a headless Chrome browser. This requires Chrome/Chromium.\n' +
770
+ 'Without it, agents can still fetch pages via HTTP (works for most sites).',
771
+ 'Browser Rendering'
772
+ );
773
+
774
+ const enableBrowser = await p.confirm({
775
+ message: 'Enable browser rendering capabilities?',
776
+ initialValue: false,
777
+ });
778
+ handleCancel(enableBrowser);
779
+
780
+ if (enableBrowser) {
781
+ const chromePath = detectChromeBinary(moxxyHome);
782
+
783
+ if (chromePath) {
784
+ p.log.success(`Chrome found: ${chromePath}`);
785
+ saveBrowserRenderingSetting(moxxyHome, true);
786
+ p.log.success('Browser rendering enabled.');
787
+ } else {
788
+ p.log.warn('Chrome/Chromium not found on this system.');
789
+
790
+ const downloadChrome = await p.confirm({
791
+ message: 'Download Chromium (~150MB) to ~/.moxxy/chromium/?',
792
+ initialValue: true,
793
+ });
794
+ handleCancel(downloadChrome);
795
+
796
+ if (downloadChrome) {
797
+ const installed = await installChromium(moxxyHome);
798
+ if (installed) {
799
+ saveBrowserRenderingSetting(moxxyHome, true);
800
+ p.log.success('Browser rendering enabled.');
801
+ } else {
802
+ p.log.warn('Chromium install failed. You can retry later with: moxxy settings browser-rendering');
803
+ }
804
+ } else {
805
+ p.log.info('Skipped. Install Chrome manually or run: moxxy settings browser-rendering');
806
+ }
807
+ }
808
+ }
809
+
766
810
  p.outro('Setup complete. Run moxxy to see available commands.');
767
811
  }
812
+
813
+ // ---------------------------------------------------------------------------
814
+ // Browser rendering helpers
815
+ // ---------------------------------------------------------------------------
816
+
817
+ function detectChromeBinary(moxxyHome) {
818
+ const os = platform();
819
+
820
+ // 1. CHROME_PATH env var
821
+ if (process.env.CHROME_PATH && existsSync(process.env.CHROME_PATH)) {
822
+ return process.env.CHROME_PATH;
823
+ }
824
+
825
+ // 2. System Chrome
826
+ const systemPaths = os === 'darwin'
827
+ ? [
828
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
829
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
830
+ ]
831
+ : [
832
+ '/usr/bin/google-chrome',
833
+ '/usr/bin/google-chrome-stable',
834
+ '/usr/bin/chromium',
835
+ '/usr/bin/chromium-browser',
836
+ ];
837
+
838
+ for (const p of systemPaths) {
839
+ if (existsSync(p)) return p;
840
+ }
841
+
842
+ // 3. which fallback (Linux)
843
+ if (os === 'linux') {
844
+ for (const name of ['google-chrome', 'chromium-browser', 'chromium']) {
845
+ try {
846
+ const result = execSync(`which ${name}`, { stdio: 'pipe', encoding: 'utf-8' }).trim();
847
+ if (result && existsSync(result)) return result;
848
+ } catch { /* not found */ }
849
+ }
850
+ }
851
+
852
+ // 4. Previously downloaded
853
+ const platDir = chromePlatformDir();
854
+ const binaryName = os === 'darwin'
855
+ ? 'Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing'
856
+ : 'chrome';
857
+ const downloaded = join(moxxyHome, 'chromium', platDir, binaryName);
858
+ if (existsSync(downloaded)) return downloaded;
859
+
860
+ return null;
861
+ }
862
+
863
+ function chromePlatformDir() {
864
+ const os = platform();
865
+ const cpuArch = arch();
866
+ if (os === 'darwin') {
867
+ return cpuArch === 'arm64' ? 'chrome-mac-arm64' : 'chrome-mac-x64';
868
+ }
869
+ return 'chrome-linux64';
870
+ }
871
+
872
+ async function installChromium(moxxyHome) {
873
+ const CHROME_FOR_TESTING_API = 'https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions-with-downloads.json';
874
+
875
+ try {
876
+ // Fetch latest stable version info
877
+ const versionInfo = await withSpinner('Fetching Chromium version info...', async () => {
878
+ const resp = await fetch(CHROME_FOR_TESTING_API, { signal: AbortSignal.timeout(15000) });
879
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
880
+ return resp.json();
881
+ }, 'Version info retrieved.');
882
+
883
+ const platKey = chromePlatformKey();
884
+ const stable = versionInfo.channels.Stable;
885
+ const chromeDownloads = stable.downloads.chrome;
886
+ const entry = chromeDownloads.find(d => d.platform === platKey);
887
+
888
+ if (!entry) {
889
+ p.log.error(`No Chromium build for platform: ${platKey}`);
890
+ return false;
891
+ }
892
+
893
+ const downloadUrl = entry.url;
894
+ const targetDir = join(moxxyHome, 'chromium');
895
+ mkdirSync(targetDir, { recursive: true });
896
+
897
+ const zipPath = join(targetDir, 'chrome.zip');
898
+
899
+ // Download
900
+ await withSpinner(`Downloading Chromium ${stable.version}...`, async () => {
901
+ const resp = await fetch(downloadUrl, { signal: AbortSignal.timeout(300000) });
902
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
903
+ const fileStream = createWriteStream(zipPath);
904
+ await pipeline(resp.body, fileStream);
905
+ }, 'Download complete.');
906
+
907
+ // Extract
908
+ await withSpinner('Extracting Chromium...', async () => {
909
+ execSync(`unzip -o -q "${zipPath}" -d "${targetDir}"`, { stdio: 'pipe' });
910
+ }, 'Extraction complete.');
911
+
912
+ // Cleanup zip
913
+ try { const { unlinkSync } = await import('node:fs'); unlinkSync(zipPath); } catch { /* ignore */ }
914
+
915
+ const chromePath = detectChromeBinary(moxxyHome);
916
+ if (chromePath) {
917
+ // Make executable on Linux
918
+ if (platform() === 'linux') {
919
+ try { chmodSync(chromePath, 0o755); } catch { /* ignore */ }
920
+ }
921
+ p.log.success(`Chromium installed: ${chromePath}`);
922
+ return true;
923
+ }
924
+
925
+ p.log.error('Extraction succeeded but Chrome binary not found');
926
+ return false;
927
+ } catch (err) {
928
+ p.log.error(`Failed to install Chromium: ${err.message}`);
929
+ return false;
930
+ }
931
+ }
932
+
933
+ function chromePlatformKey() {
934
+ const os = platform();
935
+ const cpuArch = arch();
936
+ if (os === 'darwin') {
937
+ return cpuArch === 'arm64' ? 'mac-arm64' : 'mac-x64';
938
+ }
939
+ return 'linux64';
940
+ }
941
+
942
+ function saveBrowserRenderingSetting(moxxyHome, enabled) {
943
+ const settingsFile = join(moxxyHome, 'settings.yaml');
944
+ let lines = [];
945
+ try {
946
+ const raw = readFileSync(settingsFile, 'utf-8');
947
+ lines = raw.split('\n').filter(l => !l.startsWith('browser_rendering:'));
948
+ } catch { /* no existing settings */ }
949
+
950
+ lines.push(`browser_rendering: ${enabled}`);
951
+
952
+ mkdirSync(moxxyHome, { recursive: true });
953
+ writeFileSync(settingsFile, lines.filter(l => l.trim()).join('\n') + '\n');
954
+ }
@@ -0,0 +1,224 @@
1
+ import { p, isInteractive, showResult } from '../ui.js';
2
+ import { parseFlags } from './auth.js';
3
+ import { getMoxxyHome } from './init.js';
4
+ import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+
7
+ const VALID_NETWORK_MODES = ['safe', 'unsafe'];
8
+
9
+ function settingsPath() {
10
+ return join(getMoxxyHome(), 'settings.yaml');
11
+ }
12
+
13
+ function loadSettings() {
14
+ try {
15
+ const raw = readFileSync(settingsPath(), 'utf-8');
16
+ if (!raw.trim()) return {};
17
+ const settings = {};
18
+ for (const line of raw.split('\n')) {
19
+ const match = line.match(/^(\w+):\s*(.+)$/);
20
+ if (match) settings[match[1]] = match[2].trim();
21
+ }
22
+ return settings;
23
+ } catch {
24
+ return {};
25
+ }
26
+ }
27
+
28
+ function saveSettings(settings) {
29
+ const dir = getMoxxyHome();
30
+ mkdirSync(dir, { recursive: true });
31
+ const lines = Object.entries(settings).map(([k, v]) => `${k}: ${v}`);
32
+ writeFileSync(settingsPath(), lines.join('\n') + '\n');
33
+ }
34
+
35
+ export function parseSettingsCommand(args) {
36
+ const [action, ...rest] = args;
37
+ const flags = parseFlags(rest);
38
+ return { action, flags };
39
+ }
40
+
41
+ async function settingsGet(flags) {
42
+ const settings = loadSettings();
43
+ const key = flags.key;
44
+
45
+ if (flags.json) {
46
+ console.log(JSON.stringify(key ? { key, value: settings[key] ?? null } : settings, null, 2));
47
+ return;
48
+ }
49
+
50
+ if (key) {
51
+ const value = settings[key] ?? '(not set)';
52
+ p.log.info(`${key}: ${value}`);
53
+ } else {
54
+ const entries = Object.entries(settings);
55
+ if (entries.length === 0) {
56
+ p.log.info('No settings configured. Using defaults.');
57
+ p.log.info(' network_mode: safe (default)');
58
+ } else {
59
+ for (const [k, v] of entries) {
60
+ p.log.info(`${k}: ${v}`);
61
+ }
62
+ }
63
+ }
64
+ }
65
+
66
+ async function settingsSet(flags) {
67
+ const key = flags.key;
68
+ const value = flags.value;
69
+
70
+ if (!key) throw new Error('Required: --key');
71
+ if (value === undefined) throw new Error('Required: --value');
72
+
73
+ // Validate known keys
74
+ if (key === 'network_mode' && !VALID_NETWORK_MODES.includes(value)) {
75
+ throw new Error(`Invalid network_mode '${value}'. Must be one of: ${VALID_NETWORK_MODES.join(', ')}`);
76
+ }
77
+
78
+ const settings = loadSettings();
79
+ settings[key] = value;
80
+ saveSettings(settings);
81
+
82
+ if (flags.json) {
83
+ console.log(JSON.stringify({ status: 'updated', key, value }, null, 2));
84
+ } else {
85
+ showResult({ status: 'updated', key, value }, p);
86
+ }
87
+ }
88
+
89
+ async function settingsNetworkMode(flags) {
90
+ const settings = loadSettings();
91
+ const current = settings.network_mode || 'safe';
92
+
93
+ // No value provided — show current or toggle interactively
94
+ if (!flags.mode && !flags._positional) {
95
+ if (isInteractive()) {
96
+ const selected = await p.select({
97
+ message: `Network mode (currently: ${current})`,
98
+ options: [
99
+ { value: 'safe', label: 'Safe', hint: 'agent asks user before accessing non-allowlisted domains' },
100
+ { value: 'unsafe', label: 'Unsafe', hint: 'domain allowlist is bypassed entirely' },
101
+ ],
102
+ initialValue: current,
103
+ });
104
+ if (p.isCancel(selected)) return;
105
+ settings.network_mode = selected;
106
+ saveSettings(settings);
107
+ p.log.success(`Network mode set to: ${selected}`);
108
+ } else {
109
+ if (flags.json) {
110
+ console.log(JSON.stringify({ network_mode: current }, null, 2));
111
+ } else {
112
+ p.log.info(`network_mode: ${current}`);
113
+ }
114
+ }
115
+ return;
116
+ }
117
+
118
+ const mode = flags.mode || flags._positional;
119
+ if (!VALID_NETWORK_MODES.includes(mode)) {
120
+ throw new Error(`Invalid mode '${mode}'. Must be one of: ${VALID_NETWORK_MODES.join(', ')}`);
121
+ }
122
+
123
+ settings.network_mode = mode;
124
+ saveSettings(settings);
125
+
126
+ if (flags.json) {
127
+ console.log(JSON.stringify({ status: 'updated', network_mode: mode }, null, 2));
128
+ } else {
129
+ p.log.success(`Network mode set to: ${mode}`);
130
+ }
131
+ }
132
+
133
+ async function settingsBrowserRendering(flags) {
134
+ const settings = loadSettings();
135
+ const current = settings.browser_rendering === 'true';
136
+
137
+ // No value provided — show current or toggle interactively
138
+ if (!flags._positional) {
139
+ if (isInteractive()) {
140
+ const selected = await p.select({
141
+ message: `Browser rendering (currently: ${current ? 'enabled' : 'disabled'})`,
142
+ options: [
143
+ { value: 'true', label: 'Enabled', hint: 'agents can render JS-heavy pages via headless Chrome' },
144
+ { value: 'false', label: 'Disabled', hint: 'agents use HTTP-only browsing' },
145
+ ],
146
+ initialValue: current ? 'true' : 'false',
147
+ });
148
+ if (p.isCancel(selected)) return;
149
+ settings.browser_rendering = selected;
150
+ saveSettings(settings);
151
+ p.log.success(`Browser rendering ${selected === 'true' ? 'enabled' : 'disabled'}.`);
152
+ } else {
153
+ if (flags.json) {
154
+ console.log(JSON.stringify({ browser_rendering: current }, null, 2));
155
+ } else {
156
+ p.log.info(`browser_rendering: ${current}`);
157
+ }
158
+ }
159
+ return;
160
+ }
161
+
162
+ const val = flags._positional;
163
+ if (!['true', 'false', 'on', 'off', 'enable', 'disable'].includes(val)) {
164
+ throw new Error(`Invalid value '${val}'. Use: true/false, on/off, or enable/disable`);
165
+ }
166
+
167
+ const enabled = ['true', 'on', 'enable'].includes(val);
168
+ settings.browser_rendering = String(enabled);
169
+ saveSettings(settings);
170
+
171
+ if (flags.json) {
172
+ console.log(JSON.stringify({ status: 'updated', browser_rendering: enabled }, null, 2));
173
+ } else {
174
+ p.log.success(`Browser rendering ${enabled ? 'enabled' : 'disabled'}.`);
175
+ }
176
+ }
177
+
178
+ export async function runSettings(_client, args) {
179
+ const { action, flags } = parseSettingsCommand(args);
180
+
181
+ // Collect first positional arg after the action for convenience
182
+ // e.g. `moxxy settings network-mode unsafe`
183
+ const restArgs = args.slice(1).filter(a => !a.startsWith('--'));
184
+ if (restArgs.length > 0 && !flags._positional) {
185
+ flags._positional = restArgs[0];
186
+ }
187
+
188
+ switch (action) {
189
+ case 'get':
190
+ await settingsGet(flags);
191
+ break;
192
+ case 'set':
193
+ await settingsSet(flags);
194
+ break;
195
+ case 'network-mode':
196
+ await settingsNetworkMode(flags);
197
+ break;
198
+ case 'browser-rendering':
199
+ await settingsBrowserRendering(flags);
200
+ break;
201
+ default:
202
+ if (isInteractive() && !action) {
203
+ // Interactive: show settings menu
204
+ const selected = await p.select({
205
+ message: 'Settings',
206
+ options: [
207
+ { value: 'network-mode', label: 'Network mode', hint: 'safe / unsafe domain access' },
208
+ { value: 'browser-rendering', label: 'Browser rendering', hint: 'headless Chrome for JS-heavy sites' },
209
+ { value: 'get', label: 'View all settings', hint: 'show current configuration' },
210
+ ],
211
+ });
212
+ if (p.isCancel(selected)) return;
213
+ await runSettings(_client, [selected]);
214
+ } else {
215
+ throw new Error(
216
+ 'Usage: moxxy settings <action>\n' +
217
+ ' network-mode [safe|unsafe] Get or set network mode\n' +
218
+ ' browser-rendering [true|false] Enable/disable headless Chrome rendering\n' +
219
+ ' get [--key <k>] View settings\n' +
220
+ ' set --key <k> --value <v> Set a setting'
221
+ );
222
+ }
223
+ }
224
+ }
package/src/help.js CHANGED
@@ -1,4 +1,28 @@
1
1
  export const COMMAND_HELP = {
2
+ settings: `Usage: moxxy settings <action> [options]
3
+
4
+ Manage global Moxxy settings.
5
+
6
+ Actions:
7
+ network-mode [safe|unsafe] Get or set network mode
8
+ get [--key <k>] View all settings or a single key
9
+ set --key <k> --value <v> Set a setting value
10
+
11
+ Network Modes:
12
+ safe (default) Agent asks the user before accessing non-allowlisted domains
13
+ unsafe Domain allowlist is bypassed entirely — any domain is allowed
14
+
15
+ Options:
16
+ --json Output as JSON
17
+
18
+ Examples:
19
+ moxxy settings network-mode Show current mode
20
+ moxxy settings network-mode unsafe Switch to unsafe mode
21
+ moxxy settings network-mode safe Switch back to safe mode
22
+ moxxy settings get Show all settings
23
+ moxxy settings get --key network_mode Show a single setting
24
+ moxxy settings set --key network_mode --value unsafe`,
25
+
2
26
  init: `Usage: moxxy init
3
27
 
4
28
  First-time setup wizard. Configures the Moxxy home directory, auth mode,