@naisys/supervisor 3.0.0-beta.6
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/bin/naisys-supervisor +2 -0
- package/client-dist/android-chrome-192x192.png +0 -0
- package/client-dist/android-chrome-512x512.png +0 -0
- package/client-dist/apple-touch-icon.png +0 -0
- package/client-dist/assets/index-BBrK4ItN.js +177 -0
- package/client-dist/assets/index-CKg0vgt5.css +1 -0
- package/client-dist/assets/naisys-logo-CzoPnn5I.webp +0 -0
- package/client-dist/favicon-16x16.png +0 -0
- package/client-dist/favicon-32x32.png +0 -0
- package/client-dist/favicon.ico +0 -0
- package/client-dist/index.html +49 -0
- package/client-dist/site.webmanifest +22 -0
- package/dist/api-reference.js +52 -0
- package/dist/auth-middleware.js +116 -0
- package/dist/database/hubDb.js +26 -0
- package/dist/database/supervisorDb.js +18 -0
- package/dist/error-helpers.js +13 -0
- package/dist/hateoas.js +61 -0
- package/dist/logger.js +11 -0
- package/dist/route-helpers.js +7 -0
- package/dist/routes/admin.js +209 -0
- package/dist/routes/agentChat.js +194 -0
- package/dist/routes/agentConfig.js +265 -0
- package/dist/routes/agentLifecycle.js +350 -0
- package/dist/routes/agentMail.js +171 -0
- package/dist/routes/agentRuns.js +90 -0
- package/dist/routes/agents.js +236 -0
- package/dist/routes/api.js +52 -0
- package/dist/routes/attachments.js +18 -0
- package/dist/routes/auth.js +103 -0
- package/dist/routes/costs.js +51 -0
- package/dist/routes/hosts.js +296 -0
- package/dist/routes/models.js +152 -0
- package/dist/routes/root.js +56 -0
- package/dist/routes/schemas.js +31 -0
- package/dist/routes/status.js +20 -0
- package/dist/routes/users.js +420 -0
- package/dist/routes/variables.js +103 -0
- package/dist/schema-registry.js +23 -0
- package/dist/services/agentConfigService.js +182 -0
- package/dist/services/agentHostStatusService.js +178 -0
- package/dist/services/agentService.js +291 -0
- package/dist/services/attachmentProxyService.js +130 -0
- package/dist/services/browserSocketService.js +78 -0
- package/dist/services/chatService.js +201 -0
- package/dist/services/configExportService.js +61 -0
- package/dist/services/costsService.js +127 -0
- package/dist/services/hostService.js +156 -0
- package/dist/services/hubConnectionService.js +333 -0
- package/dist/services/logFileService.js +11 -0
- package/dist/services/mailService.js +154 -0
- package/dist/services/modelService.js +92 -0
- package/dist/services/runsService.js +164 -0
- package/dist/services/userService.js +147 -0
- package/dist/services/variableService.js +23 -0
- package/dist/supervisorServer.js +221 -0
- package/package.json +79 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { AgentConfigFileSchema, assertUrlSafeKey, buildDefaultAgentConfig, } from "@naisys/common";
|
|
2
|
+
import { randomBytes } from "crypto";
|
|
3
|
+
import { hubDb } from "../database/hubDb.js";
|
|
4
|
+
import { sendUserListChanged } from "./hubConnectionService.js";
|
|
5
|
+
/**
|
|
6
|
+
* Update the modified date on the user_notifications table
|
|
7
|
+
*/
|
|
8
|
+
async function updateUserNotificationModifiedDate(userId) {
|
|
9
|
+
await hubDb.user_notifications.upsert({
|
|
10
|
+
where: { user_id: userId },
|
|
11
|
+
create: {
|
|
12
|
+
user_id: userId,
|
|
13
|
+
updated_at: new Date(),
|
|
14
|
+
},
|
|
15
|
+
update: {
|
|
16
|
+
updated_at: new Date(),
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Create a new agent with database entry.
|
|
22
|
+
*/
|
|
23
|
+
export async function createAgentConfig(name, title) {
|
|
24
|
+
assertUrlSafeKey(name, "Agent name");
|
|
25
|
+
// Check db if username already exists
|
|
26
|
+
const existingAgent = await hubDb.users.findFirst({
|
|
27
|
+
where: {
|
|
28
|
+
username: name,
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
if (existingAgent) {
|
|
32
|
+
throw new Error(`Agent '${name}' already exists in the database`);
|
|
33
|
+
}
|
|
34
|
+
// Create default config and convert to JSON
|
|
35
|
+
const defaultConfig = buildDefaultAgentConfig(name);
|
|
36
|
+
if (title) {
|
|
37
|
+
defaultConfig.title = title;
|
|
38
|
+
}
|
|
39
|
+
const jsonContent = JSON.stringify(canonicalConfigOrder(defaultConfig));
|
|
40
|
+
// Add agent to the database, let DB autoincrement
|
|
41
|
+
const user = await hubDb.users.create({
|
|
42
|
+
data: {
|
|
43
|
+
uuid: crypto.randomUUID(),
|
|
44
|
+
username: defaultConfig.username,
|
|
45
|
+
title: defaultConfig.title,
|
|
46
|
+
config: jsonContent,
|
|
47
|
+
api_key: randomBytes(32).toString("hex"),
|
|
48
|
+
enabled: true,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
// Update user notification modified date
|
|
52
|
+
await updateUserNotificationModifiedDate(user.id);
|
|
53
|
+
// Notify hub to broadcast updated user list to all NAISYS clients
|
|
54
|
+
sendUserListChanged();
|
|
55
|
+
return { id: user.id, config: defaultConfig };
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Get parsed agent configuration by user ID. Reads from DB config column.
|
|
59
|
+
*/
|
|
60
|
+
export async function getAgentConfigById(id) {
|
|
61
|
+
const user = await hubDb.users.findUnique({
|
|
62
|
+
where: { id },
|
|
63
|
+
select: { config: true },
|
|
64
|
+
});
|
|
65
|
+
if (!user) {
|
|
66
|
+
throw new Error(`User with ID ${id} not found`);
|
|
67
|
+
}
|
|
68
|
+
const parsed = JSON.parse(user.config);
|
|
69
|
+
return AgentConfigFileSchema.parse(parsed);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Build config object in canonical field order for consistent JSON output.
|
|
73
|
+
*/
|
|
74
|
+
function canonicalConfigOrder(config) {
|
|
75
|
+
const ordered = {};
|
|
76
|
+
// Identity
|
|
77
|
+
ordered.username = config.username;
|
|
78
|
+
ordered.title = config.title;
|
|
79
|
+
// Prompt
|
|
80
|
+
ordered.agentPrompt = config.agentPrompt;
|
|
81
|
+
// Models
|
|
82
|
+
ordered.shellModel = config.shellModel;
|
|
83
|
+
if (config.imageModel !== undefined)
|
|
84
|
+
ordered.imageModel = config.imageModel;
|
|
85
|
+
// Limits
|
|
86
|
+
ordered.tokenMax = config.tokenMax;
|
|
87
|
+
if (config.spendLimitDollars !== undefined)
|
|
88
|
+
ordered.spendLimitDollars = config.spendLimitDollars;
|
|
89
|
+
if (config.spendLimitHours !== undefined)
|
|
90
|
+
ordered.spendLimitHours = config.spendLimitHours;
|
|
91
|
+
// Features
|
|
92
|
+
if (config.mailEnabled !== undefined)
|
|
93
|
+
ordered.mailEnabled = config.mailEnabled;
|
|
94
|
+
if (config.chatEnabled !== undefined)
|
|
95
|
+
ordered.chatEnabled = config.chatEnabled;
|
|
96
|
+
if (config.webEnabled !== undefined)
|
|
97
|
+
ordered.webEnabled = config.webEnabled;
|
|
98
|
+
if (config.completeSessionEnabled !== undefined)
|
|
99
|
+
ordered.completeSessionEnabled = config.completeSessionEnabled;
|
|
100
|
+
if (config.wakeOnMessage !== undefined)
|
|
101
|
+
ordered.wakeOnMessage = config.wakeOnMessage;
|
|
102
|
+
if (config.workspacesEnabled !== undefined)
|
|
103
|
+
ordered.workspacesEnabled = config.workspacesEnabled;
|
|
104
|
+
if (config.multipleCommandsEnabled !== undefined)
|
|
105
|
+
ordered.multipleCommandsEnabled = config.multipleCommandsEnabled;
|
|
106
|
+
if (config.controlDesktop !== undefined)
|
|
107
|
+
ordered.controlDesktop = config.controlDesktop;
|
|
108
|
+
// Advanced
|
|
109
|
+
if (config.commandProtection !== undefined)
|
|
110
|
+
ordered.commandProtection = config.commandProtection;
|
|
111
|
+
if (config.debugPauseSeconds !== undefined)
|
|
112
|
+
ordered.debugPauseSeconds = config.debugPauseSeconds;
|
|
113
|
+
if (config.initialCommands !== undefined)
|
|
114
|
+
ordered.initialCommands = config.initialCommands;
|
|
115
|
+
return ordered;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Update agent configuration by user ID. Always updates DB.
|
|
119
|
+
* Snapshots the previous config as a revision before overwriting.
|
|
120
|
+
*/
|
|
121
|
+
export async function updateAgentConfigById(id, config, setUsername, changedById) {
|
|
122
|
+
// Snapshot the current config before overwriting
|
|
123
|
+
const currentUser = await hubDb.users.findUnique({
|
|
124
|
+
where: { id },
|
|
125
|
+
select: { config: true },
|
|
126
|
+
});
|
|
127
|
+
if (setUsername) {
|
|
128
|
+
// Normal edit: push config.username to the DB column
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
// Import: preserve the DB username, override it in the config
|
|
132
|
+
const user = await hubDb.users.findUnique({ where: { id } });
|
|
133
|
+
if (user) {
|
|
134
|
+
config = { ...config, username: user.username };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const ordered = canonicalConfigOrder(config);
|
|
138
|
+
const jsonStr = JSON.stringify(ordered);
|
|
139
|
+
// Save revision of the old config (if it exists and differs)
|
|
140
|
+
if (currentUser?.config && currentUser.config !== jsonStr) {
|
|
141
|
+
await hubDb.config_revisions.create({
|
|
142
|
+
data: {
|
|
143
|
+
user_id: id,
|
|
144
|
+
config: currentUser.config,
|
|
145
|
+
changed_by_id: changedById ?? id,
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
await hubDb.users.update({
|
|
150
|
+
where: { id },
|
|
151
|
+
data: {
|
|
152
|
+
config: jsonStr,
|
|
153
|
+
...(setUsername && { username: config.username }),
|
|
154
|
+
title: config.title,
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
// Update user notification modified date
|
|
158
|
+
await updateUserNotificationModifiedDate(id);
|
|
159
|
+
// Notify hub to broadcast updated user list to all NAISYS clients
|
|
160
|
+
sendUserListChanged();
|
|
161
|
+
return config;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Get config revision history for an agent.
|
|
165
|
+
*/
|
|
166
|
+
export async function getConfigRevisions(userId, limit = 50) {
|
|
167
|
+
const revisions = await hubDb.config_revisions.findMany({
|
|
168
|
+
where: { user_id: userId },
|
|
169
|
+
orderBy: { created_at: "desc" },
|
|
170
|
+
take: limit,
|
|
171
|
+
include: {
|
|
172
|
+
changed_by: { select: { username: true } },
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
return revisions.map((r) => ({
|
|
176
|
+
id: r.id,
|
|
177
|
+
config: r.config,
|
|
178
|
+
changedByUsername: r.changed_by.username,
|
|
179
|
+
createdAt: r.created_at,
|
|
180
|
+
}));
|
|
181
|
+
}
|
|
182
|
+
//# sourceMappingURL=agentConfigService.js.map
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { determineAgentStatus } from "@naisys/common";
|
|
2
|
+
import { getIO } from "./browserSocketService.js";
|
|
3
|
+
const activeAgentIds = new Set();
|
|
4
|
+
const disabledAgentIds = new Set();
|
|
5
|
+
const costSuspendedAgentIds = new Set();
|
|
6
|
+
const connectedHostIds = new Set();
|
|
7
|
+
const agentNotifications = new Map();
|
|
8
|
+
const hostOnlineStatus = new Map();
|
|
9
|
+
const hostRestrictedStatus = new Map();
|
|
10
|
+
const hostTypeStatus = new Map();
|
|
11
|
+
const agentHostAssignments = new Map();
|
|
12
|
+
function broadcast(room, event) {
|
|
13
|
+
try {
|
|
14
|
+
getIO().to(room).emit(room, event);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// Socket.IO not yet initialized during startup — safe to ignore
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function broadcastAgentStatus(event) {
|
|
21
|
+
broadcast("agent-status", event);
|
|
22
|
+
}
|
|
23
|
+
function broadcastHostStatus(event) {
|
|
24
|
+
broadcast("host-status", event);
|
|
25
|
+
}
|
|
26
|
+
// --- Mutation functions (called by hubConnectionService event handlers) ---
|
|
27
|
+
export function updateAgentsStatus(hostActiveAgents, notifications) {
|
|
28
|
+
activeAgentIds.clear();
|
|
29
|
+
connectedHostIds.clear();
|
|
30
|
+
for (const [hostId, userIds] of Object.entries(hostActiveAgents)) {
|
|
31
|
+
connectedHostIds.add(Number(hostId));
|
|
32
|
+
for (const id of userIds) {
|
|
33
|
+
activeAgentIds.add(id);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (notifications) {
|
|
37
|
+
for (const [key, value] of Object.entries(notifications)) {
|
|
38
|
+
agentNotifications.set(Number(key), value);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
broadcastAgentStatus(getAgentSnapshot());
|
|
42
|
+
}
|
|
43
|
+
export function updateHostsStatus(hosts) {
|
|
44
|
+
// Detect if the set of host IDs changed
|
|
45
|
+
const newHostIds = new Set(hosts.map((h) => h.hostId));
|
|
46
|
+
const prevHostIds = new Set(hostOnlineStatus.keys());
|
|
47
|
+
const hostSetChanged = newHostIds.size !== prevHostIds.size ||
|
|
48
|
+
[...newHostIds].some((id) => !prevHostIds.has(id));
|
|
49
|
+
hostOnlineStatus.clear();
|
|
50
|
+
hostRestrictedStatus.clear();
|
|
51
|
+
hostTypeStatus.clear();
|
|
52
|
+
connectedHostIds.clear();
|
|
53
|
+
for (const host of hosts) {
|
|
54
|
+
hostOnlineStatus.set(host.hostId, host.online);
|
|
55
|
+
hostRestrictedStatus.set(host.hostId, host.restricted);
|
|
56
|
+
hostTypeStatus.set(host.hostId, host.hostType);
|
|
57
|
+
if (host.online) {
|
|
58
|
+
connectedHostIds.add(host.hostId);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Host topology changes can affect agent statuses (available/offline)
|
|
62
|
+
broadcastAgentStatus(getAgentSnapshot());
|
|
63
|
+
const hostEvent = getHostSnapshot();
|
|
64
|
+
if (hostSetChanged) {
|
|
65
|
+
hostEvent.hostsListChanged = true;
|
|
66
|
+
}
|
|
67
|
+
broadcastHostStatus(hostEvent);
|
|
68
|
+
}
|
|
69
|
+
export function markAgentStarted(userId) {
|
|
70
|
+
activeAgentIds.add(userId);
|
|
71
|
+
broadcastAgentStatus(getAgentSnapshot());
|
|
72
|
+
}
|
|
73
|
+
export function markAgentStopped(userId) {
|
|
74
|
+
activeAgentIds.delete(userId);
|
|
75
|
+
broadcastAgentStatus(getAgentSnapshot());
|
|
76
|
+
}
|
|
77
|
+
export function emitAgentsListChanged() {
|
|
78
|
+
broadcastAgentStatus({
|
|
79
|
+
...getAgentSnapshot(),
|
|
80
|
+
agentsListChanged: true,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
export function emitHostsListChanged() {
|
|
84
|
+
broadcastHostStatus({
|
|
85
|
+
...getHostSnapshot(),
|
|
86
|
+
hostsListChanged: true,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
export function emitHubConnectionStatus(connected) {
|
|
90
|
+
const event = { hubConnected: connected };
|
|
91
|
+
broadcast("hub-status", event);
|
|
92
|
+
}
|
|
93
|
+
// --- Agent host assignment cache ---
|
|
94
|
+
export function updateAgentHostAssignments(assignments) {
|
|
95
|
+
for (const { agentId, hostIds } of assignments) {
|
|
96
|
+
agentHostAssignments.set(agentId, hostIds);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// --- Enabled status cache ---
|
|
100
|
+
export function updateAgentEnabledStatus(agents) {
|
|
101
|
+
for (const { agentId, enabled } of agents) {
|
|
102
|
+
if (!enabled) {
|
|
103
|
+
disabledAgentIds.add(agentId);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
disabledAgentIds.delete(agentId);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// --- Cost suspension cache ---
|
|
111
|
+
export function updateCostSuspendedAgents(agents) {
|
|
112
|
+
for (const { agentId, isSuspended } of agents) {
|
|
113
|
+
if (isSuspended) {
|
|
114
|
+
costSuspendedAgentIds.add(agentId);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
costSuspendedAgentIds.delete(agentId);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// --- Query functions ---
|
|
122
|
+
export function isAgentActive(userId) {
|
|
123
|
+
return activeAgentIds.has(userId);
|
|
124
|
+
}
|
|
125
|
+
function hasNonRestrictedOnlineHost() {
|
|
126
|
+
for (const [hostId, online] of hostOnlineStatus) {
|
|
127
|
+
if (online &&
|
|
128
|
+
!hostRestrictedStatus.get(hostId) &&
|
|
129
|
+
hostTypeStatus.get(hostId) === "naisys")
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
export function getAgentStatus(agentId) {
|
|
135
|
+
return determineAgentStatus({
|
|
136
|
+
isActive: activeAgentIds.has(agentId),
|
|
137
|
+
isEnabled: !disabledAgentIds.has(agentId),
|
|
138
|
+
isSuspended: costSuspendedAgentIds.has(agentId),
|
|
139
|
+
assignedHostIds: agentHostAssignments.get(agentId),
|
|
140
|
+
isHostOnline: (hid) => connectedHostIds.has(hid),
|
|
141
|
+
hasNonRestrictedOnlineHost: hasNonRestrictedOnlineHost(),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
export function isHostConnected(hostId) {
|
|
145
|
+
return connectedHostIds.has(hostId);
|
|
146
|
+
}
|
|
147
|
+
/** Build a snapshot of all agent statuses from current state */
|
|
148
|
+
function getAgentSnapshot() {
|
|
149
|
+
const agents = {};
|
|
150
|
+
// Include all agents we know about from notifications
|
|
151
|
+
for (const [userId, notif] of agentNotifications) {
|
|
152
|
+
agents[String(userId)] = {
|
|
153
|
+
status: getAgentStatus(userId),
|
|
154
|
+
latestLogId: notif.latestLogId,
|
|
155
|
+
latestMailId: notif.latestMailId,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
// Include active agents that may not have notifications yet
|
|
159
|
+
for (const userId of activeAgentIds) {
|
|
160
|
+
if (!agents[String(userId)]) {
|
|
161
|
+
agents[String(userId)] = {
|
|
162
|
+
status: "active",
|
|
163
|
+
latestLogId: 0,
|
|
164
|
+
latestMailId: 0,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return { agents };
|
|
169
|
+
}
|
|
170
|
+
/** Build a snapshot of all host online statuses from current state */
|
|
171
|
+
function getHostSnapshot() {
|
|
172
|
+
const hosts = {};
|
|
173
|
+
for (const [hostId, online] of hostOnlineStatus) {
|
|
174
|
+
hosts[String(hostId)] = { online };
|
|
175
|
+
}
|
|
176
|
+
return { hosts };
|
|
177
|
+
}
|
|
178
|
+
//# sourceMappingURL=agentHostStatusService.js.map
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { AgentConfigFileSchema, calculatePeriodBoundaries, } from "@naisys/common";
|
|
2
|
+
import { hubDb } from "../database/hubDb.js";
|
|
3
|
+
import { updateAgentEnabledStatus, updateAgentHostAssignments, updateCostSuspendedAgents, } from "./agentHostStatusService.js";
|
|
4
|
+
export async function getAgents(updatedSince) {
|
|
5
|
+
const users = await hubDb.users.findMany({
|
|
6
|
+
select: {
|
|
7
|
+
id: true,
|
|
8
|
+
uuid: true,
|
|
9
|
+
username: true,
|
|
10
|
+
title: true,
|
|
11
|
+
enabled: true,
|
|
12
|
+
archived: true,
|
|
13
|
+
config: true,
|
|
14
|
+
lead_user: { select: { username: true } },
|
|
15
|
+
user_hosts: { select: { host_id: true } },
|
|
16
|
+
user_notifications: {
|
|
17
|
+
select: {
|
|
18
|
+
latest_log_id: true,
|
|
19
|
+
latest_mail_id: true,
|
|
20
|
+
last_active: true,
|
|
21
|
+
cost_suspended_reason: true,
|
|
22
|
+
budget_left: true,
|
|
23
|
+
updated_at: true,
|
|
24
|
+
host: { select: { name: true } },
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
where: updatedSince
|
|
29
|
+
? {
|
|
30
|
+
user_notifications: {
|
|
31
|
+
updated_at: { gte: new Date(updatedSince) },
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
: undefined,
|
|
35
|
+
});
|
|
36
|
+
const agents = users.map((user) => ({
|
|
37
|
+
id: user.id,
|
|
38
|
+
uuid: user.uuid,
|
|
39
|
+
name: user.username,
|
|
40
|
+
title: user.title,
|
|
41
|
+
host: user.user_notifications?.host?.name ?? "",
|
|
42
|
+
lastActive: user.user_notifications?.last_active?.toISOString(),
|
|
43
|
+
leadUsername: user.lead_user?.username || undefined,
|
|
44
|
+
latestLogId: user.user_notifications?.latest_log_id ?? 0,
|
|
45
|
+
latestMailId: user.user_notifications?.latest_mail_id ?? 0,
|
|
46
|
+
enabled: user.enabled,
|
|
47
|
+
archived: user.archived,
|
|
48
|
+
budgetLeft: user.user_notifications?.budget_left != null
|
|
49
|
+
? Number(user.user_notifications.budget_left)
|
|
50
|
+
: undefined,
|
|
51
|
+
config: parseConfig(user.config),
|
|
52
|
+
}));
|
|
53
|
+
updateAgentHostAssignments(users.map((user) => ({
|
|
54
|
+
agentId: user.id,
|
|
55
|
+
hostIds: user.user_hosts.map((uh) => uh.host_id),
|
|
56
|
+
})));
|
|
57
|
+
updateCostSuspendedAgents(users.map((user) => ({
|
|
58
|
+
agentId: user.id,
|
|
59
|
+
isSuspended: !!user.user_notifications?.cost_suspended_reason,
|
|
60
|
+
})));
|
|
61
|
+
updateAgentEnabledStatus(users.map((user) => ({
|
|
62
|
+
agentId: user.id,
|
|
63
|
+
enabled: user.enabled,
|
|
64
|
+
})));
|
|
65
|
+
return agents;
|
|
66
|
+
}
|
|
67
|
+
function parseConfig(config) {
|
|
68
|
+
if (!config)
|
|
69
|
+
return null;
|
|
70
|
+
try {
|
|
71
|
+
return AgentConfigFileSchema.parse(JSON.parse(config));
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// In-memory bidirectional lookup, refreshed by refreshUserLookup()
|
|
78
|
+
const idToUsername = new Map();
|
|
79
|
+
const usernameToId = new Map();
|
|
80
|
+
export function resolveAgentId(username) {
|
|
81
|
+
return usernameToId.get(username);
|
|
82
|
+
}
|
|
83
|
+
export function resolveUsername(userId) {
|
|
84
|
+
return idToUsername.get(userId);
|
|
85
|
+
}
|
|
86
|
+
export async function refreshUserLookup() {
|
|
87
|
+
const users = await hubDb.users.findMany({
|
|
88
|
+
select: { id: true, username: true },
|
|
89
|
+
});
|
|
90
|
+
idToUsername.clear();
|
|
91
|
+
usernameToId.clear();
|
|
92
|
+
for (const user of users) {
|
|
93
|
+
idToUsername.set(user.id, user.username);
|
|
94
|
+
usernameToId.set(user.username, user.id);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
export async function getHubAgentById(id) {
|
|
98
|
+
return hubDb.users.findUnique({
|
|
99
|
+
where: { id },
|
|
100
|
+
select: { id: true, uuid: true, username: true },
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
export async function getHubAgentByUuid(uuid) {
|
|
104
|
+
return hubDb.users.findFirst({
|
|
105
|
+
where: { uuid },
|
|
106
|
+
select: { id: true, username: true },
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
export async function getAgent(id) {
|
|
110
|
+
const user = await hubDb.users.findUnique({
|
|
111
|
+
where: { id },
|
|
112
|
+
select: {
|
|
113
|
+
id: true,
|
|
114
|
+
username: true,
|
|
115
|
+
title: true,
|
|
116
|
+
enabled: true,
|
|
117
|
+
archived: true,
|
|
118
|
+
config: true,
|
|
119
|
+
lead_user: { select: { username: true } },
|
|
120
|
+
user_hosts: {
|
|
121
|
+
select: {
|
|
122
|
+
host_id: true,
|
|
123
|
+
host: { select: { id: true, name: true } },
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
user_notifications: {
|
|
127
|
+
select: {
|
|
128
|
+
latest_log_id: true,
|
|
129
|
+
latest_mail_id: true,
|
|
130
|
+
last_active: true,
|
|
131
|
+
cost_suspended_reason: true,
|
|
132
|
+
spend_limit_reset_at: true,
|
|
133
|
+
updated_at: true,
|
|
134
|
+
host: { select: { name: true } },
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
if (!user) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
updateAgentHostAssignments([
|
|
143
|
+
{
|
|
144
|
+
agentId: user.id,
|
|
145
|
+
hostIds: user.user_hosts.map((uh) => uh.host_id),
|
|
146
|
+
},
|
|
147
|
+
]);
|
|
148
|
+
const config = parseConfig(user.config);
|
|
149
|
+
const currentSpend = await getAgentCurrentSpend(user.id, config.spendLimitDollars, config.spendLimitHours, user.user_notifications?.spend_limit_reset_at ?? null);
|
|
150
|
+
return {
|
|
151
|
+
id: user.id,
|
|
152
|
+
name: user.username,
|
|
153
|
+
title: user.title,
|
|
154
|
+
host: user.user_notifications?.host?.name ?? "",
|
|
155
|
+
lastActive: user.user_notifications?.last_active?.toISOString(),
|
|
156
|
+
leadUsername: user.lead_user?.username || undefined,
|
|
157
|
+
latestLogId: user.user_notifications?.latest_log_id ?? 0,
|
|
158
|
+
latestMailId: user.user_notifications?.latest_mail_id ?? 0,
|
|
159
|
+
enabled: user.enabled,
|
|
160
|
+
archived: user.archived,
|
|
161
|
+
costSuspendedReason: user.user_notifications?.cost_suspended_reason ?? undefined,
|
|
162
|
+
currentSpend,
|
|
163
|
+
spendLimitResetAt: user.user_notifications?.spend_limit_reset_at?.toISOString() ?? undefined,
|
|
164
|
+
config,
|
|
165
|
+
assignedHosts: user.user_hosts.map((uh) => ({
|
|
166
|
+
id: uh.host.id,
|
|
167
|
+
name: uh.host.name,
|
|
168
|
+
})),
|
|
169
|
+
_links: [],
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
export async function enableAgent(id) {
|
|
173
|
+
await hubDb.users.update({
|
|
174
|
+
where: { id },
|
|
175
|
+
data: { enabled: true },
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
export async function disableAgent(id) {
|
|
179
|
+
await hubDb.users.update({
|
|
180
|
+
where: { id },
|
|
181
|
+
data: { enabled: false },
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
export async function archiveAgent(id) {
|
|
185
|
+
await hubDb.users.update({
|
|
186
|
+
where: { id },
|
|
187
|
+
data: { archived: true },
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
export async function unarchiveAgent(id) {
|
|
191
|
+
await hubDb.users.update({
|
|
192
|
+
where: { id },
|
|
193
|
+
data: { archived: false },
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
export async function updateLeadAgent(id, leadUsername) {
|
|
197
|
+
const agent = await hubDb.users.findUnique({
|
|
198
|
+
where: { id },
|
|
199
|
+
select: { id: true },
|
|
200
|
+
});
|
|
201
|
+
if (!agent) {
|
|
202
|
+
throw new Error(`Agent with ID ${id} not found`);
|
|
203
|
+
}
|
|
204
|
+
let leadUserId = null;
|
|
205
|
+
if (leadUsername) {
|
|
206
|
+
const leadAgent = await hubDb.users.findUnique({
|
|
207
|
+
where: { username: leadUsername },
|
|
208
|
+
select: { id: true },
|
|
209
|
+
});
|
|
210
|
+
if (!leadAgent) {
|
|
211
|
+
throw new Error(`Lead agent '${leadUsername}' not found`);
|
|
212
|
+
}
|
|
213
|
+
leadUserId = leadAgent.id;
|
|
214
|
+
}
|
|
215
|
+
await hubDb.users.update({
|
|
216
|
+
where: { id },
|
|
217
|
+
data: { lead_user_id: leadUserId },
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
async function getAgentCurrentSpend(userId, spendLimitDollars, spendLimitHours, spendLimitResetAt) {
|
|
221
|
+
if (spendLimitDollars === undefined)
|
|
222
|
+
return undefined;
|
|
223
|
+
const where = { user_id: userId };
|
|
224
|
+
let effectiveStart;
|
|
225
|
+
if (spendLimitHours !== undefined) {
|
|
226
|
+
const { periodStart } = calculatePeriodBoundaries(spendLimitHours);
|
|
227
|
+
effectiveStart = periodStart;
|
|
228
|
+
}
|
|
229
|
+
if (spendLimitResetAt &&
|
|
230
|
+
(!effectiveStart || spendLimitResetAt > effectiveStart)) {
|
|
231
|
+
effectiveStart = spendLimitResetAt;
|
|
232
|
+
}
|
|
233
|
+
if (effectiveStart) {
|
|
234
|
+
where.created_at = { gte: effectiveStart };
|
|
235
|
+
}
|
|
236
|
+
const result = await hubDb.costs.aggregate({
|
|
237
|
+
where,
|
|
238
|
+
_sum: { cost: true },
|
|
239
|
+
});
|
|
240
|
+
return Math.round((result._sum.cost ?? 0) * 100) / 100;
|
|
241
|
+
}
|
|
242
|
+
export async function resetAgentSpend(id) {
|
|
243
|
+
await hubDb.user_notifications.updateMany({
|
|
244
|
+
where: { user_id: id },
|
|
245
|
+
data: {
|
|
246
|
+
spend_limit_reset_at: new Date(),
|
|
247
|
+
cost_suspended_reason: null,
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
export async function deleteAgent(id) {
|
|
252
|
+
const user = await hubDb.users.findUnique({
|
|
253
|
+
where: { id },
|
|
254
|
+
select: { id: true },
|
|
255
|
+
});
|
|
256
|
+
if (!user) {
|
|
257
|
+
throw new Error(`Agent with ID ${id} not found`);
|
|
258
|
+
}
|
|
259
|
+
await hubDb.$transaction(async (hubTx) => {
|
|
260
|
+
await hubTx.context_log.deleteMany({ where: { user_id: id } });
|
|
261
|
+
await hubTx.costs.deleteMany({ where: { user_id: id } });
|
|
262
|
+
await hubTx.run_session.deleteMany({ where: { user_id: id } });
|
|
263
|
+
// Get sent message IDs so we can delete their attachments and recipients
|
|
264
|
+
const sentMessages = await hubTx.mail_messages.findMany({
|
|
265
|
+
where: { from_user_id: id },
|
|
266
|
+
select: { id: true },
|
|
267
|
+
});
|
|
268
|
+
const sentMessageIds = sentMessages.map((m) => m.id);
|
|
269
|
+
if (sentMessageIds.length > 0) {
|
|
270
|
+
await hubTx.mail_attachments.deleteMany({
|
|
271
|
+
where: { message_id: { in: sentMessageIds } },
|
|
272
|
+
});
|
|
273
|
+
await hubTx.mail_recipients.deleteMany({
|
|
274
|
+
where: { message_id: { in: sentMessageIds } },
|
|
275
|
+
});
|
|
276
|
+
await hubTx.mail_messages.deleteMany({
|
|
277
|
+
where: { id: { in: sentMessageIds } },
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
// Delete remaining recipient entries (for messages received by this agent)
|
|
281
|
+
await hubTx.mail_recipients.deleteMany({ where: { user_id: id } });
|
|
282
|
+
await hubTx.user_notifications.deleteMany({ where: { user_id: id } });
|
|
283
|
+
await hubTx.user_hosts.deleteMany({ where: { user_id: id } });
|
|
284
|
+
await hubTx.users.updateMany({
|
|
285
|
+
where: { lead_user_id: id },
|
|
286
|
+
data: { lead_user_id: null },
|
|
287
|
+
});
|
|
288
|
+
await hubTx.users.delete({ where: { id } });
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
//# sourceMappingURL=agentService.js.map
|