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 +29 -8
- package/docs/configuration.md +11 -2
- package/flutter_app/lib/main_app_shell.dart +8 -0
- package/flutter_app/lib/main_chat.dart +99 -0
- package/flutter_app/lib/main_controller.dart +30 -2
- package/lib/manager.js +156 -0
- package/package.json +1 -1
- package/server/admin/billing.js +0 -1
- package/server/public/.last_build_id +1 -1
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +50911 -50795
- package/server/routes/admin.js +8 -0
- package/server/services/ai/systemPrompt.js +6 -5
- package/server/services/ai/tools.js +109 -0
package/docs/billing.md
CHANGED
|
@@ -7,19 +7,40 @@ anywhere in the application.
|
|
|
7
7
|
|
|
8
8
|
## Enable billing
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
Run the interactive setup wizard:
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
|
-
neoagent
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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
|
|
package/docs/configuration.md
CHANGED
|
@@ -99,8 +99,17 @@ reset, email changes, and security notifications.
|
|
|
99
99
|
|
|
100
100
|
## Billing
|
|
101
101
|
|
|
102
|
-
Billing is disabled by default.
|
|
103
|
-
|
|
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
|
-
|
|
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
package/server/admin/billing.js
CHANGED
|
@@ -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
|
-
|
|
1
|
+
6909cff5608bd6620d57f74f1cca0974
|
|
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"77e2e94772b6eb43759e34ed1ad7da4674e19c
|
|
|
37
37
|
|
|
38
38
|
_flutter.loader.load({
|
|
39
39
|
serviceWorkerSettings: {
|
|
40
|
-
serviceWorkerVersion: "
|
|
40
|
+
serviceWorkerVersion: "3978533651" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
|
|
41
41
|
}
|
|
42
42
|
});
|