@dmrlzz/davecode 0.3.0 → 0.5.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/README.md +5 -4
- package/bin/davecode.js +0 -0
- package/dist/{ChatApp-FO5SABXB.js → ChatApp-CJBICQGA.js} +289 -137
- package/dist/ChatApp-CJBICQGA.js.map +1 -0
- package/dist/{RunnerView-DLVR7C22.js → RunnerView-A7JSPUW5.js} +5 -5
- package/dist/account-QWDPJWAA.js +23 -0
- package/dist/accounts-IQMR72FC.js +37 -0
- package/dist/{budget-RBHCEDPP.js → budget-XANOUVWO.js} +6 -6
- package/dist/chat-KDDRNC5V.js +359 -0
- package/dist/chat-KDDRNC5V.js.map +1 -0
- package/dist/{accounts-MTMGYKEP.js → chunk-44AFJL2X.js} +24 -17
- package/dist/chunk-44AFJL2X.js.map +1 -0
- package/dist/{chunk-BOLFK3BH.js → chunk-5LKOJBUL.js} +2 -2
- package/dist/{chunk-23KTE7IK.js → chunk-AZKACMME.js} +2 -2
- package/dist/{chunk-PIIJ3RSI.js → chunk-FYPMA55O.js} +1269 -1265
- package/dist/chunk-FYPMA55O.js.map +1 -0
- package/dist/{chunk-OYEYNDOO.js → chunk-GE22VBCY.js} +3 -3
- package/dist/{chunk-C7FDRA3U.js → chunk-HSBJDV3O.js} +660 -139
- package/dist/chunk-HSBJDV3O.js.map +1 -0
- package/dist/chunk-JLATEHS7.js +20 -0
- package/dist/chunk-JLATEHS7.js.map +1 -0
- package/dist/{chunk-VVCXGQMY.js → chunk-MC7RIPL7.js} +17 -8
- package/dist/chunk-MC7RIPL7.js.map +1 -0
- package/dist/{chunk-MSLLH5XP.js → chunk-PGAJIGUF.js} +4 -4
- package/dist/{chunk-OXRMGR72.js → chunk-PGPXE6LF.js} +2 -2
- package/dist/chunk-QA3O6RA2.js +81 -0
- package/dist/chunk-QA3O6RA2.js.map +1 -0
- package/dist/chunk-W7GPBEBZ.js +144 -0
- package/dist/chunk-W7GPBEBZ.js.map +1 -0
- package/dist/{chunk-IVZHKUCD.js → chunk-WQ7MEWDF.js} +3 -3
- package/dist/{config-FRMY5OHG.js → config-YTUPIYZT.js} +5 -5
- package/dist/dashboard/assets/Accounts-cq8RLMoI.js +1 -0
- package/dist/dashboard/assets/Badge-Z4RBdO9j.js +55 -0
- package/dist/dashboard/assets/Callout-w8BTYh4k.js +1 -0
- package/dist/dashboard/assets/Field-Cmpgg_1z.js +1 -0
- package/dist/dashboard/assets/Goals-Bw02to72.js +1 -0
- package/dist/dashboard/assets/Library-DK7mlpRb.js +1 -0
- package/dist/dashboard/assets/{Markdown-ClRsddnB.js → Markdown-DCZ4Awcc.js} +8 -8
- package/dist/dashboard/assets/Panes-_KTf8pbZ.js +8 -0
- package/dist/dashboard/assets/Panes-kHJ-D0s7.css +1 -0
- package/dist/dashboard/assets/RoutesPage-QV0oPRW9.js +4 -0
- package/dist/dashboard/assets/Runner-CkHBLQl0.js +1 -0
- package/dist/dashboard/assets/Settings-BLz2gTBR.js +1 -0
- package/dist/dashboard/assets/Switch-DPYlLseH.js +1 -0
- package/dist/dashboard/assets/Tasks-BiUQSTpt.js +2 -0
- package/dist/dashboard/assets/Traffic-BVfBuC70.js +1 -0
- package/dist/dashboard/assets/Workspace-CJTgiiU5.js +1 -0
- package/dist/dashboard/assets/arrow-up-DjhzZprp.js +1 -0
- package/dist/dashboard/assets/cn-BwE4cei0.js +1 -0
- package/dist/dashboard/assets/forms-DVIUAxAc.js +1 -0
- package/dist/dashboard/assets/index-BDBa5cb0.js +104 -0
- package/dist/dashboard/assets/index-BEmBgsbP.css +2 -0
- package/dist/dashboard/assets/task-errors-Daqwi0ue.js +1 -0
- package/dist/dashboard/assets/trash-BTuDcSaQ.js +1 -0
- package/dist/dashboard/favicon.svg +1 -7
- package/dist/dashboard/index.html +4 -4
- package/dist/{dist-ZPXEVBEC.js → dist-37VQSHHA.js} +24 -2
- package/dist/dist-37VQSHHA.js.map +1 -0
- package/dist/{doctor-7RUD4D3A.js → doctor-PYUQRRCB.js} +5 -5
- package/dist/{goal-JFRNPBSD.js → goal-SXZUDUCL.js} +5 -5
- package/dist/index.js +48 -30
- package/dist/index.js.map +1 -1
- package/dist/{init-NFYNO3A3.js → init-Z2AFQEEP.js} +6 -6
- package/dist/{prompts-L37J72YW.js → prompts-TZ7UC42M.js} +9 -5
- package/dist/prompts-TZ7UC42M.js.map +1 -0
- package/dist/{run-ARUPXFFB.js → run-AOS7L3RT.js} +10 -10
- package/dist/runtime.js +18 -0
- package/dist/runtime.js.map +1 -0
- package/dist/{start-CFPOVIAR.js → start-CEPXKZUE.js} +5 -5
- package/dist/{status-E4EACVE2.js → status-WNJ3GGKU.js} +6 -6
- package/dist/status-WNJ3GGKU.js.map +1 -0
- package/dist/{tasks-ZIK6BVEX.js → tasks-TFUTUMWB.js} +6 -6
- package/package.json +21 -15
- package/dist/ChatApp-FO5SABXB.js.map +0 -1
- package/dist/accounts-MTMGYKEP.js.map +0 -1
- package/dist/chat-ZDXX3N6X.js +0 -95
- package/dist/chat-ZDXX3N6X.js.map +0 -1
- package/dist/chunk-C7FDRA3U.js.map +0 -1
- package/dist/chunk-DYQ56FBT.js +0 -47
- package/dist/chunk-DYQ56FBT.js.map +0 -1
- package/dist/chunk-PIIJ3RSI.js.map +0 -1
- package/dist/chunk-VVCXGQMY.js.map +0 -1
- package/dist/dashboard/assets/Accounts-BR3ZfVk4.js +0 -1
- package/dist/dashboard/assets/Badge-B-qzNRrZ.js +0 -55
- package/dist/dashboard/assets/Callout-B9g_wPDi.js +0 -1
- package/dist/dashboard/assets/Field-BfabPEcY.js +0 -1
- package/dist/dashboard/assets/Goals-BsV-6m94.js +0 -1
- package/dist/dashboard/assets/Library-CEA9P1m-.js +0 -1
- package/dist/dashboard/assets/Panel-B9tSgX6l.js +0 -1
- package/dist/dashboard/assets/RoutesPage-Dh-BJ3ir.js +0 -4
- package/dist/dashboard/assets/Runner-C_e9l5l2.js +0 -1
- package/dist/dashboard/assets/Settings-C6Yc1Gjg.js +0 -1
- package/dist/dashboard/assets/Switch-CUpOJjUT.js +0 -1
- package/dist/dashboard/assets/Tasks-CTku0rR3.js +0 -2
- package/dist/dashboard/assets/Traffic-CE6wZSMr.js +0 -1
- package/dist/dashboard/assets/Workspace-CxFxvkA6.js +0 -1
- package/dist/dashboard/assets/arrow-up-DL3CuFOO.js +0 -1
- package/dist/dashboard/assets/forms-D6UIYgC5.js +0 -1
- package/dist/dashboard/assets/index-1iH716Xy.css +0 -2
- package/dist/dashboard/assets/index-BMWr9EKo.js +0 -104
- package/dist/dashboard/assets/task-errors-CE4oEr8H.js +0 -1
- package/dist/dashboard/assets/trash-CWqeZ7eg.js +0 -1
- package/dist/prompts-L37J72YW.js.map +0 -1
- /package/dist/{RunnerView-DLVR7C22.js.map → RunnerView-A7JSPUW5.js.map} +0 -0
- /package/dist/{dist-ZPXEVBEC.js.map → account-QWDPJWAA.js.map} +0 -0
- /package/dist/{status-E4EACVE2.js.map → accounts-IQMR72FC.js.map} +0 -0
- /package/dist/{budget-RBHCEDPP.js.map → budget-XANOUVWO.js.map} +0 -0
- /package/dist/{chunk-BOLFK3BH.js.map → chunk-5LKOJBUL.js.map} +0 -0
- /package/dist/{chunk-23KTE7IK.js.map → chunk-AZKACMME.js.map} +0 -0
- /package/dist/{chunk-OYEYNDOO.js.map → chunk-GE22VBCY.js.map} +0 -0
- /package/dist/{chunk-MSLLH5XP.js.map → chunk-PGAJIGUF.js.map} +0 -0
- /package/dist/{chunk-OXRMGR72.js.map → chunk-PGPXE6LF.js.map} +0 -0
- /package/dist/{chunk-IVZHKUCD.js.map → chunk-WQ7MEWDF.js.map} +0 -0
- /package/dist/{config-FRMY5OHG.js.map → config-YTUPIYZT.js.map} +0 -0
- /package/dist/{doctor-7RUD4D3A.js.map → doctor-PYUQRRCB.js.map} +0 -0
- /package/dist/{goal-JFRNPBSD.js.map → goal-SXZUDUCL.js.map} +0 -0
- /package/dist/{init-NFYNO3A3.js.map → init-Z2AFQEEP.js.map} +0 -0
- /package/dist/{run-ARUPXFFB.js.map → run-AOS7L3RT.js.map} +0 -0
- /package/dist/{start-CFPOVIAR.js.map → start-CEPXKZUE.js.map} +0 -0
- /package/dist/{tasks-ZIK6BVEX.js.map → tasks-TFUTUMWB.js.map} +0 -0
|
@@ -18,1380 +18,1382 @@ import {
|
|
|
18
18
|
newId2,
|
|
19
19
|
routeSchema,
|
|
20
20
|
taskIdSchema
|
|
21
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-HSBJDV3O.js";
|
|
22
22
|
|
|
23
|
-
// src/
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
62
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const colon = line.indexOf(":");
|
|
70
|
-
const field = colon === -1 ? line : line.slice(0, colon);
|
|
71
|
-
let value = colon === -1 ? "" : line.slice(colon + 1);
|
|
72
|
-
if (value.startsWith(" ")) value = value.slice(1);
|
|
73
|
-
switch (field) {
|
|
74
|
-
case "data":
|
|
75
|
-
this.data.push(value);
|
|
63
|
+
if (err instanceof ProviderError) {
|
|
64
|
+
let status;
|
|
65
|
+
let code = err.kind;
|
|
66
|
+
switch (err.kind) {
|
|
67
|
+
case "bad_request":
|
|
68
|
+
status = 400;
|
|
76
69
|
break;
|
|
77
|
-
case "
|
|
78
|
-
|
|
70
|
+
case "context_length":
|
|
71
|
+
status = 400;
|
|
72
|
+
code = "context_length_exceeded";
|
|
79
73
|
break;
|
|
80
|
-
case "
|
|
81
|
-
|
|
74
|
+
case "rate_limit":
|
|
75
|
+
case "quota_exhausted":
|
|
76
|
+
status = 429;
|
|
82
77
|
break;
|
|
83
|
-
case "
|
|
84
|
-
|
|
78
|
+
case "auth":
|
|
79
|
+
status = 502;
|
|
80
|
+
code = "upstream_auth_error";
|
|
85
81
|
break;
|
|
86
82
|
default:
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
dispatch(out) {
|
|
91
|
-
if (this.data.length === 0) {
|
|
92
|
-
this.event = "";
|
|
93
|
-
return;
|
|
83
|
+
status = 502;
|
|
94
84
|
}
|
|
95
|
-
|
|
96
|
-
if (this.lastId !== void 0) message.id = this.lastId;
|
|
97
|
-
if (this.retry !== void 0) message.retry = this.retry;
|
|
98
|
-
out.push(message);
|
|
99
|
-
this.data = [];
|
|
100
|
-
this.event = "";
|
|
101
|
-
}
|
|
102
|
-
};
|
|
103
|
-
async function* readSSE(body) {
|
|
104
|
-
const parser = new SSEParser();
|
|
105
|
-
const decoder = new TextDecoder();
|
|
106
|
-
for await (const chunk of body) {
|
|
107
|
-
const text = typeof chunk === "string" ? chunk : decoder.decode(chunk, { stream: true });
|
|
108
|
-
yield* parser.push(text);
|
|
85
|
+
return { status, body: { message: err.message, type: openAIType(status), code } };
|
|
109
86
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
87
|
+
return {
|
|
88
|
+
status: 500,
|
|
89
|
+
body: { message: "Internal gateway error", type: "api_error", code: "internal_error" }
|
|
90
|
+
};
|
|
113
91
|
}
|
|
114
|
-
var
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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()
|
|
121
176
|
};
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
+
});
|
|
130
192
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
+
});
|
|
137
201
|
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
|
|
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) => {
|
|
141
252
|
try {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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;
|
|
145
263
|
}
|
|
146
|
-
}
|
|
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
|
+
});
|
|
147
332
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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: {} };
|
|
158
350
|
}
|
|
159
|
-
};
|
|
160
|
-
var WILDCARD_HOSTS = /* @__PURE__ */ new Set(["0.0.0.0", "::", "[::]", ""]);
|
|
161
|
-
function connectHost(host) {
|
|
162
|
-
if (WILDCARD_HOSTS.has(host)) return "127.0.0.1";
|
|
163
|
-
if (host.includes(":") && !host.startsWith("[")) return `[${host}]`;
|
|
164
|
-
return host;
|
|
165
|
-
}
|
|
166
|
-
function targetFor(config, port = config.server.port) {
|
|
167
|
-
const target = { baseUrl: `http://${connectHost(config.server.host)}:${port}` };
|
|
168
|
-
if (config.server.authToken) target.token = config.server.authToken;
|
|
169
|
-
return target;
|
|
170
351
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
if (body.error?.message) message = body.error.message;
|
|
177
|
-
if (body.error?.code) code = body.error.code;
|
|
178
|
-
} catch {
|
|
179
|
-
}
|
|
180
|
-
return new GatewayError(res.status, code, message);
|
|
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);
|
|
181
357
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
return this.token ? { authorization: `Bearer ${this.token}`, ...extra } : extra;
|
|
193
|
-
}
|
|
194
|
-
/** Low-level fetch with auth, an optional timeout and transport errors as `GatewayError`. */
|
|
195
|
-
async fetch(path2, init = {}, options = {}) {
|
|
196
|
-
const signals = [];
|
|
197
|
-
if (options.signal) signals.push(options.signal);
|
|
198
|
-
if (options.timeoutMs !== void 0) signals.push(AbortSignal.timeout(options.timeoutMs));
|
|
199
|
-
const signal = signals.length > 1 ? AbortSignal.any(signals) : signals[0];
|
|
200
|
-
try {
|
|
201
|
-
return await this.fetchImpl(`${this.baseUrl}${path2}`, {
|
|
202
|
-
...init,
|
|
203
|
-
headers: this.headers(init.headers ?? {}),
|
|
204
|
-
...signal ? { signal } : {}
|
|
205
|
-
});
|
|
206
|
-
} catch (err) {
|
|
207
|
-
if (options.signal?.aborted) throw err;
|
|
208
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
209
|
-
throw new GatewayError(
|
|
210
|
-
0,
|
|
211
|
-
"unreachable",
|
|
212
|
-
`Cannot reach the gateway at ${this.baseUrl} (${reason})`
|
|
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)"
|
|
213
368
|
);
|
|
214
369
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
if (body !== void 0) {
|
|
219
|
-
init.body = JSON.stringify(body);
|
|
220
|
-
init.headers = { "content-type": "application/json" };
|
|
221
|
-
}
|
|
222
|
-
const res = await this.fetch(path2, init, { timeoutMs: 1e4, ...options });
|
|
223
|
-
if (!res.ok) throw await errorFrom(res);
|
|
224
|
-
if (res.status === 204) return void 0;
|
|
225
|
-
return await res.json();
|
|
226
|
-
}
|
|
227
|
-
get(path2, options) {
|
|
228
|
-
return this.request("GET", path2, void 0, options);
|
|
229
|
-
}
|
|
230
|
-
post(path2, body, options) {
|
|
231
|
-
return this.request("POST", path2, body, options);
|
|
232
|
-
}
|
|
233
|
-
patch(path2, body, options) {
|
|
234
|
-
return this.request("PATCH", path2, body, options);
|
|
235
|
-
}
|
|
236
|
-
delete(path2, options) {
|
|
237
|
-
return this.request("DELETE", path2, void 0, options);
|
|
238
|
-
}
|
|
239
|
-
/**
|
|
240
|
-
* `GET /api/health`, or `undefined` when nothing (or something that is not DaveCode) answers.
|
|
241
|
-
* Throws `GatewayError` 401 when a gateway is running but rejects our bearer token.
|
|
242
|
-
*/
|
|
243
|
-
async health(timeoutMs = 800) {
|
|
244
|
-
let res;
|
|
245
|
-
try {
|
|
246
|
-
res = await this.fetch("/api/health", {}, { timeoutMs });
|
|
247
|
-
} catch {
|
|
248
|
-
return void 0;
|
|
249
|
-
}
|
|
250
|
-
if (res.status === 401) {
|
|
251
|
-
throw new GatewayError(
|
|
252
|
-
401,
|
|
253
|
-
"unauthorized",
|
|
254
|
-
`The gateway at ${this.baseUrl} requires a bearer token`
|
|
255
|
-
);
|
|
256
|
-
}
|
|
257
|
-
if (!res.ok) return void 0;
|
|
258
|
-
try {
|
|
259
|
-
const body = await res.json();
|
|
260
|
-
return body.status === "ok" && typeof body.version === "string" ? body : void 0;
|
|
261
|
-
} catch {
|
|
262
|
-
return void 0;
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
/**
|
|
266
|
-
* `POST /v1/chat/completions` with `stream: true`. Resolves once the gateway answered (after
|
|
267
|
-
* routing and any failover), so `meta` already names the serving account.
|
|
268
|
-
*/
|
|
269
|
-
async chatStream(request, signal) {
|
|
270
|
-
const res = await this.fetch(
|
|
271
|
-
"/v1/chat/completions",
|
|
272
|
-
{
|
|
273
|
-
method: "POST",
|
|
274
|
-
headers: { "content-type": "application/json", accept: "text/event-stream" },
|
|
275
|
-
body: JSON.stringify({ ...request, stream: true })
|
|
276
|
-
},
|
|
277
|
-
signal ? { signal } : {}
|
|
278
|
-
);
|
|
279
|
-
if (!res.ok) throw await errorFrom(res);
|
|
280
|
-
return { meta: streamMeta(res.headers), chunks: chatChunks(readSSE(bodyOf(res))) };
|
|
281
|
-
}
|
|
282
|
-
/** Live `/api/events` (the server replays its recent buffer first). */
|
|
283
|
-
async *events(signal) {
|
|
284
|
-
const res = await this.fetch(
|
|
285
|
-
"/api/events",
|
|
286
|
-
{ headers: { accept: "text/event-stream" } },
|
|
287
|
-
signal ? { signal } : {}
|
|
288
|
-
);
|
|
289
|
-
if (!res.ok) throw await errorFrom(res);
|
|
290
|
-
yield* daveEvents(readSSE(bodyOf(res)));
|
|
291
|
-
}
|
|
292
|
-
};
|
|
293
|
-
function streamMeta(headers) {
|
|
294
|
-
const meta = { failovers: Number(headers.get("x-davecode-failovers") ?? 0) || 0 };
|
|
295
|
-
const requestId = headers.get("x-davecode-request-id");
|
|
296
|
-
const accountId = headers.get("x-davecode-account");
|
|
297
|
-
const provider = headers.get("x-davecode-provider");
|
|
298
|
-
if (requestId) meta.requestId = requestId;
|
|
299
|
-
if (accountId) meta.accountId = accountId;
|
|
300
|
-
if (provider) meta.provider = provider;
|
|
301
|
-
return meta;
|
|
302
|
-
}
|
|
303
|
-
function bodyOf(res) {
|
|
304
|
-
if (!res.body) throw new GatewayError(res.status, "empty_body", "The gateway sent no body");
|
|
305
|
-
return res.body;
|
|
306
|
-
}
|
|
307
|
-
async function findGateway(config, options = {}) {
|
|
308
|
-
const client = new GatewayClient(targetFor(config, options.port), options.fetch);
|
|
309
|
-
const health = await client.health(options.timeoutMs);
|
|
310
|
-
return health ? { client, health } : void 0;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// src/lib/proc.ts
|
|
314
|
-
import { spawn as nodeSpawn } from "child_process";
|
|
315
|
-
import { existsSync, statSync } from "fs";
|
|
316
|
-
import path from "path";
|
|
317
|
-
function splitPathEnv(pathEnv, delimiter) {
|
|
318
|
-
if (delimiter === ";") return pathEnv.split(";").filter(Boolean);
|
|
319
|
-
const dirs = [];
|
|
320
|
-
let pendingDrive;
|
|
321
|
-
for (const piece of pathEnv.split(":")) {
|
|
322
|
-
if (pendingDrive !== void 0) {
|
|
323
|
-
dirs.push(`${pendingDrive}:${piece}`);
|
|
324
|
-
pendingDrive = void 0;
|
|
325
|
-
continue;
|
|
326
|
-
}
|
|
327
|
-
if (/^[A-Za-z]$/.test(piece)) {
|
|
328
|
-
pendingDrive = piece;
|
|
329
|
-
continue;
|
|
330
|
-
}
|
|
331
|
-
if (piece) dirs.push(piece);
|
|
332
|
-
}
|
|
333
|
-
if (pendingDrive !== void 0) dirs.push(pendingDrive);
|
|
334
|
-
return dirs;
|
|
335
|
-
}
|
|
336
|
-
function which(command, env = process.env, platform = process.platform) {
|
|
337
|
-
const isFile = (p) => {
|
|
370
|
+
return brain;
|
|
371
|
+
};
|
|
372
|
+
const guarded = async (reply, run) => {
|
|
338
373
|
try {
|
|
339
|
-
return
|
|
340
|
-
} catch {
|
|
341
|
-
return
|
|
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;
|
|
342
382
|
}
|
|
343
383
|
};
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
const dirs = splitPathEnv(env.PATH ?? env.Path ?? "", delimiter);
|
|
350
|
-
const exts = win ? [
|
|
351
|
-
".exe",
|
|
352
|
-
".com",
|
|
353
|
-
"",
|
|
354
|
-
...(env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").map((e) => e.toLowerCase()).filter((e) => e && e !== ".exe" && e !== ".com")
|
|
355
|
-
] : [""];
|
|
356
|
-
for (const dir of dirs) {
|
|
357
|
-
for (const ext of exts) {
|
|
358
|
-
const candidate = path.join(dir, command + ext);
|
|
359
|
-
if (isFile(candidate)) return candidate;
|
|
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));
|
|
360
389
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
);
|
|
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));
|
|
375
404
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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) }
|
|
387
425
|
});
|
|
388
|
-
|
|
389
|
-
|
|
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();
|
|
390
438
|
});
|
|
391
439
|
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
import { existsSync as existsSync3 } from "fs";
|
|
395
|
-
import { access } from "fs/promises";
|
|
396
|
-
import { createRequire } from "module";
|
|
397
|
-
import { dirname, join, resolve as resolve2 } from "path";
|
|
398
|
-
import { fileURLToPath } from "url";
|
|
399
|
-
|
|
400
|
-
// ../server/dist/index.js
|
|
401
|
-
import { timingSafeEqual } from "crypto";
|
|
402
|
-
import { existsSync as existsSync2 } from "fs";
|
|
403
|
-
import { resolve } from "path";
|
|
404
|
-
import cors from "@fastify/cors";
|
|
405
|
-
import fastifyStatic from "@fastify/static";
|
|
406
|
-
import Fastify from "fastify";
|
|
407
|
-
import { z as z4 } from "zod";
|
|
408
|
-
import { z } from "zod";
|
|
409
|
-
import { z as z2 } from "zod";
|
|
410
|
-
import { z as z3 } from "zod";
|
|
411
|
-
import { z as z5 } from "zod";
|
|
412
|
-
import { Readable } from "stream";
|
|
413
|
-
import { z as z6 } from "zod";
|
|
414
|
-
function sendApiError(reply, status, code, message, extra = {}) {
|
|
415
|
-
return reply.code(status).send({ error: { message, code, ...extra } });
|
|
416
|
-
}
|
|
417
|
-
function sendOpenAIError(reply, status, body) {
|
|
418
|
-
return reply.code(status).send({ error: body });
|
|
440
|
+
function isReply(value) {
|
|
441
|
+
return typeof value === "object" && value !== null && "sent" in value && typeof value.code === "function";
|
|
419
442
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
return "api_error";
|
|
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));
|
|
425
447
|
}
|
|
426
|
-
function
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
case "auth":
|
|
449
|
-
status = 502;
|
|
450
|
-
code = "upstream_auth_error";
|
|
451
|
-
break;
|
|
452
|
-
default:
|
|
453
|
-
status = 502;
|
|
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
|
+
);
|
|
454
470
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
var contentPart = z.discriminatedUnion("type", [
|
|
464
|
-
z.object({ type: z.literal("text"), text: z.string() }),
|
|
465
|
-
z.object({
|
|
466
|
-
type: z.literal("image_url"),
|
|
467
|
-
image_url: z.object({
|
|
468
|
-
url: z.string(),
|
|
469
|
-
detail: z.enum(["auto", "low", "high"]).optional()
|
|
470
|
-
})
|
|
471
|
-
})
|
|
472
|
-
]);
|
|
473
|
-
var toolCall = z.object({
|
|
474
|
-
id: z.string(),
|
|
475
|
-
type: z.literal("function"),
|
|
476
|
-
function: z.object({ name: z.string(), arguments: z.string() })
|
|
477
|
-
});
|
|
478
|
-
var chatMessage = z.object({
|
|
479
|
-
role: z.enum(["system", "user", "assistant", "tool"]),
|
|
480
|
-
content: z.union([z.string(), z.array(contentPart), z.null()]),
|
|
481
|
-
name: z.string().optional(),
|
|
482
|
-
tool_call_id: z.string().optional(),
|
|
483
|
-
tool_calls: z.array(toolCall).optional()
|
|
484
|
-
});
|
|
485
|
-
var chatRequestSchema = z.object({
|
|
486
|
-
model: z.string().min(1),
|
|
487
|
-
messages: z.array(chatMessage).min(1),
|
|
488
|
-
stream: z.boolean().optional(),
|
|
489
|
-
temperature: z.number().min(0).max(2).optional(),
|
|
490
|
-
top_p: z.number().min(0).max(1).optional(),
|
|
491
|
-
max_tokens: z.number().int().positive().optional(),
|
|
492
|
-
stop: z.union([z.string(), z.array(z.string())]).optional(),
|
|
493
|
-
tools: z.array(
|
|
494
|
-
z.object({
|
|
495
|
-
type: z.literal("function"),
|
|
496
|
-
function: z.object({
|
|
497
|
-
name: z.string(),
|
|
498
|
-
description: z.string().optional(),
|
|
499
|
-
parameters: z.record(z.string(), z.unknown()).optional()
|
|
500
|
-
})
|
|
501
|
-
})
|
|
502
|
-
).optional(),
|
|
503
|
-
tool_choice: z.union([
|
|
504
|
-
z.enum(["auto", "none", "required"]),
|
|
505
|
-
z.object({ type: z.literal("function"), function: z.object({ name: z.string() }) })
|
|
506
|
-
]).optional(),
|
|
507
|
-
user: z.string().optional()
|
|
508
|
-
});
|
|
509
|
-
var positiveInt = z.number().int().positive();
|
|
510
|
-
var quotaLimits = z.object({
|
|
511
|
-
tpm: positiveInt.optional(),
|
|
512
|
-
rpm: positiveInt.optional(),
|
|
513
|
-
tokens5h: positiveInt.optional(),
|
|
514
|
-
requests5h: positiveInt.optional(),
|
|
515
|
-
tokensDaily: positiveInt.optional(),
|
|
516
|
-
requestsDaily: positiveInt.optional()
|
|
517
|
-
}).strict();
|
|
518
|
-
var SECRET_KEY = /^(api[-_]?key|secret|client[-_]?secret|(access|refresh|session|auth|bearer)?[-_]?token|password|passwd|cookies?|authorization|credentials?)$/i;
|
|
519
|
-
var accountConfig = z.record(z.string(), z.unknown()).refine((config) => !Object.keys(config).some((key) => SECRET_KEY.test(key)), {
|
|
520
|
-
message: 'config must not contain credentials; send them in "secret" instead'
|
|
521
|
-
});
|
|
522
|
-
var subscriptionDate = z.string().trim().refine(
|
|
523
|
-
(value) => /^\d{4}-\d{2}-\d{2}(T[\d:.]+(Z|[+-]\d{2}:?\d{2})?)?$/.test(value) && !Number.isNaN(Date.parse(value)),
|
|
524
|
-
{ message: "expected a date (YYYY-MM-DD) or an ISO timestamp" }
|
|
525
|
-
);
|
|
526
|
-
var subscriptionSchema = z.object({
|
|
527
|
-
plan: z.string().trim().min(1).max(120).optional(),
|
|
528
|
-
paidAt: subscriptionDate.optional(),
|
|
529
|
-
renewsAt: subscriptionDate.optional(),
|
|
530
|
-
price: z.object({
|
|
531
|
-
amount: z.number().min(0).max(1e6),
|
|
532
|
-
currency: z.string().trim().regex(/^[A-Za-z]{3}$/, "expected a 3-letter currency code").transform((c) => c.toUpperCase()),
|
|
533
|
-
period: z.enum(["month", "year"])
|
|
534
|
-
}).strict().optional(),
|
|
535
|
-
notes: z.string().trim().max(1e3).optional()
|
|
536
|
-
}).strict();
|
|
537
|
-
var accountFields = {
|
|
538
|
-
label: z.string().trim().min(1).max(200),
|
|
539
|
-
enabled: z.boolean().optional(),
|
|
540
|
-
priority: z.number().int().min(0).max(1e6).optional(),
|
|
541
|
-
weight: z.number().min(0).max(1e3).optional(),
|
|
542
|
-
limits: quotaLimits.optional(),
|
|
543
|
-
config: accountConfig.optional(),
|
|
544
|
-
/** API key / token. Encrypted at rest and never returned. */
|
|
545
|
-
secret: z.string().min(1).max(1e5).optional()
|
|
546
|
-
};
|
|
547
|
-
var accountCreateSchema = z.object({ provider: providerKind, ...accountFields, subscription: subscriptionSchema.optional() }).strict();
|
|
548
|
-
var accountPatchSchema = z.object({
|
|
549
|
-
...accountFields,
|
|
550
|
-
label: accountFields.label.optional(),
|
|
551
|
-
subscription: subscriptionSchema.nullable().optional()
|
|
552
|
-
}).strict();
|
|
553
|
-
var routesUpdateSchema = z.object({ routes: z.array(routeSchema).max(200), defaultRoute: z.string().min(1).optional() }).strict().superRefine((body, ctx) => {
|
|
554
|
-
const seen = /* @__PURE__ */ new Set();
|
|
555
|
-
body.routes.forEach((route, index) => {
|
|
556
|
-
if (seen.has(route.name)) {
|
|
557
|
-
ctx.addIssue({
|
|
558
|
-
code: "custom",
|
|
559
|
-
path: ["routes", index, "name"],
|
|
560
|
-
message: `duplicate route name "${route.name}"`
|
|
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 }
|
|
561
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);
|
|
562
501
|
}
|
|
563
|
-
|
|
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 };
|
|
564
515
|
});
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
});
|
|
573
|
-
var taskText = {
|
|
574
|
-
title: z.string().trim().min(1).max(300),
|
|
575
|
-
description: z.string().max(5e4),
|
|
576
|
-
acceptance: z.array(z.string().trim().min(1).max(2e3)).max(100),
|
|
577
|
-
notes: z.string().max(5e4),
|
|
578
|
-
dependsOn: z.array(z.string().min(1)).max(500),
|
|
579
|
-
priority: z.number().finite()
|
|
580
|
-
};
|
|
581
|
-
var taskCreateSchema = z.object({
|
|
582
|
-
id: taskIdSchema,
|
|
583
|
-
title: taskText.title,
|
|
584
|
-
description: taskText.description.optional(),
|
|
585
|
-
dependsOn: taskText.dependsOn.optional(),
|
|
586
|
-
priority: taskText.priority.optional(),
|
|
587
|
-
acceptance: taskText.acceptance.optional()
|
|
588
|
-
}).strict();
|
|
589
|
-
var taskPatchSchema = z.object({
|
|
590
|
-
title: taskText.title.optional(),
|
|
591
|
-
description: taskText.description.nullable().optional(),
|
|
592
|
-
dependsOn: taskText.dependsOn.optional(),
|
|
593
|
-
priority: taskText.priority.nullable().optional(),
|
|
594
|
-
acceptance: taskText.acceptance.nullable().optional(),
|
|
595
|
-
notes: taskText.notes.nullable().optional(),
|
|
596
|
-
status: z.enum(TASK_STATUSES).optional()
|
|
597
|
-
}).strict().refine((patch) => Object.values(patch).some((v) => v !== void 0), {
|
|
598
|
-
message: "send at least one field to change"
|
|
599
|
-
});
|
|
600
|
-
var runnerStartSchema = z.object({ taskId: z.string().min(1).optional() }).strict();
|
|
601
|
-
var goalStartSchema = z.object({
|
|
602
|
-
objective: z.string().trim().min(1).max(4e3),
|
|
603
|
-
maxRounds: z.number().int().min(1).max(20).optional(),
|
|
604
|
-
maxTasks: z.number().int().min(1).max(100).optional(),
|
|
605
|
-
maxTokens: z.number().int().min(1e3).optional(),
|
|
606
|
-
dryRun: z.boolean().optional()
|
|
607
|
-
}).strict();
|
|
608
|
-
var timeseriesQuerySchema = z.object({
|
|
609
|
-
minutes: z.coerce.number().int().min(1).max(7 * 24 * 60).default(60),
|
|
610
|
-
bucketSec: z.coerce.number().int().min(1).max(86400).default(60)
|
|
611
|
-
});
|
|
612
|
-
var limitQuerySchema = (fallback, max = 1e3) => z.object({ limit: z.coerce.number().int().min(1).max(max).default(fallback) });
|
|
613
|
-
function registerGoalRoutes(app, engine, options) {
|
|
614
|
-
const { audit } = engine;
|
|
615
|
-
const unavailable = (reply) => sendApiError(
|
|
616
|
-
reply,
|
|
617
|
-
501,
|
|
618
|
-
"goals_unavailable",
|
|
619
|
-
"Goal mode is not available; start the gateway inside a project (davecode init)"
|
|
620
|
-
);
|
|
621
|
-
const guarded = async (reply, run) => {
|
|
622
|
-
try {
|
|
623
|
-
return { ok: true, value: await run() };
|
|
624
|
-
} catch (error) {
|
|
625
|
-
const { statusCode, code } = error;
|
|
626
|
-
if (typeof statusCode === "number" && statusCode < 500 && typeof code === "string") {
|
|
627
|
-
return {
|
|
628
|
-
ok: false,
|
|
629
|
-
reply: sendApiError(reply, statusCode, code, error.message)
|
|
630
|
-
};
|
|
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");
|
|
631
524
|
}
|
|
632
|
-
throw error;
|
|
633
525
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
const goals = control(reply);
|
|
641
|
-
if (!goals) return reply;
|
|
642
|
-
return { goals: await goals.list() };
|
|
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();
|
|
643
532
|
});
|
|
644
|
-
app.get("/api/
|
|
645
|
-
const
|
|
646
|
-
if (!
|
|
647
|
-
|
|
648
|
-
if (!goal) return sendApiError(reply, 404, "not_found", `unknown goal "${request.params.id}"`);
|
|
649
|
-
return { goal };
|
|
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) };
|
|
650
537
|
});
|
|
651
|
-
app.post("/api/
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
return sendApiError(reply, 400, "invalid_body", z2.prettifyError(parsed.error));
|
|
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");
|
|
655
541
|
}
|
|
656
|
-
const
|
|
657
|
-
if (!
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
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 };
|
|
661
554
|
audit.record({
|
|
662
555
|
actor: "api",
|
|
663
|
-
action: "
|
|
664
|
-
target:
|
|
665
|
-
details: {
|
|
556
|
+
action: "account.connect",
|
|
557
|
+
target: account.id,
|
|
558
|
+
details: { launched }
|
|
666
559
|
});
|
|
667
|
-
|
|
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 };
|
|
668
567
|
});
|
|
669
|
-
app.
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
const
|
|
675
|
-
if (!
|
|
676
|
-
|
|
677
|
-
|
|
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 };
|
|
678
583
|
});
|
|
679
|
-
app.get(
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
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
|
+
}
|
|
692
613
|
}
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
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
|
+
};
|
|
701
639
|
});
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
return { status: 409, code: issue.code, extra: {} };
|
|
711
|
-
case "has_dependents":
|
|
712
|
-
return { status: 409, code: "has_dependents", extra: { dependents: issue.dependents ?? [] } };
|
|
713
|
-
case "cycle":
|
|
714
|
-
return { status: 400, code: "cycle", extra: { cycle: issue.cycle ?? [] } };
|
|
715
|
-
case "unknown_dependency":
|
|
716
|
-
case "self_dependency":
|
|
717
|
-
return { status: 400, code: issue.code, extra: {} };
|
|
718
|
-
default:
|
|
719
|
-
return { status: 409, code: "invalid_graph", extra: {} };
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
function sendGraphError(reply, error) {
|
|
723
|
-
const issue = error.issues.find((i) => i.code === "cycle") ?? error.issues[0];
|
|
724
|
-
if (!issue) return sendApiError(reply, 400, "invalid_graph", error.message);
|
|
725
|
-
const { status, code, extra } = describeIssue(issue);
|
|
726
|
-
return sendApiError(reply, status, code, issue.message, extra);
|
|
727
|
-
}
|
|
728
|
-
function registerTaskRoutes(app, engine, options) {
|
|
729
|
-
const { audit } = engine;
|
|
730
|
-
const writable = (reply) => {
|
|
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 () => {
|
|
731
648
|
const { brain } = options;
|
|
732
|
-
if (!brain) {
|
|
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) {
|
|
733
657
|
return sendApiError(
|
|
734
658
|
reply,
|
|
735
|
-
|
|
736
|
-
"
|
|
737
|
-
"
|
|
659
|
+
501,
|
|
660
|
+
"runner_unavailable",
|
|
661
|
+
"The autonomous runner is not available"
|
|
738
662
|
);
|
|
739
663
|
}
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
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
|
+
}
|
|
743
670
|
try {
|
|
744
|
-
|
|
671
|
+
if (action === "start") await runner.start(taskId === void 0 ? {} : { taskId });
|
|
672
|
+
else await runner[action]();
|
|
745
673
|
} catch (error) {
|
|
746
|
-
if (error instanceof TaskGraphError) return sendGraphError(reply, error);
|
|
747
674
|
const { statusCode, code } = error;
|
|
748
675
|
if (typeof statusCode === "number" && statusCode < 500 && typeof code === "string") {
|
|
749
676
|
return sendApiError(reply, statusCode, code, error.message);
|
|
750
677
|
}
|
|
751
678
|
throw error;
|
|
752
679
|
}
|
|
753
|
-
};
|
|
754
|
-
const readOnly = (reply) => sendApiError(reply, 501, "brain_read_only", "The project brain of this gateway is read-only");
|
|
755
|
-
app.post("/api/tasks", async (request, reply) => {
|
|
756
|
-
const parsed = taskCreateSchema.safeParse(request.body);
|
|
757
|
-
if (!parsed.success) {
|
|
758
|
-
return sendApiError(reply, 400, "invalid_body", z3.prettifyError(parsed.error));
|
|
759
|
-
}
|
|
760
|
-
const brain = writable(reply);
|
|
761
|
-
if (isReply(brain)) return brain;
|
|
762
|
-
if (!brain.createTask) return readOnly(reply);
|
|
763
|
-
const input = parsed.data;
|
|
764
|
-
const create = brain.createTask.bind(brain);
|
|
765
|
-
const result = await guarded(reply, () => create(input));
|
|
766
|
-
if (isReply(result)) return result;
|
|
767
|
-
audit.record({ actor: "api", action: "task.create", target: input.id });
|
|
768
|
-
return reply.code(201).send({ task: result });
|
|
769
|
-
});
|
|
770
|
-
app.patch("/api/tasks/:id", async (request, reply) => {
|
|
771
|
-
const parsed = taskPatchSchema.safeParse(request.body);
|
|
772
|
-
if (!parsed.success) {
|
|
773
|
-
return sendApiError(reply, 400, "invalid_body", z3.prettifyError(parsed.error));
|
|
774
|
-
}
|
|
775
|
-
const brain = writable(reply);
|
|
776
|
-
if (isReply(brain)) return brain;
|
|
777
|
-
if (!brain.updateTask) return readOnly(reply);
|
|
778
|
-
const { description, priority, acceptance, notes, ...rest } = parsed.data;
|
|
779
|
-
const patch = { ...rest };
|
|
780
|
-
const clear = [];
|
|
781
|
-
const optional = { description, priority, acceptance, notes };
|
|
782
|
-
for (const [key, value] of Object.entries(optional)) {
|
|
783
|
-
if (value === null) clear.push(key);
|
|
784
|
-
else if (value !== void 0) patch[key] = value;
|
|
785
|
-
}
|
|
786
|
-
const { id } = request.params;
|
|
787
|
-
const update = brain.updateTask.bind(brain);
|
|
788
|
-
const result = await guarded(reply, () => update(id, patch, { clear }));
|
|
789
|
-
if (isReply(result)) return result;
|
|
790
680
|
audit.record({
|
|
791
681
|
actor: "api",
|
|
792
|
-
action:
|
|
793
|
-
|
|
794
|
-
details: { fields: Object.keys(parsed.data) }
|
|
682
|
+
action: `runner.${action}`,
|
|
683
|
+
...taskId !== void 0 ? { details: { taskId } } : {}
|
|
795
684
|
});
|
|
796
|
-
return {
|
|
797
|
-
}
|
|
798
|
-
app.
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
if (!brain.removeTask) return readOnly(reply);
|
|
802
|
-
const { id } = request.params;
|
|
803
|
-
const remove = brain.removeTask.bind(brain);
|
|
804
|
-
const result = await guarded(reply, () => remove(id));
|
|
805
|
-
if (isReply(result)) return result;
|
|
806
|
-
audit.record({ actor: "api", action: "task.delete", target: id });
|
|
807
|
-
return reply.code(204).send();
|
|
808
|
-
});
|
|
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"));
|
|
809
690
|
}
|
|
810
|
-
function
|
|
811
|
-
return
|
|
691
|
+
function authCookie(request, name) {
|
|
692
|
+
return request.headers.cookie?.split(";").map((part) => part.trim()).find((part) => part.startsWith(`${name}=`))?.slice(name.length + 1);
|
|
812
693
|
}
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
function invalid(reply, error) {
|
|
816
|
-
return sendApiError(reply, 400, "invalid_body", z4.prettifyError(error));
|
|
694
|
+
function cookie(name, value, seconds, secure) {
|
|
695
|
+
return `${name}=${value}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${seconds}${secure ? "; Secure" : ""}`;
|
|
817
696
|
}
|
|
818
|
-
function
|
|
819
|
-
const
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
})
|
|
828
|
-
app.
|
|
829
|
-
|
|
830
|
-
const parsed = accountCreateSchema.safeParse(request.body);
|
|
831
|
-
if (!parsed.success) return invalid(reply, parsed.error);
|
|
832
|
-
const { secret, ...input } = parsed.data;
|
|
833
|
-
if (input.provider === "gemini-web" && !engine.config.experimental.geminiWeb) {
|
|
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)
|
|
834
709
|
return sendApiError(
|
|
835
710
|
reply,
|
|
836
|
-
|
|
837
|
-
"
|
|
838
|
-
"
|
|
711
|
+
403,
|
|
712
|
+
"invalid_origin",
|
|
713
|
+
"Open DaveCode from its configured origin."
|
|
839
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;
|
|
840
781
|
}
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
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"
|
|
849
841
|
});
|
|
850
|
-
return accounts.get(created.id) ?? created;
|
|
851
|
-
})();
|
|
852
|
-
emitAccount(account);
|
|
853
|
-
return reply.code(201).send({ account });
|
|
854
|
-
});
|
|
855
|
-
app.patch("/api/accounts/:id", async (request, reply) => {
|
|
856
|
-
const parsed = accountPatchSchema.safeParse(request.body);
|
|
857
|
-
if (!parsed.success) return invalid(reply, parsed.error);
|
|
858
|
-
const existing = accounts.get(request.params.id);
|
|
859
|
-
if (!existing) return sendApiError(reply, 404, "not_found", "Account not found");
|
|
860
|
-
const { secret, ...fields } = parsed.data;
|
|
861
|
-
const patch = { ...fields };
|
|
862
|
-
const enabled = fields.enabled ?? existing.enabled;
|
|
863
|
-
if (!enabled) {
|
|
864
|
-
patch.status = "disabled";
|
|
865
|
-
} else if (fields.enabled === true || secret !== void 0) {
|
|
866
|
-
patch.status = "active";
|
|
867
|
-
patch.cooldownUntil = null;
|
|
868
|
-
patch.lastError = null;
|
|
869
|
-
engine.router.breaker.reset(existing.id);
|
|
870
|
-
engine.reported.forget(existing.id);
|
|
871
842
|
}
|
|
872
|
-
const
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
return updated;
|
|
882
|
-
})();
|
|
883
|
-
emitAccount(account);
|
|
884
|
-
return { account };
|
|
885
|
-
});
|
|
886
|
-
app.delete("/api/accounts/:id", async (request, reply) => {
|
|
887
|
-
const { id } = request.params;
|
|
888
|
-
if (!accounts.delete(id)) return sendApiError(reply, 404, "not_found", "Account not found");
|
|
889
|
-
for (const cleanup of [() => engine.sandboxes.remove(id), () => engine.chromium.remove(id)]) {
|
|
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) {
|
|
890
852
|
try {
|
|
891
|
-
|
|
853
|
+
const { completion, meta } = await engine.router.complete(body, routeOptions);
|
|
854
|
+
setRoutingHeaders(reply, meta);
|
|
855
|
+
return completion;
|
|
892
856
|
} catch (err) {
|
|
893
|
-
|
|
857
|
+
return sendRoutingError(reply, err);
|
|
894
858
|
}
|
|
895
859
|
}
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
return reply.code(204).send();
|
|
902
|
-
});
|
|
903
|
-
app.get("/api/accounts/:id/connection", async (request, reply) => {
|
|
904
|
-
const account = accounts.get(request.params.id);
|
|
905
|
-
if (!account) return sendApiError(reply, 404, "not_found", "Account not found");
|
|
906
|
-
return { connection: await engine.connections.check(account) };
|
|
907
|
-
});
|
|
908
|
-
app.post("/api/accounts/:id/connect", async (request, reply) => {
|
|
909
|
-
if (!request.headers["content-type"]?.toLowerCase().startsWith("application/json")) {
|
|
910
|
-
return sendApiError(reply, 415, "json_required", "Send Content-Type: application/json");
|
|
860
|
+
let result;
|
|
861
|
+
try {
|
|
862
|
+
result = await engine.router.stream(body, routeOptions);
|
|
863
|
+
} catch (err) {
|
|
864
|
+
return sendRoutingError(reply, err);
|
|
911
865
|
}
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
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
|
+
}
|
|
921
910
|
}
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
command,
|
|
934
|
-
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}`
|
|
935
|
-
};
|
|
936
|
-
return { connect: result };
|
|
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
|
|
937
922
|
});
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
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);
|
|
951
956
|
});
|
|
952
|
-
|
|
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
|
+
}
|
|
953
969
|
});
|
|
954
|
-
app.
|
|
955
|
-
const
|
|
956
|
-
|
|
957
|
-
|
|
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);
|
|
958
982
|
});
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
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" } });
|
|
963
1002
|
});
|
|
964
|
-
app
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
app
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
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;
|
|
981
1048
|
}
|
|
982
1049
|
}
|
|
1050
|
+
this.processLine(line, out);
|
|
1051
|
+
start = i + 1;
|
|
983
1052
|
}
|
|
984
|
-
|
|
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;
|
|
985
1126
|
try {
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
});
|
|
990
|
-
} catch (error) {
|
|
991
|
-
if (error instanceof ConfigError) {
|
|
992
|
-
return sendApiError(reply, 409, "config_invalid", error.message);
|
|
993
|
-
}
|
|
994
|
-
throw error;
|
|
1127
|
+
parsed = JSON.parse(message.data);
|
|
1128
|
+
} catch {
|
|
1129
|
+
continue;
|
|
995
1130
|
}
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
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})`
|
|
1032
1213
|
);
|
|
1033
1214
|
}
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
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" };
|
|
1039
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;
|
|
1040
1245
|
try {
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
const { statusCode, code } = error;
|
|
1045
|
-
if (typeof statusCode === "number" && statusCode < 500 && typeof code === "string") {
|
|
1046
|
-
return sendApiError(reply, statusCode, code, error.message);
|
|
1047
|
-
}
|
|
1048
|
-
throw error;
|
|
1246
|
+
res = await this.fetch("/api/health", {}, { timeoutMs });
|
|
1247
|
+
} catch {
|
|
1248
|
+
return void 0;
|
|
1049
1249
|
}
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
return { status: runner.status() };
|
|
1056
|
-
};
|
|
1057
|
-
app.post("/api/runner/start", control("start"));
|
|
1058
|
-
app.post("/api/runner/pause", control("pause"));
|
|
1059
|
-
app.post("/api/runner/stop", control("stop"));
|
|
1060
|
-
}
|
|
1061
|
-
function authCookie(request, name) {
|
|
1062
|
-
return request.headers.cookie?.split(";").map((part) => part.trim()).find((part) => part.startsWith(`${name}=`))?.slice(name.length + 1);
|
|
1063
|
-
}
|
|
1064
|
-
function cookie(name, value, seconds, secure) {
|
|
1065
|
-
return `${name}=${value}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${seconds}${secure ? "; Secure" : ""}`;
|
|
1066
|
-
}
|
|
1067
|
-
function registerAuthRoutes(app, login) {
|
|
1068
|
-
const secure = login?.options.origin.startsWith("https:") ?? false;
|
|
1069
|
-
app.get("/api/auth/session", async (request, reply) => {
|
|
1070
|
-
reply.header("Cache-Control", "no-store");
|
|
1071
|
-
return login?.status(authCookie(request, "dc_session")) ?? {
|
|
1072
|
-
required: false,
|
|
1073
|
-
providers: [],
|
|
1074
|
-
user: null
|
|
1075
|
-
};
|
|
1076
|
-
});
|
|
1077
|
-
app.post("/api/auth/logout", async (request, reply) => {
|
|
1078
|
-
if (login && request.headers.origin !== login.options.origin)
|
|
1079
|
-
return sendApiError(
|
|
1080
|
-
reply,
|
|
1081
|
-
403,
|
|
1082
|
-
"invalid_origin",
|
|
1083
|
-
"Open DaveCode from its configured origin."
|
|
1250
|
+
if (res.status === 401) {
|
|
1251
|
+
throw new GatewayError(
|
|
1252
|
+
401,
|
|
1253
|
+
"unauthorized",
|
|
1254
|
+
`The gateway at ${this.baseUrl} requires a bearer token`
|
|
1084
1255
|
);
|
|
1085
|
-
login?.logout(authCookie(request, "dc_session"));
|
|
1086
|
-
return reply.header("Cache-Control", "no-store").header("Set-Cookie", cookie("dc_session", "", 0, secure)).code(204).send();
|
|
1087
|
-
});
|
|
1088
|
-
for (const provider of ["google", "github"]) {
|
|
1089
|
-
app.get(`/api/auth/${provider}/start`, async (_request, reply) => {
|
|
1090
|
-
if (!login?.options.providers[provider])
|
|
1091
|
-
return sendApiError(
|
|
1092
|
-
reply,
|
|
1093
|
-
503,
|
|
1094
|
-
"login_not_configured",
|
|
1095
|
-
"This sign-in provider is not configured."
|
|
1096
|
-
);
|
|
1097
|
-
try {
|
|
1098
|
-
const flow = login.begin(provider);
|
|
1099
|
-
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);
|
|
1100
|
-
} catch {
|
|
1101
|
-
return sendApiError(reply, 429, "login_busy", "Sign-in is busy. Try again shortly.");
|
|
1102
|
-
}
|
|
1103
|
-
});
|
|
1104
|
-
app.get(`/api/auth/${provider}/callback`, async (request, reply) => {
|
|
1105
|
-
reply.header("Cache-Control", "no-store").header("Referrer-Policy", "no-referrer");
|
|
1106
|
-
if (!login)
|
|
1107
|
-
return sendApiError(reply, 503, "login_not_configured", "Sign-in is not configured.");
|
|
1108
|
-
const parsed = z5.object({ state: z5.string().min(1).max(200), code: z5.string().min(1).max(4096) }).safeParse(request.query);
|
|
1109
|
-
const binding = authCookie(request, "dc_login");
|
|
1110
|
-
if (!parsed.success || !binding)
|
|
1111
|
-
return reply.redirect(`${login.options.origin}/#/login?error=signin_failed`);
|
|
1112
|
-
try {
|
|
1113
|
-
const session = await login.finish(provider, parsed.data.state, parsed.data.code, binding);
|
|
1114
|
-
login.logout(authCookie(request, "dc_session"));
|
|
1115
|
-
return reply.header("Set-Cookie", [
|
|
1116
|
-
cookie("dc_session", session, 8 * 60 * 60, secure),
|
|
1117
|
-
cookie("dc_login", "", 0, secure)
|
|
1118
|
-
]).redirect(`${login.options.origin}/`);
|
|
1119
|
-
} catch {
|
|
1120
|
-
return reply.header("Set-Cookie", cookie("dc_login", "", 0, secure)).redirect(`${login.options.origin}/#/login?error=signin_failed`);
|
|
1121
|
-
}
|
|
1122
|
-
});
|
|
1123
|
-
}
|
|
1124
|
-
}
|
|
1125
|
-
function frame(event, seq) {
|
|
1126
|
-
return `id: ${seq}
|
|
1127
|
-
event: ${event.type}
|
|
1128
|
-
data: ${JSON.stringify(event)}
|
|
1129
|
-
|
|
1130
|
-
`;
|
|
1131
|
-
}
|
|
1132
|
-
function resumeCursor(headers, query) {
|
|
1133
|
-
const fromQuery = typeof query === "object" && query !== null && "lastEventId" in query ? query.lastEventId : void 0;
|
|
1134
|
-
const raw = headers["last-event-id"] ?? fromQuery;
|
|
1135
|
-
const value = Number(Array.isArray(raw) ? raw[0] : raw);
|
|
1136
|
-
return Number.isSafeInteger(value) && value >= 0 ? value : 0;
|
|
1137
|
-
}
|
|
1138
|
-
function registerEventRoutes(app, engine, heartbeatMs) {
|
|
1139
|
-
const clients = /* @__PURE__ */ new Set();
|
|
1140
|
-
app.addHook("preClose", async () => {
|
|
1141
|
-
for (const res of clients) res.end();
|
|
1142
|
-
clients.clear();
|
|
1143
|
-
});
|
|
1144
|
-
app.get("/api/events", (request, reply) => {
|
|
1145
|
-
const cursor = resumeCursor(request.headers, request.query);
|
|
1146
|
-
reply.hijack();
|
|
1147
|
-
const res = reply.raw;
|
|
1148
|
-
const inherited = {};
|
|
1149
|
-
for (const [name, value] of Object.entries(reply.getHeaders())) {
|
|
1150
|
-
if (value !== void 0) inherited[name] = value;
|
|
1151
1256
|
}
|
|
1152
|
-
res.
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
"
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
}
|
|
1159
|
-
res.write("retry: 3000\n\n");
|
|
1160
|
-
clients.add(res);
|
|
1161
|
-
for (const { seq, event } of engine.events.since(cursor)) res.write(frame(event, seq));
|
|
1162
|
-
const unsubscribe = engine.events.subscribe((event, seq) => {
|
|
1163
|
-
res.write(frame(event, seq));
|
|
1164
|
-
});
|
|
1165
|
-
const heartbeat = setInterval(() => res.write(": ping\n\n"), heartbeatMs);
|
|
1166
|
-
heartbeat.unref();
|
|
1167
|
-
let closed = false;
|
|
1168
|
-
const cleanup = () => {
|
|
1169
|
-
if (closed) return;
|
|
1170
|
-
closed = true;
|
|
1171
|
-
clearInterval(heartbeat);
|
|
1172
|
-
unsubscribe();
|
|
1173
|
-
clients.delete(res);
|
|
1174
|
-
};
|
|
1175
|
-
res.on("close", cleanup);
|
|
1176
|
-
res.on("error", cleanup);
|
|
1177
|
-
});
|
|
1178
|
-
}
|
|
1179
|
-
function setRoutingHeaders(reply, meta) {
|
|
1180
|
-
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));
|
|
1181
|
-
}
|
|
1182
|
-
function sendRoutingError(reply, err) {
|
|
1183
|
-
const { status, body } = describeRoutingError(err);
|
|
1184
|
-
if (status === 500) reply.log.error({ err }, "chat completion failed");
|
|
1185
|
-
if (err instanceof Error && "failovers" in err && typeof err.failovers === "number") {
|
|
1186
|
-
reply.header("x-davecode-failovers", String(err.failovers));
|
|
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
|
+
}
|
|
1187
1264
|
}
|
|
1188
|
-
|
|
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;
|
|
1189
1311
|
}
|
|
1190
|
-
async function* sseFrames(chunks) {
|
|
1191
|
-
try {
|
|
1192
|
-
for await (const chunk of chunks) yield `data: ${JSON.stringify(chunk)}
|
|
1193
1312
|
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
yield `data: ${JSON.stringify({ error: describeRoutingError(err).body })}
|
|
1313
|
+
// src/lib/terminal.ts
|
|
1314
|
+
import { spawn as nodeSpawn2 } from "child_process";
|
|
1197
1315
|
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
function
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
});
|
|
1212
|
-
}
|
|
1213
|
-
const body = parsed.data;
|
|
1214
|
-
const requestId = newId2("req");
|
|
1215
|
-
reply.header("x-davecode-request-id", requestId);
|
|
1216
|
-
const abort = new AbortController();
|
|
1217
|
-
reply.raw.on("close", () => {
|
|
1218
|
-
if (!reply.raw.writableFinished) abort.abort();
|
|
1219
|
-
});
|
|
1220
|
-
const routeOptions = { requestId, signal: abort.signal };
|
|
1221
|
-
if (!body.stream) {
|
|
1222
|
-
try {
|
|
1223
|
-
const { completion, meta } = await engine.router.complete(body, routeOptions);
|
|
1224
|
-
setRoutingHeaders(reply, meta);
|
|
1225
|
-
return completion;
|
|
1226
|
-
} catch (err) {
|
|
1227
|
-
return sendRoutingError(reply, err);
|
|
1228
|
-
}
|
|
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;
|
|
1229
1329
|
}
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
} catch (err) {
|
|
1234
|
-
return sendRoutingError(reply, err);
|
|
1330
|
+
if (/^[A-Za-z]$/.test(piece)) {
|
|
1331
|
+
pendingDrive = piece;
|
|
1332
|
+
continue;
|
|
1235
1333
|
}
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
}
|
|
1241
|
-
var DEFAULT_CORS_ORIGINS = ["http://localhost:5173", "http://127.0.0.1:5173"];
|
|
1242
|
-
var EXPOSED_HEADERS = [
|
|
1243
|
-
"x-davecode-request-id",
|
|
1244
|
-
"x-davecode-account",
|
|
1245
|
-
"x-davecode-provider",
|
|
1246
|
-
"x-davecode-failovers"
|
|
1247
|
-
];
|
|
1248
|
-
function pathOf(url) {
|
|
1249
|
-
const q = url.indexOf("?");
|
|
1250
|
-
return q === -1 ? url : url.slice(0, q);
|
|
1251
|
-
}
|
|
1252
|
-
function surfaceOf(url) {
|
|
1253
|
-
const path2 = pathOf(url);
|
|
1254
|
-
if (path2 === "/v1" || path2.startsWith("/v1/")) return "v1";
|
|
1255
|
-
if (path2 === "/api" || path2.startsWith("/api/")) return "api";
|
|
1256
|
-
return "other";
|
|
1257
|
-
}
|
|
1258
|
-
function redactUrl(url) {
|
|
1259
|
-
return url.replace(/([?&](?:token|code|state|error_description)=)[^&]*/gi, "$1[redacted]");
|
|
1260
|
-
}
|
|
1261
|
-
function safeEqual(a, b) {
|
|
1262
|
-
const left = Buffer.from(a);
|
|
1263
|
-
const right = Buffer.from(b);
|
|
1264
|
-
return left.length === right.length && timingSafeEqual(left, right);
|
|
1265
|
-
}
|
|
1266
|
-
function bearer(request) {
|
|
1267
|
-
const header = request.headers.authorization;
|
|
1268
|
-
if (!header) return void 0;
|
|
1269
|
-
const match = /^Bearer\s+(.+)$/i.exec(header.trim());
|
|
1270
|
-
return match?.[1];
|
|
1334
|
+
if (piece) dirs.push(piece);
|
|
1335
|
+
}
|
|
1336
|
+
if (pendingDrive !== void 0) dirs.push(pendingDrive);
|
|
1337
|
+
return dirs;
|
|
1271
1338
|
}
|
|
1272
|
-
function
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
return { method: req.method, url: redactUrl(req.url), remoteAddress: req.ip };
|
|
1279
|
-
}
|
|
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;
|
|
1280
1345
|
}
|
|
1281
1346
|
};
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
const app = Fastify({ logger: loggerOption(options.logger), bodyLimit: 10 * 1024 * 1024 });
|
|
1285
|
-
const login = options.login === false ? void 0 : options.login ?? loginFromEnv(process.env);
|
|
1286
|
-
if (login) app.addHook("onClose", async () => login.clear());
|
|
1287
|
-
await app.register(cors, {
|
|
1288
|
-
origin: [...DEFAULT_CORS_ORIGINS, ...options.corsOrigins ?? []],
|
|
1289
|
-
methods: ["GET", "POST", "PATCH", "DELETE", "OPTIONS"],
|
|
1290
|
-
allowedHeaders: ["content-type", "authorization"],
|
|
1291
|
-
exposedHeaders: EXPOSED_HEADERS
|
|
1292
|
-
});
|
|
1293
|
-
const token = engine.config.server.authToken;
|
|
1294
|
-
if (token || login) {
|
|
1295
|
-
app.addHook("onRequest", async (request, reply) => {
|
|
1296
|
-
if (request.method === "OPTIONS") return;
|
|
1297
|
-
const surface = surfaceOf(request.url);
|
|
1298
|
-
if (surface === "other") return;
|
|
1299
|
-
if (pathOf(request.url).startsWith("/api/auth/")) return;
|
|
1300
|
-
let provided = bearer(request);
|
|
1301
|
-
if (!provided && pathOf(request.url) === "/api/events") {
|
|
1302
|
-
const query = request.query;
|
|
1303
|
-
if (typeof query?.token === "string") provided = query.token;
|
|
1304
|
-
}
|
|
1305
|
-
if (provided && token && safeEqual(provided, token)) return;
|
|
1306
|
-
if (surface === "api" && login?.user(authCookie(request, "dc_session"))) {
|
|
1307
|
-
if (!["GET", "HEAD"].includes(request.method) && request.headers.origin !== login.options.origin) {
|
|
1308
|
-
return sendApiError(
|
|
1309
|
-
reply,
|
|
1310
|
-
403,
|
|
1311
|
-
"invalid_origin",
|
|
1312
|
-
"Open DaveCode from its configured origin."
|
|
1313
|
-
);
|
|
1314
|
-
}
|
|
1315
|
-
return;
|
|
1316
|
-
}
|
|
1317
|
-
const message = "Missing or invalid bearer token";
|
|
1318
|
-
if (surface === "v1") {
|
|
1319
|
-
return sendOpenAIError(reply, 401, {
|
|
1320
|
-
message,
|
|
1321
|
-
type: "authentication_error",
|
|
1322
|
-
code: "invalid_api_key"
|
|
1323
|
-
});
|
|
1324
|
-
}
|
|
1325
|
-
return sendApiError(reply, 401, "unauthorized", message);
|
|
1326
|
-
});
|
|
1347
|
+
if (command.includes("/") || command.includes("\\") || path.isAbsolute(command)) {
|
|
1348
|
+
return isFile(command) ? command : void 0;
|
|
1327
1349
|
}
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
const status = typeof error.statusCode === "number" ? error.statusCode : 500;
|
|
1342
|
-
const message = status >= 500 ? "Internal gateway error" : error instanceof Error ? error.message : "Error";
|
|
1343
|
-
if (status >= 500) request.log.error({ err: error }, "request failed");
|
|
1344
|
-
if (surfaceOf(request.url) === "v1") {
|
|
1345
|
-
return sendOpenAIError(reply, status, {
|
|
1346
|
-
message,
|
|
1347
|
-
type: status >= 500 ? "api_error" : "invalid_request_error",
|
|
1348
|
-
code: status >= 500 ? "internal_error" : "bad_request"
|
|
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;
|
|
1350
1363
|
}
|
|
1351
|
-
return sendApiError(reply, status, status >= 500 ? "internal_error" : "bad_request", message);
|
|
1352
|
-
});
|
|
1353
|
-
const dashboardDir = engine.config.server.dashboard && options.dashboardDir && existsSync2(options.dashboardDir) ? resolve(options.dashboardDir) : void 0;
|
|
1354
|
-
if (dashboardDir) {
|
|
1355
|
-
await app.register(fastifyStatic, { root: dashboardDir });
|
|
1356
1364
|
}
|
|
1357
|
-
|
|
1358
|
-
const surface = surfaceOf(request.url);
|
|
1359
|
-
const message = `Unknown endpoint ${request.method} ${pathOf(request.url)}`;
|
|
1360
|
-
if (surface === "v1") {
|
|
1361
|
-
return sendOpenAIError(reply, 404, {
|
|
1362
|
-
message,
|
|
1363
|
-
type: "invalid_request_error",
|
|
1364
|
-
code: "not_found"
|
|
1365
|
-
});
|
|
1366
|
-
}
|
|
1367
|
-
if (surface === "api") return sendApiError(reply, 404, "not_found", message);
|
|
1368
|
-
if (dashboardDir && (request.method === "GET" || request.method === "HEAD")) {
|
|
1369
|
-
return reply.type("text/html; charset=utf-8").sendFile("index.html");
|
|
1370
|
-
}
|
|
1371
|
-
return reply.code(404).send({ error: { message, code: "not_found" } });
|
|
1372
|
-
});
|
|
1373
|
-
registerV1Routes(app, engine);
|
|
1374
|
-
registerAuthRoutes(app, login);
|
|
1375
|
-
registerApiRoutes(app, engine, options);
|
|
1376
|
-
registerEventRoutes(app, engine, options.heartbeatMs ?? 15e3);
|
|
1377
|
-
return app;
|
|
1365
|
+
return void 0;
|
|
1378
1366
|
}
|
|
1379
|
-
var
|
|
1380
|
-
|
|
1381
|
-
const
|
|
1382
|
-
const
|
|
1383
|
-
const
|
|
1384
|
-
if (
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
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
|
+
}
|
|
1388
1379
|
}
|
|
1389
|
-
|
|
1390
|
-
|
|
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
|
+
});
|
|
1391
1394
|
}
|
|
1392
1395
|
|
|
1393
1396
|
// src/lib/terminal.ts
|
|
1394
|
-
import { spawn as nodeSpawn2 } from "child_process";
|
|
1395
1397
|
var SAFE_ACCOUNT_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
|
|
1396
1398
|
function loginInvocation(accountId, argv = process.argv) {
|
|
1397
1399
|
if (!SAFE_ACCOUNT_ID.test(accountId)) throw new Error("Invalid account id");
|
|
@@ -1442,7 +1444,7 @@ function terminalCommand(argv, options = {}) {
|
|
|
1442
1444
|
}
|
|
1443
1445
|
function launchLoginTerminal(accountId, options = {}) {
|
|
1444
1446
|
const platform = options.platform ?? process.platform;
|
|
1445
|
-
const target = terminalCommand(loginInvocation(accountId), options);
|
|
1447
|
+
const target = terminalCommand(loginInvocation(accountId, options.argv), options);
|
|
1446
1448
|
if (!target) return Promise.resolve(false);
|
|
1447
1449
|
const spawn = options.spawn ?? nodeSpawn2;
|
|
1448
1450
|
return new Promise((resolve3) => {
|
|
@@ -1551,7 +1553,7 @@ async function startRuntime(options) {
|
|
|
1551
1553
|
} : void 0;
|
|
1552
1554
|
const app = await startGateway(engine, {
|
|
1553
1555
|
logger: options.logger ?? false,
|
|
1554
|
-
...loginLauncher ? { loginLauncher } : {},
|
|
1556
|
+
...options.loginLauncher ?? loginLauncher ? { loginLauncher: options.loginLauncher ?? loginLauncher } : {},
|
|
1555
1557
|
...dashboardDir ? { dashboardDir } : {},
|
|
1556
1558
|
...project ? { brain: project.source, runner: project.runner, goals: project.goals } : {},
|
|
1557
1559
|
...options.host !== void 0 ? { host: options.host } : {},
|
|
@@ -1602,9 +1604,11 @@ export {
|
|
|
1602
1604
|
accountPatchSchema,
|
|
1603
1605
|
which,
|
|
1604
1606
|
spawnInteractive,
|
|
1607
|
+
launchLoginTerminal,
|
|
1605
1608
|
detectProjectRoot,
|
|
1606
1609
|
wireProject,
|
|
1607
1610
|
resolveDashboardDir,
|
|
1611
|
+
resolveProjectRoot,
|
|
1608
1612
|
startRuntime
|
|
1609
1613
|
};
|
|
1610
|
-
//# sourceMappingURL=chunk-
|
|
1614
|
+
//# sourceMappingURL=chunk-FYPMA55O.js.map
|