neoagent 3.0.1-beta.12 → 3.0.1-beta.13

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/docs/billing.md CHANGED
@@ -7,19 +7,40 @@ anywhere in the application.
7
7
 
8
8
  ## Enable billing
9
9
 
10
- Set the following environment variables before starting the server:
10
+ Run the interactive setup wizard:
11
11
 
12
12
  ```bash
13
- neoagent env set NEOAGENT_BILLING_ENABLED true
14
- neoagent env set STRIPE_SECRET_KEY sk_live_...
15
- neoagent env set STRIPE_PUBLISHABLE_KEY pk_live_...
16
- neoagent env set STRIPE_WEBHOOK_SECRET whsec_...
13
+ neoagent billing setup
17
14
  ```
18
15
 
19
- Restart the server. The admin dashboard will show a **Billing** navigation
20
- item and the `/api/billing/*` endpoints will become active.
16
+ The wizard prompts for your Stripe API keys, webhook secret, and trial length,
17
+ then asks whether to enable billing immediately. Use Stripe test keys
18
+ (`sk_test_...`, `pk_test_...`) during development.
21
19
 
22
- Use Stripe test keys (`sk_test_...`, `pk_test_...`) during development.
20
+ To check the current configuration at any time:
21
+
22
+ ```bash
23
+ neoagent billing # or: neoagent billing status
24
+ ```
25
+
26
+ To toggle billing without re-running setup:
27
+
28
+ ```bash
29
+ neoagent billing enable # set NEOAGENT_BILLING_ENABLED=true and restart
30
+ neoagent billing disable # set NEOAGENT_BILLING_ENABLED=false and restart
31
+ ```
32
+
33
+ When enabled the admin dashboard will show a **Billing** navigation item and
34
+ the `/api/billing/*` endpoints become active.
35
+
36
+ > **Manual alternative** — you can also set variables directly and restart:
37
+ > ```bash
38
+ > neoagent env set STRIPE_SECRET_KEY sk_live_...
39
+ > neoagent env set STRIPE_PUBLISHABLE_KEY pk_live_...
40
+ > neoagent env set STRIPE_WEBHOOK_SECRET whsec_...
41
+ > neoagent env set NEOAGENT_BILLING_ENABLED true
42
+ > neoagent restart
43
+ > ```
23
44
 
24
45
  ## Subscription plans
25
46
 
@@ -99,8 +99,17 @@ reset, email changes, and security notifications.
99
99
 
100
100
  ## Billing
101
101
 
102
- Billing is disabled by default. See [Billing](billing.md) for the full setup
103
- guide, webhook configuration, and plan management.
102
+ Billing is disabled by default. Use the interactive wizard to configure it:
103
+
104
+ ```bash
105
+ neoagent billing setup # guided Stripe key and webhook setup
106
+ neoagent billing # show current status
107
+ neoagent billing enable # activate billing and restart
108
+ neoagent billing disable # deactivate billing and restart
109
+ ```
110
+
111
+ See [Billing](billing.md) for the full setup guide, webhook configuration, and
112
+ plan management.
104
113
 
105
114
  | Variable | Default | Purpose |
106
115
  |---|---|---|
@@ -1695,6 +1695,7 @@ class _HomeViewState extends State<HomeView> {
1695
1695
  .allowMessagingSuggestion(
1696
1696
  notice.platform,
1697
1697
  suggestion,
1698
+ chatId: notice.chatId,
1698
1699
  );
1699
1700
  },
1700
1701
  icon: Icon(Icons.verified_user_outlined),
@@ -1716,6 +1717,13 @@ class _HomeViewState extends State<HomeView> {
1716
1717
  },
1717
1718
  child: Text('Open Messaging'),
1718
1719
  ),
1720
+ TextButton(
1721
+ onPressed: () async {
1722
+ Navigator.of(dialogContext).pop();
1723
+ await widget.controller.ignoreBlockedSender(notice);
1724
+ },
1725
+ child: Text('Ignore'),
1726
+ ),
1719
1727
  TextButton(
1720
1728
  onPressed: () => Navigator.of(dialogContext).pop(),
1721
1729
  child: Text('Dismiss'),
@@ -1414,6 +1414,10 @@ class _MessagingPanelState extends State<MessagingPanel> {
1414
1414
  ),
1415
1415
  const SizedBox(height: 22),
1416
1416
  ],
1417
+ if (controller.ignoredChats.isNotEmpty) ...[
1418
+ const SizedBox(height: 18),
1419
+ _IgnoredChatsPanel(controller: controller),
1420
+ ],
1417
1421
  _MessagingActivityPanel(messages: controller.messagingMessages),
1418
1422
  ],
1419
1423
  );
@@ -1839,6 +1843,101 @@ class _MessagingGroupHeader extends StatelessWidget {
1839
1843
  }
1840
1844
  }
1841
1845
 
1846
+ class _IgnoredChatsPanel extends StatelessWidget {
1847
+ const _IgnoredChatsPanel({required this.controller});
1848
+
1849
+ final NeoAgentController controller;
1850
+
1851
+ @override
1852
+ Widget build(BuildContext context) {
1853
+ final ignored = controller.ignoredChats;
1854
+ return Container(
1855
+ padding: const EdgeInsets.all(18),
1856
+ decoration: BoxDecoration(
1857
+ color: _bgCard,
1858
+ borderRadius: BorderRadius.circular(8),
1859
+ border: Border.all(color: _borderLight),
1860
+ ),
1861
+ child: Column(
1862
+ crossAxisAlignment: CrossAxisAlignment.start,
1863
+ children: [
1864
+ Row(
1865
+ children: [
1866
+ Expanded(
1867
+ child: Column(
1868
+ crossAxisAlignment: CrossAxisAlignment.start,
1869
+ children: [
1870
+ Text(
1871
+ 'Ignored Channels',
1872
+ style: TextStyle(
1873
+ color: _textPrimary,
1874
+ fontSize: 18,
1875
+ fontWeight: FontWeight.w800,
1876
+ ),
1877
+ ),
1878
+ const SizedBox(height: 3),
1879
+ Text(
1880
+ 'These channels are permanently silenced. To receive messages from them, add them manually to the access policy for the relevant platform.',
1881
+ style: TextStyle(color: _textSecondary, fontSize: 13, height: 1.4),
1882
+ ),
1883
+ ],
1884
+ ),
1885
+ ),
1886
+ _StatusPill(label: '${ignored.length}', color: _textMuted),
1887
+ ],
1888
+ ),
1889
+ const SizedBox(height: 14),
1890
+ for (final key in ignored)
1891
+ Builder(builder: (context) {
1892
+ final sep = key.indexOf(':');
1893
+ final platform = sep > 0 ? key.substring(0, sep) : key;
1894
+ final chatId = sep > 0 ? key.substring(sep + 1) : '';
1895
+ return Container(
1896
+ margin: const EdgeInsets.only(bottom: 8),
1897
+ padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
1898
+ decoration: BoxDecoration(
1899
+ color: _bgSecondary,
1900
+ borderRadius: BorderRadius.circular(8),
1901
+ border: Border.all(color: _borderLight),
1902
+ ),
1903
+ child: Row(
1904
+ children: [
1905
+ Icon(Icons.block_rounded, size: 16, color: _textMuted),
1906
+ const SizedBox(width: 10),
1907
+ Expanded(
1908
+ child: Column(
1909
+ crossAxisAlignment: CrossAxisAlignment.start,
1910
+ children: [
1911
+ Text(
1912
+ platform.toUpperCase(),
1913
+ style: TextStyle(
1914
+ color: _textMuted,
1915
+ fontSize: 10,
1916
+ fontWeight: FontWeight.w700,
1917
+ letterSpacing: 0.6,
1918
+ ),
1919
+ ),
1920
+ Text(
1921
+ chatId.isNotEmpty ? chatId : platform,
1922
+ style: TextStyle(color: _textPrimary, fontSize: 13),
1923
+ ),
1924
+ ],
1925
+ ),
1926
+ ),
1927
+ TextButton(
1928
+ onPressed: () => controller.removeIgnoredChat(key),
1929
+ child: Text('Remove'),
1930
+ ),
1931
+ ],
1932
+ ),
1933
+ );
1934
+ }),
1935
+ ],
1936
+ ),
1937
+ );
1938
+ }
1939
+ }
1940
+
1842
1941
  class _MessagingActivityPanel extends StatelessWidget {
1843
1942
  const _MessagingActivityPanel({required this.messages});
1844
1943
 
@@ -163,6 +163,7 @@ class NeoAgentController extends ChangeNotifier {
163
163
  MessagingQrState? pendingMessagingQr;
164
164
  ToolApprovalRequest? pendingApproval;
165
165
  final List<BlockedSenderNotice> _blockedSenderQueue = <BlockedSenderNotice>[];
166
+ final Set<String> _ignoredChats = <String>{};
166
167
  List<SkillItem> skills = const <SkillItem>[];
167
168
  List<StoreSkillItem> storeSkills = const <StoreSkillItem>[];
168
169
  List<OfficialIntegrationItem> officialIntegrations =
@@ -636,6 +637,8 @@ class NeoAgentController extends ChangeNotifier {
636
637
  BlockedSenderNotice? get pendingBlockedSenderNotice =>
637
638
  _blockedSenderQueue.isEmpty ? null : _blockedSenderQueue.first;
638
639
 
640
+ List<String> get ignoredChats => _ignoredChats.toList();
641
+
639
642
  void _handleRecordingBridgeChanged() {
640
643
  _logRecording('bridge.changed');
641
644
  notifyListeners();
@@ -721,6 +724,7 @@ class NeoAgentController extends ChangeNotifier {
721
724
 
722
725
  Future<void> bootstrap() async {
723
726
  _prefs = await SharedPreferences.getInstance();
727
+ _ignoredChats.addAll(_prefs?.getStringList('messaging.ignored_chats') ?? <String>[]);
724
728
  await _desktopCompanion.bootstrap(_prefs!);
725
729
  final configured = _configuredBackendUrl.trim();
726
730
  final savedBackendUrl = _prefs?.getString('backend_url')?.trim() ?? '';
@@ -1922,14 +1926,20 @@ class NeoAgentController extends ChangeNotifier {
1922
1926
 
1923
1927
  Future<void> allowMessagingSuggestion(
1924
1928
  String platform,
1925
- QuickAllowSuggestion suggestion,
1926
- ) async {
1929
+ QuickAllowSuggestion suggestion, {
1930
+ String? chatId,
1931
+ }) async {
1927
1932
  try {
1928
1933
  final nextPolicy = _policyWithAddedRule(
1929
1934
  currentMessagingAccessPolicy(platform),
1930
1935
  suggestion,
1931
1936
  );
1932
1937
  await saveMessagingAccessPolicy(platform, nextPolicy);
1938
+ if (chatId != null) {
1939
+ _blockedSenderQueue.removeWhere(
1940
+ (notice) => notice.platform == platform && notice.chatId == chatId,
1941
+ );
1942
+ }
1933
1943
  errorMessage = null;
1934
1944
  notifyListeners();
1935
1945
  } catch (error) {
@@ -1938,6 +1948,22 @@ class NeoAgentController extends ChangeNotifier {
1938
1948
  }
1939
1949
  }
1940
1950
 
1951
+ Future<void> ignoreBlockedSender(BlockedSenderNotice notice) async {
1952
+ final key = '${notice.platform}:${notice.chatId ?? notice.sender ?? ''}';
1953
+ _ignoredChats.add(key);
1954
+ _blockedSenderQueue.removeWhere(
1955
+ (n) => n.platform == notice.platform && (n.chatId == notice.chatId || n.sender == notice.sender),
1956
+ );
1957
+ await _prefs?.setStringList('messaging.ignored_chats', _ignoredChats.toList());
1958
+ notifyListeners();
1959
+ }
1960
+
1961
+ Future<void> removeIgnoredChat(String key) async {
1962
+ _ignoredChats.remove(key);
1963
+ await _prefs?.setStringList('messaging.ignored_chats', _ignoredChats.toList());
1964
+ notifyListeners();
1965
+ }
1966
+
1941
1967
  void consumeBlockedSenderNotice(String id) {
1942
1968
  if (_blockedSenderQueue.isNotEmpty && _blockedSenderQueue.first.id == id) {
1943
1969
  _blockedSenderQueue.removeAt(0);
@@ -1948,6 +1974,8 @@ class NeoAgentController extends ChangeNotifier {
1948
1974
  }
1949
1975
 
1950
1976
  void _enqueueBlockedSenderNotice(BlockedSenderNotice notice) {
1977
+ final ignoreKey = '${notice.platform}:${notice.chatId ?? notice.sender ?? ''}';
1978
+ if (_ignoredChats.contains(ignoreKey)) return;
1951
1979
  final exists = _blockedSenderQueue.any((item) => item.id == notice.id);
1952
1980
  if (!exists) {
1953
1981
  _blockedSenderQueue.add(notice);
package/lib/manager.js CHANGED
@@ -2000,6 +2000,152 @@ function cmdVersion() {
2000
2000
  console.log(currentInstalledVersionLabel());
2001
2001
  }
2002
2002
 
2003
+ async function cmdBilling(args = []) {
2004
+ const sub = (args[0] || 'status').toLowerCase();
2005
+
2006
+ if (sub === '--help' || sub === '-h' || sub === 'help') {
2007
+ console.log('\nNeoAgent Billing');
2008
+ console.log('Usage: neoagent billing [subcommand]');
2009
+ console.log('');
2010
+ console.log('Subcommands:');
2011
+ console.log(' neoagent billing Show billing configuration status');
2012
+ console.log(' neoagent billing setup Interactive Stripe setup wizard');
2013
+ console.log(' neoagent billing enable Enable billing and restart');
2014
+ console.log(' neoagent billing disable Disable billing and restart');
2015
+ console.log('');
2016
+ return;
2017
+ }
2018
+
2019
+ if (sub === 'status') {
2020
+ cliBanner('Billing', 'configuration status');
2021
+ heading('Billing Status');
2022
+ const env = Object.fromEntries(parseEnv(readEnvFileRaw()).entries());
2023
+ const enabled = ['1', 'true', 'yes'].includes(String(env.NEOAGENT_BILLING_ENABLED || '').toLowerCase());
2024
+ const hasSecret = Boolean(env.STRIPE_SECRET_KEY);
2025
+ const hasPub = Boolean(env.STRIPE_PUBLISHABLE_KEY);
2026
+ const hasWebhook = Boolean(env.STRIPE_WEBHOOK_SECRET);
2027
+ const trialDays = env.BILLING_TRIAL_DAYS || '14';
2028
+
2029
+ cliSection('Billing config');
2030
+ statusLine(enabled, 'enabled', enabled ? 'yes' : 'no', enabled ? '' : 'run: neoagent billing enable');
2031
+ statusLine(hasSecret, 'secret', hasSecret ? maskEnvValue('SECRET', env.STRIPE_SECRET_KEY) : 'not set', hasSecret ? '' : 'run: neoagent billing setup');
2032
+ statusLine(hasPub, 'public', hasPub ? maskEnvValue('KEY', env.STRIPE_PUBLISHABLE_KEY) : 'not set');
2033
+ statusLine(hasWebhook, 'webhook', hasWebhook ? 'configured' : 'not set');
2034
+ console.log('');
2035
+ cliSection('Trial');
2036
+ console.log(` trial days ${trialDays}`);
2037
+
2038
+ const port = loadEnvPort();
2039
+ const publicUrl = env.PUBLIC_URL || `http://localhost:${port}`;
2040
+ console.log('');
2041
+ cliSection('Webhook endpoint');
2042
+ console.log(` ${publicUrl}/api/billing/webhook`);
2043
+ console.log('');
2044
+
2045
+ if (!hasSecret || !hasPub) {
2046
+ logWarn('Stripe keys not configured. Run `neoagent billing setup` to get started.');
2047
+ } else if (!enabled) {
2048
+ logInfo('Billing is configured but not enabled. Run `neoagent billing enable` to activate.');
2049
+ } else {
2050
+ logOk('Billing is active. Manage plans in Admin › Billing.');
2051
+ }
2052
+ return;
2053
+ }
2054
+
2055
+ if (sub === 'enable') {
2056
+ heading('Enable Billing');
2057
+ upsertEnvValue('NEOAGENT_BILLING_ENABLED', 'true');
2058
+ logOk('Set NEOAGENT_BILLING_ENABLED=true');
2059
+ logInfo('Restarting NeoAgent to apply changes...');
2060
+ cmdRestart();
2061
+ return;
2062
+ }
2063
+
2064
+ if (sub === 'disable') {
2065
+ heading('Disable Billing');
2066
+ upsertEnvValue('NEOAGENT_BILLING_ENABLED', 'false');
2067
+ logOk('Set NEOAGENT_BILLING_ENABLED=false');
2068
+ logInfo('Restarting NeoAgent to apply changes...');
2069
+ cmdRestart();
2070
+ return;
2071
+ }
2072
+
2073
+ if (sub === 'setup') {
2074
+ cliBanner('Billing Setup', 'Stripe configuration');
2075
+ heading('Billing Setup');
2076
+ ensureRuntimeDirs();
2077
+
2078
+ const current = Object.fromEntries(parseEnv(readEnvFileRaw()).entries());
2079
+ const port = loadEnvPort();
2080
+ const publicUrl = current.PUBLIC_URL || `http://localhost:${port}`;
2081
+
2082
+ logInfo('Press Enter to keep the current value shown in brackets.');
2083
+ logInfo(`Webhook endpoint: ${publicUrl}/api/billing/webhook`);
2084
+ console.log('');
2085
+
2086
+ heading('Stripe API keys');
2087
+ logInfo('Find these in your Stripe dashboard: https://dashboard.stripe.com/apikeys');
2088
+ const secretKey = await askSecret('Stripe secret key (sk_live_... or sk_test_...)', current.STRIPE_SECRET_KEY || '');
2089
+ const publishableKey = await askSecret('Stripe publishable key (pk_live_... or pk_test_...)', current.STRIPE_PUBLISHABLE_KEY || '');
2090
+
2091
+ if (secretKey && !secretKey.startsWith('sk_')) {
2092
+ logWarn('Secret key does not start with "sk_" — double-check the value.');
2093
+ }
2094
+ if (publishableKey && !publishableKey.startsWith('pk_')) {
2095
+ logWarn('Publishable key does not start with "pk_" — double-check the value.');
2096
+ }
2097
+
2098
+ heading('Webhook');
2099
+ logInfo(`Register this endpoint in Stripe: ${publicUrl}/api/billing/webhook`);
2100
+ logInfo('Required events: customer.subscription.*, invoice.payment_succeeded, invoice.payment_failed');
2101
+ const webhookSecret = await askSecret('Stripe webhook signing secret (whsec_...)', current.STRIPE_WEBHOOK_SECRET || '');
2102
+
2103
+ if (webhookSecret && !webhookSecret.startsWith('whsec_')) {
2104
+ logWarn('Webhook secret does not start with "whsec_" — double-check the value.');
2105
+ }
2106
+
2107
+ heading('Trial');
2108
+ const trialDaysRaw = await ask('Free trial length in days (0 to disable)', current.BILLING_TRIAL_DAYS || '14');
2109
+ const trialDays = Number(trialDaysRaw);
2110
+ if (!Number.isInteger(trialDays) || trialDays < 0) {
2111
+ throw new Error(`Invalid trial length "${trialDaysRaw}". Must be a non-negative integer.`);
2112
+ }
2113
+
2114
+ if (secretKey) upsertEnvValue('STRIPE_SECRET_KEY', secretKey);
2115
+ if (publishableKey) upsertEnvValue('STRIPE_PUBLISHABLE_KEY', publishableKey);
2116
+ if (webhookSecret) upsertEnvValue('STRIPE_WEBHOOK_SECRET', webhookSecret);
2117
+ upsertEnvValue('BILLING_TRIAL_DAYS', String(trialDays));
2118
+
2119
+ logOk('Stripe credentials saved to .env');
2120
+
2121
+ const isAlreadyEnabled = ['1', 'true', 'yes'].includes(String(current.NEOAGENT_BILLING_ENABLED || '').toLowerCase());
2122
+ let enableNow = isAlreadyEnabled;
2123
+ if (!isAlreadyEnabled) {
2124
+ const enableAnswer = await ask('Enable billing now? (y/N)', 'N');
2125
+ enableNow = enableAnswer.toLowerCase() === 'y' || enableAnswer.toLowerCase() === 'yes';
2126
+ }
2127
+
2128
+ if (enableNow) {
2129
+ upsertEnvValue('NEOAGENT_BILLING_ENABLED', 'true');
2130
+ logOk('Billing enabled');
2131
+ logInfo('Restarting NeoAgent to apply credentials...');
2132
+ cmdRestart();
2133
+ } else {
2134
+ logInfo('Billing is not yet enabled. Run `neoagent billing enable` when ready.');
2135
+ }
2136
+
2137
+ console.log('');
2138
+ heading('Next steps');
2139
+ logInfo('1. Verify the webhook endpoint is reachable from Stripe');
2140
+ logInfo('2. Create subscription plans in Admin › Billing › Plans');
2141
+ logInfo('3. Add a free plan (price = 0) for users without a paid subscription');
2142
+ logInfo(' See: neoagent billing status');
2143
+ return;
2144
+ }
2145
+
2146
+ throw new Error(`Unknown billing subcommand: ${sub}. Run "neoagent billing --help" for usage.`);
2147
+ }
2148
+
2003
2149
  function printHelp() {
2004
2150
  const c = COLORS;
2005
2151
  const W = 38;
@@ -2047,6 +2193,13 @@ function printHelp() {
2047
2193
  row('admin', 'Show admin dashboard URL and credentials');
2048
2194
  console.log('');
2049
2195
 
2196
+ cliSection('Billing');
2197
+ row('billing', 'Show billing configuration status');
2198
+ row('billing setup', 'Interactive Stripe setup wizard');
2199
+ row('billing enable', 'Enable billing and restart');
2200
+ row('billing disable', 'Disable billing and restart');
2201
+ console.log('');
2202
+
2050
2203
  cliSection('Maintenance');
2051
2204
  row('migrate', 'Migrate from another agent installation');
2052
2205
  row('migrate dry-run', 'Preview what would be migrated');
@@ -2106,6 +2259,9 @@ async function runCLI(argv) {
2106
2259
  case 'login':
2107
2260
  await cmdLogin(argv.slice(1));
2108
2261
  break;
2262
+ case 'billing':
2263
+ await cmdBilling(argv.slice(1));
2264
+ break;
2109
2265
  case 'admin': {
2110
2266
  cliBanner('Admin Dashboard', 'credentials');
2111
2267
  const adminCreds = readAdminCredentials();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neoagent",
3
- "version": "3.0.1-beta.12",
3
+ "version": "3.0.1-beta.13",
4
4
  "description": "Self-hosted AI agent for long-running tasks, automation, messaging, device control, and local memory",
5
5
  "license": "AGPL-3.0-only",
6
6
  "main": "server/index.js",
@@ -12,7 +12,6 @@ async function loadBilling() {
12
12
  document.getElementById('billing-plans-content').innerHTML =
13
13
  '<div class="empty">Billing is not enabled on this server. Set <code>NEOAGENT_BILLING_ENABLED=1</code> to enable it.</div>';
14
14
  document.getElementById('billing-subs-content').innerHTML = '';
15
- document.getElementById('nav-billing').style.display = '';
16
15
  return;
17
16
  }
18
17
  const data = await r.json();
@@ -1 +1 @@
1
- ea6fa5cc59204621d580c6c7ce0cf0ad
1
+ 6909cff5608bd6620d57f74f1cca0974
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"77e2e94772b6eb43759e34ed1ad7da4674e19c
37
37
 
38
38
  _flutter.loader.load({
39
39
  serviceWorkerSettings: {
40
- serviceWorkerVersion: "158549597" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
40
+ serviceWorkerVersion: "3978533651" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
41
41
  }
42
42
  });