neoagent 3.0.1-beta.0 → 3.0.1-beta.11
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/.env.example +19 -0
- package/README.md +11 -0
- package/docs/billing.md +203 -0
- package/docs/configuration.md +13 -0
- package/docs/index.md +1 -0
- package/flutter_app/lib/main.dart +4 -1
- package/flutter_app/lib/main_app_shell.dart +308 -0
- package/flutter_app/lib/main_billing.dart +1487 -0
- package/flutter_app/lib/main_controller.dart +98 -0
- package/flutter_app/lib/main_install.dart +1147 -0
- package/flutter_app/lib/main_navigation.dart +12 -0
- package/flutter_app/lib/main_shared.dart +1 -0
- package/flutter_app/lib/src/backend_client.dart +55 -0
- package/landing/index.html +2 -2
- package/lib/schema_migrations.js +84 -0
- package/package.json +7 -4
- package/server/admin/admin.css +78 -0
- package/server/admin/admin.js +6 -14
- package/server/admin/billing.js +374 -0
- package/server/admin/index.html +49 -12
- package/server/db/database.js +9 -11
- package/server/db/sessions_db.js +8 -0
- package/server/http/middleware.js +4 -4
- package/server/http/routes.js +9 -0
- package/server/middleware/requireBilling.js +12 -0
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +85132 -83182
- package/server/routes/admin.js +149 -16
- package/server/routes/billing.js +127 -0
- package/server/routes/billing_webhook.js +43 -0
- package/server/routes/memory.js +1 -4
- package/server/routes/settings.js +1 -2
- package/server/routes/skills.js +1 -1
- package/server/routes/triggers.js +2 -2
- package/server/services/account/sessions.js +1 -9
- package/server/services/ai/loop/agent_engine_core.js +42 -0
- package/server/services/ai/loop/completion_judge.js +45 -2
- package/server/services/ai/loop/conversation_loop.js +163 -22
- package/server/services/ai/loop/progress_classification.js +15 -0
- package/server/services/ai/loop/progress_monitor.js +6 -6
- package/server/services/ai/loopPolicy.js +37 -44
- package/server/services/ai/messagingFallback.js +3 -1
- package/server/services/ai/models.js +18 -0
- package/server/services/ai/preModelCompaction.js +25 -5
- package/server/services/ai/rate_limits.js +28 -4
- package/server/services/ai/systemPrompt.js +20 -2
- package/server/services/ai/tools.js +80 -23
- package/server/services/billing/billing_email.js +106 -0
- package/server/services/billing/config.js +17 -0
- package/server/services/billing/plans.js +121 -0
- package/server/services/billing/stripe_client.js +21 -0
- package/server/services/billing/subscriptions.js +462 -0
- package/server/services/billing/trial_guard.js +111 -0
- package/server/services/browser/contentExtractor.js +629 -0
- package/server/services/browser/controller.js +4 -8
- package/server/services/desktop/gateway.js +7 -4
- package/server/services/integrations/google/calendar.js +22 -2
- package/server/services/integrations/secrets.js +7 -4
- package/server/services/manager.js +11 -0
- package/server/services/messaging/automation.js +1 -1
- package/server/services/messaging/telnyx.js +3 -3
- package/server/services/messaging/whatsapp.js +1 -1
- package/server/services/recordings/manager.js +13 -7
- package/server/services/runtime/backends/local-vm.js +40 -22
- package/server/services/tasks/runtime.js +82 -13
- package/server/services/tasks/schedule_utils.js +5 -5
- package/server/services/voice/runtimeManager.js +19 -10
- package/server/services/wearable/gateway.js +7 -4
- package/server/services/websocket.js +5 -5
- package/server/services/workspace/manager.js +37 -3
package/.env.example
CHANGED
|
@@ -267,3 +267,22 @@ NEOAGENT_SCREEN_RECORDER_RETENTION_DAYS=7
|
|
|
267
267
|
|
|
268
268
|
MESHTASTIC_ENABLED=true
|
|
269
269
|
TELNYX_WEBHOOK_TOKEN=
|
|
270
|
+
|
|
271
|
+
########################################
|
|
272
|
+
# Stripe billing (optional)
|
|
273
|
+
########################################
|
|
274
|
+
|
|
275
|
+
# Set to true or 1 to enable the billing system. When disabled (default),
|
|
276
|
+
# no billing routes are exposed and no payment UI is shown anywhere.
|
|
277
|
+
# NEOAGENT_BILLING_ENABLED=false
|
|
278
|
+
|
|
279
|
+
# Stripe API keys — required when billing is enabled.
|
|
280
|
+
# Use test keys (sk_test_...) during development; live keys in production.
|
|
281
|
+
# STRIPE_SECRET_KEY=sk_test_...
|
|
282
|
+
# STRIPE_PUBLISHABLE_KEY=pk_test_...
|
|
283
|
+
|
|
284
|
+
# Webhook signing secret from your Stripe dashboard (Developers > Webhooks).
|
|
285
|
+
# STRIPE_WEBHOOK_SECRET=whsec_...
|
|
286
|
+
|
|
287
|
+
# Length of free trial periods in days (default: 14).
|
|
288
|
+
# BILLING_TRIAL_DAYS=14
|
package/README.md
CHANGED
|
@@ -18,6 +18,12 @@
|
|
|
18
18
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-AGPL--3.0-a855f7?style=flat-square" alt="AGPL-3.0 license"></a>
|
|
19
19
|
</p>
|
|
20
20
|
|
|
21
|
+
<p align="center">
|
|
22
|
+
<a href="https://github.com/NeoLabs-Systems/NeoAgent/releases/latest"><img alt="Android" src="https://img.shields.io/badge/Android-APK-3ddc84?style=flat-square&logo=android&logoColor=white"></a>
|
|
23
|
+
<a href="https://github.com/NeoLabs-Systems/NeoAgent/releases/latest"><img alt="iOS" src="https://img.shields.io/badge/iOS-coming_soon-lightgrey?style=flat-square&logo=apple&logoColor=white"></a>
|
|
24
|
+
<a href="https://github.com/NeoLabs-Systems/NeoAgent/releases/latest"><img alt="Windows" src="https://img.shields.io/badge/Windows-EXE-0078d4?style=flat-square&logo=windows&logoColor=white"></a>
|
|
25
|
+
</p>
|
|
26
|
+
|
|
21
27
|
<p align="center">
|
|
22
28
|
<a href="https://www.producthunt.com/products/neoagent-2?embed=true&utm_source=badge-featured&utm_medium=badge&utm_campaign=badge-neoagent-2" target="_blank" rel="noopener noreferrer"><img alt="NeoAgent - The next-gen self-hosted AI agent that works beyond chat | Product Hunt" width="250" height="54" src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=1171573&theme=light&t=1781447653039"></a>
|
|
23
29
|
</p>
|
|
@@ -37,6 +43,10 @@ npm install -g neoagent
|
|
|
37
43
|
neoagent install
|
|
38
44
|
```
|
|
39
45
|
|
|
46
|
+
<p align="center">
|
|
47
|
+
<a href="https://github.com/NeoLabs-Systems/NeoAgent/releases/latest"><img alt="Download macOS app" src="https://img.shields.io/badge/macOS_app-download-black?style=flat-square&logo=apple&logoColor=white"></a>
|
|
48
|
+
</p>
|
|
49
|
+
|
|
40
50
|
Open `http://localhost:3333`, create the first account, and configure a model.
|
|
41
51
|
Local models can run through [Ollama](https://ollama.com/); hosted providers
|
|
42
52
|
can be configured in the application.
|
|
@@ -51,6 +61,7 @@ service to a network.
|
|
|
51
61
|
- **It operates real devices.** The agent can use an isolated browser and shell, control an Android device or emulator over ADB, or work through a paired Chrome extension and desktop companion.
|
|
52
62
|
- **Automation can start without a message.** Tasks can run on a schedule or from supported Gmail, Outlook, Slack, Teams, personal WhatsApp, and weather events. Android notifications can also start an agent run.
|
|
53
63
|
- **Agents and users have separate state.** Specialist agents can have their own memory, settings, tools, account assignments, conversations, and task history. Multi-user deployments include administrative account controls and optional email confirmation.
|
|
64
|
+
- **SaaS billing is built in and off by default.** Set `NEOAGENT_BILLING_ENABLED=true` to activate Stripe subscriptions, plan management, free trials, and model access restrictions. When disabled, no payment routes or UI appear anywhere. See [Billing](docs/billing.md).
|
|
54
65
|
- **The same server has several interfaces.** NeoAgent includes web, Android, desktop, and Android launcher clients, messaging bridges, a Chrome extension, and firmware for a supported ESP32-S3 wearable.
|
|
55
66
|
|
|
56
67
|
## 🖥️ Interfaces
|
package/docs/billing.md
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# Billing
|
|
2
|
+
|
|
3
|
+
NeoAgent includes an optional Stripe-based billing system for deployments that
|
|
4
|
+
charge users for access. When disabled — the default — no billing routes are
|
|
5
|
+
exposed, no payment UI is shown, and no payment-related information appears
|
|
6
|
+
anywhere in the application.
|
|
7
|
+
|
|
8
|
+
## Enable billing
|
|
9
|
+
|
|
10
|
+
Set the following environment variables before starting the server:
|
|
11
|
+
|
|
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_...
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Restart the server. The admin dashboard will show a **Billing** navigation
|
|
20
|
+
item and the `/api/billing/*` endpoints will become active.
|
|
21
|
+
|
|
22
|
+
Use Stripe test keys (`sk_test_...`, `pk_test_...`) during development.
|
|
23
|
+
|
|
24
|
+
## Subscription plans
|
|
25
|
+
|
|
26
|
+
Plans are managed in **Admin › Billing › Plans**. Each plan controls:
|
|
27
|
+
|
|
28
|
+
| Field | Purpose |
|
|
29
|
+
|---|---|
|
|
30
|
+
| Name | Displayed to users in the settings submenu |
|
|
31
|
+
| Price | Stripe price in cents (0 = free) |
|
|
32
|
+
| Billing interval | `month`, `year`, or blank for one-time or free |
|
|
33
|
+
| Stripe Price ID | The `price_...` ID from your Stripe dashboard |
|
|
34
|
+
| 4h token limit | Per-user 4-hour rolling token budget (blank = server default) |
|
|
35
|
+
| Weekly token limit | Per-user 7-day rolling token budget (blank = server default) |
|
|
36
|
+
| Allowed models | Comma-separated model IDs; blank = all models accessible |
|
|
37
|
+
| Features | Marketing feature strings shown on pricing pages |
|
|
38
|
+
|
|
39
|
+
Token limits are written directly to the user account when a subscription
|
|
40
|
+
becomes active or is updated via webhook. The existing rate-limit enforcement
|
|
41
|
+
in the runtime reads them with no additional configuration.
|
|
42
|
+
|
|
43
|
+
### Free plan
|
|
44
|
+
|
|
45
|
+
Create a plan with **Price = 0** to serve as the default for new users. If a
|
|
46
|
+
free plan exists when billing is enabled, every new registration is
|
|
47
|
+
automatically placed on it.
|
|
48
|
+
|
|
49
|
+
### Model restrictions
|
|
50
|
+
|
|
51
|
+
If **Allowed models** is non-empty, users on that plan can only use the listed
|
|
52
|
+
model IDs. Models outside the allowlist remain visible in the UI with an
|
|
53
|
+
unavailable status so users understand what upgrading unlocks.
|
|
54
|
+
|
|
55
|
+
Leave the field blank to allow all configured models on a plan.
|
|
56
|
+
|
|
57
|
+
## User subscriptions
|
|
58
|
+
|
|
59
|
+
Users manage their subscription in **Settings › Billing** (Flutter client). From
|
|
60
|
+
there they can:
|
|
61
|
+
|
|
62
|
+
- View their current plan, status, and next renewal date
|
|
63
|
+
- Upgrade or downgrade through Stripe Checkout
|
|
64
|
+
- Open the Stripe Customer Portal to update a payment method or download invoices
|
|
65
|
+
- Cancel at the end of the current billing period
|
|
66
|
+
|
|
67
|
+
### Stripe Checkout
|
|
68
|
+
|
|
69
|
+
When a user selects a paid plan, the server creates a Stripe Checkout session
|
|
70
|
+
and redirects the client to Stripe's hosted payment page. No card data passes
|
|
71
|
+
through NeoAgent.
|
|
72
|
+
|
|
73
|
+
### Stripe Customer Portal
|
|
74
|
+
|
|
75
|
+
The Customer Portal is a Stripe-hosted page where users can update their
|
|
76
|
+
payment method, view billing history, and cancel. Configure the portal in your
|
|
77
|
+
[Stripe dashboard](https://dashboard.stripe.com/settings/billing/portal) before
|
|
78
|
+
enabling it.
|
|
79
|
+
|
|
80
|
+
## Free trials
|
|
81
|
+
|
|
82
|
+
Enable free trials in **Admin › Billing › Plans** by configuring a Stripe price
|
|
83
|
+
that supports trials, then setting `BILLING_TRIAL_DAYS` to the desired length
|
|
84
|
+
(default: 14 days).
|
|
85
|
+
|
|
86
|
+
Trials start when a user calls `POST /api/billing/trial` with a plan ID. The
|
|
87
|
+
server runs anti-abuse checks before granting a trial:
|
|
88
|
+
|
|
89
|
+
| Check | Limit |
|
|
90
|
+
|---|---|
|
|
91
|
+
| IP address | 2 trials per IP per 30 days |
|
|
92
|
+
| Email domain | 3 trials per non-common domain per 30 days |
|
|
93
|
+
| Account age | Account must be at least 1 day old |
|
|
94
|
+
| Device fingerprint | 1 trial per device per 90 days (opaque hash from client) |
|
|
95
|
+
|
|
96
|
+
The Flutter client is responsible for generating and sending the device
|
|
97
|
+
fingerprint. The server hashes it with SHA-256 before storage — the raw
|
|
98
|
+
fingerprint is never persisted.
|
|
99
|
+
|
|
100
|
+
## Webhooks
|
|
101
|
+
|
|
102
|
+
Stripe sends events to `POST /api/billing/webhook`. Register this URL in your
|
|
103
|
+
[Stripe dashboard](https://dashboard.stripe.com/webhooks) under the endpoint
|
|
104
|
+
for your account.
|
|
105
|
+
|
|
106
|
+
Handled events:
|
|
107
|
+
|
|
108
|
+
| Event | Effect |
|
|
109
|
+
|---|---|
|
|
110
|
+
| `customer.subscription.created` | Creates a local subscription row |
|
|
111
|
+
| `customer.subscription.updated` | Syncs status, period dates, and token limits |
|
|
112
|
+
| `customer.subscription.deleted` | Marks subscription canceled |
|
|
113
|
+
| `customer.subscription.trial_will_end` | Sends trial-ending email (fires 3 days before) |
|
|
114
|
+
| `invoice.payment_succeeded` | Records payment; sends renewal email on cycle |
|
|
115
|
+
| `invoice.payment_failed` | Records failure; sends payment-failed email |
|
|
116
|
+
|
|
117
|
+
Events are idempotent — replaying a webhook event produces the same result.
|
|
118
|
+
|
|
119
|
+
**Required events to enable in Stripe:**
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
customer.subscription.created
|
|
123
|
+
customer.subscription.updated
|
|
124
|
+
customer.subscription.deleted
|
|
125
|
+
customer.subscription.trial_will_end
|
|
126
|
+
invoice.payment_succeeded
|
|
127
|
+
invoice.payment_failed
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Verifying the webhook locally
|
|
131
|
+
|
|
132
|
+
Use the Stripe CLI to forward events to a local server during development:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
stripe listen --forward-to http://localhost:3333/api/billing/webhook
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Email notifications
|
|
139
|
+
|
|
140
|
+
If [service email](configuration.md#service-email) is configured, users receive
|
|
141
|
+
emails at the following billing events:
|
|
142
|
+
|
|
143
|
+
| Event | Email |
|
|
144
|
+
|---|---|
|
|
145
|
+
| Trial started | Confirmation with trial end date |
|
|
146
|
+
| Trial ending soon | Reminder 3 days before end |
|
|
147
|
+
| Subscription activated | Welcome to the paid plan |
|
|
148
|
+
| Subscription renewed | Renewal confirmation |
|
|
149
|
+
| Payment failed | Action required with next retry date |
|
|
150
|
+
| Subscription canceled | Cancellation confirmation |
|
|
151
|
+
| Plan changed | Summary of the new plan |
|
|
152
|
+
|
|
153
|
+
No emails are sent if SMTP is not configured.
|
|
154
|
+
|
|
155
|
+
## AI context awareness
|
|
156
|
+
|
|
157
|
+
When billing is enabled, each AI request includes the user's subscription in
|
|
158
|
+
the system prompt:
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
SUBSCRIPTION: User is on the "Pro" plan, status: active.
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Agents and workflows can use this context to adapt their behavior — for
|
|
165
|
+
example, acknowledging feature limitations on a free plan or confirming
|
|
166
|
+
capabilities on a paid one.
|
|
167
|
+
|
|
168
|
+
## Admin controls
|
|
169
|
+
|
|
170
|
+
The **Admin › Billing** page provides:
|
|
171
|
+
|
|
172
|
+
- **Plans**: Create, edit, and deactivate subscription plans
|
|
173
|
+
- **Subscriptions**: Browse all user subscriptions with status filter and
|
|
174
|
+
pagination; override a user's plan without going through Stripe
|
|
175
|
+
|
|
176
|
+
Override a user's plan directly from the subscriptions table using the
|
|
177
|
+
**Override** button. This is useful for comped accounts, support escalations,
|
|
178
|
+
or fixing a webhook gap.
|
|
179
|
+
|
|
180
|
+
## Configuration reference
|
|
181
|
+
|
|
182
|
+
| Variable | Default | Purpose |
|
|
183
|
+
|---|---|---|
|
|
184
|
+
| `NEOAGENT_BILLING_ENABLED` | `false` | Master on/off switch |
|
|
185
|
+
| `STRIPE_SECRET_KEY` | required | Stripe server-side API key |
|
|
186
|
+
| `STRIPE_PUBLISHABLE_KEY` | required | Stripe client-side key (returned to Flutter) |
|
|
187
|
+
| `STRIPE_WEBHOOK_SECRET` | required | Webhook signing secret from Stripe dashboard |
|
|
188
|
+
| `BILLING_TRIAL_DAYS` | `14` | Free trial length in days |
|
|
189
|
+
|
|
190
|
+
All variables can be set with `neoagent env set` or added to `~/.neoagent/.env`
|
|
191
|
+
directly. Restart the server after any change.
|
|
192
|
+
|
|
193
|
+
## Security notes
|
|
194
|
+
|
|
195
|
+
- NeoAgent stores only Stripe customer IDs and subscription IDs — no card
|
|
196
|
+
numbers, bank details, or PII beyond what Stripe already holds.
|
|
197
|
+
- Webhook payloads are verified with HMAC-SHA256 using `STRIPE_WEBHOOK_SECRET`
|
|
198
|
+
before any processing. Requests missing the `stripe-signature` header are
|
|
199
|
+
rejected with 400.
|
|
200
|
+
- Trial fingerprints are stored as SHA-256 hashes — the raw IP, email domain,
|
|
201
|
+
and device identifiers are never persisted.
|
|
202
|
+
- All billing routes return 404 when `NEOAGENT_BILLING_ENABLED` is not set,
|
|
203
|
+
regardless of whether Stripe credentials are present.
|
package/docs/configuration.md
CHANGED
|
@@ -97,6 +97,19 @@ reset, email changes, and security notifications.
|
|
|
97
97
|
| `NEOAGENT_EMAIL_PUBLIC_URL` | Base URL used in email links |
|
|
98
98
|
| `NEOAGENT_EMAIL_TOKEN_TTL_HOURS` | Confirmation token lifetime |
|
|
99
99
|
|
|
100
|
+
## Billing
|
|
101
|
+
|
|
102
|
+
Billing is disabled by default. See [Billing](billing.md) for the full setup
|
|
103
|
+
guide, webhook configuration, and plan management.
|
|
104
|
+
|
|
105
|
+
| Variable | Default | Purpose |
|
|
106
|
+
|---|---|---|
|
|
107
|
+
| `NEOAGENT_BILLING_ENABLED` | `false` | Enable the Stripe billing system |
|
|
108
|
+
| `STRIPE_SECRET_KEY` | unset | Stripe server-side API key |
|
|
109
|
+
| `STRIPE_PUBLISHABLE_KEY` | unset | Stripe client-side key (returned to clients) |
|
|
110
|
+
| `STRIPE_WEBHOOK_SECRET` | unset | Webhook signing secret |
|
|
111
|
+
| `BILLING_TRIAL_DAYS` | `14` | Free trial length in days |
|
|
112
|
+
|
|
100
113
|
## Isolated runtime
|
|
101
114
|
|
|
102
115
|
| Variable | Purpose |
|
package/docs/index.md
CHANGED
|
@@ -45,6 +45,7 @@ Continue with:
|
|
|
45
45
|
| [Recordings and health](recordings-and-health.md) | Audio transcription and Health Connect |
|
|
46
46
|
| [Skills and MCP](skills.md) | Reusable instructions and external tool servers |
|
|
47
47
|
| [Operations](operations.md) | Updates, backups, logs, and recovery |
|
|
48
|
+
| [Billing](billing.md) | Stripe subscriptions, plans, trials, and webhooks |
|
|
48
49
|
| [Configuration](configuration.md) | Environment and runtime reference |
|
|
49
50
|
| [Why NeoAgent](why-neoagent.md) | Factual comparison with OpenClaw and Hermes |
|
|
50
51
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import 'dart:async';
|
|
2
2
|
import 'dart:convert';
|
|
3
|
-
import 'dart:io' show Platform;
|
|
3
|
+
import 'dart:io' show Directory, File, Platform, Process, ProcessSignal;
|
|
4
4
|
import 'dart:math' as math;
|
|
5
5
|
import 'dart:ui' show ImageFilter;
|
|
6
6
|
|
|
@@ -20,6 +20,7 @@ import 'package:image/image.dart' as img;
|
|
|
20
20
|
import 'package:mobile_scanner/mobile_scanner.dart';
|
|
21
21
|
import 'package:qr_flutter/qr_flutter.dart';
|
|
22
22
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
23
|
+
import 'package:url_launcher/url_launcher.dart' as url_launcher;
|
|
23
24
|
import 'package:socket_io_client/socket_io_client.dart' as io;
|
|
24
25
|
import 'package:audioplayers/audioplayers.dart';
|
|
25
26
|
import 'package:tray_manager/tray_manager.dart';
|
|
@@ -71,7 +72,9 @@ part 'main_security.dart';
|
|
|
71
72
|
part 'main_model_picker.dart';
|
|
72
73
|
part 'main_operations.dart';
|
|
73
74
|
part 'main_admin.dart';
|
|
75
|
+
part 'main_billing.dart';
|
|
74
76
|
part 'main_unified.dart';
|
|
77
|
+
part 'main_install.dart';
|
|
75
78
|
|
|
76
79
|
Future<void> main() async {
|
|
77
80
|
await runNeoAgentApp(mode: _appModeFromEnvironment());
|
|
@@ -57,6 +57,7 @@ class BackendSetupView extends StatefulWidget {
|
|
|
57
57
|
|
|
58
58
|
class _BackendSetupViewState extends State<BackendSetupView> {
|
|
59
59
|
late final TextEditingController _backendUrlController;
|
|
60
|
+
bool _localInstall = false;
|
|
60
61
|
|
|
61
62
|
@override
|
|
62
63
|
void initState() {
|
|
@@ -78,6 +79,12 @@ class _BackendSetupViewState extends State<BackendSetupView> {
|
|
|
78
79
|
|
|
79
80
|
@override
|
|
80
81
|
Widget build(BuildContext context) {
|
|
82
|
+
if (_localInstall) {
|
|
83
|
+
return _LocalInstallWidget(
|
|
84
|
+
controller: widget.controller,
|
|
85
|
+
onBack: () => setState(() => _localInstall = false),
|
|
86
|
+
);
|
|
87
|
+
}
|
|
81
88
|
final controller = widget.controller;
|
|
82
89
|
return _AmbientBackdrop(
|
|
83
90
|
child: Scaffold(
|
|
@@ -195,6 +202,200 @@ class _BackendSetupViewState extends State<BackendSetupView> {
|
|
|
195
202
|
),
|
|
196
203
|
),
|
|
197
204
|
),
|
|
205
|
+
const SizedBox(height: 10),
|
|
206
|
+
Center(
|
|
207
|
+
child: TextButton.icon(
|
|
208
|
+
onPressed: () => setState(() => _localInstall = true),
|
|
209
|
+
icon: const Icon(Icons.download_outlined, size: 16),
|
|
210
|
+
label: const Text('Install NeoAgent on this machine'),
|
|
211
|
+
),
|
|
212
|
+
),
|
|
213
|
+
],
|
|
214
|
+
),
|
|
215
|
+
),
|
|
216
|
+
),
|
|
217
|
+
),
|
|
218
|
+
),
|
|
219
|
+
),
|
|
220
|
+
),
|
|
221
|
+
),
|
|
222
|
+
),
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ─── Local Install Widget ────────────────────────────────────────────────────
|
|
228
|
+
|
|
229
|
+
enum _LocalInstallPhase { checking, ready, installing, done, failed }
|
|
230
|
+
|
|
231
|
+
class _LocalInstallWidget extends StatefulWidget {
|
|
232
|
+
const _LocalInstallWidget({required this.controller, required this.onBack});
|
|
233
|
+
|
|
234
|
+
final NeoAgentController controller;
|
|
235
|
+
final VoidCallback onBack;
|
|
236
|
+
|
|
237
|
+
@override
|
|
238
|
+
State<_LocalInstallWidget> createState() => _LocalInstallWidgetState();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
class _LocalInstallWidgetState extends State<_LocalInstallWidget> {
|
|
242
|
+
_LocalInstallPhase _phase = _LocalInstallPhase.checking;
|
|
243
|
+
String? _nodePath;
|
|
244
|
+
String? _installDir;
|
|
245
|
+
final List<String> _log = [];
|
|
246
|
+
final _logScrollCtrl = ScrollController();
|
|
247
|
+
Process? _proc;
|
|
248
|
+
String? _errorMsg;
|
|
249
|
+
|
|
250
|
+
String get _home => Platform.isWindows
|
|
251
|
+
? (Platform.environment['USERPROFILE'] ?? '')
|
|
252
|
+
: (Platform.environment['HOME'] ?? '');
|
|
253
|
+
|
|
254
|
+
@override
|
|
255
|
+
void initState() {
|
|
256
|
+
super.initState();
|
|
257
|
+
_check();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
@override
|
|
261
|
+
void dispose() {
|
|
262
|
+
_logScrollCtrl.dispose();
|
|
263
|
+
_proc?.kill(ProcessSignal.sigkill);
|
|
264
|
+
super.dispose();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
Future<void> _check() async {
|
|
268
|
+
if (!mounted) return;
|
|
269
|
+
setState(() => _phase = _LocalInstallPhase.checking);
|
|
270
|
+
|
|
271
|
+
final r = await Process.run(Platform.isWindows ? 'where' : 'which', ['node']);
|
|
272
|
+
if (r.exitCode == 0) {
|
|
273
|
+
_nodePath = (r.stdout as String).trim().split('\n').first.trim();
|
|
274
|
+
} else if (!Platform.isWindows) {
|
|
275
|
+
for (final p in ['/opt/homebrew/bin/node', '/usr/local/bin/node']) {
|
|
276
|
+
if (File(p).existsSync()) {
|
|
277
|
+
_nodePath = p;
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
final defaultDir = '$_home/NeoAgent';
|
|
284
|
+
if (File('$defaultDir/bin/neoagent.js').existsSync()) {
|
|
285
|
+
_installDir = defaultDir;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (!mounted) return;
|
|
289
|
+
if (_nodePath != null && _installDir != null) {
|
|
290
|
+
setState(() => _phase = _LocalInstallPhase.ready);
|
|
291
|
+
} else {
|
|
292
|
+
setState(() {
|
|
293
|
+
_phase = _LocalInstallPhase.failed;
|
|
294
|
+
_errorMsg = _nodePath == null
|
|
295
|
+
? 'Node.js not found. Install it from nodejs.org then retry.'
|
|
296
|
+
: 'NeoAgent not found at ~/NeoAgent. Run: npm install -g neoagent && neoagent install';
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
bool _ensureEnvFile() {
|
|
302
|
+
final envPath = '$_home/.neoagent/runtime/.env';
|
|
303
|
+
if (!File(envPath).existsSync()) {
|
|
304
|
+
try {
|
|
305
|
+
Directory('$_home/.neoagent/runtime').createSync(recursive: true);
|
|
306
|
+
File(envPath).writeAsStringSync('NODE_ENV=production\nPORT=3333\n');
|
|
307
|
+
} catch (_) {
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
Future<void> _install() async {
|
|
315
|
+
if (!_ensureEnvFile()) {
|
|
316
|
+
setState(() {
|
|
317
|
+
_phase = _LocalInstallPhase.failed;
|
|
318
|
+
_errorMsg = 'Could not create ~/.neoagent/runtime/.env — check directory permissions.';
|
|
319
|
+
});
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
setState(() {
|
|
323
|
+
_phase = _LocalInstallPhase.installing;
|
|
324
|
+
_log.clear();
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
final process = await Process.start(
|
|
328
|
+
_nodePath!,
|
|
329
|
+
['bin/neoagent.js', 'install'],
|
|
330
|
+
workingDirectory: _installDir,
|
|
331
|
+
);
|
|
332
|
+
_proc = process;
|
|
333
|
+
|
|
334
|
+
void onChunk(String data) {
|
|
335
|
+
if (!mounted) return;
|
|
336
|
+
setState(() => _log.add(data));
|
|
337
|
+
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
338
|
+
if (_logScrollCtrl.hasClients) {
|
|
339
|
+
_logScrollCtrl.jumpTo(_logScrollCtrl.position.maxScrollExtent);
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
process.stdout.transform(utf8.decoder).listen(onChunk);
|
|
345
|
+
process.stderr.transform(utf8.decoder).listen(onChunk);
|
|
346
|
+
|
|
347
|
+
final exit = await process.exitCode;
|
|
348
|
+
_proc = null;
|
|
349
|
+
if (!mounted) return;
|
|
350
|
+
setState(() {
|
|
351
|
+
_phase = exit == 0 ? _LocalInstallPhase.done : _LocalInstallPhase.failed;
|
|
352
|
+
if (exit != 0) _errorMsg = 'Installation failed (exit $exit). Check the log above.';
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
@override
|
|
357
|
+
Widget build(BuildContext context) {
|
|
358
|
+
return _AmbientBackdrop(
|
|
359
|
+
child: Scaffold(
|
|
360
|
+
backgroundColor: Colors.transparent,
|
|
361
|
+
body: SafeArea(
|
|
362
|
+
child: Center(
|
|
363
|
+
child: SingleChildScrollView(
|
|
364
|
+
padding: const EdgeInsets.all(24),
|
|
365
|
+
child: ConstrainedBox(
|
|
366
|
+
constraints: const BoxConstraints(maxWidth: 560),
|
|
367
|
+
child: _EntranceMotion(
|
|
368
|
+
child: _GlassSurface(
|
|
369
|
+
borderRadius: BorderRadius.circular(34),
|
|
370
|
+
blurSigma: 28,
|
|
371
|
+
boxShadow: _softPanelShadow,
|
|
372
|
+
overlayGradient: _panelGradient,
|
|
373
|
+
fillColor: _glassFill,
|
|
374
|
+
child: Padding(
|
|
375
|
+
padding: const EdgeInsets.fromLTRB(34, 28, 34, 30),
|
|
376
|
+
child: Column(
|
|
377
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
378
|
+
children: <Widget>[
|
|
379
|
+
TextButton.icon(
|
|
380
|
+
onPressed: widget.onBack,
|
|
381
|
+
icon: const Icon(Icons.arrow_back, size: 16),
|
|
382
|
+
label: const Text('Back'),
|
|
383
|
+
style: TextButton.styleFrom(
|
|
384
|
+
padding: EdgeInsets.zero,
|
|
385
|
+
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
|
386
|
+
),
|
|
387
|
+
),
|
|
388
|
+
const SizedBox(height: 16),
|
|
389
|
+
const _BrandLockup(logoSize: 60),
|
|
390
|
+
const SizedBox(height: 22),
|
|
391
|
+
Text('INSTALL LOCALLY', style: _sectionEyebrowStyle()),
|
|
392
|
+
const SizedBox(height: 10),
|
|
393
|
+
Text(
|
|
394
|
+
'Install NeoAgent as a background service',
|
|
395
|
+
style: _displayTitleStyle(28),
|
|
396
|
+
),
|
|
397
|
+
const SizedBox(height: 20),
|
|
398
|
+
_buildContent(),
|
|
198
399
|
],
|
|
199
400
|
),
|
|
200
401
|
),
|
|
@@ -207,6 +408,109 @@ class _BackendSetupViewState extends State<BackendSetupView> {
|
|
|
207
408
|
),
|
|
208
409
|
);
|
|
209
410
|
}
|
|
411
|
+
|
|
412
|
+
Widget _buildContent() {
|
|
413
|
+
switch (_phase) {
|
|
414
|
+
case _LocalInstallPhase.checking:
|
|
415
|
+
return const Center(
|
|
416
|
+
child: Padding(
|
|
417
|
+
padding: EdgeInsets.all(24),
|
|
418
|
+
child: CircularProgressIndicator(),
|
|
419
|
+
),
|
|
420
|
+
);
|
|
421
|
+
case _LocalInstallPhase.ready:
|
|
422
|
+
return Column(
|
|
423
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
424
|
+
children: <Widget>[
|
|
425
|
+
Text(
|
|
426
|
+
'NeoAgent found at $_installDir. Click Install to set up the background service.',
|
|
427
|
+
style: TextStyle(color: _textSecondary, height: 1.55),
|
|
428
|
+
),
|
|
429
|
+
const SizedBox(height: 20),
|
|
430
|
+
SizedBox(
|
|
431
|
+
width: double.infinity,
|
|
432
|
+
child: FilledButton.icon(
|
|
433
|
+
onPressed: _install,
|
|
434
|
+
style: FilledButton.styleFrom(
|
|
435
|
+
backgroundColor: _accent,
|
|
436
|
+
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
437
|
+
),
|
|
438
|
+
icon: const Icon(Icons.download_outlined),
|
|
439
|
+
label: const Text('Install NeoAgent'),
|
|
440
|
+
),
|
|
441
|
+
),
|
|
442
|
+
],
|
|
443
|
+
);
|
|
444
|
+
case _LocalInstallPhase.installing:
|
|
445
|
+
return Column(
|
|
446
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
447
|
+
children: <Widget>[
|
|
448
|
+
Row(
|
|
449
|
+
children: <Widget>[
|
|
450
|
+
const SizedBox(
|
|
451
|
+
width: 18,
|
|
452
|
+
height: 18,
|
|
453
|
+
child: CircularProgressIndicator(strokeWidth: 2),
|
|
454
|
+
),
|
|
455
|
+
const SizedBox(width: 12),
|
|
456
|
+
const Expanded(child: Text('Installing...')),
|
|
457
|
+
TextButton(
|
|
458
|
+
onPressed: () async {
|
|
459
|
+
final proc = _proc;
|
|
460
|
+
proc?.kill(ProcessSignal.sigterm);
|
|
461
|
+
await proc?.exitCode;
|
|
462
|
+
if (mounted) setState(() => _phase = _LocalInstallPhase.ready);
|
|
463
|
+
},
|
|
464
|
+
child: const Text('Cancel'),
|
|
465
|
+
),
|
|
466
|
+
],
|
|
467
|
+
),
|
|
468
|
+
const SizedBox(height: 12),
|
|
469
|
+
_InstallLogCard(lines: _log, scrollController: _logScrollCtrl),
|
|
470
|
+
],
|
|
471
|
+
);
|
|
472
|
+
case _LocalInstallPhase.done:
|
|
473
|
+
return Column(
|
|
474
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
475
|
+
children: <Widget>[
|
|
476
|
+
Row(
|
|
477
|
+
children: <Widget>[
|
|
478
|
+
Icon(Icons.check_circle_outline, color: _success),
|
|
479
|
+
const SizedBox(width: 8),
|
|
480
|
+
const Text('NeoAgent is running.'),
|
|
481
|
+
],
|
|
482
|
+
),
|
|
483
|
+
const SizedBox(height: 20),
|
|
484
|
+
SizedBox(
|
|
485
|
+
width: double.infinity,
|
|
486
|
+
child: FilledButton.icon(
|
|
487
|
+
onPressed: () =>
|
|
488
|
+
widget.controller.saveBackendUrl('http://localhost:3333'),
|
|
489
|
+
style: FilledButton.styleFrom(
|
|
490
|
+
backgroundColor: _accent,
|
|
491
|
+
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
492
|
+
),
|
|
493
|
+
icon: const Icon(Icons.arrow_forward_rounded),
|
|
494
|
+
label: const Text('Connect to local NeoAgent'),
|
|
495
|
+
),
|
|
496
|
+
),
|
|
497
|
+
],
|
|
498
|
+
);
|
|
499
|
+
case _LocalInstallPhase.failed:
|
|
500
|
+
return Column(
|
|
501
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
502
|
+
children: <Widget>[
|
|
503
|
+
_InlineError(message: _errorMsg ?? 'Installation failed.'),
|
|
504
|
+
const SizedBox(height: 16),
|
|
505
|
+
OutlinedButton.icon(
|
|
506
|
+
onPressed: _check,
|
|
507
|
+
icon: const Icon(Icons.refresh),
|
|
508
|
+
label: const Text('Retry'),
|
|
509
|
+
),
|
|
510
|
+
],
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
210
514
|
}
|
|
211
515
|
|
|
212
516
|
class AuthView extends StatefulWidget {
|
|
@@ -2216,6 +2520,10 @@ class _SectionBody extends StatelessWidget {
|
|
|
2216
2520
|
return controller.showHealthSection
|
|
2217
2521
|
? HealthPanel(controller: controller)
|
|
2218
2522
|
: ChatPanel(controller: controller);
|
|
2523
|
+
case AppSection.server:
|
|
2524
|
+
return ServerPanel(controller: controller);
|
|
2525
|
+
case AppSection.billing:
|
|
2526
|
+
return BillingPanel(controller: controller);
|
|
2219
2527
|
}
|
|
2220
2528
|
}
|
|
2221
2529
|
}
|