neoagent 3.0.1-beta.2 → 3.0.1-beta.21
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 +20 -3
- package/docs/benchmarking.md +102 -0
- package/docs/billing.md +224 -0
- package/docs/configuration.md +22 -0
- package/docs/getting-started.md +10 -8
- package/docs/index.md +1 -0
- package/docs/operations.md +1 -1
- package/flutter_app/lib/main.dart +4 -1
- package/flutter_app/lib/main_account_settings.dart +138 -0
- package/flutter_app/lib/main_app_shell.dart +316 -0
- package/flutter_app/lib/main_billing.dart +1465 -0
- package/flutter_app/lib/main_chat.dart +1594 -224
- package/flutter_app/lib/main_controller.dart +263 -26
- package/flutter_app/lib/main_devices.dart +1 -1
- package/flutter_app/lib/main_install.dart +1147 -0
- package/flutter_app/lib/main_navigation.dart +12 -0
- package/flutter_app/lib/main_operations.dart +134 -9
- package/flutter_app/lib/main_settings.dart +148 -52
- package/flutter_app/lib/main_shared.dart +1 -0
- package/flutter_app/lib/src/backend_client.dart +86 -1
- package/landing/index.html +2 -2
- package/lib/manager.js +184 -66
- package/lib/schema_migrations.js +84 -0
- package/package.json +10 -4
- package/server/admin/access.js +12 -7
- package/server/admin/admin.css +78 -0
- package/server/admin/admin.js +511 -23
- package/server/admin/billing.js +415 -0
- package/server/admin/index.html +120 -13
- package/server/admin/users.js +15 -15
- package/server/db/database.js +13 -21
- package/server/db/sessions_db.js +8 -0
- package/server/http/middleware.js +4 -4
- package/server/http/routes.js +9 -0
- package/server/http/static.js +4 -2
- 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 +89347 -85449
- package/server/routes/account.js +53 -0
- package/server/routes/admin.js +515 -63
- package/server/routes/agents.js +203 -21
- 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/erasure.js +263 -0
- package/server/services/account/sessions.js +1 -9
- package/server/services/ai/loop/agent_engine_core.js +42 -0
- package/server/services/ai/loop/blank_recovery.js +36 -0
- package/server/services/ai/loop/completion_judge.js +42 -0
- package/server/services/ai/loop/conversation_loop.js +248 -34
- package/server/services/ai/loop/progress_monitor.js +6 -6
- package/server/services/ai/loopPolicy.js +37 -44
- package/server/services/ai/messagingFallback.js +25 -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 +23 -5
- package/server/services/ai/taskAnalysis.js +2 -0
- package/server/services/ai/tools.js +231 -20
- package/server/services/android/controller.js +6 -2
- 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 +30 -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 +12 -11
- 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/runtime/docker-vm-manager.js +157 -266
- package/server/services/runtime/guest_bootstrap.js +17 -5
- package/server/services/runtime/guest_image.js +182 -0
- package/server/services/runtime/manager.js +0 -1
- package/server/services/runtime/validation.js +3 -8
- package/server/services/tasks/runtime.js +103 -15
- package/server/services/tasks/schedule_utils.js +5 -5
- package/server/services/voice/runtimeManager.js +19 -10
- package/server/services/wearable/gateway.js +8 -5
- package/server/services/websocket.js +26 -8
- package/server/services/workspace/manager.js +37 -3
package/server/routes/agents.js
CHANGED
|
@@ -10,6 +10,154 @@ const { buildAgentRunContext } = require('./_helpers/agentRunContext');
|
|
|
10
10
|
|
|
11
11
|
router.use(requireAuth);
|
|
12
12
|
|
|
13
|
+
const CHAT_HISTORY_DEFAULT_LIMIT = 40;
|
|
14
|
+
const CHAT_HISTORY_MAX_LIMIT = 100;
|
|
15
|
+
|
|
16
|
+
function normalizedTimestampExpression(valueSql) {
|
|
17
|
+
return `julianday(CASE WHEN instr(${valueSql}, 'T') > 0 THEN ${valueSql} ELSE replace(${valueSql}, ' ', 'T') || 'Z' END)`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function normalizeChatHistoryLimit(rawLimit) {
|
|
21
|
+
const parsed = Number.parseInt(rawLimit, 10);
|
|
22
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
23
|
+
return CHAT_HISTORY_DEFAULT_LIMIT;
|
|
24
|
+
}
|
|
25
|
+
return Math.min(parsed, CHAT_HISTORY_MAX_LIMIT);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function parseChatHistoryCursor(query) {
|
|
29
|
+
const createdAt = query.beforeCreatedAt?.toString().trim() || '';
|
|
30
|
+
const source = query.beforeSource?.toString().trim() || '';
|
|
31
|
+
const id = query.beforeId?.toString().trim() || '';
|
|
32
|
+
if (!createdAt || !source || !id) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
return { createdAt, source, id };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function chatHistoryTimestampMs(value) {
|
|
39
|
+
if (!value) return 0;
|
|
40
|
+
const normalized = value.includes('T') ? value : `${value.replace(' ', 'T')}Z`;
|
|
41
|
+
const parsed = new Date(normalized).getTime();
|
|
42
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function compareChatHistoryDesc(left, right) {
|
|
46
|
+
const timestampDiff =
|
|
47
|
+
chatHistoryTimestampMs(right.created_at) - chatHistoryTimestampMs(left.created_at);
|
|
48
|
+
if (timestampDiff !== 0) {
|
|
49
|
+
return timestampDiff;
|
|
50
|
+
}
|
|
51
|
+
const sourceDiff = String(right.history_source || '').localeCompare(
|
|
52
|
+
String(left.history_source || '')
|
|
53
|
+
);
|
|
54
|
+
if (sourceDiff !== 0) {
|
|
55
|
+
return sourceDiff;
|
|
56
|
+
}
|
|
57
|
+
return String(right.id || '').localeCompare(String(left.id || ''));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function buildRunUsageSummary(runId) {
|
|
61
|
+
const rows = db.prepare(`
|
|
62
|
+
SELECT
|
|
63
|
+
provider,
|
|
64
|
+
model,
|
|
65
|
+
phase,
|
|
66
|
+
input_tokens,
|
|
67
|
+
output_tokens,
|
|
68
|
+
reasoning_tokens,
|
|
69
|
+
cached_read_tokens,
|
|
70
|
+
cache_write_tokens,
|
|
71
|
+
total_tokens,
|
|
72
|
+
estimated_cost_usd,
|
|
73
|
+
latency_ms
|
|
74
|
+
FROM agent_model_usage
|
|
75
|
+
WHERE run_id = ?
|
|
76
|
+
ORDER BY id ASC
|
|
77
|
+
`).all(runId);
|
|
78
|
+
const totals = {
|
|
79
|
+
inputTokens: 0,
|
|
80
|
+
outputTokens: 0,
|
|
81
|
+
reasoningTokens: 0,
|
|
82
|
+
cachedReadTokens: 0,
|
|
83
|
+
cacheWriteTokens: 0,
|
|
84
|
+
totalTokens: 0,
|
|
85
|
+
estimatedCostUsd: 0,
|
|
86
|
+
pricedCallCount: 0,
|
|
87
|
+
latencyMs: 0,
|
|
88
|
+
};
|
|
89
|
+
const models = new Map();
|
|
90
|
+
|
|
91
|
+
for (const row of rows) {
|
|
92
|
+
const inputTokens = Number(row.input_tokens || 0);
|
|
93
|
+
const outputTokens = Number(row.output_tokens || 0);
|
|
94
|
+
const reasoningTokens = Number(row.reasoning_tokens || 0);
|
|
95
|
+
const cachedReadTokens = Number(row.cached_read_tokens || 0);
|
|
96
|
+
const cacheWriteTokens = Number(row.cache_write_tokens || 0);
|
|
97
|
+
const totalTokens = Number(row.total_tokens || 0);
|
|
98
|
+
const latencyMs = Number(row.latency_ms || 0);
|
|
99
|
+
const estimatedCostUsd = Number(row.estimated_cost_usd);
|
|
100
|
+
const key = `${row.provider}:${row.model}`;
|
|
101
|
+
|
|
102
|
+
totals.inputTokens += inputTokens;
|
|
103
|
+
totals.outputTokens += outputTokens;
|
|
104
|
+
totals.reasoningTokens += reasoningTokens;
|
|
105
|
+
totals.cachedReadTokens += cachedReadTokens;
|
|
106
|
+
totals.cacheWriteTokens += cacheWriteTokens;
|
|
107
|
+
totals.totalTokens += totalTokens;
|
|
108
|
+
totals.latencyMs += latencyMs;
|
|
109
|
+
if (Number.isFinite(estimatedCostUsd)) {
|
|
110
|
+
totals.estimatedCostUsd += estimatedCostUsd;
|
|
111
|
+
totals.pricedCallCount += 1;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!models.has(key)) {
|
|
115
|
+
models.set(key, {
|
|
116
|
+
provider: row.provider,
|
|
117
|
+
model: row.model,
|
|
118
|
+
callCount: 0,
|
|
119
|
+
inputTokens: 0,
|
|
120
|
+
outputTokens: 0,
|
|
121
|
+
reasoningTokens: 0,
|
|
122
|
+
cachedReadTokens: 0,
|
|
123
|
+
cacheWriteTokens: 0,
|
|
124
|
+
totalTokens: 0,
|
|
125
|
+
estimatedCostUsd: 0,
|
|
126
|
+
pricedCallCount: 0,
|
|
127
|
+
latencyMs: 0,
|
|
128
|
+
phases: new Set(),
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const aggregate = models.get(key);
|
|
133
|
+
aggregate.callCount += 1;
|
|
134
|
+
aggregate.inputTokens += inputTokens;
|
|
135
|
+
aggregate.outputTokens += outputTokens;
|
|
136
|
+
aggregate.reasoningTokens += reasoningTokens;
|
|
137
|
+
aggregate.cachedReadTokens += cachedReadTokens;
|
|
138
|
+
aggregate.cacheWriteTokens += cacheWriteTokens;
|
|
139
|
+
aggregate.totalTokens += totalTokens;
|
|
140
|
+
aggregate.latencyMs += latencyMs;
|
|
141
|
+
aggregate.phases.add(String(row.phase || '').trim() || 'model_turn');
|
|
142
|
+
if (Number.isFinite(estimatedCostUsd)) {
|
|
143
|
+
aggregate.estimatedCostUsd += estimatedCostUsd;
|
|
144
|
+
aggregate.pricedCallCount += 1;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
totals: {
|
|
150
|
+
...totals,
|
|
151
|
+
estimatedCostUsd: totals.pricedCallCount > 0 ? totals.estimatedCostUsd : null,
|
|
152
|
+
},
|
|
153
|
+
models: [...models.values()].map((entry) => ({
|
|
154
|
+
...entry,
|
|
155
|
+
estimatedCostUsd: entry.pricedCallCount > 0 ? entry.estimatedCostUsd : null,
|
|
156
|
+
phases: [...entry.phases],
|
|
157
|
+
})),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
13
161
|
// List agent runs
|
|
14
162
|
router.get('/', (req, res) => {
|
|
15
163
|
const limit = Math.min(parseInt(req.query.limit) || 50, 200);
|
|
@@ -23,9 +171,31 @@ router.get('/', (req, res) => {
|
|
|
23
171
|
|
|
24
172
|
// Chat history (web + social messages merged)
|
|
25
173
|
router.get('/chat-history', (req, res) => {
|
|
26
|
-
const limit =
|
|
174
|
+
const limit = normalizeChatHistoryLimit(req.query.limit);
|
|
175
|
+
const queryLimit = limit + 1;
|
|
27
176
|
const userId = req.session.userId;
|
|
28
177
|
const agentId = resolveAgentId(userId, getAgentIdFromRequest(req));
|
|
178
|
+
const cursor = parseChatHistoryCursor(req.query);
|
|
179
|
+
const cursorTimestampSql = normalizedTimestampExpression('@beforeCreatedAt');
|
|
180
|
+
const historyTimestampSql = normalizedTimestampExpression('created_at');
|
|
181
|
+
const cursorClause = (sourceName) =>
|
|
182
|
+
cursor
|
|
183
|
+
? `AND (
|
|
184
|
+
${historyTimestampSql} < ${cursorTimestampSql}
|
|
185
|
+
OR (${historyTimestampSql} = ${cursorTimestampSql} AND '${sourceName}' < @beforeSource)
|
|
186
|
+
OR (${historyTimestampSql} = ${cursorTimestampSql} AND '${sourceName}' = @beforeSource AND CAST(id AS TEXT) < @beforeId)
|
|
187
|
+
)`
|
|
188
|
+
: '';
|
|
189
|
+
const queryParams = cursor
|
|
190
|
+
? {
|
|
191
|
+
userId,
|
|
192
|
+
agentId,
|
|
193
|
+
limit: queryLimit,
|
|
194
|
+
beforeCreatedAt: cursor.createdAt,
|
|
195
|
+
beforeSource: cursor.source,
|
|
196
|
+
beforeId: cursor.id,
|
|
197
|
+
}
|
|
198
|
+
: { userId, agentId, limit: queryLimit };
|
|
29
199
|
|
|
30
200
|
const webMsgs = db.prepare(`
|
|
31
201
|
SELECT
|
|
@@ -37,9 +207,14 @@ router.get('/chat-history', (req, res) => {
|
|
|
37
207
|
created_at,
|
|
38
208
|
agent_run_id AS run_id,
|
|
39
209
|
metadata,
|
|
40
|
-
NULL AS tool_calls
|
|
41
|
-
|
|
42
|
-
|
|
210
|
+
NULL AS tool_calls,
|
|
211
|
+
'conversation' AS history_source
|
|
212
|
+
FROM conversation_history
|
|
213
|
+
WHERE user_id = @userId AND agent_id = @agentId
|
|
214
|
+
${cursorClause('conversation')}
|
|
215
|
+
ORDER BY ${historyTimestampSql} DESC, CAST(id AS TEXT) DESC
|
|
216
|
+
LIMIT @limit
|
|
217
|
+
`).all(queryParams);
|
|
43
218
|
|
|
44
219
|
const socialMsgs = db.prepare(`
|
|
45
220
|
SELECT
|
|
@@ -51,23 +226,27 @@ router.get('/chat-history', (req, res) => {
|
|
|
51
226
|
created_at,
|
|
52
227
|
run_id,
|
|
53
228
|
metadata,
|
|
54
|
-
tool_calls
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const normalized = s.includes('T') ? s : s.replace(' ', 'T') + 'Z';
|
|
63
|
-
return new Date(normalized).getTime();
|
|
64
|
-
};
|
|
229
|
+
tool_calls,
|
|
230
|
+
'message' AS history_source
|
|
231
|
+
FROM messages
|
|
232
|
+
WHERE user_id = @userId AND agent_id = @agentId AND platform != 'web'
|
|
233
|
+
${cursorClause('message')}
|
|
234
|
+
ORDER BY ${historyTimestampSql} DESC, CAST(id AS TEXT) DESC
|
|
235
|
+
LIMIT @limit
|
|
236
|
+
`).all(queryParams);
|
|
65
237
|
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
238
|
+
const merged = [...webMsgs, ...socialMsgs].sort(compareChatHistoryDesc);
|
|
239
|
+
const page = merged.slice(0, limit);
|
|
240
|
+
const oldest = page.length > 0 ? page[page.length - 1] : null;
|
|
69
241
|
|
|
70
|
-
res.json({
|
|
242
|
+
res.json({
|
|
243
|
+
messages: [...page].reverse(),
|
|
244
|
+
agentId,
|
|
245
|
+
hasMore: merged.length > limit,
|
|
246
|
+
nextBeforeCreatedAt: oldest?.created_at || null,
|
|
247
|
+
nextBeforeSource: oldest?.history_source || null,
|
|
248
|
+
nextBeforeId: oldest?.id?.toString() || null,
|
|
249
|
+
});
|
|
71
250
|
});
|
|
72
251
|
|
|
73
252
|
// Create new agent run
|
|
@@ -130,6 +309,7 @@ router.post('/', async (req, res) => {
|
|
|
130
309
|
|
|
131
310
|
res.json(result);
|
|
132
311
|
} catch (err) {
|
|
312
|
+
console.error('[Agents] Run failed:', err?.stack || err);
|
|
133
313
|
res.status(err?.statusCode || err?.status || 500).json({
|
|
134
314
|
error: sanitizeError(err),
|
|
135
315
|
code: err?.code,
|
|
@@ -145,8 +325,9 @@ router.get('/:id', (req, res) => {
|
|
|
145
325
|
|
|
146
326
|
const steps = db.prepare('SELECT * FROM agent_steps WHERE run_id = ? ORDER BY step_index ASC').all(run.id);
|
|
147
327
|
const history = db.prepare('SELECT * FROM conversation_history WHERE agent_run_id = ? ORDER BY created_at ASC').all(run.id);
|
|
328
|
+
const usage = buildRunUsageSummary(run.id);
|
|
148
329
|
|
|
149
|
-
res.json({ run, steps, history });
|
|
330
|
+
res.json({ run, steps, history, usage });
|
|
150
331
|
});
|
|
151
332
|
|
|
152
333
|
// Get detailed steps for a run (for activity history replay)
|
|
@@ -180,8 +361,9 @@ router.get('/:id/steps', (req, res) => {
|
|
|
180
361
|
|| latestHistoryAssistant?.content
|
|
181
362
|
|| run.final_response
|
|
182
363
|
|| null;
|
|
364
|
+
const usage = buildRunUsageSummary(run.id);
|
|
183
365
|
|
|
184
|
-
res.json({ run, steps, events: listRunEvents(run.id), response });
|
|
366
|
+
res.json({ run, steps, events: listRunEvents(run.id), response, usage });
|
|
185
367
|
});
|
|
186
368
|
|
|
187
369
|
// Abort a run
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const express = require('express');
|
|
4
|
+
const router = express.Router();
|
|
5
|
+
const db = require('../db/database');
|
|
6
|
+
const { requireAuth } = require('../middleware/auth');
|
|
7
|
+
const { requireBilling } = require('../middleware/requireBilling');
|
|
8
|
+
const { getStripeConfig } = require('../services/billing/config');
|
|
9
|
+
const { listPlans } = require('../services/billing/plans');
|
|
10
|
+
const subs = require('../services/billing/subscriptions');
|
|
11
|
+
const { getStripeClient } = require('../services/billing/stripe_client');
|
|
12
|
+
|
|
13
|
+
function isValidHttpsUrl(value) {
|
|
14
|
+
try {
|
|
15
|
+
const u = new URL(value);
|
|
16
|
+
return u.protocol === 'https:' || u.protocol === 'http:';
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
router.use(requireBilling);
|
|
23
|
+
|
|
24
|
+
// Public — pricing page needs this without login.
|
|
25
|
+
router.get('/plans', (req, res) => {
|
|
26
|
+
try {
|
|
27
|
+
res.json({ plans: listPlans() });
|
|
28
|
+
} catch (err) {
|
|
29
|
+
res.status(500).json({ error: err.message });
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// All remaining routes require authentication.
|
|
34
|
+
router.use(requireAuth);
|
|
35
|
+
|
|
36
|
+
router.get('/', (req, res) => {
|
|
37
|
+
try {
|
|
38
|
+
const userId = req.session.userId;
|
|
39
|
+
const subscription = subs.getActiveSubscription(userId);
|
|
40
|
+
const { publicKey } = getStripeConfig();
|
|
41
|
+
res.json({ subscription, stripePublishableKey: publicKey || null });
|
|
42
|
+
} catch (err) {
|
|
43
|
+
res.status(500).json({ error: err.message });
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
router.post('/checkout', async (req, res) => {
|
|
48
|
+
try {
|
|
49
|
+
const { planId, successUrl, cancelUrl } = req.body;
|
|
50
|
+
if (!planId || !successUrl || !cancelUrl) {
|
|
51
|
+
return res.status(400).json({ error: 'planId, successUrl and cancelUrl are required.' });
|
|
52
|
+
}
|
|
53
|
+
if (!isValidHttpsUrl(successUrl) || !isValidHttpsUrl(cancelUrl)) {
|
|
54
|
+
return res.status(400).json({ error: 'successUrl and cancelUrl must be valid URLs.' });
|
|
55
|
+
}
|
|
56
|
+
const url = await subs.createCheckoutSession(req.session.userId, planId, successUrl, cancelUrl);
|
|
57
|
+
res.json({ url });
|
|
58
|
+
} catch (err) {
|
|
59
|
+
res.status(err.statusCode || 500).json({ error: err.message });
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
router.post('/portal', async (req, res) => {
|
|
64
|
+
try {
|
|
65
|
+
const { returnUrl } = req.body;
|
|
66
|
+
if (!returnUrl) return res.status(400).json({ error: 'returnUrl is required.' });
|
|
67
|
+
if (!isValidHttpsUrl(returnUrl)) {
|
|
68
|
+
return res.status(400).json({ error: 'returnUrl must be a valid URL.' });
|
|
69
|
+
}
|
|
70
|
+
const url = await subs.createCustomerPortalSession(req.session.userId, returnUrl);
|
|
71
|
+
res.json({ url });
|
|
72
|
+
} catch (err) {
|
|
73
|
+
res.status(err.statusCode || 500).json({ error: err.message });
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
router.post('/trial', async (req, res) => {
|
|
78
|
+
try {
|
|
79
|
+
const { planId, deviceFingerprint } = req.body;
|
|
80
|
+
if (!planId) return res.status(400).json({ error: 'planId is required.' });
|
|
81
|
+
if (deviceFingerprint !== undefined) {
|
|
82
|
+
if (typeof deviceFingerprint !== 'string' || deviceFingerprint.length > 256 || !/^[a-zA-Z0-9_+/=.-]{1,256}$/.test(deviceFingerprint)) {
|
|
83
|
+
return res.status(400).json({ error: 'Invalid deviceFingerprint.' });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const ip = req.ip || req.socket?.remoteAddress;
|
|
87
|
+
const result = await subs.startTrial(req.session.userId, planId, { ip, deviceFp: deviceFingerprint });
|
|
88
|
+
res.json({ subscription: result.subscription });
|
|
89
|
+
} catch (err) {
|
|
90
|
+
res.status(err.statusCode || 500).json({ error: err.message });
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
router.post('/cancel', async (req, res) => {
|
|
95
|
+
try {
|
|
96
|
+
await subs.cancelSubscription(req.session.userId);
|
|
97
|
+
res.json({ ok: true });
|
|
98
|
+
} catch (err) {
|
|
99
|
+
res.status(err.statusCode || 500).json({ error: err.message });
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
router.get('/invoices', async (req, res) => {
|
|
104
|
+
try {
|
|
105
|
+
const stripe = getStripeClient();
|
|
106
|
+
const userId = req.session.userId;
|
|
107
|
+
const customer = db
|
|
108
|
+
.prepare('SELECT stripe_customer_id FROM billing_customers WHERE user_id = ?')
|
|
109
|
+
.get(userId);
|
|
110
|
+
if (!customer?.stripe_customer_id) return res.json({ invoices: [] });
|
|
111
|
+
const list = await stripe.invoices.list({ customer: customer.stripe_customer_id, limit: 12 });
|
|
112
|
+
const invoices = list.data.map((inv) => ({
|
|
113
|
+
id: inv.id,
|
|
114
|
+
amount_paid: inv.amount_paid,
|
|
115
|
+
currency: inv.currency,
|
|
116
|
+
status: inv.status,
|
|
117
|
+
created: inv.created,
|
|
118
|
+
hosted_invoice_url: inv.hosted_invoice_url,
|
|
119
|
+
invoice_pdf: inv.invoice_pdf,
|
|
120
|
+
}));
|
|
121
|
+
res.json({ invoices });
|
|
122
|
+
} catch (err) {
|
|
123
|
+
res.status(err.statusCode || 500).json({ error: err.message });
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
module.exports = router;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const express = require('express');
|
|
4
|
+
const router = express.Router();
|
|
5
|
+
const { isBillingEnabled, getStripeConfig } = require('../services/billing/config');
|
|
6
|
+
const { getStripeClient } = require('../services/billing/stripe_client');
|
|
7
|
+
const { handleWebhookEvent } = require('../services/billing/subscriptions');
|
|
8
|
+
|
|
9
|
+
// Raw body required for Stripe signature verification — applied inline.
|
|
10
|
+
router.post('/', express.raw({ type: 'application/json' }), async (req, res) => {
|
|
11
|
+
if (!isBillingEnabled()) return res.status(404).json({ error: 'Billing is not enabled.' });
|
|
12
|
+
|
|
13
|
+
const sig = req.headers['stripe-signature'];
|
|
14
|
+
if (!sig) {
|
|
15
|
+
return res.status(400).json({ error: 'Missing stripe-signature header.' });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const { webhookSecret } = getStripeConfig();
|
|
19
|
+
if (!webhookSecret) {
|
|
20
|
+
console.warn('[billing] STRIPE_WEBHOOK_SECRET is not set; rejecting webhook.');
|
|
21
|
+
return res.status(500).json({ error: 'Webhook secret not configured.' });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let event;
|
|
25
|
+
try {
|
|
26
|
+
const stripe = getStripeClient();
|
|
27
|
+
event = stripe.webhooks.constructEvent(req.body, sig, webhookSecret);
|
|
28
|
+
} catch (err) {
|
|
29
|
+
console.warn('[billing] Webhook signature verification failed:', err.message);
|
|
30
|
+
return res.status(400).json({ error: `Webhook error: ${err.message}` });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
await handleWebhookEvent(event);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
console.error('[billing] Webhook handler error:', err);
|
|
37
|
+
// Still return 200 so Stripe does not retry immediately for handler bugs.
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
res.json({ received: true });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
module.exports = router;
|
package/server/routes/memory.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const express = require('express');
|
|
2
2
|
const router = express.Router();
|
|
3
3
|
const rateLimit = require('express-rate-limit');
|
|
4
|
+
const db = require('../db/database');
|
|
4
5
|
const { requireAuth } = require('../middleware/auth');
|
|
5
6
|
const { sanitizeError } = require('../utils/security');
|
|
6
7
|
const { getAgentIdFromRequest, resolveAgentId } = require('../services/agents/manager');
|
|
@@ -317,7 +318,6 @@ router.post('/memories', async (req, res) => {
|
|
|
317
318
|
// Update a memory
|
|
318
319
|
router.put('/memories/:id', async (req, res) => {
|
|
319
320
|
const mm = req.app.locals.memoryManager;
|
|
320
|
-
const db = require('../db/database');
|
|
321
321
|
// Verify ownership before updating
|
|
322
322
|
const agentId = resolveAgentId(req.session.userId, getAgentIdFromRequest(req));
|
|
323
323
|
const existing = db.prepare('SELECT id FROM memories WHERE id = ? AND user_id = ? AND agent_id = ?').get(req.params.id, req.session.userId, agentId);
|
|
@@ -335,7 +335,6 @@ router.put('/memories/:id', async (req, res) => {
|
|
|
335
335
|
// Delete a memory
|
|
336
336
|
router.delete('/memories/:id', (req, res) => {
|
|
337
337
|
const mm = req.app.locals.memoryManager;
|
|
338
|
-
const db = require('../db/database');
|
|
339
338
|
// Verify ownership before deleting
|
|
340
339
|
const agentId = resolveAgentId(req.session.userId, getAgentIdFromRequest(req));
|
|
341
340
|
const existing = db.prepare('SELECT id FROM memories WHERE id = ? AND user_id = ? AND agent_id = ?').get(req.params.id, req.session.userId, agentId);
|
|
@@ -346,7 +345,6 @@ router.delete('/memories/:id', (req, res) => {
|
|
|
346
345
|
|
|
347
346
|
router.post('/memories/bulk-delete', (req, res) => {
|
|
348
347
|
const mm = req.app.locals.memoryManager;
|
|
349
|
-
const db = require('../db/database');
|
|
350
348
|
const ids = normalizeMemoryIds(req.body?.ids);
|
|
351
349
|
if (!ids.length) {
|
|
352
350
|
return res.status(400).json({ error: 'ids is required' });
|
|
@@ -366,7 +364,6 @@ router.post('/memories/bulk-delete', (req, res) => {
|
|
|
366
364
|
|
|
367
365
|
router.post('/memories/bulk-archive', (req, res) => {
|
|
368
366
|
const mm = req.app.locals.memoryManager;
|
|
369
|
-
const db = require('../db/database');
|
|
370
367
|
const ids = normalizeMemoryIds(req.body?.ids);
|
|
371
368
|
const archived = req.body?.archived !== false;
|
|
372
369
|
if (!ids.length) {
|
|
@@ -34,6 +34,7 @@ const {
|
|
|
34
34
|
} = require('../services/runtime/settings');
|
|
35
35
|
const { isManagedDeployment } = require('../utils/deployment');
|
|
36
36
|
const { getAgentIdFromRequest, isMainAgent, resolveAgentId } = require('../services/agents/manager');
|
|
37
|
+
const { getProviderHealthCatalog, getSupportedModels } = require('../services/ai/models');
|
|
37
38
|
|
|
38
39
|
const AGENT_SETTING_KEYS = new Set([
|
|
39
40
|
'cost_mode',
|
|
@@ -193,14 +194,12 @@ async function resetEnvBackedSettingValue(req, key) {
|
|
|
193
194
|
|
|
194
195
|
// Get supported models metadata
|
|
195
196
|
router.get('/meta/models', async (req, res) => {
|
|
196
|
-
const { getSupportedModels } = require('../services/ai/models');
|
|
197
197
|
const agentId = resolveAgentId(req.session.userId, getAgentIdFromRequest(req));
|
|
198
198
|
const models = await getSupportedModels(req.session.userId, agentId);
|
|
199
199
|
res.json({ models });
|
|
200
200
|
});
|
|
201
201
|
|
|
202
202
|
router.get('/meta/ai-providers', async (req, res) => {
|
|
203
|
-
const { getProviderHealthCatalog, getSupportedModels } = require('../services/ai/models');
|
|
204
203
|
const agentId = resolveAgentId(req.session.userId, getAgentIdFromRequest(req));
|
|
205
204
|
const [providers, models] = await Promise.all([
|
|
206
205
|
getProviderHealthCatalog(req.session.userId, agentId),
|
package/server/routes/skills.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
1
2
|
const express = require('express');
|
|
2
3
|
const router = express.Router();
|
|
3
4
|
const { requireAuth } = require('../middleware/auth');
|
|
@@ -109,7 +110,6 @@ router.get('/:name', async (req, res) => {
|
|
|
109
110
|
const runner = await getSkillRunner(req.app);
|
|
110
111
|
const skill = runner.getSkill(req.params.name, req.session.userId);
|
|
111
112
|
if (!skill) return res.status(404).json({ error: 'Skill not found' });
|
|
112
|
-
const fs = require('fs');
|
|
113
113
|
const content = fs.readFileSync(skill.filePath, 'utf-8');
|
|
114
114
|
res.json({
|
|
115
115
|
name: skill.name,
|
|
@@ -16,7 +16,7 @@ router.post('/geofence', async (req, res) => {
|
|
|
16
16
|
const userRow = db.prepare('SELECT id FROM users WHERE id = ?').get(req.session.userId);
|
|
17
17
|
if (!userRow) return res.status(401).json({ error: 'Unauthorized' });
|
|
18
18
|
|
|
19
|
-
console.log(`[Triggers] Geofence
|
|
19
|
+
console.log(`[Triggers] Geofence event for user ${req.session.userId}`);
|
|
20
20
|
|
|
21
21
|
res.json({ success: true, message: 'Geofence trigger processed' });
|
|
22
22
|
} catch (err) {
|
|
@@ -51,7 +51,7 @@ router.post('/notification', async (req, res) => {
|
|
|
51
51
|
const userRow = db.prepare('SELECT id FROM users WHERE id = ?').get(req.session.userId);
|
|
52
52
|
if (!userRow) return res.status(401).json({ error: 'Unauthorized' });
|
|
53
53
|
|
|
54
|
-
console.log(`[Triggers] Notification
|
|
54
|
+
console.log(`[Triggers] Notification event for user ${req.session.userId}`);
|
|
55
55
|
|
|
56
56
|
db.prepare(`
|
|
57
57
|
INSERT INTO notification_history (user_id, app_package, title, body, action_taken)
|