termux-dev 1.2.2 → 1.4.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/dist/cli/index.js CHANGED
@@ -27,6 +27,11 @@ import { askPrompt } from './prompt.js';
27
27
  import { resolveAtMentions } from './files.js';
28
28
  import { runStartupUpdateCheck, checkForUpdates, performSelfUpdate } from './updater.js';
29
29
  import { setActiveTheme, getCurrentTheme, listThemes, findTheme } from './theme.js';
30
+ import { runHeadlessMode } from './headless.js';
31
+ import { CustomCommandManager } from '../core/commands.js';
32
+ import { UsageTracker } from '../core/usage.js';
33
+ import { SessionExporter } from './export.js';
34
+ import { MCPManager } from '../mcp/manager.js';
30
35
  const CONFIG_PATH = path.join(os.homedir(), '.devxrc.json');
31
36
  function maskApiKey(key) {
32
37
  if (!key)
@@ -316,23 +321,111 @@ async function runOnboarding() {
316
321
  p.outro(pc.green('Setup complete! Configuration saved to ~/.devxrc.json'));
317
322
  return res;
318
323
  }
319
- async function loadConfig() {
324
+ async function loadConfig(interactive = true) {
325
+ let globalConfig = {};
320
326
  try {
321
327
  const data = await fs.readFile(CONFIG_PATH, 'utf8');
322
- const parsed = JSON.parse(data);
323
- if (!parsed.model || !parsed.baseUrl)
328
+ globalConfig = JSON.parse(data);
329
+ if (!globalConfig.model || !globalConfig.baseUrl)
324
330
  throw new Error('Invalid config');
325
- parsed.maxContextTokens = parsed.maxContextTokens || getModelContextLimit(parsed.model);
326
- parsed.apiKeys = parsed.apiKeys || {};
327
- parsed.baseUrls = parsed.baseUrls || {};
328
- if (parsed.provider && parsed.apiKey) {
329
- parsed.apiKeys[parsed.provider] = parsed.apiKey;
331
+ globalConfig.maxContextTokens = globalConfig.maxContextTokens || getModelContextLimit(globalConfig.model);
332
+ globalConfig.apiKeys = globalConfig.apiKeys || {};
333
+ globalConfig.baseUrls = globalConfig.baseUrls || {};
334
+ globalConfig.trustedProjects = globalConfig.trustedProjects || {};
335
+ if (globalConfig.provider && globalConfig.apiKey) {
336
+ globalConfig.apiKeys[globalConfig.provider] = globalConfig.apiKey;
330
337
  }
331
- return parsed;
332
338
  }
333
339
  catch {
334
- return await runOnboarding();
340
+ if (interactive) {
341
+ globalConfig = await runOnboarding();
342
+ }
343
+ else {
344
+ throw new Error('No configuration found in ~/.devxrc.json. Please run devx interactively first to set up your AI provider.');
345
+ }
346
+ }
347
+ // Check for local project config (.devx.json or .devxrc.json)
348
+ const localConfigPaths = [
349
+ path.join(process.cwd(), '.devx.json'),
350
+ path.join(process.cwd(), '.devxrc.json')
351
+ ];
352
+ let localConfig = null;
353
+ for (const lp of localConfigPaths) {
354
+ try {
355
+ if (fsSync.existsSync(lp)) {
356
+ const raw = await fs.readFile(lp, 'utf8');
357
+ localConfig = JSON.parse(raw);
358
+ break;
359
+ }
360
+ }
361
+ catch { }
335
362
  }
363
+ const effectiveConfig = { ...globalConfig };
364
+ const cwd = process.cwd();
365
+ let isTrusted = globalConfig.trustedProjects?.[cwd];
366
+ // Workspace & Project Trust Check on first time opening this directory
367
+ if (isTrusted === undefined) {
368
+ if (interactive) {
369
+ console.log('');
370
+ p.log.warn(pc.bold(pc.yellow(`🛡️ [WORKSPACE TRUST] First time opening this workspace:`)));
371
+ console.log(pc.dim(` Directory: ${cwd}`));
372
+ if (localConfig) {
373
+ if (localConfig.autoApprove === true) {
374
+ console.log(pc.yellow(` • Local .devx.json requests Auto-approval (YOLO mode)`));
375
+ }
376
+ if (localConfig.bashAllowlist) {
377
+ console.log(pc.yellow(` • Local .devx.json requests Bash allowlist: ${localConfig.bashAllowlist.join(', ')}`));
378
+ }
379
+ }
380
+ const answer = await p.confirm({
381
+ message: 'Do you trust this workspace and allow agent operations?',
382
+ initialValue: true
383
+ });
384
+ if (p.isCancel(answer) || answer !== true) {
385
+ resetTerminalTheme();
386
+ p.outro(pc.yellow('Workspace not trusted. Exiting devx.'));
387
+ process.exit(0);
388
+ }
389
+ globalConfig.trustedProjects = globalConfig.trustedProjects || {};
390
+ globalConfig.trustedProjects[cwd] = true;
391
+ await saveConfig(globalConfig);
392
+ p.log.success(pc.green('Workspace marked as trusted. Full capabilities enabled.'));
393
+ }
394
+ else {
395
+ isTrusted = false; // Never auto-trust in non-interactive headless mode
396
+ }
397
+ }
398
+ if (localConfig && typeof localConfig === 'object') {
399
+ // Apply safe configuration overrides
400
+ if (localConfig.model)
401
+ effectiveConfig.model = localConfig.model;
402
+ if (localConfig.provider)
403
+ effectiveConfig.provider = localConfig.provider;
404
+ if (localConfig.apiKey)
405
+ effectiveConfig.apiKey = localConfig.apiKey;
406
+ if (localConfig.baseUrl)
407
+ effectiveConfig.baseUrl = localConfig.baseUrl;
408
+ if (localConfig.maxIterations)
409
+ effectiveConfig.maxIterations = localConfig.maxIterations;
410
+ if (localConfig.maxContextTokens)
411
+ effectiveConfig.maxContextTokens = localConfig.maxContextTokens;
412
+ if (localConfig.dataSaverLimitMB)
413
+ effectiveConfig.dataSaverLimitMB = localConfig.dataSaverLimitMB;
414
+ if (localConfig.pureBlackTheme !== undefined)
415
+ effectiveConfig.pureBlackTheme = localConfig.pureBlackTheme;
416
+ // Apply elevated permissions only if explicitly trusted
417
+ if (isTrusted) {
418
+ if (localConfig.autoApprove !== undefined)
419
+ effectiveConfig.autoApprove = localConfig.autoApprove;
420
+ if (Array.isArray(localConfig.bashAllowlist))
421
+ effectiveConfig.bashAllowlist = localConfig.bashAllowlist;
422
+ }
423
+ }
424
+ // Configure Data Saver threshold
425
+ if (effectiveConfig.dataSaverLimitMB) {
426
+ UsageTracker.getInstance().setLimit(effectiveConfig.dataSaverLimitMB);
427
+ }
428
+ return effectiveConfig;
336
429
  }
337
430
  function enableDarkTheme(enabled = true) {
338
431
  if (!enabled)
@@ -391,7 +484,7 @@ function drawLogo() {
391
484
  theme.colorFn(' █▀▀▄ █▀▀▀ █ █ █ █'),
392
485
  theme.colorFn(' █ █ █▀▀▀ ▀▄▀ ▀▄▀ '),
393
486
  theme.colorFn(' █▄▄▀ █▄▄▄ ▀ ▀ ▀ '),
394
- ' ' + theme.boldFn('v1.2.2'),
487
+ ' ' + theme.boldFn('v1.4.0'),
395
488
  ''
396
489
  ];
397
490
  for (const line of logo) {
@@ -406,7 +499,7 @@ function drawLogo() {
406
499
  indent + theme.colorFn('▀▀▀█▀▀▀ █▀▀▀ █▀▀█ █▄ ▄█ █ █ ▀▄ ▄▀ █▀▀▄ █▀▀▀ █ █'),
407
500
  indent + theme.colorFn(' █ █▀▀▀ █▄▄▀ █ █ █ █ █ █ ▀▀ █ █ █▀▀▀ █ █'),
408
501
  indent + theme.colorFn(' █ █▄▄▄ █ ▀▄ █ █ ▀▄▄▀ ▄▀ ▀▄ █▄▄▀ █▄▄▄ ▀▄▀ '),
409
- indent + theme.boldFn('v1.2.2'),
502
+ indent + theme.boldFn('v1.4.0'),
410
503
  ''
411
504
  ];
412
505
  for (const line of logo) {
@@ -523,16 +616,191 @@ async function handleSessionDelete() {
523
616
  }
524
617
  }
525
618
  }
619
+ async function handleThemeSelect(config) {
620
+ const currentTh = getCurrentTheme();
621
+ const themeChoices = listThemes().map(t => ({
622
+ name: `${t.emoji} ${t.boldFn(t.name.padEnd(18))} ${pc.dim(t.desc)} ${t.id === currentTh.id ? pc.green('(Active)') : ''}`,
623
+ value: t.id,
624
+ description: `Apply ${t.name} color palette (${t.hex}) to banners, prompts, and actions`
625
+ }));
626
+ try {
627
+ const selected = await select({
628
+ message: `${pc.bold('🎨 Select UI Theme / Выберите цветовую тему:')}`,
629
+ choices: themeChoices
630
+ });
631
+ if (selected) {
632
+ config.theme = selected;
633
+ await saveConfig(config);
634
+ const th = setActiveTheme(selected);
635
+ drawLogo();
636
+ p.log.success(th.boldFn(`🎨 Theme switched to ${th.emoji} ${th.name}!`));
637
+ }
638
+ }
639
+ catch { }
640
+ }
641
+ async function handleSettings(config) {
642
+ while (true) {
643
+ try {
644
+ const maxIter = config.maxIterations || 100;
645
+ const maxIterLabel = maxIter >= 9999 ? 'Unlimited' : `${maxIter} steps`;
646
+ const currentTh = getCurrentTheme();
647
+ const choice = await select({
648
+ message: `${pc.bold('⚙️ Settings')} ${pc.dim(`(devx v1.4.0 • theme: ${currentTh.name})`)}`,
649
+ choices: [
650
+ {
651
+ name: `🎨 Color Theme: ${currentTh.emoji} ${currentTh.name}`,
652
+ value: 'change_theme',
653
+ description: `Switch UI accent colors (${currentTh.desc})`
654
+ },
655
+ {
656
+ name: `${config.pureBlackTheme !== false ? pc.green('🖤 Pure Black Background: ON') : pc.yellow('🖤 Pure Black Background: OFF')}`,
657
+ value: 'toggle_black_theme',
658
+ description: config.pureBlackTheme !== false
659
+ ? 'Apply deep OLED obsidian black background (#0a0a0c) like OpenCode'
660
+ : 'Use standard system terminal background color'
661
+ },
662
+ {
663
+ name: `${config.autoApprove ? pc.green('⚡ Auto-Approve (YOLO Mode): ON') : pc.yellow('🛡️ Auto-Approve (YOLO Mode): OFF')}`,
664
+ value: 'toggle_auto_approve',
665
+ description: config.autoApprove
666
+ ? 'Permissions are automatically granted (no confirmation prompts for commands/files)'
667
+ : 'Agent asks for confirmation before executing bash commands or writing files'
668
+ },
669
+ {
670
+ name: `${config.enableMemory !== false ? pc.green('🧠 Project Memory Bank: ON') : pc.yellow('🧠 Project Memory Bank: OFF')}`,
671
+ value: 'toggle_memory',
672
+ description: config.enableMemory !== false
673
+ ? 'Load persistent project rules and preferences from .devx/memory.md into AI context'
674
+ : 'Start sessions with a clean state without loading project memory'
675
+ },
676
+ {
677
+ name: `${config.checkUpdates !== false ? pc.green('🔔 Check for Updates on Startup: ON') : pc.yellow('🔔 Check for Updates on Startup: OFF')}`,
678
+ value: 'toggle_check_updates',
679
+ description: config.checkUpdates !== false
680
+ ? 'Automatically check for updates from GitHub repository when launching devx'
681
+ : 'Disable update checking on startup (run /update manually instead)'
682
+ },
683
+ {
684
+ name: `🔄 Max Agent Iterations: ${currentTh.colorFn(maxIterLabel)}`,
685
+ value: 'change_max_iterations',
686
+ description: 'Limit how many tool steps (file edits, terminal commands) agent can do per request'
687
+ },
688
+ {
689
+ name: `${currentTh.colorFn('✨ About devx')} ${pc.dim('(v1.4.0 by ApvCode)')}`,
690
+ value: 'about',
691
+ description: 'Terminal-Native AI Coding Agent created by ApvCode (https://github.com/apvcode/Termux-Dev)'
692
+ },
693
+ {
694
+ name: '⬅️ Back / Save',
695
+ value: 'back',
696
+ description: 'Return to chat'
697
+ }
698
+ ]
699
+ });
700
+ if (choice === 'change_theme') {
701
+ await handleThemeSelect(config);
702
+ continue;
703
+ }
704
+ if (choice === 'about') {
705
+ p.note(`⚡ devx v1.4.0 — Terminal-Native AI Coding Agent\n` +
706
+ `🎨 Theme: ${currentTh.emoji} ${currentTh.name}\n` +
707
+ `👤 Author: ApvCode (https://github.com/apvcode)\n` +
708
+ `🌟 Repository: https://github.com/apvcode/Termux-Dev\n` +
709
+ `📜 License: MIT License (2026)\n` +
710
+ `Built for Android Termux, Windows, macOS, and Linux.`, 'About devx');
711
+ continue;
712
+ }
713
+ if (choice === 'toggle_black_theme') {
714
+ config.pureBlackTheme = config.pureBlackTheme === false ? true : false;
715
+ await saveConfig(config);
716
+ if (config.pureBlackTheme) {
717
+ enableDarkTheme(true);
718
+ }
719
+ else {
720
+ resetTerminalTheme();
721
+ }
722
+ drawLogo();
723
+ p.log.success(`Pure Black background: ${config.pureBlackTheme ? pc.bold(pc.green('ON (Deep Black)')) : pc.bold(pc.yellow('OFF (System Default)'))}`);
724
+ continue;
725
+ }
726
+ if (choice === 'toggle_auto_approve') {
727
+ config.autoApprove = !config.autoApprove;
728
+ await saveConfig(config);
729
+ p.log.success(`Auto-approve permissions: ${config.autoApprove ? pc.bold(pc.green('ON (Automatic Yes)')) : pc.bold(pc.yellow('OFF (Ask every time)'))}`);
730
+ continue;
731
+ }
732
+ if (choice === 'toggle_memory') {
733
+ config.enableMemory = config.enableMemory === false ? true : false;
734
+ await saveConfig(config);
735
+ p.log.success(`Project memory bank: ${config.enableMemory !== false ? pc.bold(pc.green('ON (Persistent .devx/memory.md)')) : pc.bold(pc.yellow('OFF'))}`);
736
+ continue;
737
+ }
738
+ if (choice === 'toggle_check_updates') {
739
+ config.checkUpdates = config.checkUpdates === false ? true : false;
740
+ await saveConfig(config);
741
+ p.log.success(`Check for updates: ${config.checkUpdates !== false ? pc.bold(pc.green('ON (Checked on startup)')) : pc.bold(pc.yellow('OFF (Manual only)'))}`);
742
+ continue;
743
+ }
744
+ if (choice === 'change_max_iterations') {
745
+ const val = await select({
746
+ message: 'Select maximum iterations limit per prompt:',
747
+ choices: [
748
+ { name: '30 steps (Strict / Safe)', value: 30 },
749
+ { name: '50 steps (Moderate)', value: 50 },
750
+ { name: '100 steps (Recommended / Default)', value: 100 },
751
+ { name: '200 steps (Very large refactors)', value: 200 },
752
+ { name: 'Unlimited (No limit)', value: 9999 }
753
+ ]
754
+ });
755
+ config.maxIterations = val;
756
+ await saveConfig(config);
757
+ p.log.success(`Max iterations updated to: ${pc.bold(val >= 9999 ? 'Unlimited' : `${val} steps`)}`);
758
+ continue;
759
+ }
760
+ break;
761
+ }
762
+ catch {
763
+ break;
764
+ }
765
+ }
766
+ process.stdin.resume();
767
+ return config;
768
+ }
526
769
  export async function main() {
527
770
  const program = new Command();
528
771
  program
529
772
  .name('devx')
530
- .description('CLI tool for vibe-coding')
531
- .option('--plan', 'Start in plan mode')
773
+ .description('Terminal-native AI coding assistant and vibe-coding agent')
774
+ .version('1.4.0')
775
+ .option('-p, --prompt <task>', 'Run one-shot task non-interactively (headless mode)')
776
+ .option('-y, --yolo', 'Automatically approve all tool executions without confirmation')
777
+ .option('-m, --model <model>', 'Specify AI model to use for this execution')
778
+ .option('-q, --quiet', 'Quiet output in headless mode (suppress banners and tool logs)')
779
+ .option('--json', 'Output structured JSON in headless mode')
780
+ .option('--plan', 'Start in plan mode (architect & planner)')
532
781
  .parse(process.argv);
533
782
  const options = program.opts();
534
- let planMode = !!options.plan;
535
- let config = await loadConfig();
783
+ const planModeInitial = !!options.plan;
784
+ const isHeadless = !!options.prompt;
785
+ let config = await loadConfig(!isHeadless);
786
+ if (options.model) {
787
+ config.model = options.model;
788
+ config.maxContextTokens = getModelContextLimit(options.model);
789
+ }
790
+ if (options.yolo) {
791
+ config.autoApprove = true;
792
+ }
793
+ // If -p / --prompt is provided, execute one-shot headless mode and exit!
794
+ if (isHeadless) {
795
+ const exitCode = await runHeadlessMode(options.prompt, config, {
796
+ planMode: planModeInitial,
797
+ yolo: !!options.yolo,
798
+ quiet: !!options.quiet,
799
+ json: !!options.json
800
+ });
801
+ process.exit(exitCode);
802
+ }
803
+ let planMode = planModeInitial;
536
804
  if (!config.onboarded) {
537
805
  const cols = Math.min(process.stdout.columns || 40, 42);
538
806
  const fill = Math.max(2, cols - 16);
@@ -573,6 +841,7 @@ export async function main() {
573
841
  const sessionManager = new SessionManager(config.model, planMode);
574
842
  drawLogo();
575
843
  await runStartupUpdateCheck(config);
844
+ await MCPManager.getInstance().init();
576
845
  let totalSessionCost = 0;
577
846
  let currentDraft = '';
578
847
  let autoTriggerPrompt = '';
@@ -656,215 +925,84 @@ export async function main() {
656
925
  if (answer.startsWith('/')) {
657
926
  const parts = answer.split(' ');
658
927
  let cmd = parts[0];
659
- async function handleThemeSelect(config) {
660
- const currentTh = getCurrentTheme();
661
- const themeChoices = listThemes().map(t => ({
662
- name: `${t.emoji} ${t.boldFn(t.name.padEnd(18))} ${pc.dim(t.desc)} ${t.id === currentTh.id ? pc.green('(Active)') : ''}`,
663
- value: t.id,
664
- description: `Apply ${t.name} color palette (${t.hex}) to banners, prompts, and actions`
665
- }));
666
- try {
667
- const selected = await select({
668
- message: `${pc.bold('🎨 Select UI Theme / Выберите цветовую тему:')}`,
669
- choices: themeChoices
670
- });
671
- if (selected) {
672
- config.theme = selected;
673
- await saveConfig(config);
674
- const th = setActiveTheme(selected);
675
- drawLogo();
676
- p.log.success(th.boldFn(`🎨 Theme switched to ${th.emoji} ${th.name}!`));
677
- }
678
- }
679
- catch { }
928
+ // 1. Check for custom slash commands (.devx/commands/*.md)
929
+ const customCmd = await CustomCommandManager.findCommand(cmd);
930
+ if (customCmd) {
931
+ const cmdArgs = answer.slice(cmd.length).trim();
932
+ const expandedPrompt = CustomCommandManager.expandTemplate(customCmd.promptTemplate, cmdArgs);
933
+ answer = expandedPrompt;
934
+ p.log.info(pc.cyan(`⚡ Executing custom command: ${pc.bold(customCmd.cmd)} (${customCmd.desc})`));
680
935
  }
681
- async function handleSettings(config) {
682
- while (true) {
936
+ else {
937
+ const VALID_COMMANDS = [
938
+ '/new', '/reset', '/resume', '/session', '/sessions', '/history',
939
+ '/theme', '/themes', '/usage', '/export', '/mcp',
940
+ '/settings', '/update', '/model', '/provider', '/providers',
941
+ '/plan', '/agent', '/image', '/serve', '/memory', '/undo',
942
+ '/diff', '/commit', '/status', '/compact', '/init', '/doctor',
943
+ '/config', '/clear', '/exit', '/quit', '/help'
944
+ ];
945
+ if (!VALID_COMMANDS.includes(cmd)) {
946
+ const customList = await CustomCommandManager.listCommands();
947
+ const customSlashItems = customList.map(c => ({
948
+ name: `${c.cmd.padEnd(14)} - ${c.desc}`,
949
+ value: c.cmd
950
+ }));
951
+ const SLASH_COMMANDS = [
952
+ { name: '/new - Start a new clean chat session', value: '/new' },
953
+ { name: '/resume - Resume a previous chat session', value: '/resume' },
954
+ { name: '/session del - Select and delete saved sessions', value: '/session del' },
955
+ { name: '/usage - Show network bandwidth, data saver & token cost', value: '/usage' },
956
+ { name: '/export - Export session conversation to Markdown', value: '/export' },
957
+ { name: '/mcp - Manage Model Context Protocol (MCP) servers & tools', value: '/mcp' },
958
+ { name: '/theme - Switch UI color theme', value: '/theme' },
959
+ { name: '/doctor - Run system & environment health diagnostics', value: '/doctor' },
960
+ { name: '/settings - Configure permissions & auto-approval', value: '/settings' },
961
+ { name: '/update - Check and install updates from GitHub', value: '/update' },
962
+ { name: '/model - Switch model for current provider', value: '/model' },
963
+ { name: '/provider - Change AI provider (Google, OpenRouter...)', value: '/provider' },
964
+ { name: '/plan - Switch to PLAN mode (architect)', value: '/plan' },
965
+ { name: '/agent - Switch to AGENT mode (coder)', value: '/agent' },
966
+ { name: '/serve - Start local web server for web preview', value: '/serve' },
967
+ { name: '/memory - View or edit project memory bank', value: '/memory' },
968
+ { name: '/undo - Revert last file changes made by AI', value: '/undo' },
969
+ { name: '/diff - Show git diff of modified files', value: '/diff' },
970
+ { name: '/commit - AI-generated git commit message', value: '/commit' },
971
+ { name: '/status - Show git repository status', value: '/status' },
972
+ { name: '/compact - Compact conversation context', value: '/compact' },
973
+ { name: '/config - View current configuration', value: '/config' },
974
+ { name: '/clear - Clear message history', value: '/clear' },
975
+ { name: '/help - Show commands overview', value: '/help' },
976
+ { name: '/exit - Exit devx', value: '/exit' },
977
+ ...customSlashItems
978
+ ];
683
979
  try {
684
- const maxIter = config.maxIterations || 100;
685
- const maxIterLabel = maxIter >= 9999 ? 'Unlimited' : `${maxIter} steps`;
686
- const currentTh = getCurrentTheme();
687
- const choice = await select({
688
- message: `${pc.bold('⚙️ Settings')} ${pc.dim(`(devx v1.2.2 • theme: ${currentTh.name})`)}`,
689
- choices: [
690
- {
691
- name: `🎨 Color Theme: ${currentTh.emoji} ${currentTh.name}`,
692
- value: 'change_theme',
693
- description: `Switch UI accent colors (${currentTh.desc})`
694
- },
695
- {
696
- name: `${config.pureBlackTheme !== false ? pc.green('🖤 Pure Black Background: ON') : pc.yellow('🖤 Pure Black Background: OFF')}`,
697
- value: 'toggle_black_theme',
698
- description: config.pureBlackTheme !== false
699
- ? 'Apply deep OLED obsidian black background (#0a0a0c) like OpenCode'
700
- : 'Use standard system terminal background color'
701
- },
702
- {
703
- name: `${config.autoApprove ? pc.green('⚡ Auto-Approve (YOLO Mode): ON') : pc.yellow('🛡️ Auto-Approve (YOLO Mode): OFF')}`,
704
- value: 'toggle_auto_approve',
705
- description: config.autoApprove
706
- ? 'Permissions are automatically granted (no confirmation prompts for commands/files)'
707
- : 'Agent asks for confirmation before executing bash commands or writing files'
708
- },
709
- {
710
- name: `${config.enableMemory !== false ? pc.green('🧠 Project Memory Bank: ON') : pc.yellow('🧠 Project Memory Bank: OFF')}`,
711
- value: 'toggle_memory',
712
- description: config.enableMemory !== false
713
- ? 'Load persistent project rules and preferences from .devx/memory.md into AI context'
714
- : 'Start sessions with a clean state without loading project memory'
715
- },
716
- {
717
- name: `${config.checkUpdates !== false ? pc.green('🔔 Check for Updates on Startup: ON') : pc.yellow('🔔 Check for Updates on Startup: OFF')}`,
718
- value: 'toggle_check_updates',
719
- description: config.checkUpdates !== false
720
- ? 'Automatically check for updates from GitHub repository when launching devx'
721
- : 'Disable update checking on startup (run /update manually instead)'
722
- },
723
- {
724
- name: `🔄 Max Agent Iterations: ${currentTh.colorFn(maxIterLabel)}`,
725
- value: 'change_max_iterations',
726
- description: 'Limit how many tool steps (file edits, terminal commands) agent can do per request'
727
- },
728
- {
729
- name: `${currentTh.colorFn('✨ About devx')} ${pc.dim('(v1.2.2 by ApvCode)')}`,
730
- value: 'about',
731
- description: 'Terminal-Native AI Coding Agent created by ApvCode (https://github.com/apvcode/Termux-Dev)'
732
- },
733
- {
734
- name: '⬅️ Back / Save',
735
- value: 'back',
736
- description: 'Return to chat'
737
- }
738
- ]
980
+ const picked = await search({
981
+ message: 'Commands (type to search or select):',
982
+ source: async (term) => {
983
+ const q = (term || '').trim().toLowerCase();
984
+ const list = [
985
+ { name: pc.yellow('⬅️ Cancel'), value: '__cancel__' },
986
+ ...SLASH_COMMANDS
987
+ ];
988
+ if (!q)
989
+ return list;
990
+ return list.filter(item => item.name.toLowerCase().includes(q) || item.value.toLowerCase().includes(q));
991
+ },
992
+ pageSize: 12
739
993
  });
740
- if (choice === 'change_theme') {
741
- await handleThemeSelect(config);
994
+ if (!picked || picked === '__cancel__') {
742
995
  continue;
743
996
  }
744
- if (choice === 'about') {
745
- p.note(`⚡ devx v1.2.2 — Terminal-Native AI Coding Agent\n` +
746
- `🎨 Theme: ${currentTh.emoji} ${currentTh.name}\n` +
747
- `👤 Author: ApvCode (https://github.com/apvcode)\n` +
748
- `🌟 Repository: https://github.com/apvcode/Termux-Dev\n` +
749
- `📜 License: MIT License (2026)\n` +
750
- `Built for Android Termux, Windows, macOS, and Linux.`, 'About devx');
997
+ if (picked === '/session del') {
998
+ await handleSessionDelete();
751
999
  continue;
752
1000
  }
753
- if (choice === 'toggle_black_theme') {
754
- config.pureBlackTheme = config.pureBlackTheme === false ? true : false;
755
- await saveConfig(config);
756
- if (config.pureBlackTheme) {
757
- enableDarkTheme(true);
758
- }
759
- else {
760
- resetTerminalTheme();
761
- }
762
- drawLogo();
763
- p.log.success(`Pure Black background: ${config.pureBlackTheme ? pc.bold(pc.green('ON (Deep Black)')) : pc.bold(pc.yellow('OFF (System Default)'))}`);
764
- continue;
765
- }
766
- if (choice === 'toggle_auto_approve') {
767
- config.autoApprove = !config.autoApprove;
768
- await saveConfig(config);
769
- p.log.success(`Auto-approve permissions: ${config.autoApprove ? pc.bold(pc.green('ON (Automatic Yes)')) : pc.bold(pc.yellow('OFF (Ask every time)'))}`);
770
- continue;
771
- }
772
- if (choice === 'toggle_memory') {
773
- config.enableMemory = config.enableMemory === false ? true : false;
774
- await saveConfig(config);
775
- p.log.success(`Project memory bank: ${config.enableMemory !== false ? pc.bold(pc.green('ON (Persistent .devx/memory.md)')) : pc.bold(pc.yellow('OFF'))}`);
776
- continue;
777
- }
778
- if (choice === 'toggle_check_updates') {
779
- config.checkUpdates = config.checkUpdates === false ? true : false;
780
- await saveConfig(config);
781
- p.log.success(`Check for updates: ${config.checkUpdates !== false ? pc.bold(pc.green('ON (Checked on startup)')) : pc.bold(pc.yellow('OFF (Manual only)'))}`);
782
- continue;
783
- }
784
- if (choice === 'change_max_iterations') {
785
- const val = await select({
786
- message: 'Select maximum iterations limit per prompt:',
787
- choices: [
788
- { name: '30 steps (Strict / Safe)', value: 30 },
789
- { name: '50 steps (Moderate)', value: 50 },
790
- { name: '100 steps (Recommended / Default)', value: 100 },
791
- { name: '200 steps (Very large refactors)', value: 200 },
792
- { name: 'Unlimited (No limit)', value: 9999 }
793
- ]
794
- });
795
- config.maxIterations = val;
796
- await saveConfig(config);
797
- p.log.success(`Max iterations updated to: ${pc.bold(val >= 9999 ? 'Unlimited' : `${val} steps`)}`);
798
- continue;
799
- }
800
- break;
1001
+ cmd = picked;
801
1002
  }
802
1003
  catch {
803
- break;
804
- }
805
- }
806
- process.stdin.resume();
807
- return config;
808
- }
809
- const VALID_COMMANDS = [
810
- '/new', '/reset', '/resume', '/session', '/sessions', '/history',
811
- '/theme', '/themes',
812
- '/settings', '/update', '/model', '/provider', '/providers',
813
- '/plan', '/agent', '/image', '/serve', '/memory', '/undo',
814
- '/diff', '/commit', '/status', '/compact', '/init', '/doctor',
815
- '/config', '/clear', '/exit', '/quit', '/help'
816
- ];
817
- if (!VALID_COMMANDS.includes(cmd)) {
818
- const SLASH_COMMANDS = [
819
- { name: '/new - Start a new clean chat session', value: '/new' },
820
- { name: '/resume - Resume a previous chat session', value: '/resume' },
821
- { name: '/session del - Select and delete saved sessions', value: '/session del' },
822
- { name: '/theme - Switch UI color theme', value: '/theme' },
823
- { name: '/doctor - Run system & environment health diagnostics', value: '/doctor' },
824
- { name: '/settings - Configure permissions & auto-approval', value: '/settings' },
825
- { name: '/update - Check and install updates from GitHub', value: '/update' },
826
- { name: '/model - Switch model for current provider', value: '/model' },
827
- { name: '/provider - Change AI provider (Google, OpenRouter...)', value: '/provider' },
828
- { name: '/plan - Switch to PLAN mode (architect)', value: '/plan' },
829
- { name: '/agent - Switch to AGENT mode (coder)', value: '/agent' },
830
- { name: '/serve - Start local web server for web preview', value: '/serve' },
831
- { name: '/memory - View or edit project memory bank', value: '/memory' },
832
- { name: '/undo - Revert last file changes made by AI', value: '/undo' },
833
- { name: '/diff - Show git diff of modified files', value: '/diff' },
834
- { name: '/commit - AI-generated git commit message', value: '/commit' },
835
- { name: '/status - Show git repository status', value: '/status' },
836
- { name: '/compact - Compact conversation context', value: '/compact' },
837
- { name: '/config - View current configuration', value: '/config' },
838
- { name: '/clear - Clear message history', value: '/clear' },
839
- { name: '/help - Show commands overview', value: '/help' },
840
- { name: '/exit - Exit devx', value: '/exit' },
841
- ];
842
- try {
843
- const picked = await search({
844
- message: 'Commands (type to search or select):',
845
- source: async (term) => {
846
- const q = (term || '').trim().toLowerCase();
847
- const list = [
848
- { name: pc.yellow('⬅️ Cancel'), value: '__cancel__' },
849
- ...SLASH_COMMANDS
850
- ];
851
- if (!q)
852
- return list;
853
- return list.filter(item => item.name.toLowerCase().includes(q) || item.value.toLowerCase().includes(q));
854
- },
855
- pageSize: 12
856
- });
857
- if (!picked || picked === '__cancel__') {
858
- continue;
859
- }
860
- if (picked === '/session del') {
861
- await handleSessionDelete();
862
1004
  continue;
863
1005
  }
864
- cmd = picked;
865
- }
866
- catch {
867
- continue;
868
1006
  }
869
1007
  }
870
1008
  if (cmd === '/settings') {
@@ -1055,13 +1193,7 @@ export async function main() {
1055
1193
  p.log.warn('No changes to undo.');
1056
1194
  }
1057
1195
  else {
1058
- const msgs = history.getMessages();
1059
- while (msgs.length > 1 && msgs[msgs.length - 1].role !== 'user') {
1060
- msgs.pop();
1061
- }
1062
- if (msgs.length > 1 && msgs[msgs.length - 1].role === 'user') {
1063
- msgs.pop();
1064
- }
1196
+ history.popLastTurn();
1065
1197
  p.log.success(pc.bold(pc.green(`⏪ Successfully reverted changes in ${count} file(s):`)));
1066
1198
  for (const f of revertedFiles) {
1067
1199
  console.log(pc.cyan(` • ${f}`));
@@ -1322,8 +1454,47 @@ export async function main() {
1322
1454
  }
1323
1455
  continue;
1324
1456
  }
1457
+ if (cmd === '/usage') {
1458
+ const card = UsageTracker.getInstance().renderUsageCard();
1459
+ console.log('\n' + card);
1460
+ continue;
1461
+ }
1462
+ if (cmd === '/export') {
1463
+ const targetName = parts.slice(1).join(' ').trim();
1464
+ const res = await SessionExporter.exportToMarkdown(history.getMessages(), config.model, targetName || undefined);
1465
+ if (res.success) {
1466
+ p.log.success(pc.bold(pc.green(`📄 Session exported to: ${res.filePath}`)));
1467
+ }
1468
+ else {
1469
+ p.log.error(`Failed to export session: ${res.error}`);
1470
+ }
1471
+ continue;
1472
+ }
1473
+ if (cmd === '/mcp') {
1474
+ const sub = (parts[1] || '').toLowerCase();
1475
+ if (sub === 'reload' || sub === 'restart' || sub === 'r') {
1476
+ const s = p.spinner();
1477
+ s.start('Reloading MCP servers...');
1478
+ await MCPManager.getInstance().reload();
1479
+ s.stop();
1480
+ console.log('\n' + MCPManager.getInstance().renderStatusCard());
1481
+ }
1482
+ else {
1483
+ console.log('\n' + MCPManager.getInstance().renderStatusCard());
1484
+ }
1485
+ continue;
1486
+ }
1325
1487
  if (cmd === '/config') {
1326
- p.note(JSON.stringify(config, null, 2), 'Configuration');
1488
+ const masked = { ...config };
1489
+ if (masked.apiKey)
1490
+ masked.apiKey = maskApiKey(masked.apiKey);
1491
+ if (masked.apiKeys) {
1492
+ masked.apiKeys = { ...masked.apiKeys };
1493
+ for (const k of Object.keys(masked.apiKeys)) {
1494
+ masked.apiKeys[k] = maskApiKey(masked.apiKeys[k]);
1495
+ }
1496
+ }
1497
+ p.note(JSON.stringify(masked, null, 2), 'Configuration (Secrets Masked)');
1327
1498
  continue;
1328
1499
  }
1329
1500
  if (cmd === '/model') {
@@ -1408,10 +1579,12 @@ export async function main() {
1408
1579
  });
1409
1580
  const provider = createProvider(config);
1410
1581
  const tools = getTools(planMode);
1411
- const guard = new CLIConsoleGuard(config.autoApprove);
1582
+ const guard = new CLIConsoleGuard(config.autoApprove, config.bashAllowlist || []);
1412
1583
  const agentConfig = {
1413
1584
  maxContextTokens: config.maxContextTokens || 100000,
1414
- maxIterations: config.maxIterations || 100
1585
+ maxIterations: config.maxIterations || 100,
1586
+ autoApprove: config.autoApprove,
1587
+ bashAllowlist: config.bashAllowlist
1415
1588
  };
1416
1589
  const agent = new Agent(agentConfig, provider, tools, history, guard);
1417
1590
  const taskStartTime = Date.now();
@@ -1755,5 +1928,19 @@ export async function main() {
1755
1928
  }
1756
1929
  }
1757
1930
  }
1931
+ MCPManager.getInstance().stopAll();
1758
1932
  }
1933
+ process.on('exit', () => {
1934
+ try {
1935
+ MCPManager.getInstance().stopAll();
1936
+ }
1937
+ catch { }
1938
+ });
1939
+ process.on('SIGINT', () => {
1940
+ try {
1941
+ MCPManager.getInstance().stopAll();
1942
+ }
1943
+ catch { }
1944
+ process.exit(130);
1945
+ });
1759
1946
  main().catch(console.error);