@dmrlzz/davecode 0.0.0-stage → 0.4.0
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/LICENSE +21 -0
- package/README.md +34 -2
- package/bin/davecode.js +2 -0
- package/dist/ChatApp-BO7QMBWD.js +1045 -0
- package/dist/ChatApp-BO7QMBWD.js.map +1 -0
- package/dist/RunnerView-A7JSPUW5.js +18 -0
- package/dist/RunnerView-A7JSPUW5.js.map +1 -0
- package/dist/account-UGNLJPQ4.js +23 -0
- package/dist/account-UGNLJPQ4.js.map +1 -0
- package/dist/accounts-NM7IWP53.js +37 -0
- package/dist/accounts-NM7IWP53.js.map +1 -0
- package/dist/budget-INAW6FXD.js +115 -0
- package/dist/budget-INAW6FXD.js.map +1 -0
- package/dist/chat-U4QJQ7M6.js +359 -0
- package/dist/chat-U4QJQ7M6.js.map +1 -0
- package/dist/chunk-3Q2KCXLN.js +9608 -0
- package/dist/chunk-3Q2KCXLN.js.map +1 -0
- package/dist/chunk-5LKOJBUL.js +20 -0
- package/dist/chunk-5LKOJBUL.js.map +1 -0
- package/dist/chunk-7XVXEO7U.js +23 -0
- package/dist/chunk-7XVXEO7U.js.map +1 -0
- package/dist/chunk-CWBVNS3L.js +731 -0
- package/dist/chunk-CWBVNS3L.js.map +1 -0
- package/dist/chunk-DZ2FDABF.js +1614 -0
- package/dist/chunk-DZ2FDABF.js.map +1 -0
- package/dist/chunk-FRFSJMN4.js +79 -0
- package/dist/chunk-FRFSJMN4.js.map +1 -0
- package/dist/chunk-GE22VBCY.js +200 -0
- package/dist/chunk-GE22VBCY.js.map +1 -0
- package/dist/chunk-GHDOUII5.js +144 -0
- package/dist/chunk-GHDOUII5.js.map +1 -0
- package/dist/chunk-GZJ6JSO7.js +213 -0
- package/dist/chunk-GZJ6JSO7.js.map +1 -0
- package/dist/chunk-IC3TDONV.js +12 -0
- package/dist/chunk-IC3TDONV.js.map +1 -0
- package/dist/chunk-MC7RIPL7.js +116 -0
- package/dist/chunk-MC7RIPL7.js.map +1 -0
- package/dist/chunk-MOK5D3XJ.js +114 -0
- package/dist/chunk-MOK5D3XJ.js.map +1 -0
- package/dist/chunk-PGPXE6LF.js +20 -0
- package/dist/chunk-PGPXE6LF.js.map +1 -0
- package/dist/chunk-QA3O6RA2.js +81 -0
- package/dist/chunk-QA3O6RA2.js.map +1 -0
- package/dist/chunk-ROIUTNQV.js +20 -0
- package/dist/chunk-ROIUTNQV.js.map +1 -0
- package/dist/chunk-SF5ZEAP4.js +105 -0
- package/dist/chunk-SF5ZEAP4.js.map +1 -0
- package/dist/chunk-Z6NQDJEN.js +180 -0
- package/dist/chunk-Z6NQDJEN.js.map +1 -0
- package/dist/config-YEAPBMBX.js +158 -0
- package/dist/config-YEAPBMBX.js.map +1 -0
- package/dist/dashboard/assets/Accounts-CeVyX4a3.js +1 -0
- package/dist/dashboard/assets/Badge-CGY6DgGx.js +55 -0
- package/dist/dashboard/assets/Callout-DauCk_cW.js +1 -0
- package/dist/dashboard/assets/Field-OXMnT9Tb.js +1 -0
- package/dist/dashboard/assets/Goals-DH-KDh8M.js +1 -0
- package/dist/dashboard/assets/Library-C15szLjZ.js +1 -0
- package/dist/dashboard/assets/Markdown-Xhao3sgL.js +69 -0
- package/dist/dashboard/assets/Panel-Du40pM7n.js +1 -0
- package/dist/dashboard/assets/RoutesPage-Cj1OelGO.js +4 -0
- package/dist/dashboard/assets/Runner-BK1-ZF7h.js +1 -0
- package/dist/dashboard/assets/Settings-C1nzQQhe.js +1 -0
- package/dist/dashboard/assets/Switch-bwtIJ00H.js +1 -0
- package/dist/dashboard/assets/Tasks-x4s8IiPG.js +2 -0
- package/dist/dashboard/assets/Traffic-CV7TaTid.js +1 -0
- package/dist/dashboard/assets/Workspace-DAL-MBc0.js +1 -0
- package/dist/dashboard/assets/arrow-up-goM9r-I9.js +1 -0
- package/dist/dashboard/assets/forms-DtpfI65-.js +1 -0
- package/dist/dashboard/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/dist/dashboard/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/dist/dashboard/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/dist/dashboard/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/dist/dashboard/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
- package/dist/dashboard/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
- package/dist/dashboard/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
- package/dist/dashboard/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
- package/dist/dashboard/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
- package/dist/dashboard/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
- package/dist/dashboard/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/dist/dashboard/assets/index-BCi-Vuvh.js +104 -0
- package/dist/dashboard/assets/index-CcJFhc98.css +2 -0
- package/dist/dashboard/assets/task-errors-B2pgjUGN.js +1 -0
- package/dist/dashboard/assets/trash-BVojETtF.js +1 -0
- package/dist/dashboard/favicon.svg +1 -0
- package/dist/dashboard/github.svg +1 -0
- package/dist/dashboard/index.html +31 -0
- package/dist/dist-RRQGUBL3.js +379 -0
- package/dist/dist-RRQGUBL3.js.map +1 -0
- package/dist/doctor-MQYK44S2.js +279 -0
- package/dist/doctor-MQYK44S2.js.map +1 -0
- package/dist/goal-6NUX4AIT.js +302 -0
- package/dist/goal-6NUX4AIT.js.map +1 -0
- package/dist/index.js +399 -0
- package/dist/index.js.map +1 -0
- package/dist/init-AXYIWAB3.js +194 -0
- package/dist/init-AXYIWAB3.js.map +1 -0
- package/dist/prompts-TZ7UC42M.js +219 -0
- package/dist/prompts-TZ7UC42M.js.map +1 -0
- package/dist/run-QRTLHFB3.js +376 -0
- package/dist/run-QRTLHFB3.js.map +1 -0
- package/dist/runner-state-QFU3LJVJ.js +17 -0
- package/dist/runner-state-QFU3LJVJ.js.map +1 -0
- package/dist/runtime.js +18 -0
- package/dist/runtime.js.map +1 -0
- package/dist/start-O5I5TSO2.js +285 -0
- package/dist/start-O5I5TSO2.js.map +1 -0
- package/dist/status-LEA3RVUX.js +25 -0
- package/dist/status-LEA3RVUX.js.map +1 -0
- package/dist/tasks-XOTVICVZ.js +184 -0
- package/dist/tasks-XOTVICVZ.js.map +1 -0
- package/package.json +73 -3
|
@@ -0,0 +1,1614 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ConfigError,
|
|
3
|
+
PROVIDER_KINDS,
|
|
4
|
+
ProjectBrain,
|
|
5
|
+
ProjectBrainSource,
|
|
6
|
+
ProviderError,
|
|
7
|
+
RouterError,
|
|
8
|
+
TASK_STATUSES,
|
|
9
|
+
TaskGraphError,
|
|
10
|
+
VERSION,
|
|
11
|
+
WorkspaceLock,
|
|
12
|
+
connectionMethod,
|
|
13
|
+
createEngine,
|
|
14
|
+
createGoalService,
|
|
15
|
+
createRunner,
|
|
16
|
+
loginCommandFor,
|
|
17
|
+
loginFromEnv,
|
|
18
|
+
newId2,
|
|
19
|
+
routeSchema,
|
|
20
|
+
taskIdSchema
|
|
21
|
+
} from "./chunk-3Q2KCXLN.js";
|
|
22
|
+
|
|
23
|
+
// src/runtime.ts
|
|
24
|
+
import { existsSync as existsSync3 } from "fs";
|
|
25
|
+
import { access } from "fs/promises";
|
|
26
|
+
import { createRequire } from "module";
|
|
27
|
+
import { dirname, join, resolve as resolve2 } from "path";
|
|
28
|
+
import { fileURLToPath } from "url";
|
|
29
|
+
|
|
30
|
+
// ../server/dist/index.js
|
|
31
|
+
import { timingSafeEqual } from "crypto";
|
|
32
|
+
import { existsSync } from "fs";
|
|
33
|
+
import { resolve } from "path";
|
|
34
|
+
import cors from "@fastify/cors";
|
|
35
|
+
import fastifyStatic from "@fastify/static";
|
|
36
|
+
import Fastify from "fastify";
|
|
37
|
+
import { z as z4 } from "zod";
|
|
38
|
+
import { z } from "zod";
|
|
39
|
+
import { z as z2 } from "zod";
|
|
40
|
+
import { z as z3 } from "zod";
|
|
41
|
+
import { z as z5 } from "zod";
|
|
42
|
+
import { Readable } from "stream";
|
|
43
|
+
import { z as z6 } from "zod";
|
|
44
|
+
function sendApiError(reply, status, code, message, extra = {}) {
|
|
45
|
+
return reply.code(status).send({ error: { message, code, ...extra } });
|
|
46
|
+
}
|
|
47
|
+
function sendOpenAIError(reply, status, body) {
|
|
48
|
+
return reply.code(status).send({ error: body });
|
|
49
|
+
}
|
|
50
|
+
function openAIType(status) {
|
|
51
|
+
if (status === 401) return "authentication_error";
|
|
52
|
+
if (status === 404 || status === 400 || status === 422) return "invalid_request_error";
|
|
53
|
+
if (status === 429) return "rate_limit_error";
|
|
54
|
+
return "api_error";
|
|
55
|
+
}
|
|
56
|
+
function describeRoutingError(err) {
|
|
57
|
+
if (err instanceof RouterError) {
|
|
58
|
+
return {
|
|
59
|
+
status: err.status,
|
|
60
|
+
body: { message: err.message, type: openAIType(err.status), code: err.code }
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
if (err instanceof ProviderError) {
|
|
64
|
+
let status;
|
|
65
|
+
let code = err.kind;
|
|
66
|
+
switch (err.kind) {
|
|
67
|
+
case "bad_request":
|
|
68
|
+
status = 400;
|
|
69
|
+
break;
|
|
70
|
+
case "context_length":
|
|
71
|
+
status = 400;
|
|
72
|
+
code = "context_length_exceeded";
|
|
73
|
+
break;
|
|
74
|
+
case "rate_limit":
|
|
75
|
+
case "quota_exhausted":
|
|
76
|
+
status = 429;
|
|
77
|
+
break;
|
|
78
|
+
case "auth":
|
|
79
|
+
status = 502;
|
|
80
|
+
code = "upstream_auth_error";
|
|
81
|
+
break;
|
|
82
|
+
default:
|
|
83
|
+
status = 502;
|
|
84
|
+
}
|
|
85
|
+
return { status, body: { message: err.message, type: openAIType(status), code } };
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
status: 500,
|
|
89
|
+
body: { message: "Internal gateway error", type: "api_error", code: "internal_error" }
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
var providerKind = z.enum(PROVIDER_KINDS);
|
|
93
|
+
var contentPart = z.discriminatedUnion("type", [
|
|
94
|
+
z.object({ type: z.literal("text"), text: z.string() }),
|
|
95
|
+
z.object({
|
|
96
|
+
type: z.literal("image_url"),
|
|
97
|
+
image_url: z.object({
|
|
98
|
+
url: z.string(),
|
|
99
|
+
detail: z.enum(["auto", "low", "high"]).optional()
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
]);
|
|
103
|
+
var toolCall = z.object({
|
|
104
|
+
id: z.string(),
|
|
105
|
+
type: z.literal("function"),
|
|
106
|
+
function: z.object({ name: z.string(), arguments: z.string() })
|
|
107
|
+
});
|
|
108
|
+
var chatMessage = z.object({
|
|
109
|
+
role: z.enum(["system", "user", "assistant", "tool"]),
|
|
110
|
+
content: z.union([z.string(), z.array(contentPart), z.null()]),
|
|
111
|
+
name: z.string().optional(),
|
|
112
|
+
tool_call_id: z.string().optional(),
|
|
113
|
+
tool_calls: z.array(toolCall).optional()
|
|
114
|
+
});
|
|
115
|
+
var chatRequestSchema = z.object({
|
|
116
|
+
model: z.string().min(1),
|
|
117
|
+
messages: z.array(chatMessage).min(1),
|
|
118
|
+
stream: z.boolean().optional(),
|
|
119
|
+
temperature: z.number().min(0).max(2).optional(),
|
|
120
|
+
top_p: z.number().min(0).max(1).optional(),
|
|
121
|
+
max_tokens: z.number().int().positive().optional(),
|
|
122
|
+
stop: z.union([z.string(), z.array(z.string())]).optional(),
|
|
123
|
+
tools: z.array(
|
|
124
|
+
z.object({
|
|
125
|
+
type: z.literal("function"),
|
|
126
|
+
function: z.object({
|
|
127
|
+
name: z.string(),
|
|
128
|
+
description: z.string().optional(),
|
|
129
|
+
parameters: z.record(z.string(), z.unknown()).optional()
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
).optional(),
|
|
133
|
+
tool_choice: z.union([
|
|
134
|
+
z.enum(["auto", "none", "required"]),
|
|
135
|
+
z.object({ type: z.literal("function"), function: z.object({ name: z.string() }) })
|
|
136
|
+
]).optional(),
|
|
137
|
+
user: z.string().optional()
|
|
138
|
+
});
|
|
139
|
+
var positiveInt = z.number().int().positive();
|
|
140
|
+
var quotaLimits = z.object({
|
|
141
|
+
tpm: positiveInt.optional(),
|
|
142
|
+
rpm: positiveInt.optional(),
|
|
143
|
+
tokens5h: positiveInt.optional(),
|
|
144
|
+
requests5h: positiveInt.optional(),
|
|
145
|
+
tokensDaily: positiveInt.optional(),
|
|
146
|
+
requestsDaily: positiveInt.optional()
|
|
147
|
+
}).strict();
|
|
148
|
+
var SECRET_KEY = /^(api[-_]?key|secret|client[-_]?secret|(access|refresh|session|auth|bearer)?[-_]?token|password|passwd|cookies?|authorization|credentials?)$/i;
|
|
149
|
+
var accountConfig = z.record(z.string(), z.unknown()).refine((config) => !Object.keys(config).some((key) => SECRET_KEY.test(key)), {
|
|
150
|
+
message: 'config must not contain credentials; send them in "secret" instead'
|
|
151
|
+
});
|
|
152
|
+
var subscriptionDate = z.string().trim().refine(
|
|
153
|
+
(value) => /^\d{4}-\d{2}-\d{2}(T[\d:.]+(Z|[+-]\d{2}:?\d{2})?)?$/.test(value) && !Number.isNaN(Date.parse(value)),
|
|
154
|
+
{ message: "expected a date (YYYY-MM-DD) or an ISO timestamp" }
|
|
155
|
+
);
|
|
156
|
+
var subscriptionSchema = z.object({
|
|
157
|
+
plan: z.string().trim().min(1).max(120).optional(),
|
|
158
|
+
paidAt: subscriptionDate.optional(),
|
|
159
|
+
renewsAt: subscriptionDate.optional(),
|
|
160
|
+
price: z.object({
|
|
161
|
+
amount: z.number().min(0).max(1e6),
|
|
162
|
+
currency: z.string().trim().regex(/^[A-Za-z]{3}$/, "expected a 3-letter currency code").transform((c) => c.toUpperCase()),
|
|
163
|
+
period: z.enum(["month", "year"])
|
|
164
|
+
}).strict().optional(),
|
|
165
|
+
notes: z.string().trim().max(1e3).optional()
|
|
166
|
+
}).strict();
|
|
167
|
+
var accountFields = {
|
|
168
|
+
label: z.string().trim().min(1).max(200),
|
|
169
|
+
enabled: z.boolean().optional(),
|
|
170
|
+
priority: z.number().int().min(0).max(1e6).optional(),
|
|
171
|
+
weight: z.number().min(0).max(1e3).optional(),
|
|
172
|
+
limits: quotaLimits.optional(),
|
|
173
|
+
config: accountConfig.optional(),
|
|
174
|
+
/** API key / token. Encrypted at rest and never returned. */
|
|
175
|
+
secret: z.string().min(1).max(1e5).optional()
|
|
176
|
+
};
|
|
177
|
+
var accountCreateSchema = z.object({ provider: providerKind, ...accountFields, subscription: subscriptionSchema.optional() }).strict();
|
|
178
|
+
var accountPatchSchema = z.object({
|
|
179
|
+
...accountFields,
|
|
180
|
+
label: accountFields.label.optional(),
|
|
181
|
+
subscription: subscriptionSchema.nullable().optional()
|
|
182
|
+
}).strict();
|
|
183
|
+
var routesUpdateSchema = z.object({ routes: z.array(routeSchema).max(200), defaultRoute: z.string().min(1).optional() }).strict().superRefine((body, ctx) => {
|
|
184
|
+
const seen = /* @__PURE__ */ new Set();
|
|
185
|
+
body.routes.forEach((route, index) => {
|
|
186
|
+
if (seen.has(route.name)) {
|
|
187
|
+
ctx.addIssue({
|
|
188
|
+
code: "custom",
|
|
189
|
+
path: ["routes", index, "name"],
|
|
190
|
+
message: `duplicate route name "${route.name}"`
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
seen.add(route.name);
|
|
194
|
+
});
|
|
195
|
+
if (body.defaultRoute !== void 0 && !seen.has(body.defaultRoute)) {
|
|
196
|
+
ctx.addIssue({
|
|
197
|
+
code: "custom",
|
|
198
|
+
path: ["defaultRoute"],
|
|
199
|
+
message: `defaultRoute "${body.defaultRoute}" is not one of the routes`
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
var taskText = {
|
|
204
|
+
title: z.string().trim().min(1).max(300),
|
|
205
|
+
description: z.string().max(5e4),
|
|
206
|
+
acceptance: z.array(z.string().trim().min(1).max(2e3)).max(100),
|
|
207
|
+
notes: z.string().max(5e4),
|
|
208
|
+
dependsOn: z.array(z.string().min(1)).max(500),
|
|
209
|
+
priority: z.number().finite()
|
|
210
|
+
};
|
|
211
|
+
var taskCreateSchema = z.object({
|
|
212
|
+
id: taskIdSchema,
|
|
213
|
+
title: taskText.title,
|
|
214
|
+
description: taskText.description.optional(),
|
|
215
|
+
dependsOn: taskText.dependsOn.optional(),
|
|
216
|
+
priority: taskText.priority.optional(),
|
|
217
|
+
acceptance: taskText.acceptance.optional()
|
|
218
|
+
}).strict();
|
|
219
|
+
var taskPatchSchema = z.object({
|
|
220
|
+
title: taskText.title.optional(),
|
|
221
|
+
description: taskText.description.nullable().optional(),
|
|
222
|
+
dependsOn: taskText.dependsOn.optional(),
|
|
223
|
+
priority: taskText.priority.nullable().optional(),
|
|
224
|
+
acceptance: taskText.acceptance.nullable().optional(),
|
|
225
|
+
notes: taskText.notes.nullable().optional(),
|
|
226
|
+
status: z.enum(TASK_STATUSES).optional()
|
|
227
|
+
}).strict().refine((patch) => Object.values(patch).some((v) => v !== void 0), {
|
|
228
|
+
message: "send at least one field to change"
|
|
229
|
+
});
|
|
230
|
+
var runnerStartSchema = z.object({ taskId: z.string().min(1).optional() }).strict();
|
|
231
|
+
var goalStartSchema = z.object({
|
|
232
|
+
objective: z.string().trim().min(1).max(4e3),
|
|
233
|
+
maxRounds: z.number().int().min(1).max(20).optional(),
|
|
234
|
+
maxTasks: z.number().int().min(1).max(100).optional(),
|
|
235
|
+
maxTokens: z.number().int().min(1e3).optional(),
|
|
236
|
+
dryRun: z.boolean().optional()
|
|
237
|
+
}).strict();
|
|
238
|
+
var timeseriesQuerySchema = z.object({
|
|
239
|
+
minutes: z.coerce.number().int().min(1).max(7 * 24 * 60).default(60),
|
|
240
|
+
bucketSec: z.coerce.number().int().min(1).max(86400).default(60)
|
|
241
|
+
});
|
|
242
|
+
var limitQuerySchema = (fallback, max = 1e3) => z.object({ limit: z.coerce.number().int().min(1).max(max).default(fallback) });
|
|
243
|
+
function registerGoalRoutes(app, engine, options) {
|
|
244
|
+
const { audit } = engine;
|
|
245
|
+
const unavailable = (reply) => sendApiError(
|
|
246
|
+
reply,
|
|
247
|
+
501,
|
|
248
|
+
"goals_unavailable",
|
|
249
|
+
"Goal mode is not available; start the gateway inside a project (davecode init)"
|
|
250
|
+
);
|
|
251
|
+
const guarded = async (reply, run) => {
|
|
252
|
+
try {
|
|
253
|
+
return { ok: true, value: await run() };
|
|
254
|
+
} catch (error) {
|
|
255
|
+
const { statusCode, code } = error;
|
|
256
|
+
if (typeof statusCode === "number" && statusCode < 500 && typeof code === "string") {
|
|
257
|
+
return {
|
|
258
|
+
ok: false,
|
|
259
|
+
reply: sendApiError(reply, statusCode, code, error.message)
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
throw error;
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
const control = (reply) => {
|
|
266
|
+
if (!options.goals) unavailable(reply);
|
|
267
|
+
return options.goals;
|
|
268
|
+
};
|
|
269
|
+
app.get("/api/goals", async (_request, reply) => {
|
|
270
|
+
const goals = control(reply);
|
|
271
|
+
if (!goals) return reply;
|
|
272
|
+
return { goals: await goals.list() };
|
|
273
|
+
});
|
|
274
|
+
app.get("/api/goals/:id", async (request, reply) => {
|
|
275
|
+
const goals = control(reply);
|
|
276
|
+
if (!goals) return reply;
|
|
277
|
+
const goal = await goals.get(request.params.id);
|
|
278
|
+
if (!goal) return sendApiError(reply, 404, "not_found", `unknown goal "${request.params.id}"`);
|
|
279
|
+
return { goal };
|
|
280
|
+
});
|
|
281
|
+
app.post("/api/goals", async (request, reply) => {
|
|
282
|
+
const parsed = goalStartSchema.safeParse(request.body);
|
|
283
|
+
if (!parsed.success) {
|
|
284
|
+
return sendApiError(reply, 400, "invalid_body", z2.prettifyError(parsed.error));
|
|
285
|
+
}
|
|
286
|
+
const goals = control(reply);
|
|
287
|
+
if (!goals) return reply;
|
|
288
|
+
const input = parsed.data;
|
|
289
|
+
const result = await guarded(reply, () => goals.start(input));
|
|
290
|
+
if (!result.ok) return result.reply;
|
|
291
|
+
audit.record({
|
|
292
|
+
actor: "api",
|
|
293
|
+
action: "goal.start",
|
|
294
|
+
target: result.value.id,
|
|
295
|
+
details: { dryRun: input.dryRun === true }
|
|
296
|
+
});
|
|
297
|
+
return reply.code(202).send({ goal: result.value });
|
|
298
|
+
});
|
|
299
|
+
app.post("/api/goals/:id/execute", async (request, reply) => {
|
|
300
|
+
const goals = control(reply);
|
|
301
|
+
if (!goals) return reply;
|
|
302
|
+
if (!goals.execute) return unavailable(reply);
|
|
303
|
+
const execute = goals.execute.bind(goals);
|
|
304
|
+
const result = await guarded(reply, () => execute(request.params.id));
|
|
305
|
+
if (!result.ok) return result.reply;
|
|
306
|
+
audit.record({ actor: "api", action: "goal.execute", target: request.params.id });
|
|
307
|
+
return reply.code(202).send({ goal: result.value });
|
|
308
|
+
});
|
|
309
|
+
app.get(
|
|
310
|
+
"/api/goals/:id/artifacts/:taskId",
|
|
311
|
+
async (request, reply) => {
|
|
312
|
+
const goals = control(reply);
|
|
313
|
+
if (!goals) return reply;
|
|
314
|
+
if (!goals.readArtifact) return unavailable(reply);
|
|
315
|
+
const readArtifact = goals.readArtifact.bind(goals);
|
|
316
|
+
const result = await guarded(
|
|
317
|
+
reply,
|
|
318
|
+
() => readArtifact(request.params.id, request.params.taskId)
|
|
319
|
+
);
|
|
320
|
+
if (!result.ok) return result.reply;
|
|
321
|
+
return { artifact: result.value };
|
|
322
|
+
}
|
|
323
|
+
);
|
|
324
|
+
app.post("/api/goals/:id/cancel", async (request, reply) => {
|
|
325
|
+
const goals = control(reply);
|
|
326
|
+
if (!goals) return reply;
|
|
327
|
+
const result = await guarded(reply, () => goals.cancel(request.params.id));
|
|
328
|
+
if (!result.ok) return result.reply;
|
|
329
|
+
audit.record({ actor: "api", action: "goal.cancel", target: request.params.id });
|
|
330
|
+
return { goal: result.value };
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
function describeIssue(issue) {
|
|
334
|
+
switch (issue.code) {
|
|
335
|
+
case "unknown_task":
|
|
336
|
+
return { status: 404, code: "not_found", extra: {} };
|
|
337
|
+
case "duplicate_id":
|
|
338
|
+
case "invalid_transition":
|
|
339
|
+
case "task_in_progress":
|
|
340
|
+
return { status: 409, code: issue.code, extra: {} };
|
|
341
|
+
case "has_dependents":
|
|
342
|
+
return { status: 409, code: "has_dependents", extra: { dependents: issue.dependents ?? [] } };
|
|
343
|
+
case "cycle":
|
|
344
|
+
return { status: 400, code: "cycle", extra: { cycle: issue.cycle ?? [] } };
|
|
345
|
+
case "unknown_dependency":
|
|
346
|
+
case "self_dependency":
|
|
347
|
+
return { status: 400, code: issue.code, extra: {} };
|
|
348
|
+
default:
|
|
349
|
+
return { status: 409, code: "invalid_graph", extra: {} };
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
function sendGraphError(reply, error) {
|
|
353
|
+
const issue = error.issues.find((i) => i.code === "cycle") ?? error.issues[0];
|
|
354
|
+
if (!issue) return sendApiError(reply, 400, "invalid_graph", error.message);
|
|
355
|
+
const { status, code, extra } = describeIssue(issue);
|
|
356
|
+
return sendApiError(reply, status, code, issue.message, extra);
|
|
357
|
+
}
|
|
358
|
+
function registerTaskRoutes(app, engine, options) {
|
|
359
|
+
const { audit } = engine;
|
|
360
|
+
const writable = (reply) => {
|
|
361
|
+
const { brain } = options;
|
|
362
|
+
if (!brain) {
|
|
363
|
+
return sendApiError(
|
|
364
|
+
reply,
|
|
365
|
+
409,
|
|
366
|
+
"no_brain",
|
|
367
|
+
"No project brain is loaded; start the gateway inside a project (davecode init)"
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
return brain;
|
|
371
|
+
};
|
|
372
|
+
const guarded = async (reply, run) => {
|
|
373
|
+
try {
|
|
374
|
+
return await run();
|
|
375
|
+
} catch (error) {
|
|
376
|
+
if (error instanceof TaskGraphError) return sendGraphError(reply, error);
|
|
377
|
+
const { statusCode, code } = error;
|
|
378
|
+
if (typeof statusCode === "number" && statusCode < 500 && typeof code === "string") {
|
|
379
|
+
return sendApiError(reply, statusCode, code, error.message);
|
|
380
|
+
}
|
|
381
|
+
throw error;
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
const readOnly = (reply) => sendApiError(reply, 501, "brain_read_only", "The project brain of this gateway is read-only");
|
|
385
|
+
app.post("/api/tasks", async (request, reply) => {
|
|
386
|
+
const parsed = taskCreateSchema.safeParse(request.body);
|
|
387
|
+
if (!parsed.success) {
|
|
388
|
+
return sendApiError(reply, 400, "invalid_body", z3.prettifyError(parsed.error));
|
|
389
|
+
}
|
|
390
|
+
const brain = writable(reply);
|
|
391
|
+
if (isReply(brain)) return brain;
|
|
392
|
+
if (!brain.createTask) return readOnly(reply);
|
|
393
|
+
const input = parsed.data;
|
|
394
|
+
const create = brain.createTask.bind(brain);
|
|
395
|
+
const result = await guarded(reply, () => create(input));
|
|
396
|
+
if (isReply(result)) return result;
|
|
397
|
+
audit.record({ actor: "api", action: "task.create", target: input.id });
|
|
398
|
+
return reply.code(201).send({ task: result });
|
|
399
|
+
});
|
|
400
|
+
app.patch("/api/tasks/:id", async (request, reply) => {
|
|
401
|
+
const parsed = taskPatchSchema.safeParse(request.body);
|
|
402
|
+
if (!parsed.success) {
|
|
403
|
+
return sendApiError(reply, 400, "invalid_body", z3.prettifyError(parsed.error));
|
|
404
|
+
}
|
|
405
|
+
const brain = writable(reply);
|
|
406
|
+
if (isReply(brain)) return brain;
|
|
407
|
+
if (!brain.updateTask) return readOnly(reply);
|
|
408
|
+
const { description, priority, acceptance, notes, ...rest } = parsed.data;
|
|
409
|
+
const patch = { ...rest };
|
|
410
|
+
const clear = [];
|
|
411
|
+
const optional = { description, priority, acceptance, notes };
|
|
412
|
+
for (const [key, value] of Object.entries(optional)) {
|
|
413
|
+
if (value === null) clear.push(key);
|
|
414
|
+
else if (value !== void 0) patch[key] = value;
|
|
415
|
+
}
|
|
416
|
+
const { id } = request.params;
|
|
417
|
+
const update = brain.updateTask.bind(brain);
|
|
418
|
+
const result = await guarded(reply, () => update(id, patch, { clear }));
|
|
419
|
+
if (isReply(result)) return result;
|
|
420
|
+
audit.record({
|
|
421
|
+
actor: "api",
|
|
422
|
+
action: "task.update",
|
|
423
|
+
target: id,
|
|
424
|
+
details: { fields: Object.keys(parsed.data) }
|
|
425
|
+
});
|
|
426
|
+
return { task: result };
|
|
427
|
+
});
|
|
428
|
+
app.delete("/api/tasks/:id", async (request, reply) => {
|
|
429
|
+
const brain = writable(reply);
|
|
430
|
+
if (isReply(brain)) return brain;
|
|
431
|
+
if (!brain.removeTask) return readOnly(reply);
|
|
432
|
+
const { id } = request.params;
|
|
433
|
+
const remove = brain.removeTask.bind(brain);
|
|
434
|
+
const result = await guarded(reply, () => remove(id));
|
|
435
|
+
if (isReply(result)) return result;
|
|
436
|
+
audit.record({ actor: "api", action: "task.delete", target: id });
|
|
437
|
+
return reply.code(204).send();
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
function isReply(value) {
|
|
441
|
+
return typeof value === "object" && value !== null && "sent" in value && typeof value.code === "function";
|
|
442
|
+
}
|
|
443
|
+
var EMPTY_GRAPH = { version: 1, tasks: [] };
|
|
444
|
+
var IDLE = { state: "idle" };
|
|
445
|
+
function invalid(reply, error) {
|
|
446
|
+
return sendApiError(reply, 400, "invalid_body", z4.prettifyError(error));
|
|
447
|
+
}
|
|
448
|
+
function registerApiRoutes(app, engine, options) {
|
|
449
|
+
const { accounts, events, audit, quota, keyring } = engine;
|
|
450
|
+
const now = () => quota.clock();
|
|
451
|
+
const emitAccount = (account) => events.emit({ type: "account.updated", account });
|
|
452
|
+
app.get("/api/health", async () => ({
|
|
453
|
+
status: "ok",
|
|
454
|
+
version: VERSION,
|
|
455
|
+
uptimeSec: Math.max(0, Math.floor((now() - engine.startedAt) / 1e3)),
|
|
456
|
+
experimental: { ...engine.config.experimental }
|
|
457
|
+
}));
|
|
458
|
+
app.get("/api/accounts", async () => ({ accounts: accounts.list() }));
|
|
459
|
+
app.post("/api/accounts", async (request, reply) => {
|
|
460
|
+
const parsed = accountCreateSchema.safeParse(request.body);
|
|
461
|
+
if (!parsed.success) return invalid(reply, parsed.error);
|
|
462
|
+
const { secret, ...input } = parsed.data;
|
|
463
|
+
if (input.provider === "gemini-web" && !engine.config.experimental.geminiWeb) {
|
|
464
|
+
return sendApiError(
|
|
465
|
+
reply,
|
|
466
|
+
400,
|
|
467
|
+
"experimental_disabled",
|
|
468
|
+
"gemini-web is experimental and may violate Google\u2019s Terms of Service. Set experimental.geminiWeb to true to opt in."
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
const account = engine.db.transaction(() => {
|
|
472
|
+
const created = accounts.create(input);
|
|
473
|
+
if (secret !== void 0) keyring.set(created.id, secret);
|
|
474
|
+
audit.record({
|
|
475
|
+
actor: "api",
|
|
476
|
+
action: "account.create",
|
|
477
|
+
target: created.id,
|
|
478
|
+
details: { provider: created.provider, label: created.label, hasSecret: !!secret }
|
|
479
|
+
});
|
|
480
|
+
return accounts.get(created.id) ?? created;
|
|
481
|
+
})();
|
|
482
|
+
emitAccount(account);
|
|
483
|
+
return reply.code(201).send({ account });
|
|
484
|
+
});
|
|
485
|
+
app.patch("/api/accounts/:id", async (request, reply) => {
|
|
486
|
+
const parsed = accountPatchSchema.safeParse(request.body);
|
|
487
|
+
if (!parsed.success) return invalid(reply, parsed.error);
|
|
488
|
+
const existing = accounts.get(request.params.id);
|
|
489
|
+
if (!existing) return sendApiError(reply, 404, "not_found", "Account not found");
|
|
490
|
+
const { secret, ...fields } = parsed.data;
|
|
491
|
+
const patch = { ...fields };
|
|
492
|
+
const enabled = fields.enabled ?? existing.enabled;
|
|
493
|
+
if (!enabled) {
|
|
494
|
+
patch.status = "disabled";
|
|
495
|
+
} else if (fields.enabled === true || secret !== void 0) {
|
|
496
|
+
patch.status = "active";
|
|
497
|
+
patch.cooldownUntil = null;
|
|
498
|
+
patch.lastError = null;
|
|
499
|
+
engine.router.breaker.reset(existing.id);
|
|
500
|
+
engine.reported.forget(existing.id);
|
|
501
|
+
}
|
|
502
|
+
const account = engine.db.transaction(() => {
|
|
503
|
+
if (secret !== void 0) keyring.set(existing.id, secret);
|
|
504
|
+
const updated = accounts.update(existing.id, patch);
|
|
505
|
+
audit.record({
|
|
506
|
+
actor: "api",
|
|
507
|
+
action: "account.update",
|
|
508
|
+
target: existing.id,
|
|
509
|
+
details: { fields: Object.keys(fields), secretChanged: secret !== void 0 }
|
|
510
|
+
});
|
|
511
|
+
return updated;
|
|
512
|
+
})();
|
|
513
|
+
emitAccount(account);
|
|
514
|
+
return { account };
|
|
515
|
+
});
|
|
516
|
+
app.delete("/api/accounts/:id", async (request, reply) => {
|
|
517
|
+
const { id } = request.params;
|
|
518
|
+
if (!accounts.delete(id)) return sendApiError(reply, 404, "not_found", "Account not found");
|
|
519
|
+
for (const cleanup of [() => engine.sandboxes.remove(id), () => engine.chromium.remove(id)]) {
|
|
520
|
+
try {
|
|
521
|
+
cleanup();
|
|
522
|
+
} catch (err) {
|
|
523
|
+
request.log.warn({ err, accountId: id }, "failed to remove account directory");
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
quota.forget(id);
|
|
527
|
+
engine.reported.forget(id);
|
|
528
|
+
engine.router.breaker.reset(id);
|
|
529
|
+
audit.record({ actor: "api", action: "account.delete", target: id });
|
|
530
|
+
events.emit({ type: "account.removed", accountId: id });
|
|
531
|
+
return reply.code(204).send();
|
|
532
|
+
});
|
|
533
|
+
app.get("/api/accounts/:id/connection", async (request, reply) => {
|
|
534
|
+
const account = accounts.get(request.params.id);
|
|
535
|
+
if (!account) return sendApiError(reply, 404, "not_found", "Account not found");
|
|
536
|
+
return { connection: await engine.connections.check(account) };
|
|
537
|
+
});
|
|
538
|
+
app.post("/api/accounts/:id/connect", async (request, reply) => {
|
|
539
|
+
if (!request.headers["content-type"]?.toLowerCase().startsWith("application/json")) {
|
|
540
|
+
return sendApiError(reply, 415, "json_required", "Send Content-Type: application/json");
|
|
541
|
+
}
|
|
542
|
+
const account = accounts.get(request.params.id);
|
|
543
|
+
if (!account) return sendApiError(reply, 404, "not_found", "Account not found");
|
|
544
|
+
if (connectionMethod(account.provider) !== "cli-login") {
|
|
545
|
+
return sendApiError(
|
|
546
|
+
reply,
|
|
547
|
+
400,
|
|
548
|
+
"not_cli_login",
|
|
549
|
+
account.provider === "gemini-web" ? "gemini-web signs in through its own browser profile, not a CLI login" : "This account connects with an API key: set it in the account settings"
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
const command = loginCommandFor(account.id);
|
|
553
|
+
const { launched } = options.loginLauncher ? await options.loginLauncher.launch(account) : { launched: false };
|
|
554
|
+
audit.record({
|
|
555
|
+
actor: "api",
|
|
556
|
+
action: "account.connect",
|
|
557
|
+
target: account.id,
|
|
558
|
+
details: { launched }
|
|
559
|
+
});
|
|
560
|
+
const result = {
|
|
561
|
+
connection: await engine.connections.check(account),
|
|
562
|
+
launched,
|
|
563
|
+
command,
|
|
564
|
+
message: launched ? "A terminal opened on the gateway machine with the official sign-in. Finish it there; this page updates when the login is detected." : `Run this in a terminal on the machine running the gateway: ${command}`
|
|
565
|
+
};
|
|
566
|
+
return { connect: result };
|
|
567
|
+
});
|
|
568
|
+
app.get("/api/usage", async () => ({
|
|
569
|
+
usage: accounts.list().map((account) => quota.usage(account))
|
|
570
|
+
}));
|
|
571
|
+
app.get("/api/budget", async () => ({ budget: engine.budget.snapshot() }));
|
|
572
|
+
app.get("/api/usage/timeseries", async (request, reply) => {
|
|
573
|
+
const parsed = timeseriesQuerySchema.safeParse(request.query);
|
|
574
|
+
if (!parsed.success) return invalid(reply, parsed.error);
|
|
575
|
+
const { minutes, bucketSec } = parsed.data;
|
|
576
|
+
const until = now() + 1;
|
|
577
|
+
const buckets = engine.usage.timeseries({
|
|
578
|
+
since: until - minutes * 6e4,
|
|
579
|
+
until,
|
|
580
|
+
bucketMs: bucketSec * 1e3
|
|
581
|
+
});
|
|
582
|
+
return { buckets };
|
|
583
|
+
});
|
|
584
|
+
app.get("/api/requests", async (request, reply) => {
|
|
585
|
+
const parsed = limitQuerySchema(100).safeParse(request.query);
|
|
586
|
+
if (!parsed.success) return invalid(reply, parsed.error);
|
|
587
|
+
return { requests: engine.usage.recent(parsed.data.limit) };
|
|
588
|
+
});
|
|
589
|
+
app.get("/api/logs", async (request, reply) => {
|
|
590
|
+
const parsed = limitQuerySchema(200).safeParse(request.query);
|
|
591
|
+
if (!parsed.success) return invalid(reply, parsed.error);
|
|
592
|
+
return { events: events.recent(parsed.data.limit) };
|
|
593
|
+
});
|
|
594
|
+
app.get("/api/routes", async () => ({
|
|
595
|
+
defaultRoute: engine.config.routing.defaultRoute,
|
|
596
|
+
routes: engine.config.routing.routes
|
|
597
|
+
}));
|
|
598
|
+
app.put("/api/routes", async (request, reply) => {
|
|
599
|
+
const parsed = routesUpdateSchema.safeParse(request.body);
|
|
600
|
+
if (!parsed.success) return invalid(reply, parsed.error);
|
|
601
|
+
const { routes, defaultRoute } = parsed.data;
|
|
602
|
+
for (const route of routes) {
|
|
603
|
+
for (const target of route.targets) {
|
|
604
|
+
if (target.accountId !== void 0 && !accounts.get(target.accountId)) {
|
|
605
|
+
return sendApiError(
|
|
606
|
+
reply,
|
|
607
|
+
400,
|
|
608
|
+
"unknown_account",
|
|
609
|
+
`route "${route.name}" pins unknown account "${target.accountId}"`
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
let result;
|
|
615
|
+
try {
|
|
616
|
+
result = await engine.updateRouting({
|
|
617
|
+
routes,
|
|
618
|
+
...defaultRoute !== void 0 ? { defaultRoute } : {}
|
|
619
|
+
});
|
|
620
|
+
} catch (error) {
|
|
621
|
+
if (error instanceof ConfigError) {
|
|
622
|
+
return sendApiError(reply, 409, "config_invalid", error.message);
|
|
623
|
+
}
|
|
624
|
+
throw error;
|
|
625
|
+
}
|
|
626
|
+
audit.record({
|
|
627
|
+
actor: "api",
|
|
628
|
+
action: "routes.update",
|
|
629
|
+
details: {
|
|
630
|
+
routes: routes.map((r) => r.name),
|
|
631
|
+
defaultRoute: engine.config.routing.defaultRoute
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
return {
|
|
635
|
+
defaultRoute: engine.config.routing.defaultRoute,
|
|
636
|
+
routes: engine.config.routing.routes,
|
|
637
|
+
shadowedByProject: result.shadowedByProject
|
|
638
|
+
};
|
|
639
|
+
});
|
|
640
|
+
app.get("/api/tasks", async () => {
|
|
641
|
+
const { brain } = options;
|
|
642
|
+
if (!brain) return { project: null, graph: EMPTY_GRAPH };
|
|
643
|
+
return { project: brain.project(), graph: await brain.graph() };
|
|
644
|
+
});
|
|
645
|
+
registerTaskRoutes(app, engine, options);
|
|
646
|
+
registerGoalRoutes(app, engine, options);
|
|
647
|
+
app.get("/api/brain", async () => {
|
|
648
|
+
const { brain } = options;
|
|
649
|
+
if (!brain) return { state: "", architecture: "" };
|
|
650
|
+
const [state, architecture] = await Promise.all([brain.state(), brain.architecture()]);
|
|
651
|
+
return { state, architecture };
|
|
652
|
+
});
|
|
653
|
+
app.get("/api/runner", async () => ({ status: options.runner?.status() ?? IDLE }));
|
|
654
|
+
const control = (action) => async function handler(request, reply) {
|
|
655
|
+
const { runner } = options;
|
|
656
|
+
if (!runner) {
|
|
657
|
+
return sendApiError(
|
|
658
|
+
reply,
|
|
659
|
+
501,
|
|
660
|
+
"runner_unavailable",
|
|
661
|
+
"The autonomous runner is not available"
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
let taskId;
|
|
665
|
+
if (action === "start" && request.body !== void 0 && request.body !== null) {
|
|
666
|
+
const parsed = runnerStartSchema.safeParse(request.body);
|
|
667
|
+
if (!parsed.success) return invalid(reply, parsed.error);
|
|
668
|
+
taskId = parsed.data.taskId;
|
|
669
|
+
}
|
|
670
|
+
try {
|
|
671
|
+
if (action === "start") await runner.start(taskId === void 0 ? {} : { taskId });
|
|
672
|
+
else await runner[action]();
|
|
673
|
+
} catch (error) {
|
|
674
|
+
const { statusCode, code } = error;
|
|
675
|
+
if (typeof statusCode === "number" && statusCode < 500 && typeof code === "string") {
|
|
676
|
+
return sendApiError(reply, statusCode, code, error.message);
|
|
677
|
+
}
|
|
678
|
+
throw error;
|
|
679
|
+
}
|
|
680
|
+
audit.record({
|
|
681
|
+
actor: "api",
|
|
682
|
+
action: `runner.${action}`,
|
|
683
|
+
...taskId !== void 0 ? { details: { taskId } } : {}
|
|
684
|
+
});
|
|
685
|
+
return { status: runner.status() };
|
|
686
|
+
};
|
|
687
|
+
app.post("/api/runner/start", control("start"));
|
|
688
|
+
app.post("/api/runner/pause", control("pause"));
|
|
689
|
+
app.post("/api/runner/stop", control("stop"));
|
|
690
|
+
}
|
|
691
|
+
function authCookie(request, name) {
|
|
692
|
+
return request.headers.cookie?.split(";").map((part) => part.trim()).find((part) => part.startsWith(`${name}=`))?.slice(name.length + 1);
|
|
693
|
+
}
|
|
694
|
+
function cookie(name, value, seconds, secure) {
|
|
695
|
+
return `${name}=${value}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${seconds}${secure ? "; Secure" : ""}`;
|
|
696
|
+
}
|
|
697
|
+
function registerAuthRoutes(app, login) {
|
|
698
|
+
const secure = login?.options.origin.startsWith("https:") ?? false;
|
|
699
|
+
app.get("/api/auth/session", async (request, reply) => {
|
|
700
|
+
reply.header("Cache-Control", "no-store");
|
|
701
|
+
return login?.status(authCookie(request, "dc_session")) ?? {
|
|
702
|
+
required: false,
|
|
703
|
+
providers: [],
|
|
704
|
+
user: null
|
|
705
|
+
};
|
|
706
|
+
});
|
|
707
|
+
app.post("/api/auth/logout", async (request, reply) => {
|
|
708
|
+
if (login && request.headers.origin !== login.options.origin)
|
|
709
|
+
return sendApiError(
|
|
710
|
+
reply,
|
|
711
|
+
403,
|
|
712
|
+
"invalid_origin",
|
|
713
|
+
"Open DaveCode from its configured origin."
|
|
714
|
+
);
|
|
715
|
+
login?.logout(authCookie(request, "dc_session"));
|
|
716
|
+
return reply.header("Cache-Control", "no-store").header("Set-Cookie", cookie("dc_session", "", 0, secure)).code(204).send();
|
|
717
|
+
});
|
|
718
|
+
for (const provider of ["google", "github"]) {
|
|
719
|
+
app.get(`/api/auth/${provider}/start`, async (_request, reply) => {
|
|
720
|
+
if (!login?.options.providers[provider])
|
|
721
|
+
return sendApiError(
|
|
722
|
+
reply,
|
|
723
|
+
503,
|
|
724
|
+
"login_not_configured",
|
|
725
|
+
"This sign-in provider is not configured."
|
|
726
|
+
);
|
|
727
|
+
try {
|
|
728
|
+
const flow = login.begin(provider);
|
|
729
|
+
return reply.header("Cache-Control", "no-store").header("Referrer-Policy", "no-referrer").header("Set-Cookie", cookie("dc_login", flow.binding, 600, secure)).redirect(flow.url);
|
|
730
|
+
} catch {
|
|
731
|
+
return sendApiError(reply, 429, "login_busy", "Sign-in is busy. Try again shortly.");
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
app.get(`/api/auth/${provider}/callback`, async (request, reply) => {
|
|
735
|
+
reply.header("Cache-Control", "no-store").header("Referrer-Policy", "no-referrer");
|
|
736
|
+
if (!login)
|
|
737
|
+
return sendApiError(reply, 503, "login_not_configured", "Sign-in is not configured.");
|
|
738
|
+
const parsed = z5.object({ state: z5.string().min(1).max(200), code: z5.string().min(1).max(4096) }).safeParse(request.query);
|
|
739
|
+
const binding = authCookie(request, "dc_login");
|
|
740
|
+
if (!parsed.success || !binding)
|
|
741
|
+
return reply.redirect(`${login.options.origin}/#/login?error=signin_failed`);
|
|
742
|
+
try {
|
|
743
|
+
const session = await login.finish(provider, parsed.data.state, parsed.data.code, binding);
|
|
744
|
+
login.logout(authCookie(request, "dc_session"));
|
|
745
|
+
return reply.header("Set-Cookie", [
|
|
746
|
+
cookie("dc_session", session, 8 * 60 * 60, secure),
|
|
747
|
+
cookie("dc_login", "", 0, secure)
|
|
748
|
+
]).redirect(`${login.options.origin}/`);
|
|
749
|
+
} catch {
|
|
750
|
+
return reply.header("Set-Cookie", cookie("dc_login", "", 0, secure)).redirect(`${login.options.origin}/#/login?error=signin_failed`);
|
|
751
|
+
}
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
function frame(event, seq) {
|
|
756
|
+
return `id: ${seq}
|
|
757
|
+
event: ${event.type}
|
|
758
|
+
data: ${JSON.stringify(event)}
|
|
759
|
+
|
|
760
|
+
`;
|
|
761
|
+
}
|
|
762
|
+
function resumeCursor(headers, query) {
|
|
763
|
+
const fromQuery = typeof query === "object" && query !== null && "lastEventId" in query ? query.lastEventId : void 0;
|
|
764
|
+
const raw = headers["last-event-id"] ?? fromQuery;
|
|
765
|
+
const value = Number(Array.isArray(raw) ? raw[0] : raw);
|
|
766
|
+
return Number.isSafeInteger(value) && value >= 0 ? value : 0;
|
|
767
|
+
}
|
|
768
|
+
function registerEventRoutes(app, engine, heartbeatMs) {
|
|
769
|
+
const clients = /* @__PURE__ */ new Set();
|
|
770
|
+
app.addHook("preClose", async () => {
|
|
771
|
+
for (const res of clients) res.end();
|
|
772
|
+
clients.clear();
|
|
773
|
+
});
|
|
774
|
+
app.get("/api/events", (request, reply) => {
|
|
775
|
+
const cursor = resumeCursor(request.headers, request.query);
|
|
776
|
+
reply.hijack();
|
|
777
|
+
const res = reply.raw;
|
|
778
|
+
const inherited = {};
|
|
779
|
+
for (const [name, value] of Object.entries(reply.getHeaders())) {
|
|
780
|
+
if (value !== void 0) inherited[name] = value;
|
|
781
|
+
}
|
|
782
|
+
res.writeHead(200, {
|
|
783
|
+
...inherited,
|
|
784
|
+
"content-type": "text/event-stream; charset=utf-8",
|
|
785
|
+
"cache-control": "no-cache, no-transform",
|
|
786
|
+
connection: "keep-alive",
|
|
787
|
+
"x-accel-buffering": "no"
|
|
788
|
+
});
|
|
789
|
+
res.write("retry: 3000\n\n");
|
|
790
|
+
clients.add(res);
|
|
791
|
+
for (const { seq, event } of engine.events.since(cursor)) res.write(frame(event, seq));
|
|
792
|
+
const unsubscribe = engine.events.subscribe((event, seq) => {
|
|
793
|
+
res.write(frame(event, seq));
|
|
794
|
+
});
|
|
795
|
+
const heartbeat = setInterval(() => res.write(": ping\n\n"), heartbeatMs);
|
|
796
|
+
heartbeat.unref();
|
|
797
|
+
let closed = false;
|
|
798
|
+
const cleanup = () => {
|
|
799
|
+
if (closed) return;
|
|
800
|
+
closed = true;
|
|
801
|
+
clearInterval(heartbeat);
|
|
802
|
+
unsubscribe();
|
|
803
|
+
clients.delete(res);
|
|
804
|
+
};
|
|
805
|
+
res.on("close", cleanup);
|
|
806
|
+
res.on("error", cleanup);
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
function setRoutingHeaders(reply, meta) {
|
|
810
|
+
reply.header("x-davecode-request-id", meta.requestId).header("x-davecode-account", meta.accountId).header("x-davecode-provider", meta.provider).header("x-davecode-failovers", String(meta.failovers));
|
|
811
|
+
}
|
|
812
|
+
function sendRoutingError(reply, err) {
|
|
813
|
+
const { status, body } = describeRoutingError(err);
|
|
814
|
+
if (status === 500) reply.log.error({ err }, "chat completion failed");
|
|
815
|
+
if (err instanceof Error && "failovers" in err && typeof err.failovers === "number") {
|
|
816
|
+
reply.header("x-davecode-failovers", String(err.failovers));
|
|
817
|
+
}
|
|
818
|
+
return sendOpenAIError(reply, status, body);
|
|
819
|
+
}
|
|
820
|
+
async function* sseFrames(chunks) {
|
|
821
|
+
try {
|
|
822
|
+
for await (const chunk of chunks) yield `data: ${JSON.stringify(chunk)}
|
|
823
|
+
|
|
824
|
+
`;
|
|
825
|
+
} catch (err) {
|
|
826
|
+
yield `data: ${JSON.stringify({ error: describeRoutingError(err).body })}
|
|
827
|
+
|
|
828
|
+
`;
|
|
829
|
+
}
|
|
830
|
+
yield "data: [DONE]\n\n";
|
|
831
|
+
}
|
|
832
|
+
function registerV1Routes(app, engine) {
|
|
833
|
+
app.get("/v1/models", async () => ({ object: "list", data: await engine.router.listModels() }));
|
|
834
|
+
app.post("/v1/chat/completions", async (request, reply) => {
|
|
835
|
+
const parsed = chatRequestSchema.safeParse(request.body);
|
|
836
|
+
if (!parsed.success) {
|
|
837
|
+
return sendOpenAIError(reply, 400, {
|
|
838
|
+
message: z6.prettifyError(parsed.error),
|
|
839
|
+
type: "invalid_request_error",
|
|
840
|
+
code: "invalid_body"
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
const body = parsed.data;
|
|
844
|
+
const requestId = newId2("req");
|
|
845
|
+
reply.header("x-davecode-request-id", requestId);
|
|
846
|
+
const abort = new AbortController();
|
|
847
|
+
reply.raw.on("close", () => {
|
|
848
|
+
if (!reply.raw.writableFinished) abort.abort();
|
|
849
|
+
});
|
|
850
|
+
const routeOptions = { requestId, signal: abort.signal };
|
|
851
|
+
if (!body.stream) {
|
|
852
|
+
try {
|
|
853
|
+
const { completion, meta } = await engine.router.complete(body, routeOptions);
|
|
854
|
+
setRoutingHeaders(reply, meta);
|
|
855
|
+
return completion;
|
|
856
|
+
} catch (err) {
|
|
857
|
+
return sendRoutingError(reply, err);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
let result;
|
|
861
|
+
try {
|
|
862
|
+
result = await engine.router.stream(body, routeOptions);
|
|
863
|
+
} catch (err) {
|
|
864
|
+
return sendRoutingError(reply, err);
|
|
865
|
+
}
|
|
866
|
+
setRoutingHeaders(reply, result.meta);
|
|
867
|
+
reply.header("content-type", "text/event-stream; charset=utf-8").header("cache-control", "no-cache, no-transform").header("x-accel-buffering", "no");
|
|
868
|
+
return reply.send(Readable.from(sseFrames(result.chunks)));
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
var DEFAULT_CORS_ORIGINS = ["http://localhost:5173", "http://127.0.0.1:5173"];
|
|
872
|
+
var EXPOSED_HEADERS = [
|
|
873
|
+
"x-davecode-request-id",
|
|
874
|
+
"x-davecode-account",
|
|
875
|
+
"x-davecode-provider",
|
|
876
|
+
"x-davecode-failovers"
|
|
877
|
+
];
|
|
878
|
+
function pathOf(url) {
|
|
879
|
+
const q = url.indexOf("?");
|
|
880
|
+
return q === -1 ? url : url.slice(0, q);
|
|
881
|
+
}
|
|
882
|
+
function surfaceOf(url) {
|
|
883
|
+
const path2 = pathOf(url);
|
|
884
|
+
if (path2 === "/v1" || path2.startsWith("/v1/")) return "v1";
|
|
885
|
+
if (path2 === "/api" || path2.startsWith("/api/")) return "api";
|
|
886
|
+
return "other";
|
|
887
|
+
}
|
|
888
|
+
function redactUrl(url) {
|
|
889
|
+
return url.replace(/([?&](?:token|code|state|error_description)=)[^&]*/gi, "$1[redacted]");
|
|
890
|
+
}
|
|
891
|
+
function safeEqual(a, b) {
|
|
892
|
+
const left = Buffer.from(a);
|
|
893
|
+
const right = Buffer.from(b);
|
|
894
|
+
return left.length === right.length && timingSafeEqual(left, right);
|
|
895
|
+
}
|
|
896
|
+
function bearer(request) {
|
|
897
|
+
const header = request.headers.authorization;
|
|
898
|
+
if (!header) return void 0;
|
|
899
|
+
const match = /^Bearer\s+(.+)$/i.exec(header.trim());
|
|
900
|
+
return match?.[1];
|
|
901
|
+
}
|
|
902
|
+
function loggerOption(logger) {
|
|
903
|
+
if (!logger) return false;
|
|
904
|
+
return {
|
|
905
|
+
...typeof logger === "object" ? logger : {},
|
|
906
|
+
serializers: {
|
|
907
|
+
req(req) {
|
|
908
|
+
return { method: req.method, url: redactUrl(req.url), remoteAddress: req.ip };
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
async function buildGateway(engine, options = {}) {
|
|
914
|
+
const app = Fastify({ logger: loggerOption(options.logger), bodyLimit: 10 * 1024 * 1024 });
|
|
915
|
+
const login = options.login === false ? void 0 : options.login ?? loginFromEnv(process.env);
|
|
916
|
+
if (login) app.addHook("onClose", async () => login.clear());
|
|
917
|
+
await app.register(cors, {
|
|
918
|
+
origin: [...DEFAULT_CORS_ORIGINS, ...options.corsOrigins ?? []],
|
|
919
|
+
methods: ["GET", "POST", "PATCH", "DELETE", "OPTIONS"],
|
|
920
|
+
allowedHeaders: ["content-type", "authorization"],
|
|
921
|
+
exposedHeaders: EXPOSED_HEADERS
|
|
922
|
+
});
|
|
923
|
+
const token = engine.config.server.authToken;
|
|
924
|
+
if (token || login) {
|
|
925
|
+
app.addHook("onRequest", async (request, reply) => {
|
|
926
|
+
if (request.method === "OPTIONS") return;
|
|
927
|
+
const surface = surfaceOf(request.url);
|
|
928
|
+
if (surface === "other") return;
|
|
929
|
+
if (pathOf(request.url).startsWith("/api/auth/")) return;
|
|
930
|
+
let provided = bearer(request);
|
|
931
|
+
if (!provided && pathOf(request.url) === "/api/events") {
|
|
932
|
+
const query = request.query;
|
|
933
|
+
if (typeof query?.token === "string") provided = query.token;
|
|
934
|
+
}
|
|
935
|
+
if (provided && token && safeEqual(provided, token)) return;
|
|
936
|
+
if (surface === "api" && login?.user(authCookie(request, "dc_session"))) {
|
|
937
|
+
if (!["GET", "HEAD"].includes(request.method) && request.headers.origin !== login.options.origin) {
|
|
938
|
+
return sendApiError(
|
|
939
|
+
reply,
|
|
940
|
+
403,
|
|
941
|
+
"invalid_origin",
|
|
942
|
+
"Open DaveCode from its configured origin."
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
const message = "Missing or invalid bearer token";
|
|
948
|
+
if (surface === "v1") {
|
|
949
|
+
return sendOpenAIError(reply, 401, {
|
|
950
|
+
message,
|
|
951
|
+
type: "authentication_error",
|
|
952
|
+
code: "invalid_api_key"
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
return sendApiError(reply, 401, "unauthorized", message);
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
app.addContentTypeParser("application/json", { parseAs: "string" }, (_request, body, done) => {
|
|
959
|
+
const text = typeof body === "string" ? body : body.toString("utf8");
|
|
960
|
+
if (text.trim() === "") return done(null, void 0);
|
|
961
|
+
try {
|
|
962
|
+
done(null, JSON.parse(text));
|
|
963
|
+
} catch (err) {
|
|
964
|
+
const error = Object.assign(err instanceof Error ? err : new Error("Invalid JSON"), {
|
|
965
|
+
statusCode: 400
|
|
966
|
+
});
|
|
967
|
+
done(error, void 0);
|
|
968
|
+
}
|
|
969
|
+
});
|
|
970
|
+
app.setErrorHandler((error, request, reply) => {
|
|
971
|
+
const status = typeof error.statusCode === "number" ? error.statusCode : 500;
|
|
972
|
+
const message = status >= 500 ? "Internal gateway error" : error instanceof Error ? error.message : "Error";
|
|
973
|
+
if (status >= 500) request.log.error({ err: error }, "request failed");
|
|
974
|
+
if (surfaceOf(request.url) === "v1") {
|
|
975
|
+
return sendOpenAIError(reply, status, {
|
|
976
|
+
message,
|
|
977
|
+
type: status >= 500 ? "api_error" : "invalid_request_error",
|
|
978
|
+
code: status >= 500 ? "internal_error" : "bad_request"
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
return sendApiError(reply, status, status >= 500 ? "internal_error" : "bad_request", message);
|
|
982
|
+
});
|
|
983
|
+
const dashboardDir = engine.config.server.dashboard && options.dashboardDir && existsSync(options.dashboardDir) ? resolve(options.dashboardDir) : void 0;
|
|
984
|
+
if (dashboardDir) {
|
|
985
|
+
await app.register(fastifyStatic, { root: dashboardDir });
|
|
986
|
+
}
|
|
987
|
+
app.setNotFoundHandler((request, reply) => {
|
|
988
|
+
const surface = surfaceOf(request.url);
|
|
989
|
+
const message = `Unknown endpoint ${request.method} ${pathOf(request.url)}`;
|
|
990
|
+
if (surface === "v1") {
|
|
991
|
+
return sendOpenAIError(reply, 404, {
|
|
992
|
+
message,
|
|
993
|
+
type: "invalid_request_error",
|
|
994
|
+
code: "not_found"
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
if (surface === "api") return sendApiError(reply, 404, "not_found", message);
|
|
998
|
+
if (dashboardDir && (request.method === "GET" || request.method === "HEAD")) {
|
|
999
|
+
return reply.type("text/html; charset=utf-8").sendFile("index.html");
|
|
1000
|
+
}
|
|
1001
|
+
return reply.code(404).send({ error: { message, code: "not_found" } });
|
|
1002
|
+
});
|
|
1003
|
+
registerV1Routes(app, engine);
|
|
1004
|
+
registerAuthRoutes(app, login);
|
|
1005
|
+
registerApiRoutes(app, engine, options);
|
|
1006
|
+
registerEventRoutes(app, engine, options.heartbeatMs ?? 15e3);
|
|
1007
|
+
return app;
|
|
1008
|
+
}
|
|
1009
|
+
var LOOPBACK = /* @__PURE__ */ new Set(["127.0.0.1", "::1", "localhost"]);
|
|
1010
|
+
async function startGateway(engine, options = {}) {
|
|
1011
|
+
const app = await buildGateway(engine, options);
|
|
1012
|
+
const host = options.host ?? engine.config.server.host;
|
|
1013
|
+
const port = options.port ?? engine.config.server.port;
|
|
1014
|
+
if (!engine.config.server.authToken && !LOOPBACK.has(host)) {
|
|
1015
|
+
app.log.warn(
|
|
1016
|
+
`Listening on ${host} without server.authToken: anyone on the network can use your accounts.`
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
await app.listen({ host, port });
|
|
1020
|
+
return app;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// src/sse.ts
|
|
1024
|
+
var SSEParser = class {
|
|
1025
|
+
buffer = "";
|
|
1026
|
+
pendingCR = false;
|
|
1027
|
+
data = [];
|
|
1028
|
+
event = "";
|
|
1029
|
+
lastId;
|
|
1030
|
+
retry;
|
|
1031
|
+
/** Feed decoded text; returns the messages completed by it. */
|
|
1032
|
+
push(chunk) {
|
|
1033
|
+
const out = [];
|
|
1034
|
+
let text = chunk;
|
|
1035
|
+
if (this.pendingCR && text.startsWith("\n")) text = text.slice(1);
|
|
1036
|
+
this.pendingCR = false;
|
|
1037
|
+
this.buffer += text;
|
|
1038
|
+
let start = 0;
|
|
1039
|
+
for (let i = 0; i < this.buffer.length; i++) {
|
|
1040
|
+
const ch = this.buffer[i];
|
|
1041
|
+
if (ch !== "\n" && ch !== "\r") continue;
|
|
1042
|
+
const line = this.buffer.slice(start, i);
|
|
1043
|
+
if (ch === "\r") {
|
|
1044
|
+
if (i + 1 < this.buffer.length) {
|
|
1045
|
+
if (this.buffer[i + 1] === "\n") i++;
|
|
1046
|
+
} else {
|
|
1047
|
+
this.pendingCR = true;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
this.processLine(line, out);
|
|
1051
|
+
start = i + 1;
|
|
1052
|
+
}
|
|
1053
|
+
this.buffer = this.buffer.slice(start);
|
|
1054
|
+
return out;
|
|
1055
|
+
}
|
|
1056
|
+
/** End of stream. An unterminated final event is discarded, as the spec requires. */
|
|
1057
|
+
end() {
|
|
1058
|
+
this.buffer = "";
|
|
1059
|
+
this.data = [];
|
|
1060
|
+
this.event = "";
|
|
1061
|
+
return [];
|
|
1062
|
+
}
|
|
1063
|
+
processLine(line, out) {
|
|
1064
|
+
if (line === "") {
|
|
1065
|
+
this.dispatch(out);
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
if (line.startsWith(":")) return;
|
|
1069
|
+
const colon = line.indexOf(":");
|
|
1070
|
+
const field = colon === -1 ? line : line.slice(0, colon);
|
|
1071
|
+
let value = colon === -1 ? "" : line.slice(colon + 1);
|
|
1072
|
+
if (value.startsWith(" ")) value = value.slice(1);
|
|
1073
|
+
switch (field) {
|
|
1074
|
+
case "data":
|
|
1075
|
+
this.data.push(value);
|
|
1076
|
+
break;
|
|
1077
|
+
case "event":
|
|
1078
|
+
this.event = value;
|
|
1079
|
+
break;
|
|
1080
|
+
case "id":
|
|
1081
|
+
if (!value.includes("\0")) this.lastId = value;
|
|
1082
|
+
break;
|
|
1083
|
+
case "retry":
|
|
1084
|
+
if (/^\d+$/.test(value)) this.retry = Number(value);
|
|
1085
|
+
break;
|
|
1086
|
+
default:
|
|
1087
|
+
break;
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
dispatch(out) {
|
|
1091
|
+
if (this.data.length === 0) {
|
|
1092
|
+
this.event = "";
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
const message = { event: this.event || "message", data: this.data.join("\n") };
|
|
1096
|
+
if (this.lastId !== void 0) message.id = this.lastId;
|
|
1097
|
+
if (this.retry !== void 0) message.retry = this.retry;
|
|
1098
|
+
out.push(message);
|
|
1099
|
+
this.data = [];
|
|
1100
|
+
this.event = "";
|
|
1101
|
+
}
|
|
1102
|
+
};
|
|
1103
|
+
async function* readSSE(body) {
|
|
1104
|
+
const parser = new SSEParser();
|
|
1105
|
+
const decoder = new TextDecoder();
|
|
1106
|
+
for await (const chunk of body) {
|
|
1107
|
+
const text = typeof chunk === "string" ? chunk : decoder.decode(chunk, { stream: true });
|
|
1108
|
+
yield* parser.push(text);
|
|
1109
|
+
}
|
|
1110
|
+
const tail = decoder.decode();
|
|
1111
|
+
if (tail) yield* parser.push(tail);
|
|
1112
|
+
yield* parser.end();
|
|
1113
|
+
}
|
|
1114
|
+
var ChatStreamError = class extends Error {
|
|
1115
|
+
code;
|
|
1116
|
+
constructor(message, code) {
|
|
1117
|
+
super(message);
|
|
1118
|
+
this.name = "ChatStreamError";
|
|
1119
|
+
this.code = code ?? void 0;
|
|
1120
|
+
}
|
|
1121
|
+
};
|
|
1122
|
+
async function* chatChunks(messages) {
|
|
1123
|
+
for await (const message of messages) {
|
|
1124
|
+
if (message.data === "[DONE]") return;
|
|
1125
|
+
let parsed;
|
|
1126
|
+
try {
|
|
1127
|
+
parsed = JSON.parse(message.data);
|
|
1128
|
+
} catch {
|
|
1129
|
+
continue;
|
|
1130
|
+
}
|
|
1131
|
+
if (!parsed || typeof parsed !== "object") continue;
|
|
1132
|
+
const record = parsed;
|
|
1133
|
+
if (record.error && !record.choices) {
|
|
1134
|
+
throw new ChatStreamError(record.error.message ?? "Upstream error", record.error.code);
|
|
1135
|
+
}
|
|
1136
|
+
if (Array.isArray(record.choices)) yield parsed;
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
async function* daveEvents(messages) {
|
|
1140
|
+
for await (const message of messages) {
|
|
1141
|
+
try {
|
|
1142
|
+
const event = JSON.parse(message.data);
|
|
1143
|
+
if (event && typeof event.type === "string") yield event;
|
|
1144
|
+
} catch {
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
// src/client.ts
|
|
1150
|
+
var GatewayError = class extends Error {
|
|
1151
|
+
status;
|
|
1152
|
+
code;
|
|
1153
|
+
constructor(status, code, message) {
|
|
1154
|
+
super(message);
|
|
1155
|
+
this.name = "GatewayError";
|
|
1156
|
+
this.status = status;
|
|
1157
|
+
this.code = code;
|
|
1158
|
+
}
|
|
1159
|
+
};
|
|
1160
|
+
var WILDCARD_HOSTS = /* @__PURE__ */ new Set(["0.0.0.0", "::", "[::]", ""]);
|
|
1161
|
+
function connectHost(host) {
|
|
1162
|
+
if (WILDCARD_HOSTS.has(host)) return "127.0.0.1";
|
|
1163
|
+
if (host.includes(":") && !host.startsWith("[")) return `[${host}]`;
|
|
1164
|
+
return host;
|
|
1165
|
+
}
|
|
1166
|
+
function targetFor(config, port = config.server.port) {
|
|
1167
|
+
const target = { baseUrl: `http://${connectHost(config.server.host)}:${port}` };
|
|
1168
|
+
if (config.server.authToken) target.token = config.server.authToken;
|
|
1169
|
+
return target;
|
|
1170
|
+
}
|
|
1171
|
+
async function errorFrom(res) {
|
|
1172
|
+
let code = `http_${res.status}`;
|
|
1173
|
+
let message = `${res.status} ${res.statusText}`.trim();
|
|
1174
|
+
try {
|
|
1175
|
+
const body = await res.json();
|
|
1176
|
+
if (body.error?.message) message = body.error.message;
|
|
1177
|
+
if (body.error?.code) code = body.error.code;
|
|
1178
|
+
} catch {
|
|
1179
|
+
}
|
|
1180
|
+
return new GatewayError(res.status, code, message);
|
|
1181
|
+
}
|
|
1182
|
+
var GatewayClient = class {
|
|
1183
|
+
baseUrl;
|
|
1184
|
+
token;
|
|
1185
|
+
fetchImpl;
|
|
1186
|
+
constructor(target, fetchImpl = fetch) {
|
|
1187
|
+
this.baseUrl = target.baseUrl.replace(/\/+$/, "");
|
|
1188
|
+
this.token = target.token;
|
|
1189
|
+
this.fetchImpl = fetchImpl;
|
|
1190
|
+
}
|
|
1191
|
+
headers(extra = {}) {
|
|
1192
|
+
return this.token ? { authorization: `Bearer ${this.token}`, ...extra } : extra;
|
|
1193
|
+
}
|
|
1194
|
+
/** Low-level fetch with auth, an optional timeout and transport errors as `GatewayError`. */
|
|
1195
|
+
async fetch(path2, init = {}, options = {}) {
|
|
1196
|
+
const signals = [];
|
|
1197
|
+
if (options.signal) signals.push(options.signal);
|
|
1198
|
+
if (options.timeoutMs !== void 0) signals.push(AbortSignal.timeout(options.timeoutMs));
|
|
1199
|
+
const signal = signals.length > 1 ? AbortSignal.any(signals) : signals[0];
|
|
1200
|
+
try {
|
|
1201
|
+
return await this.fetchImpl(`${this.baseUrl}${path2}`, {
|
|
1202
|
+
...init,
|
|
1203
|
+
headers: this.headers(init.headers ?? {}),
|
|
1204
|
+
...signal ? { signal } : {}
|
|
1205
|
+
});
|
|
1206
|
+
} catch (err) {
|
|
1207
|
+
if (options.signal?.aborted) throw err;
|
|
1208
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
1209
|
+
throw new GatewayError(
|
|
1210
|
+
0,
|
|
1211
|
+
"unreachable",
|
|
1212
|
+
`Cannot reach the gateway at ${this.baseUrl} (${reason})`
|
|
1213
|
+
);
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
async request(method, path2, body, options = {}) {
|
|
1217
|
+
const init = { method };
|
|
1218
|
+
if (body !== void 0) {
|
|
1219
|
+
init.body = JSON.stringify(body);
|
|
1220
|
+
init.headers = { "content-type": "application/json" };
|
|
1221
|
+
}
|
|
1222
|
+
const res = await this.fetch(path2, init, { timeoutMs: 1e4, ...options });
|
|
1223
|
+
if (!res.ok) throw await errorFrom(res);
|
|
1224
|
+
if (res.status === 204) return void 0;
|
|
1225
|
+
return await res.json();
|
|
1226
|
+
}
|
|
1227
|
+
get(path2, options) {
|
|
1228
|
+
return this.request("GET", path2, void 0, options);
|
|
1229
|
+
}
|
|
1230
|
+
post(path2, body, options) {
|
|
1231
|
+
return this.request("POST", path2, body, options);
|
|
1232
|
+
}
|
|
1233
|
+
patch(path2, body, options) {
|
|
1234
|
+
return this.request("PATCH", path2, body, options);
|
|
1235
|
+
}
|
|
1236
|
+
delete(path2, options) {
|
|
1237
|
+
return this.request("DELETE", path2, void 0, options);
|
|
1238
|
+
}
|
|
1239
|
+
/**
|
|
1240
|
+
* `GET /api/health`, or `undefined` when nothing (or something that is not DaveCode) answers.
|
|
1241
|
+
* Throws `GatewayError` 401 when a gateway is running but rejects our bearer token.
|
|
1242
|
+
*/
|
|
1243
|
+
async health(timeoutMs = 800) {
|
|
1244
|
+
let res;
|
|
1245
|
+
try {
|
|
1246
|
+
res = await this.fetch("/api/health", {}, { timeoutMs });
|
|
1247
|
+
} catch {
|
|
1248
|
+
return void 0;
|
|
1249
|
+
}
|
|
1250
|
+
if (res.status === 401) {
|
|
1251
|
+
throw new GatewayError(
|
|
1252
|
+
401,
|
|
1253
|
+
"unauthorized",
|
|
1254
|
+
`The gateway at ${this.baseUrl} requires a bearer token`
|
|
1255
|
+
);
|
|
1256
|
+
}
|
|
1257
|
+
if (!res.ok) return void 0;
|
|
1258
|
+
try {
|
|
1259
|
+
const body = await res.json();
|
|
1260
|
+
return body.status === "ok" && typeof body.version === "string" ? body : void 0;
|
|
1261
|
+
} catch {
|
|
1262
|
+
return void 0;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
/**
|
|
1266
|
+
* `POST /v1/chat/completions` with `stream: true`. Resolves once the gateway answered (after
|
|
1267
|
+
* routing and any failover), so `meta` already names the serving account.
|
|
1268
|
+
*/
|
|
1269
|
+
async chatStream(request, signal) {
|
|
1270
|
+
const res = await this.fetch(
|
|
1271
|
+
"/v1/chat/completions",
|
|
1272
|
+
{
|
|
1273
|
+
method: "POST",
|
|
1274
|
+
headers: { "content-type": "application/json", accept: "text/event-stream" },
|
|
1275
|
+
body: JSON.stringify({ ...request, stream: true })
|
|
1276
|
+
},
|
|
1277
|
+
signal ? { signal } : {}
|
|
1278
|
+
);
|
|
1279
|
+
if (!res.ok) throw await errorFrom(res);
|
|
1280
|
+
return { meta: streamMeta(res.headers), chunks: chatChunks(readSSE(bodyOf(res))) };
|
|
1281
|
+
}
|
|
1282
|
+
/** Live `/api/events` (the server replays its recent buffer first). */
|
|
1283
|
+
async *events(signal) {
|
|
1284
|
+
const res = await this.fetch(
|
|
1285
|
+
"/api/events",
|
|
1286
|
+
{ headers: { accept: "text/event-stream" } },
|
|
1287
|
+
signal ? { signal } : {}
|
|
1288
|
+
);
|
|
1289
|
+
if (!res.ok) throw await errorFrom(res);
|
|
1290
|
+
yield* daveEvents(readSSE(bodyOf(res)));
|
|
1291
|
+
}
|
|
1292
|
+
};
|
|
1293
|
+
function streamMeta(headers) {
|
|
1294
|
+
const meta = { failovers: Number(headers.get("x-davecode-failovers") ?? 0) || 0 };
|
|
1295
|
+
const requestId = headers.get("x-davecode-request-id");
|
|
1296
|
+
const accountId = headers.get("x-davecode-account");
|
|
1297
|
+
const provider = headers.get("x-davecode-provider");
|
|
1298
|
+
if (requestId) meta.requestId = requestId;
|
|
1299
|
+
if (accountId) meta.accountId = accountId;
|
|
1300
|
+
if (provider) meta.provider = provider;
|
|
1301
|
+
return meta;
|
|
1302
|
+
}
|
|
1303
|
+
function bodyOf(res) {
|
|
1304
|
+
if (!res.body) throw new GatewayError(res.status, "empty_body", "The gateway sent no body");
|
|
1305
|
+
return res.body;
|
|
1306
|
+
}
|
|
1307
|
+
async function findGateway(config, options = {}) {
|
|
1308
|
+
const client = new GatewayClient(targetFor(config, options.port), options.fetch);
|
|
1309
|
+
const health = await client.health(options.timeoutMs);
|
|
1310
|
+
return health ? { client, health } : void 0;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// src/lib/terminal.ts
|
|
1314
|
+
import { spawn as nodeSpawn2 } from "child_process";
|
|
1315
|
+
|
|
1316
|
+
// src/lib/proc.ts
|
|
1317
|
+
import { spawn as nodeSpawn } from "child_process";
|
|
1318
|
+
import { existsSync as existsSync2, statSync } from "fs";
|
|
1319
|
+
import path from "path";
|
|
1320
|
+
function splitPathEnv(pathEnv, delimiter) {
|
|
1321
|
+
if (delimiter === ";") return pathEnv.split(";").filter(Boolean);
|
|
1322
|
+
const dirs = [];
|
|
1323
|
+
let pendingDrive;
|
|
1324
|
+
for (const piece of pathEnv.split(":")) {
|
|
1325
|
+
if (pendingDrive !== void 0) {
|
|
1326
|
+
dirs.push(`${pendingDrive}:${piece}`);
|
|
1327
|
+
pendingDrive = void 0;
|
|
1328
|
+
continue;
|
|
1329
|
+
}
|
|
1330
|
+
if (/^[A-Za-z]$/.test(piece)) {
|
|
1331
|
+
pendingDrive = piece;
|
|
1332
|
+
continue;
|
|
1333
|
+
}
|
|
1334
|
+
if (piece) dirs.push(piece);
|
|
1335
|
+
}
|
|
1336
|
+
if (pendingDrive !== void 0) dirs.push(pendingDrive);
|
|
1337
|
+
return dirs;
|
|
1338
|
+
}
|
|
1339
|
+
function which(command, env = process.env, platform = process.platform) {
|
|
1340
|
+
const isFile = (p) => {
|
|
1341
|
+
try {
|
|
1342
|
+
return existsSync2(p) && statSync(p).isFile();
|
|
1343
|
+
} catch {
|
|
1344
|
+
return false;
|
|
1345
|
+
}
|
|
1346
|
+
};
|
|
1347
|
+
if (command.includes("/") || command.includes("\\") || path.isAbsolute(command)) {
|
|
1348
|
+
return isFile(command) ? command : void 0;
|
|
1349
|
+
}
|
|
1350
|
+
const win = platform === "win32";
|
|
1351
|
+
const delimiter = win ? ";" : ":";
|
|
1352
|
+
const dirs = splitPathEnv(env.PATH ?? env.Path ?? "", delimiter);
|
|
1353
|
+
const exts = win ? [
|
|
1354
|
+
".exe",
|
|
1355
|
+
".com",
|
|
1356
|
+
"",
|
|
1357
|
+
...(env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").map((e) => e.toLowerCase()).filter((e) => e && e !== ".exe" && e !== ".com")
|
|
1358
|
+
] : [""];
|
|
1359
|
+
for (const dir of dirs) {
|
|
1360
|
+
for (const ext of exts) {
|
|
1361
|
+
const candidate = path.join(dir, command + ext);
|
|
1362
|
+
if (isFile(candidate)) return candidate;
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
return void 0;
|
|
1366
|
+
}
|
|
1367
|
+
var SHELL_SAFE = /^[\w.:=@+\-/\\[\]]+$/;
|
|
1368
|
+
function spawnInteractive(command, args, options) {
|
|
1369
|
+
const spawn = options.spawn ?? nodeSpawn;
|
|
1370
|
+
const platform = options.platform ?? process.platform;
|
|
1371
|
+
const shim = platform === "win32" && /\.(cmd|bat)$/i.test(command);
|
|
1372
|
+
if (shim) {
|
|
1373
|
+
const unsafe = args.find((a) => !SHELL_SAFE.test(a));
|
|
1374
|
+
if (unsafe !== void 0) {
|
|
1375
|
+
return Promise.reject(
|
|
1376
|
+
new Error(`Argument ${JSON.stringify(unsafe)} cannot be passed safely to ${command}`)
|
|
1377
|
+
);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
return new Promise((resolve3, reject) => {
|
|
1381
|
+
const child = shim ? spawn(`"${command}"`, args, {
|
|
1382
|
+
stdio: "inherit",
|
|
1383
|
+
env: options.env,
|
|
1384
|
+
shell: true,
|
|
1385
|
+
...options.cwd ? { cwd: options.cwd } : {}
|
|
1386
|
+
}) : spawn(command, args, {
|
|
1387
|
+
stdio: "inherit",
|
|
1388
|
+
env: options.env,
|
|
1389
|
+
...options.cwd ? { cwd: options.cwd } : {}
|
|
1390
|
+
});
|
|
1391
|
+
child.on("error", reject);
|
|
1392
|
+
child.on("exit", (code, signal) => resolve3(code ?? (signal ? 130 : 1)));
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
// src/lib/terminal.ts
|
|
1397
|
+
var SAFE_ACCOUNT_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
|
|
1398
|
+
function loginInvocation(accountId, argv = process.argv) {
|
|
1399
|
+
if (!SAFE_ACCOUNT_ID.test(accountId)) throw new Error("Invalid account id");
|
|
1400
|
+
const entry = argv[1];
|
|
1401
|
+
if (!entry) throw new Error("Cannot locate the davecode entry point");
|
|
1402
|
+
return [process.execPath, entry, "accounts", "login", accountId, "--hold"];
|
|
1403
|
+
}
|
|
1404
|
+
function posixQuote(arg) {
|
|
1405
|
+
return `'${arg.replace(/'/g, `'\\''`)}'`;
|
|
1406
|
+
}
|
|
1407
|
+
var LINUX_TERMINALS = [
|
|
1408
|
+
{ bin: "x-terminal-emulator", flag: ["-e"] },
|
|
1409
|
+
{ bin: "gnome-terminal", flag: ["--"] },
|
|
1410
|
+
{ bin: "konsole", flag: ["-e"] },
|
|
1411
|
+
{ bin: "xfce4-terminal", flag: ["-x"] },
|
|
1412
|
+
{ bin: "xterm", flag: ["-e"] }
|
|
1413
|
+
];
|
|
1414
|
+
function terminalCommand(argv, options = {}) {
|
|
1415
|
+
const platform = options.platform ?? process.platform;
|
|
1416
|
+
const env = options.env ?? process.env;
|
|
1417
|
+
if (platform === "win32") {
|
|
1418
|
+
if (argv.some((a) => /["%\r\n]/.test(a))) return void 0;
|
|
1419
|
+
const line = argv.map((a) => `"${a}"`).join(" ");
|
|
1420
|
+
return {
|
|
1421
|
+
command: env.ComSpec ?? env.COMSPEC ?? "cmd.exe",
|
|
1422
|
+
args: ["/d", "/c", `start "DaveCode login" ${line}`]
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
if (platform === "darwin") {
|
|
1426
|
+
const script = argv.map(posixQuote).join(" ").replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
1427
|
+
return {
|
|
1428
|
+
command: "osascript",
|
|
1429
|
+
args: [
|
|
1430
|
+
"-e",
|
|
1431
|
+
`tell application "Terminal" to do script "${script}"`,
|
|
1432
|
+
"-e",
|
|
1433
|
+
'tell application "Terminal" to activate'
|
|
1434
|
+
]
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
if (!env.DISPLAY && !env.WAYLAND_DISPLAY) return void 0;
|
|
1438
|
+
for (const terminal of LINUX_TERMINALS) {
|
|
1439
|
+
if (which(terminal.bin, env, platform)) {
|
|
1440
|
+
return { command: terminal.bin, args: [...terminal.flag, ...argv] };
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
return void 0;
|
|
1444
|
+
}
|
|
1445
|
+
function launchLoginTerminal(accountId, options = {}) {
|
|
1446
|
+
const platform = options.platform ?? process.platform;
|
|
1447
|
+
const target = terminalCommand(loginInvocation(accountId, options.argv), options);
|
|
1448
|
+
if (!target) return Promise.resolve(false);
|
|
1449
|
+
const spawn = options.spawn ?? nodeSpawn2;
|
|
1450
|
+
return new Promise((resolve3) => {
|
|
1451
|
+
try {
|
|
1452
|
+
const child = spawn(target.command, target.args, {
|
|
1453
|
+
detached: true,
|
|
1454
|
+
stdio: "ignore",
|
|
1455
|
+
env: options.env ?? process.env,
|
|
1456
|
+
...platform === "win32" ? { windowsVerbatimArguments: true } : {}
|
|
1457
|
+
});
|
|
1458
|
+
child.once("spawn", () => {
|
|
1459
|
+
child.unref();
|
|
1460
|
+
resolve3(true);
|
|
1461
|
+
});
|
|
1462
|
+
child.once("error", () => resolve3(false));
|
|
1463
|
+
} catch {
|
|
1464
|
+
resolve3(false);
|
|
1465
|
+
}
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
// src/runtime.ts
|
|
1470
|
+
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "::1", "localhost"]);
|
|
1471
|
+
async function exists(path2) {
|
|
1472
|
+
try {
|
|
1473
|
+
await access(path2);
|
|
1474
|
+
return true;
|
|
1475
|
+
} catch {
|
|
1476
|
+
return false;
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
async function detectProjectRoot(startDir, home) {
|
|
1480
|
+
const homeDir = resolve2(home);
|
|
1481
|
+
let dir = resolve2(startDir);
|
|
1482
|
+
for (; ; ) {
|
|
1483
|
+
const brainDir = join(dir, ".davecode");
|
|
1484
|
+
if (await exists(join(dir, ".git"))) return dir;
|
|
1485
|
+
if (resolve2(brainDir) !== homeDir && await exists(brainDir)) return dir;
|
|
1486
|
+
const parent = dirname(dir);
|
|
1487
|
+
if (parent === dir) return void 0;
|
|
1488
|
+
dir = parent;
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
function wireProject(engine, root, options = {}) {
|
|
1492
|
+
const brain = new ProjectBrain(root, { events: engine.events });
|
|
1493
|
+
const lock = new WorkspaceLock();
|
|
1494
|
+
const budgetContext = () => engine.budget.context();
|
|
1495
|
+
return {
|
|
1496
|
+
brain,
|
|
1497
|
+
source: new ProjectBrainSource(brain, options.name),
|
|
1498
|
+
runner: createRunner(engine, {
|
|
1499
|
+
brain,
|
|
1500
|
+
lock: { lock, owner: "runner" },
|
|
1501
|
+
contextExtras: budgetContext,
|
|
1502
|
+
...options.env ? { env: options.env } : {}
|
|
1503
|
+
}),
|
|
1504
|
+
goals: createGoalService(engine, { brain, lock, budgetContext }),
|
|
1505
|
+
lock
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1508
|
+
function isDashboard(dir) {
|
|
1509
|
+
return dir !== void 0 && existsSync3(join(dir, "index.html"));
|
|
1510
|
+
}
|
|
1511
|
+
function resolveDashboardDir(env = process.env, moduleUrl = import.meta.url, lookupPackage = true) {
|
|
1512
|
+
const candidates = [
|
|
1513
|
+
() => env.DAVECODE_DASHBOARD_DIR ? resolve2(env.DAVECODE_DASHBOARD_DIR) : void 0,
|
|
1514
|
+
() => fileURLToPath(new URL("./dashboard", moduleUrl)),
|
|
1515
|
+
() => fileURLToPath(new URL("../../ui/dist", moduleUrl)),
|
|
1516
|
+
() => {
|
|
1517
|
+
if (!lookupPackage) return void 0;
|
|
1518
|
+
try {
|
|
1519
|
+
const pkg = createRequire(moduleUrl).resolve("@davecode/ui/package.json");
|
|
1520
|
+
return join(dirname(pkg), "dist");
|
|
1521
|
+
} catch {
|
|
1522
|
+
return void 0;
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
];
|
|
1526
|
+
for (const candidate of candidates) {
|
|
1527
|
+
const dir = candidate();
|
|
1528
|
+
if (isDashboard(dir)) return dir;
|
|
1529
|
+
}
|
|
1530
|
+
return void 0;
|
|
1531
|
+
}
|
|
1532
|
+
async function resolveProjectRoot(options) {
|
|
1533
|
+
if (options.noProject) return void 0;
|
|
1534
|
+
return options.projectDir ? resolve2(options.cwd, options.projectDir) : detectProjectRoot(options.cwd, options.home);
|
|
1535
|
+
}
|
|
1536
|
+
async function startRuntime(options) {
|
|
1537
|
+
const projectRoot = await resolveProjectRoot(options);
|
|
1538
|
+
const engine = options.engine ?? createEngine({
|
|
1539
|
+
home: options.home,
|
|
1540
|
+
env: options.env,
|
|
1541
|
+
...projectRoot ? { projectRoot } : {},
|
|
1542
|
+
...options.config ? { config: options.config } : {}
|
|
1543
|
+
});
|
|
1544
|
+
try {
|
|
1545
|
+
const project = projectRoot ? wireProject(engine, projectRoot, { env: options.env }) : void 0;
|
|
1546
|
+
const wantDashboard = options.dashboard !== false && engine.config.server.dashboard;
|
|
1547
|
+
const dashboardDir = wantDashboard ? resolveDashboardDir(options.env) : void 0;
|
|
1548
|
+
const bindHost = options.host ?? engine.config.server.host;
|
|
1549
|
+
const loginLauncher = LOOPBACK_HOSTS.has(bindHost) ? {
|
|
1550
|
+
launch: async (account) => ({
|
|
1551
|
+
launched: await launchLoginTerminal(account.id, { env: options.env })
|
|
1552
|
+
})
|
|
1553
|
+
} : void 0;
|
|
1554
|
+
const app = await startGateway(engine, {
|
|
1555
|
+
logger: options.logger ?? false,
|
|
1556
|
+
...options.loginLauncher ?? loginLauncher ? { loginLauncher: options.loginLauncher ?? loginLauncher } : {},
|
|
1557
|
+
...dashboardDir ? { dashboardDir } : {},
|
|
1558
|
+
...project ? { brain: project.source, runner: project.runner, goals: project.goals } : {},
|
|
1559
|
+
...options.host !== void 0 ? { host: options.host } : {},
|
|
1560
|
+
...options.port !== void 0 ? { port: options.port } : {}
|
|
1561
|
+
});
|
|
1562
|
+
const address = app.server.address();
|
|
1563
|
+
const host = options.host ?? engine.config.server.host;
|
|
1564
|
+
const port = address?.port ?? options.port ?? engine.config.server.port;
|
|
1565
|
+
let closed = false;
|
|
1566
|
+
const runner = project?.runner;
|
|
1567
|
+
const goals = project?.goals;
|
|
1568
|
+
return {
|
|
1569
|
+
engine,
|
|
1570
|
+
brain: project?.brain,
|
|
1571
|
+
project: project?.source.project(),
|
|
1572
|
+
runner,
|
|
1573
|
+
goals,
|
|
1574
|
+
url: `http://${connectHost(host)}:${port}`,
|
|
1575
|
+
host,
|
|
1576
|
+
port,
|
|
1577
|
+
dashboardDir,
|
|
1578
|
+
async close() {
|
|
1579
|
+
if (closed) return;
|
|
1580
|
+
closed = true;
|
|
1581
|
+
try {
|
|
1582
|
+
const active = goals?.activeGoalId;
|
|
1583
|
+
if (active) await goals?.cancel(active);
|
|
1584
|
+
await runner?.stop();
|
|
1585
|
+
await app.close();
|
|
1586
|
+
} finally {
|
|
1587
|
+
engine.close();
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
};
|
|
1591
|
+
} catch (err) {
|
|
1592
|
+
engine.close();
|
|
1593
|
+
throw err;
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
export {
|
|
1598
|
+
GatewayError,
|
|
1599
|
+
connectHost,
|
|
1600
|
+
targetFor,
|
|
1601
|
+
GatewayClient,
|
|
1602
|
+
findGateway,
|
|
1603
|
+
accountCreateSchema,
|
|
1604
|
+
accountPatchSchema,
|
|
1605
|
+
which,
|
|
1606
|
+
spawnInteractive,
|
|
1607
|
+
launchLoginTerminal,
|
|
1608
|
+
detectProjectRoot,
|
|
1609
|
+
wireProject,
|
|
1610
|
+
resolveDashboardDir,
|
|
1611
|
+
resolveProjectRoot,
|
|
1612
|
+
startRuntime
|
|
1613
|
+
};
|
|
1614
|
+
//# sourceMappingURL=chunk-DZ2FDABF.js.map
|