@desplega.ai/agent-swarm 1.56.6 → 1.57.1
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/openapi.json +32 -1
- package/package.json +1 -1
- package/src/be/date-utils.ts +28 -0
- package/src/be/db-queries/oauth.ts +23 -4
- package/src/be/db-queries/tracker.ts +42 -15
- package/src/be/db.ts +70 -26
- package/src/be/migrations/029_task_credential_key_type.sql +2 -0
- package/src/be/migrations/030_iso8601_date_consistency.sql +108 -0
- package/src/http/api-keys.ts +32 -0
- package/src/tests/api-key-tracking.test.ts +6 -2
- package/src/tests/date-utils.test.ts +48 -0
- package/src/tests/heartbeat.test.ts +1 -1
- package/src/types.ts +4 -0
- package/src/utils/credentials.ts +3 -3
package/openapi.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"openapi": "3.1.0",
|
|
3
3
|
"info": {
|
|
4
4
|
"title": "Agent Swarm API",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.57.0",
|
|
6
6
|
"description": "Multi-agent orchestration API for Claude Code, Codex, and Gemini CLI. Enables task distribution, agent communication, and service discovery.\n\nMCP tools are documented separately in [MCP.md](./MCP.md)."
|
|
7
7
|
},
|
|
8
8
|
"servers": [
|
|
@@ -1646,6 +1646,37 @@
|
|
|
1646
1646
|
}
|
|
1647
1647
|
}
|
|
1648
1648
|
},
|
|
1649
|
+
"/api/keys/costs": {
|
|
1650
|
+
"get": {
|
|
1651
|
+
"summary": "Get aggregated cost data per API key",
|
|
1652
|
+
"tags": [
|
|
1653
|
+
"API Keys"
|
|
1654
|
+
],
|
|
1655
|
+
"security": [
|
|
1656
|
+
{
|
|
1657
|
+
"bearerAuth": []
|
|
1658
|
+
}
|
|
1659
|
+
],
|
|
1660
|
+
"parameters": [
|
|
1661
|
+
{
|
|
1662
|
+
"schema": {
|
|
1663
|
+
"type": "string"
|
|
1664
|
+
},
|
|
1665
|
+
"required": false,
|
|
1666
|
+
"name": "keyType",
|
|
1667
|
+
"in": "query"
|
|
1668
|
+
}
|
|
1669
|
+
],
|
|
1670
|
+
"responses": {
|
|
1671
|
+
"200": {
|
|
1672
|
+
"description": "Per-key cost aggregation"
|
|
1673
|
+
},
|
|
1674
|
+
"401": {
|
|
1675
|
+
"description": "Unauthorized"
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
},
|
|
1649
1680
|
"/api/events": {
|
|
1650
1681
|
"post": {
|
|
1651
1682
|
"summary": "Store a single event",
|
package/package.json
CHANGED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalizes date strings from SQLite to ISO 8601 UTC format.
|
|
3
|
+
*
|
|
4
|
+
* SQLite's `datetime('now')` and `CURRENT_TIMESTAMP` produce bare format
|
|
5
|
+
* `YYYY-MM-DD HH:MM:SS` which browsers parse as local time. This utility
|
|
6
|
+
* converts them to `YYYY-MM-DDTHH:MM:SS.000Z` so they're unambiguously UTC.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const BARE_DATETIME_RE = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/;
|
|
10
|
+
|
|
11
|
+
export function normalizeDate(date: string | null | undefined): string | null {
|
|
12
|
+
if (date == null) return null;
|
|
13
|
+
if (BARE_DATETIME_RE.test(date)) {
|
|
14
|
+
return `${date.replace(" ", "T")}.000Z`;
|
|
15
|
+
}
|
|
16
|
+
return date;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Non-null variant for required date fields.
|
|
21
|
+
* Returns the input unchanged if already ISO 8601, or converts bare format.
|
|
22
|
+
*/
|
|
23
|
+
export function normalizeDateRequired(date: string): string {
|
|
24
|
+
if (BARE_DATETIME_RE.test(date)) {
|
|
25
|
+
return `${date.replace(" ", "T")}.000Z`;
|
|
26
|
+
}
|
|
27
|
+
return date;
|
|
28
|
+
}
|
|
@@ -1,12 +1,30 @@
|
|
|
1
1
|
import type { OAuthApp, OAuthTokens } from "../../tracker/types";
|
|
2
|
+
import { normalizeDateRequired } from "../date-utils";
|
|
2
3
|
import { getDb } from "../db";
|
|
3
4
|
|
|
4
5
|
// ── OAuth Apps ──
|
|
5
6
|
|
|
7
|
+
function normalizeOAuthApp(row: OAuthApp): OAuthApp {
|
|
8
|
+
return {
|
|
9
|
+
...row,
|
|
10
|
+
createdAt: normalizeDateRequired(row.createdAt),
|
|
11
|
+
updatedAt: normalizeDateRequired(row.updatedAt),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function normalizeOAuthTokens(row: OAuthTokens): OAuthTokens {
|
|
16
|
+
return {
|
|
17
|
+
...row,
|
|
18
|
+
createdAt: normalizeDateRequired(row.createdAt),
|
|
19
|
+
updatedAt: normalizeDateRequired(row.updatedAt),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
6
23
|
export function getOAuthApp(provider: string): OAuthApp | null {
|
|
7
|
-
|
|
24
|
+
const row = getDb()
|
|
8
25
|
.query("SELECT * FROM oauth_apps WHERE provider = ?")
|
|
9
26
|
.get(provider) as OAuthApp | null;
|
|
27
|
+
return row ? normalizeOAuthApp(row) : null;
|
|
10
28
|
}
|
|
11
29
|
|
|
12
30
|
export function upsertOAuthApp(
|
|
@@ -33,7 +51,7 @@ export function upsertOAuthApp(
|
|
|
33
51
|
redirectUri = excluded.redirectUri,
|
|
34
52
|
scopes = excluded.scopes,
|
|
35
53
|
metadata = excluded.metadata,
|
|
36
|
-
updatedAt =
|
|
54
|
+
updatedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')`,
|
|
37
55
|
)
|
|
38
56
|
.run(
|
|
39
57
|
provider,
|
|
@@ -50,9 +68,10 @@ export function upsertOAuthApp(
|
|
|
50
68
|
// ── OAuth Tokens ──
|
|
51
69
|
|
|
52
70
|
export function getOAuthTokens(provider: string): OAuthTokens | null {
|
|
53
|
-
|
|
71
|
+
const row = getDb()
|
|
54
72
|
.query("SELECT * FROM oauth_tokens WHERE provider = ?")
|
|
55
73
|
.get(provider) as OAuthTokens | null;
|
|
74
|
+
return row ? normalizeOAuthTokens(row) : null;
|
|
56
75
|
}
|
|
57
76
|
|
|
58
77
|
export function storeOAuthTokens(
|
|
@@ -73,7 +92,7 @@ export function storeOAuthTokens(
|
|
|
73
92
|
refreshToken = COALESCE(excluded.refreshToken, oauth_tokens.refreshToken),
|
|
74
93
|
expiresAt = excluded.expiresAt,
|
|
75
94
|
scope = COALESCE(excluded.scope, oauth_tokens.scope),
|
|
76
|
-
updatedAt =
|
|
95
|
+
updatedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')`,
|
|
77
96
|
)
|
|
78
97
|
.run(provider, data.accessToken, data.refreshToken ?? null, data.expiresAt, data.scope ?? null);
|
|
79
98
|
}
|
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
import type { TrackerAgentMapping, TrackerSync } from "../../tracker/types";
|
|
2
|
+
import { normalizeDateRequired } from "../date-utils";
|
|
2
3
|
import { getDb } from "../db";
|
|
3
4
|
|
|
5
|
+
function normalizeTrackerSync(row: TrackerSync): TrackerSync {
|
|
6
|
+
return {
|
|
7
|
+
...row,
|
|
8
|
+
lastSyncedAt: normalizeDateRequired(row.lastSyncedAt),
|
|
9
|
+
createdAt: normalizeDateRequired(row.createdAt),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function normalizeTrackerAgentMapping(row: TrackerAgentMapping): TrackerAgentMapping {
|
|
14
|
+
return {
|
|
15
|
+
...row,
|
|
16
|
+
createdAt: normalizeDateRequired(row.createdAt),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
4
20
|
// ── Tracker Sync ──
|
|
5
21
|
|
|
6
22
|
export function getTrackerSync(
|
|
@@ -8,9 +24,10 @@ export function getTrackerSync(
|
|
|
8
24
|
entityType: "task",
|
|
9
25
|
swarmId: string,
|
|
10
26
|
): TrackerSync | null {
|
|
11
|
-
|
|
27
|
+
const row = getDb()
|
|
12
28
|
.query("SELECT * FROM tracker_sync WHERE provider = ? AND entityType = ? AND swarmId = ?")
|
|
13
29
|
.get(provider, entityType, swarmId) as TrackerSync | null;
|
|
30
|
+
return row ? normalizeTrackerSync(row) : null;
|
|
14
31
|
}
|
|
15
32
|
|
|
16
33
|
export function getTrackerSyncByExternalId(
|
|
@@ -18,9 +35,10 @@ export function getTrackerSyncByExternalId(
|
|
|
18
35
|
entityType: "task",
|
|
19
36
|
externalId: string,
|
|
20
37
|
): TrackerSync | null {
|
|
21
|
-
|
|
38
|
+
const row = getDb()
|
|
22
39
|
.query("SELECT * FROM tracker_sync WHERE provider = ? AND entityType = ? AND externalId = ?")
|
|
23
40
|
.get(provider, entityType, externalId) as TrackerSync | null;
|
|
41
|
+
return row ? normalizeTrackerSync(row) : null;
|
|
24
42
|
}
|
|
25
43
|
|
|
26
44
|
export function createTrackerSync(data: {
|
|
@@ -53,7 +71,7 @@ export function createTrackerSync(data: {
|
|
|
53
71
|
data.lastDeliveryId ?? null,
|
|
54
72
|
data.syncDirection ?? "inbound",
|
|
55
73
|
) as TrackerSync;
|
|
56
|
-
return result;
|
|
74
|
+
return normalizeTrackerSync(result);
|
|
57
75
|
}
|
|
58
76
|
|
|
59
77
|
export function updateTrackerSync(
|
|
@@ -124,9 +142,11 @@ export function getAllTrackerSyncs(provider?: string, entityType?: "task"): Trac
|
|
|
124
142
|
}
|
|
125
143
|
|
|
126
144
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
127
|
-
return
|
|
128
|
-
|
|
129
|
-
|
|
145
|
+
return (
|
|
146
|
+
getDb()
|
|
147
|
+
.query(`SELECT * FROM tracker_sync ${where} ORDER BY createdAt DESC`)
|
|
148
|
+
.all(...values) as TrackerSync[]
|
|
149
|
+
).map(normalizeTrackerSync);
|
|
130
150
|
}
|
|
131
151
|
|
|
132
152
|
// ── Tracker Agent Mapping ──
|
|
@@ -135,18 +155,20 @@ export function getTrackerAgentMapping(
|
|
|
135
155
|
provider: string,
|
|
136
156
|
agentId: string,
|
|
137
157
|
): TrackerAgentMapping | null {
|
|
138
|
-
|
|
158
|
+
const row = getDb()
|
|
139
159
|
.query("SELECT * FROM tracker_agent_mapping WHERE provider = ? AND agentId = ?")
|
|
140
160
|
.get(provider, agentId) as TrackerAgentMapping | null;
|
|
161
|
+
return row ? normalizeTrackerAgentMapping(row) : null;
|
|
141
162
|
}
|
|
142
163
|
|
|
143
164
|
export function getTrackerAgentMappingByExternalUser(
|
|
144
165
|
provider: string,
|
|
145
166
|
externalUserId: string,
|
|
146
167
|
): TrackerAgentMapping | null {
|
|
147
|
-
|
|
168
|
+
const row = getDb()
|
|
148
169
|
.query("SELECT * FROM tracker_agent_mapping WHERE provider = ? AND externalUserId = ?")
|
|
149
170
|
.get(provider, externalUserId) as TrackerAgentMapping | null;
|
|
171
|
+
return row ? normalizeTrackerAgentMapping(row) : null;
|
|
150
172
|
}
|
|
151
173
|
|
|
152
174
|
export function createTrackerAgentMapping(data: {
|
|
@@ -155,13 +177,14 @@ export function createTrackerAgentMapping(data: {
|
|
|
155
177
|
externalUserId: string;
|
|
156
178
|
agentName: string;
|
|
157
179
|
}): TrackerAgentMapping {
|
|
158
|
-
|
|
180
|
+
const result = getDb()
|
|
159
181
|
.query(
|
|
160
182
|
`INSERT INTO tracker_agent_mapping (provider, agentId, externalUserId, agentName)
|
|
161
183
|
VALUES (?, ?, ?, ?)
|
|
162
184
|
RETURNING *`,
|
|
163
185
|
)
|
|
164
186
|
.get(data.provider, data.agentId, data.externalUserId, data.agentName) as TrackerAgentMapping;
|
|
187
|
+
return normalizeTrackerAgentMapping(result);
|
|
165
188
|
}
|
|
166
189
|
|
|
167
190
|
export function deleteTrackerAgentMapping(provider: string, agentId: string): void {
|
|
@@ -172,11 +195,15 @@ export function deleteTrackerAgentMapping(provider: string, agentId: string): vo
|
|
|
172
195
|
|
|
173
196
|
export function getAllTrackerAgentMappings(provider?: string): TrackerAgentMapping[] {
|
|
174
197
|
if (provider) {
|
|
175
|
-
return
|
|
176
|
-
|
|
177
|
-
|
|
198
|
+
return (
|
|
199
|
+
getDb()
|
|
200
|
+
.query("SELECT * FROM tracker_agent_mapping WHERE provider = ? ORDER BY createdAt DESC")
|
|
201
|
+
.all(provider) as TrackerAgentMapping[]
|
|
202
|
+
).map(normalizeTrackerAgentMapping);
|
|
178
203
|
}
|
|
179
|
-
return
|
|
180
|
-
|
|
181
|
-
|
|
204
|
+
return (
|
|
205
|
+
getDb()
|
|
206
|
+
.query("SELECT * FROM tracker_agent_mapping ORDER BY createdAt DESC")
|
|
207
|
+
.all() as TrackerAgentMapping[]
|
|
208
|
+
).map(normalizeTrackerAgentMapping);
|
|
182
209
|
}
|
package/src/be/db.ts
CHANGED
|
@@ -55,6 +55,7 @@ import type {
|
|
|
55
55
|
WorkflowSnapshot,
|
|
56
56
|
WorkflowVersion,
|
|
57
57
|
} from "../types";
|
|
58
|
+
import { normalizeDate, normalizeDateRequired } from "./date-utils";
|
|
58
59
|
import { runMigrations } from "./migrations/runner";
|
|
59
60
|
import { seedDefaultTemplates } from "./seed";
|
|
60
61
|
|
|
@@ -727,6 +728,8 @@ type AgentTaskRow = {
|
|
|
727
728
|
totalContextTokensUsed: number | null;
|
|
728
729
|
contextWindowSize: number | null;
|
|
729
730
|
was_paused: number;
|
|
731
|
+
credentialKeySuffix: string | null;
|
|
732
|
+
credentialKeyType: string | null;
|
|
730
733
|
};
|
|
731
734
|
|
|
732
735
|
function rowToAgentTask(row: AgentTaskRow): AgentTask {
|
|
@@ -780,6 +783,8 @@ function rowToAgentTask(row: AgentTaskRow): AgentTask {
|
|
|
780
783
|
output: row.output ?? undefined,
|
|
781
784
|
progress: row.progress ?? undefined,
|
|
782
785
|
wasPaused: !!row.was_paused,
|
|
786
|
+
credentialKeySuffix: row.credentialKeySuffix ?? undefined,
|
|
787
|
+
credentialKeyType: row.credentialKeyType ?? undefined,
|
|
783
788
|
};
|
|
784
789
|
}
|
|
785
790
|
|
|
@@ -3956,17 +3961,17 @@ function rowToScheduledTask(row: ScheduledTaskRow): ScheduledTask {
|
|
|
3956
3961
|
priority: row.priority,
|
|
3957
3962
|
targetAgentId: row.targetAgentId ?? undefined,
|
|
3958
3963
|
enabled: row.enabled === 1,
|
|
3959
|
-
lastRunAt: row.lastRunAt ?? undefined,
|
|
3960
|
-
nextRunAt: row.nextRunAt ?? undefined,
|
|
3964
|
+
lastRunAt: normalizeDate(row.lastRunAt) ?? undefined,
|
|
3965
|
+
nextRunAt: normalizeDate(row.nextRunAt) ?? undefined,
|
|
3961
3966
|
createdByAgentId: row.createdByAgentId ?? undefined,
|
|
3962
3967
|
timezone: row.timezone,
|
|
3963
3968
|
consecutiveErrors: row.consecutiveErrors ?? 0,
|
|
3964
|
-
lastErrorAt: row.lastErrorAt ?? undefined,
|
|
3969
|
+
lastErrorAt: normalizeDate(row.lastErrorAt) ?? undefined,
|
|
3965
3970
|
lastErrorMessage: row.lastErrorMessage ?? undefined,
|
|
3966
3971
|
model: (row.model as "haiku" | "sonnet" | "opus" | null) ?? undefined,
|
|
3967
3972
|
scheduleType: row.scheduleType as "recurring" | "one_time",
|
|
3968
|
-
createdAt: row.createdAt,
|
|
3969
|
-
lastUpdatedAt: row.lastUpdatedAt,
|
|
3973
|
+
createdAt: normalizeDateRequired(row.createdAt),
|
|
3974
|
+
lastUpdatedAt: normalizeDateRequired(row.lastUpdatedAt),
|
|
3970
3975
|
};
|
|
3971
3976
|
}
|
|
3972
3977
|
|
|
@@ -5237,8 +5242,8 @@ function rowToWorkflow(row: WorkflowRow): Workflow {
|
|
|
5237
5242
|
dir: row.dir ?? undefined,
|
|
5238
5243
|
vcsRepo: row.vcs_repo ?? undefined,
|
|
5239
5244
|
createdByAgentId: row.createdByAgentId ?? undefined,
|
|
5240
|
-
createdAt: row.createdAt,
|
|
5241
|
-
lastUpdatedAt: row.lastUpdatedAt,
|
|
5245
|
+
createdAt: normalizeDateRequired(row.createdAt),
|
|
5246
|
+
lastUpdatedAt: normalizeDateRequired(row.lastUpdatedAt),
|
|
5242
5247
|
};
|
|
5243
5248
|
}
|
|
5244
5249
|
|
|
@@ -5442,9 +5447,9 @@ function rowToWorkflowRun(row: WorkflowRunRow): WorkflowRun {
|
|
|
5442
5447
|
triggerData: row.triggerData ? JSON.parse(row.triggerData) : undefined,
|
|
5443
5448
|
context: row.context ? (JSON.parse(row.context) as Record<string, unknown>) : undefined,
|
|
5444
5449
|
error: row.error ?? undefined,
|
|
5445
|
-
startedAt: row.startedAt,
|
|
5446
|
-
lastUpdatedAt: row.lastUpdatedAt,
|
|
5447
|
-
finishedAt: row.finishedAt ?? undefined,
|
|
5450
|
+
startedAt: normalizeDateRequired(row.startedAt),
|
|
5451
|
+
lastUpdatedAt: normalizeDateRequired(row.lastUpdatedAt),
|
|
5452
|
+
finishedAt: normalizeDate(row.finishedAt) ?? undefined,
|
|
5448
5453
|
};
|
|
5449
5454
|
}
|
|
5450
5455
|
|
|
@@ -5551,11 +5556,11 @@ function rowToWorkflowRunStep(row: WorkflowRunStepRow): WorkflowRunStep {
|
|
|
5551
5556
|
input: row.input ? JSON.parse(row.input) : undefined,
|
|
5552
5557
|
output: row.output ? JSON.parse(row.output) : undefined,
|
|
5553
5558
|
error: row.error ?? undefined,
|
|
5554
|
-
startedAt: row.startedAt,
|
|
5555
|
-
finishedAt: row.finishedAt ?? undefined,
|
|
5559
|
+
startedAt: normalizeDateRequired(row.startedAt),
|
|
5560
|
+
finishedAt: normalizeDate(row.finishedAt) ?? undefined,
|
|
5556
5561
|
retryCount: row.retryCount,
|
|
5557
5562
|
maxRetries: row.maxRetries,
|
|
5558
|
-
nextRetryAt: row.nextRetryAt ?? undefined,
|
|
5563
|
+
nextRetryAt: normalizeDate(row.nextRetryAt) ?? undefined,
|
|
5559
5564
|
idempotencyKey: row.idempotencyKey ?? undefined,
|
|
5560
5565
|
diagnostics: row.diagnostics ?? undefined,
|
|
5561
5566
|
nextPort: row.nextPort ?? undefined,
|
|
@@ -5802,7 +5807,7 @@ function rowToWorkflowVersion(row: WorkflowVersionRow): WorkflowVersion {
|
|
|
5802
5807
|
version: row.version,
|
|
5803
5808
|
snapshot: JSON.parse(row.snapshot) as WorkflowSnapshot,
|
|
5804
5809
|
changedByAgentId: row.changedByAgentId ?? undefined,
|
|
5805
|
-
createdAt: row.createdAt,
|
|
5810
|
+
createdAt: normalizeDateRequired(row.createdAt),
|
|
5806
5811
|
};
|
|
5807
5812
|
}
|
|
5808
5813
|
|
|
@@ -6311,7 +6316,7 @@ function rowToChannelActivityCursor(row: ChannelActivityCursorRow): ChannelActiv
|
|
|
6311
6316
|
return {
|
|
6312
6317
|
channelId: row.channelId,
|
|
6313
6318
|
lastSeenTs: row.lastSeenTs,
|
|
6314
|
-
updatedAt: row.updatedAt,
|
|
6319
|
+
updatedAt: normalizeDateRequired(row.updatedAt),
|
|
6315
6320
|
};
|
|
6316
6321
|
}
|
|
6317
6322
|
|
|
@@ -6335,7 +6340,7 @@ export function upsertChannelActivityCursor(channelId: string, lastSeenTs: strin
|
|
|
6335
6340
|
getDb()
|
|
6336
6341
|
.prepare(
|
|
6337
6342
|
`INSERT INTO channel_activity_cursors (channelId, lastSeenTs, updatedAt)
|
|
6338
|
-
VALUES (?, ?,
|
|
6343
|
+
VALUES (?, ?, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
6339
6344
|
ON CONFLICT(channelId) DO UPDATE SET lastSeenTs = excluded.lastSeenTs, updatedAt = excluded.updatedAt`,
|
|
6340
6345
|
)
|
|
6341
6346
|
.run(channelId, lastSeenTs);
|
|
@@ -6395,12 +6400,12 @@ function rowToApprovalRequest(row: ApprovalRequestRow): ApprovalRequest {
|
|
|
6395
6400
|
status: row.status as ApprovalRequest["status"],
|
|
6396
6401
|
responses: row.responses ? JSON.parse(row.responses) : null,
|
|
6397
6402
|
resolvedBy: row.resolvedBy,
|
|
6398
|
-
resolvedAt: row.resolvedAt,
|
|
6403
|
+
resolvedAt: normalizeDate(row.resolvedAt),
|
|
6399
6404
|
timeoutSeconds: row.timeoutSeconds,
|
|
6400
|
-
expiresAt: row.expiresAt,
|
|
6405
|
+
expiresAt: normalizeDate(row.expiresAt),
|
|
6401
6406
|
notificationChannels: row.notificationChannels ? JSON.parse(row.notificationChannels) : null,
|
|
6402
|
-
createdAt: row.createdAt,
|
|
6403
|
-
updatedAt: row.updatedAt,
|
|
6407
|
+
createdAt: normalizeDateRequired(row.createdAt),
|
|
6408
|
+
updatedAt: normalizeDateRequired(row.updatedAt),
|
|
6404
6409
|
};
|
|
6405
6410
|
}
|
|
6406
6411
|
|
|
@@ -6561,7 +6566,7 @@ export function getStuckApprovalRuns(): StuckApprovalRun[] {
|
|
|
6561
6566
|
JOIN approval_requests ar ON ar.workflowRunStepId = wrs.id
|
|
6562
6567
|
WHERE wr.status = 'waiting'
|
|
6563
6568
|
AND (ar.status IN ('approved', 'rejected', 'timeout')
|
|
6564
|
-
OR (ar.status = 'pending' AND ar.expiresAt IS NOT NULL AND ar.expiresAt <
|
|
6569
|
+
OR (ar.status = 'pending' AND ar.expiresAt IS NOT NULL AND ar.expiresAt < strftime('%Y-%m-%dT%H:%M:%fZ', 'now')))`,
|
|
6565
6570
|
)
|
|
6566
6571
|
.all();
|
|
6567
6572
|
}
|
|
@@ -6582,7 +6587,7 @@ export function getExpiredPendingApprovals(): ApprovalRequest[] {
|
|
|
6582
6587
|
`SELECT * FROM approval_requests
|
|
6583
6588
|
WHERE status = 'pending'
|
|
6584
6589
|
AND expiresAt IS NOT NULL
|
|
6585
|
-
AND expiresAt <
|
|
6590
|
+
AND expiresAt < strftime('%Y-%m-%dT%H:%M:%fZ', 'now')`,
|
|
6586
6591
|
)
|
|
6587
6592
|
.all();
|
|
6588
6593
|
return rows.map(rowToApprovalRequest);
|
|
@@ -7575,10 +7580,9 @@ export function recordKeyUsage(
|
|
|
7575
7580
|
|
|
7576
7581
|
// Record which key was used on the task
|
|
7577
7582
|
if (taskId) {
|
|
7578
|
-
db.prepare(
|
|
7579
|
-
|
|
7580
|
-
|
|
7581
|
-
);
|
|
7583
|
+
db.prepare(
|
|
7584
|
+
"UPDATE agent_tasks SET credentialKeySuffix = ?, credentialKeyType = ? WHERE id = ?",
|
|
7585
|
+
).run(keySuffix, keyType, taskId);
|
|
7582
7586
|
}
|
|
7583
7587
|
}
|
|
7584
7588
|
|
|
@@ -7641,3 +7645,43 @@ export function getKeyStatuses(
|
|
|
7641
7645
|
.prepare<ApiKeyStatus, string[]>(`SELECT * FROM api_key_status ${where} ORDER BY keyIndex`)
|
|
7642
7646
|
.all(...params);
|
|
7643
7647
|
}
|
|
7648
|
+
|
|
7649
|
+
export interface KeyCostSummary {
|
|
7650
|
+
keyType: string;
|
|
7651
|
+
keySuffix: string;
|
|
7652
|
+
totalCost: number;
|
|
7653
|
+
totalInputTokens: number;
|
|
7654
|
+
totalOutputTokens: number;
|
|
7655
|
+
taskCount: number;
|
|
7656
|
+
}
|
|
7657
|
+
|
|
7658
|
+
/**
|
|
7659
|
+
* Aggregate cost data per API key by joining session_costs through agent_tasks.
|
|
7660
|
+
*/
|
|
7661
|
+
export function getKeyCostSummary(keyType?: string): KeyCostSummary[] {
|
|
7662
|
+
const db = getDb();
|
|
7663
|
+
const conditions = ["t.credentialKeySuffix IS NOT NULL"];
|
|
7664
|
+
const params: string[] = [];
|
|
7665
|
+
|
|
7666
|
+
if (keyType) {
|
|
7667
|
+
conditions.push("t.credentialKeyType = ?");
|
|
7668
|
+
params.push(keyType);
|
|
7669
|
+
}
|
|
7670
|
+
|
|
7671
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
7672
|
+
return db
|
|
7673
|
+
.prepare<KeyCostSummary, string[]>(
|
|
7674
|
+
`SELECT
|
|
7675
|
+
t.credentialKeyType as keyType,
|
|
7676
|
+
t.credentialKeySuffix as keySuffix,
|
|
7677
|
+
COALESCE(SUM(sc.totalCostUsd), 0) as totalCost,
|
|
7678
|
+
COALESCE(SUM(sc.inputTokens), 0) as totalInputTokens,
|
|
7679
|
+
COALESCE(SUM(sc.outputTokens), 0) as totalOutputTokens,
|
|
7680
|
+
COUNT(DISTINCT sc.taskId) as taskCount
|
|
7681
|
+
FROM session_costs sc
|
|
7682
|
+
JOIN agent_tasks t ON sc.taskId = t.id
|
|
7683
|
+
${where}
|
|
7684
|
+
GROUP BY t.credentialKeyType, t.credentialKeySuffix`,
|
|
7685
|
+
)
|
|
7686
|
+
.all(...params);
|
|
7687
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
-- Fix date storage format inconsistency: convert bare datetime format
|
|
2
|
+
-- (YYYY-MM-DD HH:MM:SS) to ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS.000Z).
|
|
3
|
+
--
|
|
4
|
+
-- Affected tables use datetime('now') or CURRENT_TIMESTAMP defaults which
|
|
5
|
+
-- produce bare format. Browsers parse these as local time instead of UTC.
|
|
6
|
+
|
|
7
|
+
-- Update existing bare-format dates to ISO 8601 in all affected tables.
|
|
8
|
+
-- The pattern: replace space with T and append .000Z for dates matching bare format.
|
|
9
|
+
|
|
10
|
+
-- workflows
|
|
11
|
+
UPDATE workflows SET
|
|
12
|
+
createdAt = replace(createdAt, ' ', 'T') || '.000Z'
|
|
13
|
+
WHERE createdAt LIKE '____-__-__ __:__:__' AND createdAt NOT LIKE '%T%';
|
|
14
|
+
UPDATE workflows SET
|
|
15
|
+
lastUpdatedAt = replace(lastUpdatedAt, ' ', 'T') || '.000Z'
|
|
16
|
+
WHERE lastUpdatedAt LIKE '____-__-__ __:__:__' AND lastUpdatedAt NOT LIKE '%T%';
|
|
17
|
+
|
|
18
|
+
-- workflow_runs
|
|
19
|
+
UPDATE workflow_runs SET
|
|
20
|
+
startedAt = replace(startedAt, ' ', 'T') || '.000Z'
|
|
21
|
+
WHERE startedAt LIKE '____-__-__ __:__:__' AND startedAt NOT LIKE '%T%';
|
|
22
|
+
UPDATE workflow_runs SET
|
|
23
|
+
lastUpdatedAt = replace(lastUpdatedAt, ' ', 'T') || '.000Z'
|
|
24
|
+
WHERE lastUpdatedAt LIKE '____-__-__ __:__:__' AND lastUpdatedAt NOT LIKE '%T%';
|
|
25
|
+
UPDATE workflow_runs SET
|
|
26
|
+
finishedAt = replace(finishedAt, ' ', 'T') || '.000Z'
|
|
27
|
+
WHERE finishedAt IS NOT NULL AND finishedAt LIKE '____-__-__ __:__:__' AND finishedAt NOT LIKE '%T%';
|
|
28
|
+
|
|
29
|
+
-- workflow_run_steps
|
|
30
|
+
UPDATE workflow_run_steps SET
|
|
31
|
+
startedAt = replace(startedAt, ' ', 'T') || '.000Z'
|
|
32
|
+
WHERE startedAt LIKE '____-__-__ __:__:__' AND startedAt NOT LIKE '%T%';
|
|
33
|
+
UPDATE workflow_run_steps SET
|
|
34
|
+
finishedAt = replace(finishedAt, ' ', 'T') || '.000Z'
|
|
35
|
+
WHERE finishedAt IS NOT NULL AND finishedAt LIKE '____-__-__ __:__:__' AND finishedAt NOT LIKE '%T%';
|
|
36
|
+
UPDATE workflow_run_steps SET
|
|
37
|
+
nextRetryAt = replace(nextRetryAt, ' ', 'T') || '.000Z'
|
|
38
|
+
WHERE nextRetryAt IS NOT NULL AND nextRetryAt LIKE '____-__-__ __:__:__' AND nextRetryAt NOT LIKE '%T%';
|
|
39
|
+
|
|
40
|
+
-- tracker_sync
|
|
41
|
+
UPDATE tracker_sync SET
|
|
42
|
+
lastSyncedAt = replace(lastSyncedAt, ' ', 'T') || '.000Z'
|
|
43
|
+
WHERE lastSyncedAt LIKE '____-__-__ __:__:__' AND lastSyncedAt NOT LIKE '%T%';
|
|
44
|
+
UPDATE tracker_sync SET
|
|
45
|
+
createdAt = replace(createdAt, ' ', 'T') || '.000Z'
|
|
46
|
+
WHERE createdAt LIKE '____-__-__ __:__:__' AND createdAt NOT LIKE '%T%';
|
|
47
|
+
|
|
48
|
+
-- tracker_agent_mapping
|
|
49
|
+
UPDATE tracker_agent_mapping SET
|
|
50
|
+
createdAt = replace(createdAt, ' ', 'T') || '.000Z'
|
|
51
|
+
WHERE createdAt LIKE '____-__-__ __:__:__' AND createdAt NOT LIKE '%T%';
|
|
52
|
+
|
|
53
|
+
-- approval_requests
|
|
54
|
+
UPDATE approval_requests SET
|
|
55
|
+
createdAt = replace(createdAt, ' ', 'T') || '.000Z'
|
|
56
|
+
WHERE createdAt LIKE '____-__-__ __:__:__' AND createdAt NOT LIKE '%T%';
|
|
57
|
+
UPDATE approval_requests SET
|
|
58
|
+
updatedAt = replace(updatedAt, ' ', 'T') || '.000Z'
|
|
59
|
+
WHERE updatedAt LIKE '____-__-__ __:__:__' AND updatedAt NOT LIKE '%T%';
|
|
60
|
+
UPDATE approval_requests SET
|
|
61
|
+
resolvedAt = replace(resolvedAt, ' ', 'T') || '.000Z'
|
|
62
|
+
WHERE resolvedAt IS NOT NULL AND resolvedAt LIKE '____-__-__ __:__:__' AND resolvedAt NOT LIKE '%T%';
|
|
63
|
+
UPDATE approval_requests SET
|
|
64
|
+
expiresAt = replace(expiresAt, ' ', 'T') || '.000Z'
|
|
65
|
+
WHERE expiresAt IS NOT NULL AND expiresAt LIKE '____-__-__ __:__:__' AND expiresAt NOT LIKE '%T%';
|
|
66
|
+
|
|
67
|
+
-- channel_activity_cursors
|
|
68
|
+
UPDATE channel_activity_cursors SET
|
|
69
|
+
updatedAt = replace(updatedAt, ' ', 'T') || '.000Z'
|
|
70
|
+
WHERE updatedAt LIKE '____-__-__ __:__:__' AND updatedAt NOT LIKE '%T%';
|
|
71
|
+
|
|
72
|
+
-- oauth_apps
|
|
73
|
+
UPDATE oauth_apps SET
|
|
74
|
+
createdAt = replace(createdAt, ' ', 'T') || '.000Z'
|
|
75
|
+
WHERE createdAt LIKE '____-__-__ __:__:__' AND createdAt NOT LIKE '%T%';
|
|
76
|
+
UPDATE oauth_apps SET
|
|
77
|
+
updatedAt = replace(updatedAt, ' ', 'T') || '.000Z'
|
|
78
|
+
WHERE updatedAt LIKE '____-__-__ __:__:__' AND updatedAt NOT LIKE '%T%';
|
|
79
|
+
|
|
80
|
+
-- oauth_tokens
|
|
81
|
+
UPDATE oauth_tokens SET
|
|
82
|
+
createdAt = replace(createdAt, ' ', 'T') || '.000Z'
|
|
83
|
+
WHERE createdAt LIKE '____-__-__ __:__:__' AND createdAt NOT LIKE '%T%';
|
|
84
|
+
UPDATE oauth_tokens SET
|
|
85
|
+
updatedAt = replace(updatedAt, ' ', 'T') || '.000Z'
|
|
86
|
+
WHERE updatedAt LIKE '____-__-__ __:__:__' AND updatedAt NOT LIKE '%T%';
|
|
87
|
+
|
|
88
|
+
-- workflow_versions
|
|
89
|
+
UPDATE workflow_versions SET
|
|
90
|
+
createdAt = replace(createdAt, ' ', 'T') || '.000Z'
|
|
91
|
+
WHERE createdAt LIKE '____-__-__ __:__:__' AND createdAt NOT LIKE '%T%';
|
|
92
|
+
|
|
93
|
+
-- scheduled_tasks (defense-in-depth — dates are written from JS toISOString() but normalize any legacy data)
|
|
94
|
+
UPDATE scheduled_tasks SET
|
|
95
|
+
createdAt = replace(createdAt, ' ', 'T') || '.000Z'
|
|
96
|
+
WHERE createdAt LIKE '____-__-__ __:__:__' AND createdAt NOT LIKE '%T%';
|
|
97
|
+
UPDATE scheduled_tasks SET
|
|
98
|
+
lastUpdatedAt = replace(lastUpdatedAt, ' ', 'T') || '.000Z'
|
|
99
|
+
WHERE lastUpdatedAt LIKE '____-__-__ __:__:__' AND lastUpdatedAt NOT LIKE '%T%';
|
|
100
|
+
UPDATE scheduled_tasks SET
|
|
101
|
+
lastRunAt = replace(lastRunAt, ' ', 'T') || '.000Z'
|
|
102
|
+
WHERE lastRunAt IS NOT NULL AND lastRunAt LIKE '____-__-__ __:__:__' AND lastRunAt NOT LIKE '%T%';
|
|
103
|
+
UPDATE scheduled_tasks SET
|
|
104
|
+
nextRunAt = replace(nextRunAt, ' ', 'T') || '.000Z'
|
|
105
|
+
WHERE nextRunAt IS NOT NULL AND nextRunAt LIKE '____-__-__ __:__:__' AND nextRunAt NOT LIKE '%T%';
|
|
106
|
+
UPDATE scheduled_tasks SET
|
|
107
|
+
lastErrorAt = replace(lastErrorAt, ' ', 'T') || '.000Z'
|
|
108
|
+
WHERE lastErrorAt IS NOT NULL AND lastErrorAt LIKE '____-__-__ __:__:__' AND lastErrorAt NOT LIKE '%T%';
|
package/src/http/api-keys.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { IncomingMessage, ServerResponse } from "node:http";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import {
|
|
4
4
|
getAvailableKeyIndices,
|
|
5
|
+
getKeyCostSummary,
|
|
5
6
|
getKeyStatuses,
|
|
6
7
|
markKeyRateLimited,
|
|
7
8
|
recordKeyUsage,
|
|
@@ -93,6 +94,22 @@ const listStatuses = route({
|
|
|
93
94
|
auth: { apiKey: true },
|
|
94
95
|
});
|
|
95
96
|
|
|
97
|
+
const getCosts = route({
|
|
98
|
+
method: "get",
|
|
99
|
+
path: "/api/keys/costs",
|
|
100
|
+
pattern: ["api", "keys", "costs"],
|
|
101
|
+
summary: "Get aggregated cost data per API key",
|
|
102
|
+
tags: ["API Keys"],
|
|
103
|
+
query: z.object({
|
|
104
|
+
keyType: z.string().optional(),
|
|
105
|
+
}),
|
|
106
|
+
responses: {
|
|
107
|
+
200: { description: "Per-key cost aggregation" },
|
|
108
|
+
401: { description: "Unauthorized" },
|
|
109
|
+
},
|
|
110
|
+
auth: { apiKey: true },
|
|
111
|
+
});
|
|
112
|
+
|
|
96
113
|
// ─── Handler ─────────────────────────────────────────────────────────────────
|
|
97
114
|
|
|
98
115
|
export async function handleApiKeys(
|
|
@@ -149,6 +166,21 @@ export async function handleApiKeys(
|
|
|
149
166
|
return true;
|
|
150
167
|
}
|
|
151
168
|
|
|
169
|
+
// GET /api/keys/costs
|
|
170
|
+
if (getCosts.match(req.method, pathSegments)) {
|
|
171
|
+
const parsed = await getCosts.parse(req, res, pathSegments, queryParams);
|
|
172
|
+
if (!parsed) return true;
|
|
173
|
+
|
|
174
|
+
const { keyType } = parsed.query;
|
|
175
|
+
try {
|
|
176
|
+
const costs = getKeyCostSummary(keyType);
|
|
177
|
+
json(res, { success: true, costs });
|
|
178
|
+
} catch (err) {
|
|
179
|
+
jsonError(res, err instanceof Error ? err.message : "Failed to get key costs", 500);
|
|
180
|
+
}
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
|
|
152
184
|
// GET /api/keys/status
|
|
153
185
|
if (listStatuses.match(req.method, pathSegments)) {
|
|
154
186
|
const parsed = await listStatuses.parse(req, res, pathSegments, queryParams);
|
|
@@ -103,12 +103,16 @@ describe("resolveCredentialPools", () => {
|
|
|
103
103
|
expect(env.ANTHROPIC_API_KEY).toBe("key-ccc33");
|
|
104
104
|
});
|
|
105
105
|
|
|
106
|
-
test("
|
|
106
|
+
test("single keys are tracked with index 0", async () => {
|
|
107
107
|
const env: Record<string, string | undefined> = {
|
|
108
108
|
ANTHROPIC_API_KEY: "single-key",
|
|
109
109
|
};
|
|
110
110
|
const selections = await resolveCredentialPools(env);
|
|
111
|
-
expect(selections.length).toBe(
|
|
111
|
+
expect(selections.length).toBe(1);
|
|
112
|
+
expect(selections[0]!.index).toBe(0);
|
|
113
|
+
expect(selections[0]!.total).toBe(1);
|
|
114
|
+
expect(selections[0]!.keySuffix).toBe("e-key");
|
|
115
|
+
expect(selections[0]!.keyType).toBe("ANTHROPIC_API_KEY");
|
|
112
116
|
expect(env.ANTHROPIC_API_KEY).toBe("single-key");
|
|
113
117
|
});
|
|
114
118
|
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { normalizeDate, normalizeDateRequired } from "../be/date-utils";
|
|
3
|
+
|
|
4
|
+
describe("normalizeDate", () => {
|
|
5
|
+
it("returns null for null input", () => {
|
|
6
|
+
expect(normalizeDate(null)).toBe(null);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("returns null for undefined input", () => {
|
|
10
|
+
expect(normalizeDate(undefined)).toBe(null);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("converts bare datetime format to ISO 8601", () => {
|
|
14
|
+
expect(normalizeDate("2026-03-31 14:30:00")).toBe("2026-03-31T14:30:00.000Z");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("converts midnight bare datetime", () => {
|
|
18
|
+
expect(normalizeDate("2026-01-01 00:00:00")).toBe("2026-01-01T00:00:00.000Z");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("passes through ISO 8601 with Z suffix unchanged", () => {
|
|
22
|
+
expect(normalizeDate("2026-03-31T14:30:00.000Z")).toBe("2026-03-31T14:30:00.000Z");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("passes through ISO 8601 with milliseconds and Z", () => {
|
|
26
|
+
expect(normalizeDate("2026-03-31T14:30:00.123Z")).toBe("2026-03-31T14:30:00.123Z");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("passes through toISOString() output unchanged", () => {
|
|
30
|
+
const iso = new Date().toISOString();
|
|
31
|
+
expect(normalizeDate(iso)).toBe(iso);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("passes through strftime output unchanged", () => {
|
|
35
|
+
// strftime('%Y-%m-%dT%H:%M:%fZ', 'now') produces this format
|
|
36
|
+
expect(normalizeDate("2026-03-31T14:30:00.000Z")).toBe("2026-03-31T14:30:00.000Z");
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("normalizeDateRequired", () => {
|
|
41
|
+
it("converts bare datetime format to ISO 8601", () => {
|
|
42
|
+
expect(normalizeDateRequired("2026-03-31 14:30:00")).toBe("2026-03-31T14:30:00.000Z");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("passes through ISO 8601 unchanged", () => {
|
|
46
|
+
expect(normalizeDateRequired("2026-03-31T14:30:00.123Z")).toBe("2026-03-31T14:30:00.123Z");
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -68,7 +68,7 @@ describe("Heartbeat Triage", () => {
|
|
|
68
68
|
createTaskExtended("Completed task", { agentId: agent.id });
|
|
69
69
|
// Manually mark as completed
|
|
70
70
|
getDb().run(
|
|
71
|
-
"UPDATE agent_tasks SET status = 'completed', finishedAt =
|
|
71
|
+
"UPDATE agent_tasks SET status = 'completed', finishedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE agentId = ?",
|
|
72
72
|
[agent.id],
|
|
73
73
|
);
|
|
74
74
|
|
package/src/types.ts
CHANGED
|
@@ -149,6 +149,10 @@ export const AgentTaskSchema = z.object({
|
|
|
149
149
|
peakContextPercent: z.number().min(0).max(100).optional(),
|
|
150
150
|
totalContextTokensUsed: z.number().int().min(0).optional(),
|
|
151
151
|
contextWindowSize: z.number().int().min(0).optional(),
|
|
152
|
+
|
|
153
|
+
// Credential tracking
|
|
154
|
+
credentialKeySuffix: z.string().optional(),
|
|
155
|
+
credentialKeyType: z.string().optional(),
|
|
152
156
|
});
|
|
153
157
|
|
|
154
158
|
export const AgentStatusSchema = z.enum(["idle", "busy", "offline"]);
|
package/src/utils/credentials.ts
CHANGED
|
@@ -91,8 +91,8 @@ async function fetchAvailableIndices(
|
|
|
91
91
|
const availableIndicesMap: Record<string, number[]> = {};
|
|
92
92
|
for (const envVar of CREDENTIAL_POOL_VARS) {
|
|
93
93
|
const val = env[envVar];
|
|
94
|
-
if (val
|
|
95
|
-
const totalKeys = val.split(",").filter((s) => s.trim()).length;
|
|
94
|
+
if (val) {
|
|
95
|
+
const totalKeys = val.includes(",") ? val.split(",").filter((s) => s.trim()).length : 1;
|
|
96
96
|
try {
|
|
97
97
|
const resp = await fetch(
|
|
98
98
|
`${apiUrl}/api/keys/available?keyType=${encodeURIComponent(envVar)}&totalKeys=${totalKeys}`,
|
|
@@ -134,7 +134,7 @@ export async function resolveCredentialPools(
|
|
|
134
134
|
const selections: CredentialSelection[] = [];
|
|
135
135
|
for (const envVar of CREDENTIAL_POOL_VARS) {
|
|
136
136
|
const val = env[envVar];
|
|
137
|
-
if (val
|
|
137
|
+
if (val) {
|
|
138
138
|
const available = availableIndicesMap?.[envVar];
|
|
139
139
|
const result = selectCredential(val, available, envVar);
|
|
140
140
|
env[envVar] = result.selected;
|