apteva 0.4.12 → 0.4.14

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/src/openapi.ts CHANGED
@@ -162,6 +162,110 @@ The new access token will be returned and a new refresh token cookie will be set
162
162
  },
163
163
  },
164
164
  },
165
+ "/version/update": {
166
+ post: {
167
+ tags: ["System"],
168
+ summary: "Update agent binary",
169
+ description: "Downloads the latest agent binary. Stops all running agents, updates, then restarts them.",
170
+ responses: {
171
+ "200": {
172
+ description: "Update result",
173
+ content: {
174
+ "application/json": {
175
+ schema: {
176
+ type: "object",
177
+ properties: {
178
+ success: { type: "boolean" },
179
+ version: { type: "string" },
180
+ restarted: {
181
+ type: "array",
182
+ items: {
183
+ type: "object",
184
+ properties: {
185
+ id: { type: "string" },
186
+ name: { type: "string" },
187
+ success: { type: "boolean" },
188
+ error: { type: "string", nullable: true },
189
+ },
190
+ },
191
+ },
192
+ },
193
+ },
194
+ },
195
+ },
196
+ },
197
+ "500": { description: "Update failed" },
198
+ },
199
+ },
200
+ },
201
+ "/features": {
202
+ get: {
203
+ tags: ["System"],
204
+ summary: "Get feature flags",
205
+ security: [],
206
+ responses: {
207
+ "200": {
208
+ description: "Feature flags",
209
+ content: {
210
+ "application/json": {
211
+ schema: {
212
+ type: "object",
213
+ properties: {
214
+ projects: { type: "boolean" },
215
+ metaAgent: { type: "boolean" },
216
+ },
217
+ },
218
+ },
219
+ },
220
+ },
221
+ },
222
+ },
223
+ },
224
+ "/stats": {
225
+ get: {
226
+ tags: ["System"],
227
+ summary: "Get agent statistics",
228
+ responses: {
229
+ "200": {
230
+ description: "Agent statistics",
231
+ content: {
232
+ "application/json": {
233
+ schema: {
234
+ type: "object",
235
+ properties: {
236
+ totalAgents: { type: "integer" },
237
+ runningAgents: { type: "integer" },
238
+ },
239
+ },
240
+ },
241
+ },
242
+ },
243
+ },
244
+ },
245
+ },
246
+ "/binary": {
247
+ get: {
248
+ tags: ["System"],
249
+ summary: "Get binary status",
250
+ responses: {
251
+ "200": {
252
+ description: "Binary availability info",
253
+ content: {
254
+ "application/json": {
255
+ schema: {
256
+ type: "object",
257
+ properties: {
258
+ exists: { type: "boolean" },
259
+ platform: { type: "string" },
260
+ arch: { type: "string" },
261
+ },
262
+ },
263
+ },
264
+ },
265
+ },
266
+ },
267
+ },
268
+ },
165
269
  "/dashboard": {
166
270
  get: {
167
271
  tags: ["System"],
@@ -546,12 +650,39 @@ while (true) {
546
650
  },
547
651
  },
548
652
  },
653
+ "/agents/{agentId}/threads/{threadId}/messages": {
654
+ get: {
655
+ tags: ["Threads"],
656
+ summary: "List thread messages",
657
+ description: "Get all messages in a thread. Agent must be running.",
658
+ parameters: [
659
+ { name: "agentId", in: "path", required: true, schema: { type: "string" } },
660
+ { name: "threadId", in: "path", required: true, schema: { type: "string" } },
661
+ ],
662
+ responses: {
663
+ "200": {
664
+ description: "List of messages",
665
+ content: {
666
+ "application/json": {
667
+ schema: {
668
+ type: "array",
669
+ items: { $ref: "#/components/schemas/Message" },
670
+ },
671
+ },
672
+ },
673
+ },
674
+ "400": { description: "Agent is not running" },
675
+ "404": { description: "Agent not found" },
676
+ },
677
+ },
678
+ },
549
679
  "/agents/{agentId}/memories": {
550
680
  get: {
551
681
  tags: ["Memory"],
552
682
  summary: "List agent memories",
553
683
  parameters: [
554
684
  { name: "agentId", in: "path", required: true, schema: { type: "string" } },
685
+ { name: "thread_id", in: "query", schema: { type: "string" }, description: "Filter memories by thread ID" },
555
686
  ],
556
687
  responses: {
557
688
  "200": {
@@ -592,6 +723,17 @@ while (true) {
592
723
  "201": { description: "Memory added" },
593
724
  },
594
725
  },
726
+ delete: {
727
+ tags: ["Memory"],
728
+ summary: "Clear all memories",
729
+ description: "Deletes all memories for the agent.",
730
+ parameters: [
731
+ { name: "agentId", in: "path", required: true, schema: { type: "string" } },
732
+ ],
733
+ responses: {
734
+ "200": { description: "All memories cleared" },
735
+ },
736
+ },
595
737
  },
596
738
  "/agents/{agentId}/memories/{memoryId}": {
597
739
  delete: {
@@ -612,6 +754,8 @@ while (true) {
612
754
  summary: "List agent files",
613
755
  parameters: [
614
756
  { name: "agentId", in: "path", required: true, schema: { type: "string" } },
757
+ { name: "thread_id", in: "query", schema: { type: "string" }, description: "Filter files by thread ID" },
758
+ { name: "limit", in: "query", schema: { type: "integer" }, description: "Max files to return" },
615
759
  ],
616
760
  responses: {
617
761
  "200": {
@@ -706,6 +850,7 @@ while (true) {
706
850
  summary: "List agent tasks",
707
851
  parameters: [
708
852
  { name: "agentId", in: "path", required: true, schema: { type: "string" } },
853
+ { name: "status", in: "query", schema: { type: "string", enum: ["all", "pending", "running", "completed", "failed", "cancelled"], default: "all" }, description: "Filter tasks by status" },
709
854
  ],
710
855
  responses: {
711
856
  "200": {
@@ -722,6 +867,58 @@ while (true) {
722
867
  },
723
868
  },
724
869
  },
870
+ "/agents/{agentId}/api-key": {
871
+ get: {
872
+ tags: ["Agents"],
873
+ summary: "Get agent API key (masked)",
874
+ description: "Returns the agent's API key with most characters masked for security.",
875
+ parameters: [
876
+ { name: "agentId", in: "path", required: true, schema: { type: "string" } },
877
+ ],
878
+ responses: {
879
+ "200": {
880
+ description: "Masked API key",
881
+ content: {
882
+ "application/json": {
883
+ schema: {
884
+ type: "object",
885
+ properties: {
886
+ apiKey: { type: "string", description: "Masked API key (first 8 chars + last 4)" },
887
+ hasKey: { type: "boolean" },
888
+ },
889
+ },
890
+ },
891
+ },
892
+ },
893
+ "404": { description: "Agent or key not found" },
894
+ },
895
+ },
896
+ post: {
897
+ tags: ["Agents"],
898
+ summary: "Regenerate agent API key",
899
+ description: "Generates a new API key for the agent. The full key is only shown once in the response.",
900
+ parameters: [
901
+ { name: "agentId", in: "path", required: true, schema: { type: "string" } },
902
+ ],
903
+ responses: {
904
+ "200": {
905
+ description: "New API key (only time full key is visible)",
906
+ content: {
907
+ "application/json": {
908
+ schema: {
909
+ type: "object",
910
+ properties: {
911
+ apiKey: { type: "string", description: "Full new API key" },
912
+ message: { type: "string" },
913
+ },
914
+ },
915
+ },
916
+ },
917
+ },
918
+ "404": { description: "Agent not found" },
919
+ },
920
+ },
921
+ },
725
922
  "/agents/{agentId}/peers": {
726
923
  get: {
727
924
  tags: ["Agents"],
@@ -744,18 +941,64 @@ while (true) {
744
941
  },
745
942
  },
746
943
  },
944
+ "/discovery/agents": {
945
+ get: {
946
+ tags: ["Agents"],
947
+ summary: "Central agent discovery",
948
+ description: "Discovery endpoint for agents to find running peers in the same group.",
949
+ parameters: [
950
+ { name: "group", in: "query", schema: { type: "string" }, description: "Filter by agent group" },
951
+ { name: "exclude", in: "query", schema: { type: "string" }, description: "Agent ID to exclude from results" },
952
+ ],
953
+ responses: {
954
+ "200": {
955
+ description: "List of discoverable agents",
956
+ content: {
957
+ "application/json": {
958
+ schema: {
959
+ type: "object",
960
+ properties: {
961
+ agents: {
962
+ type: "array",
963
+ items: {
964
+ type: "object",
965
+ properties: {
966
+ id: { type: "string" },
967
+ name: { type: "string" },
968
+ url: { type: "string" },
969
+ mode: { type: "string" },
970
+ group: { type: "string", nullable: true },
971
+ },
972
+ },
973
+ },
974
+ },
975
+ },
976
+ },
977
+ },
978
+ },
979
+ },
980
+ },
981
+ },
747
982
  "/tasks": {
748
983
  get: {
749
984
  tags: ["Tasks"],
750
985
  summary: "List all tasks",
986
+ description: "Fetches tasks from all running agents. Supports filtering by status and project.",
987
+ parameters: [
988
+ { name: "status", in: "query", schema: { type: "string", enum: ["all", "pending", "running", "completed", "failed", "cancelled"], default: "all" }, description: "Filter tasks by status" },
989
+ { name: "project_id", in: "query", schema: { type: "string" }, description: "Filter by project ID. Use 'unassigned' for agents without a project." },
990
+ ],
751
991
  responses: {
752
992
  "200": {
753
993
  description: "List of tasks",
754
994
  content: {
755
995
  "application/json": {
756
996
  schema: {
757
- type: "array",
758
- items: { $ref: "#/components/schemas/Task" },
997
+ type: "object",
998
+ properties: {
999
+ tasks: { type: "array", items: { $ref: "#/components/schemas/Task" } },
1000
+ count: { type: "integer" },
1001
+ },
759
1002
  },
760
1003
  },
761
1004
  },
@@ -763,6 +1006,34 @@ while (true) {
763
1006
  },
764
1007
  },
765
1008
  },
1009
+ "/tasks/{agentId}/{taskId}": {
1010
+ get: {
1011
+ tags: ["Tasks"],
1012
+ summary: "Get a single task",
1013
+ description: "Get full details for a specific task from a specific agent, including execution trajectory.",
1014
+ parameters: [
1015
+ { name: "agentId", in: "path", required: true, schema: { type: "string" }, description: "Agent ID that owns the task" },
1016
+ { name: "taskId", in: "path", required: true, schema: { type: "string" }, description: "Task ID" },
1017
+ ],
1018
+ responses: {
1019
+ "200": {
1020
+ description: "Task details with trajectory",
1021
+ content: {
1022
+ "application/json": {
1023
+ schema: {
1024
+ type: "object",
1025
+ properties: {
1026
+ task: { $ref: "#/components/schemas/TaskDetail" },
1027
+ },
1028
+ },
1029
+ },
1030
+ },
1031
+ },
1032
+ "400": { description: "Agent is not running" },
1033
+ "404": { description: "Agent not found" },
1034
+ },
1035
+ },
1036
+ },
766
1037
  "/mcp/servers": {
767
1038
  get: {
768
1039
  tags: ["MCP"],
@@ -1118,13 +1389,29 @@ Events are batched and sent periodically. Debug-level events are filtered out.`,
1118
1389
  description: `Server-Sent Events stream for real-time telemetry updates.
1119
1390
 
1120
1391
  Connect to this endpoint to receive live telemetry events as they are received from agents.
1392
+ Events are broadcast immediately when agents send them, enabling real-time activity feeds.
1121
1393
 
1122
- **Example:**
1394
+ **Initial Connection:**
1395
+ On connect, you'll receive: \`{"connected":true}\`
1396
+
1397
+ **Event Format:**
1398
+ Each event is a JSON object matching the TelemetryEvent schema.
1399
+
1400
+ **Example - React hook for real-time activity:**
1123
1401
  \`\`\`javascript
1124
1402
  const eventSource = new EventSource('/api/telemetry/stream');
1403
+
1125
1404
  eventSource.onmessage = (event) => {
1126
1405
  const data = JSON.parse(event.data);
1127
- console.log('Telemetry:', data);
1406
+ if (data.type === 'thread_activity') {
1407
+ // Update activity feed
1408
+ console.log(\`[\${data.agent_id}] \${data.data?.activity}\`);
1409
+ }
1410
+ };
1411
+
1412
+ eventSource.onerror = () => {
1413
+ // Reconnect logic
1414
+ eventSource.close();
1128
1415
  };
1129
1416
  \`\`\``,
1130
1417
  responses: {
@@ -1146,12 +1433,27 @@ eventSource.onmessage = (event) => {
1146
1433
  get: {
1147
1434
  tags: ["Telemetry"],
1148
1435
  summary: "Query telemetry events",
1149
- description: "Query stored telemetry events with optional filters.",
1436
+ description: `Query stored telemetry events with optional filters.
1437
+
1438
+ **Common Event Types:**
1439
+ - \`thread_activity\` - Agent activity updates (used for dashboard activity feed)
1440
+ - \`llm_request\` / \`llm_response\` - LLM API calls
1441
+ - \`tool_call\` / \`tool_result\` - Tool executions
1442
+ - \`memory_store\` / \`memory_recall\` - Memory operations
1443
+ - \`task_start\` / \`task_complete\` - Task lifecycle
1444
+ - \`agent_start\` / \`agent_stop\` - Agent lifecycle
1445
+ - \`error\` - Error events
1446
+
1447
+ **Example - Get recent activity for dashboard:**
1448
+ \`\`\`
1449
+ GET /api/telemetry/events?type=thread_activity&limit=20
1450
+ \`\`\``,
1150
1451
  parameters: [
1151
1452
  { name: "agent_id", in: "query", schema: { type: "string" }, description: "Filter by agent ID" },
1152
1453
  { name: "project_id", in: "query", schema: { type: "string" }, description: "Filter by project ID (use 'null' for unassigned)" },
1153
- { name: "category", in: "query", schema: { type: "string" }, description: "Filter by category (llm, tool, memory, etc.)" },
1154
- { name: "level", in: "query", schema: { type: "string" }, description: "Filter by level (info, warn, error)" },
1454
+ { name: "category", in: "query", schema: { type: "string", enum: ["llm", "tool", "memory", "task", "agent", "mcp", "system"] }, description: "Filter by category" },
1455
+ { name: "type", in: "query", schema: { type: "string" }, description: "Filter by event type (e.g., thread_activity, llm_request, tool_call)" },
1456
+ { name: "level", in: "query", schema: { type: "string", enum: ["info", "warn", "error"] }, description: "Filter by level" },
1155
1457
  { name: "trace_id", in: "query", schema: { type: "string" }, description: "Filter by trace ID" },
1156
1458
  { name: "since", in: "query", schema: { type: "string", format: "date-time" }, description: "Events after this timestamp" },
1157
1459
  { name: "until", in: "query", schema: { type: "string", format: "date-time" }, description: "Events before this timestamp" },
@@ -1640,6 +1942,16 @@ eventSource.onmessage = (event) => {
1640
1942
  createdAt: { type: "string", format: "date-time" },
1641
1943
  },
1642
1944
  },
1945
+ Message: {
1946
+ type: "object",
1947
+ properties: {
1948
+ id: { type: "string" },
1949
+ role: { type: "string", enum: ["user", "assistant", "system"] },
1950
+ content: { type: "string" },
1951
+ threadId: { type: "string" },
1952
+ createdAt: { type: "string", format: "date-time" },
1953
+ },
1954
+ },
1643
1955
  Task: {
1644
1956
  type: "object",
1645
1957
  properties: {
@@ -1650,11 +1962,75 @@ eventSource.onmessage = (event) => {
1650
1962
  status: { type: "string", enum: ["pending", "running", "completed", "failed", "cancelled"] },
1651
1963
  priority: { type: "integer" },
1652
1964
  source: { type: "string", enum: ["local", "delegated"] },
1653
- createdAt: { type: "string", format: "date-time" },
1654
- executeAt: { type: "string", format: "date-time" },
1655
- executedAt: { type: "string", format: "date-time" },
1965
+ created_at: { type: "string", format: "date-time" },
1966
+ execute_at: { type: "string", format: "date-time", nullable: true },
1967
+ executed_at: { type: "string", format: "date-time", nullable: true },
1968
+ completed_at: { type: "string", format: "date-time", nullable: true },
1969
+ recurrence: { type: "string", nullable: true, description: "Cron expression for recurring tasks" },
1970
+ next_run: { type: "string", format: "date-time", nullable: true, description: "Next scheduled run for recurring tasks" },
1971
+ result: { type: "object", nullable: true, description: "Task result data (present when completed)" },
1972
+ error: { type: "string", nullable: true, description: "Error message (present when failed)" },
1973
+ agentId: { type: "string", description: "ID of the agent that owns this task" },
1974
+ agentName: { type: "string", description: "Name of the agent that owns this task" },
1975
+ },
1976
+ },
1977
+ TaskDetail: {
1978
+ type: "object",
1979
+ description: "Full task detail including trajectory (returned by single task endpoint)",
1980
+ properties: {
1981
+ id: { type: "string" },
1982
+ title: { type: "string" },
1983
+ description: { type: "string" },
1984
+ type: { type: "string", enum: ["once", "recurring"] },
1985
+ status: { type: "string", enum: ["pending", "running", "completed", "failed", "cancelled"] },
1986
+ priority: { type: "integer" },
1987
+ source: { type: "string", enum: ["local", "delegated"] },
1988
+ created_at: { type: "string", format: "date-time" },
1989
+ execute_at: { type: "string", format: "date-time", nullable: true },
1990
+ executed_at: { type: "string", format: "date-time", nullable: true },
1991
+ completed_at: { type: "string", format: "date-time", nullable: true },
1992
+ recurrence: { type: "string", nullable: true },
1993
+ next_run: { type: "string", format: "date-time", nullable: true },
1994
+ result: { type: "object", nullable: true },
1995
+ error: { type: "string", nullable: true },
1656
1996
  agentId: { type: "string" },
1657
1997
  agentName: { type: "string" },
1998
+ trajectory: {
1999
+ type: "array",
2000
+ nullable: true,
2001
+ description: "Step-by-step execution trajectory of the task",
2002
+ items: { $ref: "#/components/schemas/TaskTrajectoryStep" },
2003
+ },
2004
+ },
2005
+ },
2006
+ TaskTrajectoryStep: {
2007
+ type: "object",
2008
+ properties: {
2009
+ id: { type: "string" },
2010
+ role: { type: "string", enum: ["user", "assistant"] },
2011
+ content: {
2012
+ description: "Text content or array of tool use/result blocks",
2013
+ oneOf: [
2014
+ { type: "string" },
2015
+ {
2016
+ type: "array",
2017
+ items: {
2018
+ type: "object",
2019
+ properties: {
2020
+ type: { type: "string", enum: ["tool_use", "tool_result"] },
2021
+ id: { type: "string" },
2022
+ name: { type: "string", description: "Tool name (for tool_use)" },
2023
+ input: { type: "object", description: "Tool input (for tool_use)" },
2024
+ tool_use_id: { type: "string", description: "Referenced tool_use ID (for tool_result)" },
2025
+ content: { type: "string", description: "Result text (for tool_result)" },
2026
+ is_error: { type: "boolean", description: "Whether the tool errored (for tool_result)" },
2027
+ },
2028
+ },
2029
+ },
2030
+ ],
2031
+ },
2032
+ created_at: { type: "string", format: "date-time" },
2033
+ model: { type: "string", nullable: true },
1658
2034
  },
1659
2035
  },
1660
2036
  Skill: {
@@ -1702,6 +2078,12 @@ eventSource.onmessage = (event) => {
1702
2078
  source: { type: "string", nullable: true },
1703
2079
  },
1704
2080
  },
2081
+ McpServerResponse: {
2082
+ type: "object",
2083
+ properties: {
2084
+ server: { $ref: "#/components/schemas/McpServer" },
2085
+ },
2086
+ },
1705
2087
  McpServerListResponse: {
1706
2088
  type: "object",
1707
2089
  properties: {
@@ -1847,18 +2229,31 @@ eventSource.onmessage = (event) => {
1847
2229
  },
1848
2230
  TelemetryEvent: {
1849
2231
  type: "object",
2232
+ description: `A telemetry event from an agent. Events capture LLM calls, tool usage, memory operations, and agent activity.
2233
+
2234
+ **Common type values by category:**
2235
+ - **llm**: request, response, error
2236
+ - **tool**: call, result, error
2237
+ - **memory**: store, recall, consolidate
2238
+ - **task**: start, complete, fail
2239
+ - **agent**: start, stop, thread_activity
2240
+ - **system**: startup, shutdown, error`,
1850
2241
  properties: {
1851
- id: { type: "string" },
1852
- agent_id: { type: "string" },
1853
- timestamp: { type: "string", format: "date-time" },
1854
- category: { type: "string" },
1855
- type: { type: "string" },
1856
- level: { type: "string" },
1857
- trace_id: { type: "string", nullable: true },
1858
- thread_id: { type: "string", nullable: true },
1859
- data: { type: "object", nullable: true },
1860
- duration_ms: { type: "integer", nullable: true },
1861
- error: { type: "string", nullable: true },
2242
+ id: { type: "string", description: "Unique event ID" },
2243
+ agent_id: { type: "string", description: "ID of the agent that generated this event" },
2244
+ timestamp: { type: "string", format: "date-time", description: "When the event occurred" },
2245
+ category: { type: "string", enum: ["llm", "tool", "memory", "task", "agent", "mcp", "system"], description: "Event category" },
2246
+ type: { type: "string", description: "Event type within category (e.g., 'thread_activity', 'request', 'call')" },
2247
+ level: { type: "string", enum: ["info", "warn", "error"], description: "Severity level" },
2248
+ trace_id: { type: "string", nullable: true, description: "Trace ID for correlating related events" },
2249
+ thread_id: { type: "string", nullable: true, description: "Conversation thread ID" },
2250
+ data: {
2251
+ type: "object",
2252
+ nullable: true,
2253
+ description: "Event-specific data. For thread_activity: { activity: string }. For llm: { model, tokens, prompt_tokens, completion_tokens }. For tool: { tool_name, input, output }."
2254
+ },
2255
+ duration_ms: { type: "integer", nullable: true, description: "Duration in milliseconds (for timed operations)" },
2256
+ error: { type: "string", nullable: true, description: "Error message if this is an error event" },
1862
2257
  },
1863
2258
  },
1864
2259
  TelemetryUsage: {
@@ -503,8 +503,8 @@ export async function startAgentProcess(
503
503
  const proc = spawn({
504
504
  cmd: [binaryPath],
505
505
  env,
506
- stdout: "inherit",
507
- stderr: "inherit",
506
+ stdout: "ignore",
507
+ stderr: "ignore",
508
508
  });
509
509
 
510
510
  // Store process with port for tracking
@@ -0,0 +1,95 @@
1
+ import type { AuthContext } from "../../auth/middleware";
2
+ import { ApiKeyDB } from "../../db";
3
+ import { json } from "./helpers";
4
+
5
+ const MAX_KEYS_PER_USER = 10;
6
+
7
+ export async function handleApiKeyRoutes(
8
+ req: Request,
9
+ path: string,
10
+ method: string,
11
+ authContext?: AuthContext,
12
+ ): Promise<Response | null> {
13
+ // All API key routes require authentication
14
+ if (!authContext?.isAuthenticated || !authContext.user) {
15
+ return null; // Let other handlers deal with it
16
+ }
17
+
18
+ const userId = authContext.user.id;
19
+
20
+ // POST /api/keys/personal - Create a new API key
21
+ if (path === "/api/keys/personal" && method === "POST") {
22
+ try {
23
+ const body = await req.json();
24
+ const { name, expires_in_days } = body;
25
+
26
+ if (!name || typeof name !== "string" || name.trim().length === 0) {
27
+ return json({ error: "Name is required" }, 400);
28
+ }
29
+
30
+ if (name.length > 100) {
31
+ return json({ error: "Name must be 100 characters or less" }, 400);
32
+ }
33
+
34
+ // Limit keys per user
35
+ const count = ApiKeyDB.countByUser(userId);
36
+ if (count >= MAX_KEYS_PER_USER) {
37
+ return json({ error: `Maximum ${MAX_KEYS_PER_USER} API keys allowed per user` }, 400);
38
+ }
39
+
40
+ // Calculate expiration
41
+ let expires_at: string | null = null;
42
+ if (expires_in_days && typeof expires_in_days === "number" && expires_in_days > 0) {
43
+ const expiry = new Date();
44
+ expiry.setDate(expiry.getDate() + expires_in_days);
45
+ expires_at = expiry.toISOString();
46
+ }
47
+
48
+ const { apiKey, rawKey } = ApiKeyDB.create({
49
+ name: name.trim(),
50
+ user_id: userId,
51
+ expires_at,
52
+ });
53
+
54
+ return json({
55
+ id: apiKey.id,
56
+ name: apiKey.name,
57
+ key: rawKey,
58
+ prefix: apiKey.key_prefix,
59
+ expires_at: apiKey.expires_at,
60
+ created_at: apiKey.created_at,
61
+ }, 201);
62
+ } catch (e: any) {
63
+ return json({ error: e.message || "Failed to create API key" }, 500);
64
+ }
65
+ }
66
+
67
+ // GET /api/keys/personal - List user's API keys
68
+ if (path === "/api/keys/personal" && method === "GET") {
69
+ const keys = ApiKeyDB.findByUser(userId);
70
+ return json({
71
+ keys: keys.map(k => ({
72
+ id: k.id,
73
+ name: k.name,
74
+ prefix: k.key_prefix,
75
+ is_active: k.is_active,
76
+ expires_at: k.expires_at,
77
+ last_used_at: k.last_used_at,
78
+ created_at: k.created_at,
79
+ })),
80
+ });
81
+ }
82
+
83
+ // DELETE /api/keys/personal/:id - Revoke/delete an API key
84
+ const deleteMatch = path.match(/^\/api\/keys\/personal\/([^/]+)$/);
85
+ if (deleteMatch && method === "DELETE") {
86
+ const keyId = deleteMatch[1];
87
+ const deleted = ApiKeyDB.delete(keyId, userId);
88
+ if (!deleted) {
89
+ return json({ error: "API key not found" }, 404);
90
+ }
91
+ return json({ success: true });
92
+ }
93
+
94
+ return null;
95
+ }
@@ -243,8 +243,8 @@ export async function handleMcpRoutes(
243
243
  return json({ error: "No command or package specified" }, 400);
244
244
  }
245
245
 
246
- // Get a port for the HTTP proxy
247
- const port = await getNextPort();
246
+ // Use permanently assigned port from DB, fallback to dynamic
247
+ const port = server.port || await getNextPort();
248
248
 
249
249
  console.log(`Starting MCP server ${server.name}...`);
250
250
  console.log(` Command: ${cmd.join(" ")}`);