@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.
Files changed (120) hide show
  1. package/README.md +5 -4
  2. package/bin/davecode.js +0 -0
  3. package/dist/{ChatApp-FO5SABXB.js → ChatApp-CJBICQGA.js} +289 -137
  4. package/dist/ChatApp-CJBICQGA.js.map +1 -0
  5. package/dist/{RunnerView-DLVR7C22.js → RunnerView-A7JSPUW5.js} +5 -5
  6. package/dist/account-QWDPJWAA.js +23 -0
  7. package/dist/accounts-IQMR72FC.js +37 -0
  8. package/dist/{budget-RBHCEDPP.js → budget-XANOUVWO.js} +6 -6
  9. package/dist/chat-KDDRNC5V.js +359 -0
  10. package/dist/chat-KDDRNC5V.js.map +1 -0
  11. package/dist/{accounts-MTMGYKEP.js → chunk-44AFJL2X.js} +24 -17
  12. package/dist/chunk-44AFJL2X.js.map +1 -0
  13. package/dist/{chunk-BOLFK3BH.js → chunk-5LKOJBUL.js} +2 -2
  14. package/dist/{chunk-23KTE7IK.js → chunk-AZKACMME.js} +2 -2
  15. package/dist/{chunk-PIIJ3RSI.js → chunk-FYPMA55O.js} +1269 -1265
  16. package/dist/chunk-FYPMA55O.js.map +1 -0
  17. package/dist/{chunk-OYEYNDOO.js → chunk-GE22VBCY.js} +3 -3
  18. package/dist/{chunk-C7FDRA3U.js → chunk-HSBJDV3O.js} +660 -139
  19. package/dist/chunk-HSBJDV3O.js.map +1 -0
  20. package/dist/chunk-JLATEHS7.js +20 -0
  21. package/dist/chunk-JLATEHS7.js.map +1 -0
  22. package/dist/{chunk-VVCXGQMY.js → chunk-MC7RIPL7.js} +17 -8
  23. package/dist/chunk-MC7RIPL7.js.map +1 -0
  24. package/dist/{chunk-MSLLH5XP.js → chunk-PGAJIGUF.js} +4 -4
  25. package/dist/{chunk-OXRMGR72.js → chunk-PGPXE6LF.js} +2 -2
  26. package/dist/chunk-QA3O6RA2.js +81 -0
  27. package/dist/chunk-QA3O6RA2.js.map +1 -0
  28. package/dist/chunk-W7GPBEBZ.js +144 -0
  29. package/dist/chunk-W7GPBEBZ.js.map +1 -0
  30. package/dist/{chunk-IVZHKUCD.js → chunk-WQ7MEWDF.js} +3 -3
  31. package/dist/{config-FRMY5OHG.js → config-YTUPIYZT.js} +5 -5
  32. package/dist/dashboard/assets/Accounts-cq8RLMoI.js +1 -0
  33. package/dist/dashboard/assets/Badge-Z4RBdO9j.js +55 -0
  34. package/dist/dashboard/assets/Callout-w8BTYh4k.js +1 -0
  35. package/dist/dashboard/assets/Field-Cmpgg_1z.js +1 -0
  36. package/dist/dashboard/assets/Goals-Bw02to72.js +1 -0
  37. package/dist/dashboard/assets/Library-DK7mlpRb.js +1 -0
  38. package/dist/dashboard/assets/{Markdown-ClRsddnB.js → Markdown-DCZ4Awcc.js} +8 -8
  39. package/dist/dashboard/assets/Panes-_KTf8pbZ.js +8 -0
  40. package/dist/dashboard/assets/Panes-kHJ-D0s7.css +1 -0
  41. package/dist/dashboard/assets/RoutesPage-QV0oPRW9.js +4 -0
  42. package/dist/dashboard/assets/Runner-CkHBLQl0.js +1 -0
  43. package/dist/dashboard/assets/Settings-BLz2gTBR.js +1 -0
  44. package/dist/dashboard/assets/Switch-DPYlLseH.js +1 -0
  45. package/dist/dashboard/assets/Tasks-BiUQSTpt.js +2 -0
  46. package/dist/dashboard/assets/Traffic-BVfBuC70.js +1 -0
  47. package/dist/dashboard/assets/Workspace-CJTgiiU5.js +1 -0
  48. package/dist/dashboard/assets/arrow-up-DjhzZprp.js +1 -0
  49. package/dist/dashboard/assets/cn-BwE4cei0.js +1 -0
  50. package/dist/dashboard/assets/forms-DVIUAxAc.js +1 -0
  51. package/dist/dashboard/assets/index-BDBa5cb0.js +104 -0
  52. package/dist/dashboard/assets/index-BEmBgsbP.css +2 -0
  53. package/dist/dashboard/assets/task-errors-Daqwi0ue.js +1 -0
  54. package/dist/dashboard/assets/trash-BTuDcSaQ.js +1 -0
  55. package/dist/dashboard/favicon.svg +1 -7
  56. package/dist/dashboard/index.html +4 -4
  57. package/dist/{dist-ZPXEVBEC.js → dist-37VQSHHA.js} +24 -2
  58. package/dist/dist-37VQSHHA.js.map +1 -0
  59. package/dist/{doctor-7RUD4D3A.js → doctor-PYUQRRCB.js} +5 -5
  60. package/dist/{goal-JFRNPBSD.js → goal-SXZUDUCL.js} +5 -5
  61. package/dist/index.js +48 -30
  62. package/dist/index.js.map +1 -1
  63. package/dist/{init-NFYNO3A3.js → init-Z2AFQEEP.js} +6 -6
  64. package/dist/{prompts-L37J72YW.js → prompts-TZ7UC42M.js} +9 -5
  65. package/dist/prompts-TZ7UC42M.js.map +1 -0
  66. package/dist/{run-ARUPXFFB.js → run-AOS7L3RT.js} +10 -10
  67. package/dist/runtime.js +18 -0
  68. package/dist/runtime.js.map +1 -0
  69. package/dist/{start-CFPOVIAR.js → start-CEPXKZUE.js} +5 -5
  70. package/dist/{status-E4EACVE2.js → status-WNJ3GGKU.js} +6 -6
  71. package/dist/status-WNJ3GGKU.js.map +1 -0
  72. package/dist/{tasks-ZIK6BVEX.js → tasks-TFUTUMWB.js} +6 -6
  73. package/package.json +21 -15
  74. package/dist/ChatApp-FO5SABXB.js.map +0 -1
  75. package/dist/accounts-MTMGYKEP.js.map +0 -1
  76. package/dist/chat-ZDXX3N6X.js +0 -95
  77. package/dist/chat-ZDXX3N6X.js.map +0 -1
  78. package/dist/chunk-C7FDRA3U.js.map +0 -1
  79. package/dist/chunk-DYQ56FBT.js +0 -47
  80. package/dist/chunk-DYQ56FBT.js.map +0 -1
  81. package/dist/chunk-PIIJ3RSI.js.map +0 -1
  82. package/dist/chunk-VVCXGQMY.js.map +0 -1
  83. package/dist/dashboard/assets/Accounts-BR3ZfVk4.js +0 -1
  84. package/dist/dashboard/assets/Badge-B-qzNRrZ.js +0 -55
  85. package/dist/dashboard/assets/Callout-B9g_wPDi.js +0 -1
  86. package/dist/dashboard/assets/Field-BfabPEcY.js +0 -1
  87. package/dist/dashboard/assets/Goals-BsV-6m94.js +0 -1
  88. package/dist/dashboard/assets/Library-CEA9P1m-.js +0 -1
  89. package/dist/dashboard/assets/Panel-B9tSgX6l.js +0 -1
  90. package/dist/dashboard/assets/RoutesPage-Dh-BJ3ir.js +0 -4
  91. package/dist/dashboard/assets/Runner-C_e9l5l2.js +0 -1
  92. package/dist/dashboard/assets/Settings-C6Yc1Gjg.js +0 -1
  93. package/dist/dashboard/assets/Switch-CUpOJjUT.js +0 -1
  94. package/dist/dashboard/assets/Tasks-CTku0rR3.js +0 -2
  95. package/dist/dashboard/assets/Traffic-CE6wZSMr.js +0 -1
  96. package/dist/dashboard/assets/Workspace-CxFxvkA6.js +0 -1
  97. package/dist/dashboard/assets/arrow-up-DL3CuFOO.js +0 -1
  98. package/dist/dashboard/assets/forms-D6UIYgC5.js +0 -1
  99. package/dist/dashboard/assets/index-1iH716Xy.css +0 -2
  100. package/dist/dashboard/assets/index-BMWr9EKo.js +0 -104
  101. package/dist/dashboard/assets/task-errors-CE4oEr8H.js +0 -1
  102. package/dist/dashboard/assets/trash-CWqeZ7eg.js +0 -1
  103. package/dist/prompts-L37J72YW.js.map +0 -1
  104. /package/dist/{RunnerView-DLVR7C22.js.map → RunnerView-A7JSPUW5.js.map} +0 -0
  105. /package/dist/{dist-ZPXEVBEC.js.map → account-QWDPJWAA.js.map} +0 -0
  106. /package/dist/{status-E4EACVE2.js.map → accounts-IQMR72FC.js.map} +0 -0
  107. /package/dist/{budget-RBHCEDPP.js.map → budget-XANOUVWO.js.map} +0 -0
  108. /package/dist/{chunk-BOLFK3BH.js.map → chunk-5LKOJBUL.js.map} +0 -0
  109. /package/dist/{chunk-23KTE7IK.js.map → chunk-AZKACMME.js.map} +0 -0
  110. /package/dist/{chunk-OYEYNDOO.js.map → chunk-GE22VBCY.js.map} +0 -0
  111. /package/dist/{chunk-MSLLH5XP.js.map → chunk-PGAJIGUF.js.map} +0 -0
  112. /package/dist/{chunk-OXRMGR72.js.map → chunk-PGPXE6LF.js.map} +0 -0
  113. /package/dist/{chunk-IVZHKUCD.js.map → chunk-WQ7MEWDF.js.map} +0 -0
  114. /package/dist/{config-FRMY5OHG.js.map → config-YTUPIYZT.js.map} +0 -0
  115. /package/dist/{doctor-7RUD4D3A.js.map → doctor-PYUQRRCB.js.map} +0 -0
  116. /package/dist/{goal-JFRNPBSD.js.map → goal-SXZUDUCL.js.map} +0 -0
  117. /package/dist/{init-NFYNO3A3.js.map → init-Z2AFQEEP.js.map} +0 -0
  118. /package/dist/{run-ARUPXFFB.js.map → run-AOS7L3RT.js.map} +0 -0
  119. /package/dist/{start-CFPOVIAR.js.map → start-CEPXKZUE.js.map} +0 -0
  120. /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-C7FDRA3U.js";
21
+ } from "./chunk-HSBJDV3O.js";
22
22
 
23
- // src/sse.ts
24
- var SSEParser = class {
25
- buffer = "";
26
- pendingCR = false;
27
- data = [];
28
- event = "";
29
- lastId;
30
- retry;
31
- /** Feed decoded text; returns the messages completed by it. */
32
- push(chunk) {
33
- const out = [];
34
- let text = chunk;
35
- if (this.pendingCR && text.startsWith("\n")) text = text.slice(1);
36
- this.pendingCR = false;
37
- this.buffer += text;
38
- let start = 0;
39
- for (let i = 0; i < this.buffer.length; i++) {
40
- const ch = this.buffer[i];
41
- if (ch !== "\n" && ch !== "\r") continue;
42
- const line = this.buffer.slice(start, i);
43
- if (ch === "\r") {
44
- if (i + 1 < this.buffer.length) {
45
- if (this.buffer[i + 1] === "\n") i++;
46
- } else {
47
- this.pendingCR = true;
48
- }
49
- }
50
- this.processLine(line, out);
51
- start = i + 1;
52
- }
53
- this.buffer = this.buffer.slice(start);
54
- return out;
55
- }
56
- /** End of stream. An unterminated final event is discarded, as the spec requires. */
57
- end() {
58
- this.buffer = "";
59
- this.data = [];
60
- this.event = "";
61
- return [];
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
- processLine(line, out) {
64
- if (line === "") {
65
- this.dispatch(out);
66
- return;
67
- }
68
- if (line.startsWith(":")) return;
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 "event":
78
- this.event = value;
70
+ case "context_length":
71
+ status = 400;
72
+ code = "context_length_exceeded";
79
73
  break;
80
- case "id":
81
- if (!value.includes("\0")) this.lastId = value;
74
+ case "rate_limit":
75
+ case "quota_exhausted":
76
+ status = 429;
82
77
  break;
83
- case "retry":
84
- if (/^\d+$/.test(value)) this.retry = Number(value);
78
+ case "auth":
79
+ status = 502;
80
+ code = "upstream_auth_error";
85
81
  break;
86
82
  default:
87
- break;
88
- }
89
- }
90
- dispatch(out) {
91
- if (this.data.length === 0) {
92
- this.event = "";
93
- return;
83
+ status = 502;
94
84
  }
95
- const message = { event: this.event || "message", data: this.data.join("\n") };
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
- const tail = decoder.decode();
111
- if (tail) yield* parser.push(tail);
112
- yield* parser.end();
87
+ return {
88
+ status: 500,
89
+ body: { message: "Internal gateway error", type: "api_error", code: "internal_error" }
90
+ };
113
91
  }
114
- var ChatStreamError = class extends Error {
115
- code;
116
- constructor(message, code) {
117
- super(message);
118
- this.name = "ChatStreamError";
119
- this.code = code ?? void 0;
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
- async function* chatChunks(messages) {
123
- for await (const message of messages) {
124
- if (message.data === "[DONE]") return;
125
- let parsed;
126
- try {
127
- parsed = JSON.parse(message.data);
128
- } catch {
129
- continue;
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
- if (!parsed || typeof parsed !== "object") continue;
132
- const record = parsed;
133
- if (record.error && !record.choices) {
134
- throw new ChatStreamError(record.error.message ?? "Upstream error", record.error.code);
135
- }
136
- if (Array.isArray(record.choices)) yield parsed;
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
- async function* daveEvents(messages) {
140
- for await (const message of messages) {
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
- const event = JSON.parse(message.data);
143
- if (event && typeof event.type === "string") yield event;
144
- } catch {
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
- // src/client.ts
150
- var GatewayError = class extends Error {
151
- status;
152
- code;
153
- constructor(status, code, message) {
154
- super(message);
155
- this.name = "GatewayError";
156
- this.status = status;
157
- this.code = code;
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
- async function errorFrom(res) {
172
- let code = `http_${res.status}`;
173
- let message = `${res.status} ${res.statusText}`.trim();
174
- try {
175
- const body = await res.json();
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
- var GatewayClient = class {
183
- baseUrl;
184
- token;
185
- fetchImpl;
186
- constructor(target, fetchImpl = fetch) {
187
- this.baseUrl = target.baseUrl.replace(/\/+$/, "");
188
- this.token = target.token;
189
- this.fetchImpl = fetchImpl;
190
- }
191
- headers(extra = {}) {
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
- async request(method, path2, body, options = {}) {
217
- const init = { method };
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 existsSync(p) && statSync(p).isFile();
340
- } catch {
341
- return false;
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
- if (command.includes("/") || command.includes("\\") || path.isAbsolute(command)) {
345
- return isFile(command) ? command : void 0;
346
- }
347
- const win = platform === "win32";
348
- const delimiter = win ? ";" : ":";
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
- return void 0;
363
- }
364
- var SHELL_SAFE = /^[\w.:=@+\-/\\[\]]+$/;
365
- function spawnInteractive(command, args, options) {
366
- const spawn = options.spawn ?? nodeSpawn;
367
- const platform = options.platform ?? process.platform;
368
- const shim = platform === "win32" && /\.(cmd|bat)$/i.test(command);
369
- if (shim) {
370
- const unsafe = args.find((a) => !SHELL_SAFE.test(a));
371
- if (unsafe !== void 0) {
372
- return Promise.reject(
373
- new Error(`Argument ${JSON.stringify(unsafe)} cannot be passed safely to ${command}`)
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
- return new Promise((resolve3, reject) => {
378
- const child = shim ? spawn(`"${command}"`, args, {
379
- stdio: "inherit",
380
- env: options.env,
381
- shell: true,
382
- ...options.cwd ? { cwd: options.cwd } : {}
383
- }) : spawn(command, args, {
384
- stdio: "inherit",
385
- env: options.env,
386
- ...options.cwd ? { cwd: options.cwd } : {}
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
- child.on("error", reject);
389
- child.on("exit", (code, signal) => resolve3(code ?? (signal ? 130 : 1)));
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
- // src/runtime.ts
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
- function openAIType(status) {
421
- if (status === 401) return "authentication_error";
422
- if (status === 404 || status === 400 || status === 422) return "invalid_request_error";
423
- if (status === 429) return "rate_limit_error";
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 describeRoutingError(err) {
427
- if (err instanceof RouterError) {
428
- return {
429
- status: err.status,
430
- body: { message: err.message, type: openAIType(err.status), code: err.code }
431
- };
432
- }
433
- if (err instanceof ProviderError) {
434
- let status;
435
- let code = err.kind;
436
- switch (err.kind) {
437
- case "bad_request":
438
- status = 400;
439
- break;
440
- case "context_length":
441
- status = 400;
442
- code = "context_length_exceeded";
443
- break;
444
- case "rate_limit":
445
- case "quota_exhausted":
446
- status = 429;
447
- break;
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
- return { status, body: { message: err.message, type: openAIType(status), code } };
456
- }
457
- return {
458
- status: 500,
459
- body: { message: "Internal gateway error", type: "api_error", code: "internal_error" }
460
- };
461
- }
462
- var providerKind = z.enum(PROVIDER_KINDS);
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
- seen.add(route.name);
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
- if (body.defaultRoute !== void 0 && !seen.has(body.defaultRoute)) {
566
- ctx.addIssue({
567
- code: "custom",
568
- path: ["defaultRoute"],
569
- message: `defaultRoute "${body.defaultRoute}" is not one of the routes`
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
- const control = (reply) => {
636
- if (!options.goals) unavailable(reply);
637
- return options.goals;
638
- };
639
- app.get("/api/goals", async (_request, reply) => {
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/goals/:id", async (request, reply) => {
645
- const goals = control(reply);
646
- if (!goals) return reply;
647
- const goal = await goals.get(request.params.id);
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/goals", async (request, reply) => {
652
- const parsed = goalStartSchema.safeParse(request.body);
653
- if (!parsed.success) {
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 goals = control(reply);
657
- if (!goals) return reply;
658
- const input = parsed.data;
659
- const result = await guarded(reply, () => goals.start(input));
660
- if (!result.ok) return result.reply;
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: "goal.start",
664
- target: result.value.id,
665
- details: { dryRun: input.dryRun === true }
556
+ action: "account.connect",
557
+ target: account.id,
558
+ details: { launched }
666
559
  });
667
- return reply.code(202).send({ goal: result.value });
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.post("/api/goals/:id/execute", async (request, reply) => {
670
- const goals = control(reply);
671
- if (!goals) return reply;
672
- if (!goals.execute) return unavailable(reply);
673
- const execute = goals.execute.bind(goals);
674
- const result = await guarded(reply, () => execute(request.params.id));
675
- if (!result.ok) return result.reply;
676
- audit.record({ actor: "api", action: "goal.execute", target: request.params.id });
677
- return reply.code(202).send({ goal: result.value });
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
- "/api/goals/:id/artifacts/:taskId",
681
- async (request, reply) => {
682
- const goals = control(reply);
683
- if (!goals) return reply;
684
- if (!goals.readArtifact) return unavailable(reply);
685
- const readArtifact = goals.readArtifact.bind(goals);
686
- const result = await guarded(
687
- reply,
688
- () => readArtifact(request.params.id, request.params.taskId)
689
- );
690
- if (!result.ok) return result.reply;
691
- return { artifact: result.value };
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
- app.post("/api/goals/:id/cancel", async (request, reply) => {
695
- const goals = control(reply);
696
- if (!goals) return reply;
697
- const result = await guarded(reply, () => goals.cancel(request.params.id));
698
- if (!result.ok) return result.reply;
699
- audit.record({ actor: "api", action: "goal.cancel", target: request.params.id });
700
- return { goal: result.value };
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
- function describeIssue(issue) {
704
- switch (issue.code) {
705
- case "unknown_task":
706
- return { status: 404, code: "not_found", extra: {} };
707
- case "duplicate_id":
708
- case "invalid_transition":
709
- case "task_in_progress":
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
- 409,
736
- "no_brain",
737
- "No project brain is loaded; start the gateway inside a project (davecode init)"
659
+ 501,
660
+ "runner_unavailable",
661
+ "The autonomous runner is not available"
738
662
  );
739
663
  }
740
- return brain;
741
- };
742
- const guarded = async (reply, run) => {
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
- return await run();
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: "task.update",
793
- target: id,
794
- details: { fields: Object.keys(parsed.data) }
682
+ action: `runner.${action}`,
683
+ ...taskId !== void 0 ? { details: { taskId } } : {}
795
684
  });
796
- return { task: result };
797
- });
798
- app.delete("/api/tasks/:id", async (request, reply) => {
799
- const brain = writable(reply);
800
- if (isReply(brain)) return brain;
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 isReply(value) {
811
- return typeof value === "object" && value !== null && "sent" in value && typeof value.code === "function";
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
- var EMPTY_GRAPH = { version: 1, tasks: [] };
814
- var IDLE = { state: "idle" };
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 registerApiRoutes(app, engine, options) {
819
- const { accounts, events, audit, quota, keyring } = engine;
820
- const now = () => quota.clock();
821
- const emitAccount = (account) => events.emit({ type: "account.updated", account });
822
- app.get("/api/health", async () => ({
823
- status: "ok",
824
- version: VERSION,
825
- uptimeSec: Math.max(0, Math.floor((now() - engine.startedAt) / 1e3)),
826
- experimental: { ...engine.config.experimental }
827
- }));
828
- app.get("/api/accounts", async () => ({ accounts: accounts.list() }));
829
- app.post("/api/accounts", async (request, reply) => {
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
- 400,
837
- "experimental_disabled",
838
- "gemini-web is experimental and may violate Google\u2019s Terms of Service. Set experimental.geminiWeb to true to opt in."
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
- const account = engine.db.transaction(() => {
842
- const created = accounts.create(input);
843
- if (secret !== void 0) keyring.set(created.id, secret);
844
- audit.record({
845
- actor: "api",
846
- action: "account.create",
847
- target: created.id,
848
- details: { provider: created.provider, label: created.label, hasSecret: !!secret }
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 account = engine.db.transaction(() => {
873
- if (secret !== void 0) keyring.set(existing.id, secret);
874
- const updated = accounts.update(existing.id, patch);
875
- audit.record({
876
- actor: "api",
877
- action: "account.update",
878
- target: existing.id,
879
- details: { fields: Object.keys(fields), secretChanged: secret !== void 0 }
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
- cleanup();
853
+ const { completion, meta } = await engine.router.complete(body, routeOptions);
854
+ setRoutingHeaders(reply, meta);
855
+ return completion;
892
856
  } catch (err) {
893
- request.log.warn({ err, accountId: id }, "failed to remove account directory");
857
+ return sendRoutingError(reply, err);
894
858
  }
895
859
  }
896
- quota.forget(id);
897
- engine.reported.forget(id);
898
- engine.router.breaker.reset(id);
899
- audit.record({ actor: "api", action: "account.delete", target: id });
900
- events.emit({ type: "account.removed", accountId: id });
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
- const account = accounts.get(request.params.id);
913
- if (!account) return sendApiError(reply, 404, "not_found", "Account not found");
914
- if (connectionMethod(account.provider) !== "cli-login") {
915
- return sendApiError(
916
- reply,
917
- 400,
918
- "not_cli_login",
919
- 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"
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
- const command = loginCommandFor(account.id);
923
- const { launched } = options.loginLauncher ? await options.loginLauncher.launch(account) : { launched: false };
924
- audit.record({
925
- actor: "api",
926
- action: "account.connect",
927
- target: account.id,
928
- details: { launched }
929
- });
930
- const result = {
931
- connection: await engine.connections.check(account),
932
- launched,
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
- app.get("/api/usage", async () => ({
939
- usage: accounts.list().map((account) => quota.usage(account))
940
- }));
941
- app.get("/api/budget", async () => ({ budget: engine.budget.snapshot() }));
942
- app.get("/api/usage/timeseries", async (request, reply) => {
943
- const parsed = timeseriesQuerySchema.safeParse(request.query);
944
- if (!parsed.success) return invalid(reply, parsed.error);
945
- const { minutes, bucketSec } = parsed.data;
946
- const until = now() + 1;
947
- const buckets = engine.usage.timeseries({
948
- since: until - minutes * 6e4,
949
- until,
950
- bucketMs: bucketSec * 1e3
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
- return { buckets };
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.get("/api/requests", async (request, reply) => {
955
- const parsed = limitQuerySchema(100).safeParse(request.query);
956
- if (!parsed.success) return invalid(reply, parsed.error);
957
- return { requests: engine.usage.recent(parsed.data.limit) };
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
- app.get("/api/logs", async (request, reply) => {
960
- const parsed = limitQuerySchema(200).safeParse(request.query);
961
- if (!parsed.success) return invalid(reply, parsed.error);
962
- return { events: events.recent(parsed.data.limit) };
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.get("/api/routes", async () => ({
965
- defaultRoute: engine.config.routing.defaultRoute,
966
- routes: engine.config.routing.routes
967
- }));
968
- app.put("/api/routes", async (request, reply) => {
969
- const parsed = routesUpdateSchema.safeParse(request.body);
970
- if (!parsed.success) return invalid(reply, parsed.error);
971
- const { routes, defaultRoute } = parsed.data;
972
- for (const route of routes) {
973
- for (const target of route.targets) {
974
- if (target.accountId !== void 0 && !accounts.get(target.accountId)) {
975
- return sendApiError(
976
- reply,
977
- 400,
978
- "unknown_account",
979
- `route "${route.name}" pins unknown account "${target.accountId}"`
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
- let result;
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
- result = await engine.updateRouting({
987
- routes,
988
- ...defaultRoute !== void 0 ? { defaultRoute } : {}
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
- audit.record({
997
- actor: "api",
998
- action: "routes.update",
999
- details: {
1000
- routes: routes.map((r) => r.name),
1001
- defaultRoute: engine.config.routing.defaultRoute
1002
- }
1003
- });
1004
- return {
1005
- defaultRoute: engine.config.routing.defaultRoute,
1006
- routes: engine.config.routing.routes,
1007
- shadowedByProject: result.shadowedByProject
1008
- };
1009
- });
1010
- app.get("/api/tasks", async () => {
1011
- const { brain } = options;
1012
- if (!brain) return { project: null, graph: EMPTY_GRAPH };
1013
- return { project: brain.project(), graph: await brain.graph() };
1014
- });
1015
- registerTaskRoutes(app, engine, options);
1016
- registerGoalRoutes(app, engine, options);
1017
- app.get("/api/brain", async () => {
1018
- const { brain } = options;
1019
- if (!brain) return { state: "", architecture: "" };
1020
- const [state, architecture] = await Promise.all([brain.state(), brain.architecture()]);
1021
- return { state, architecture };
1022
- });
1023
- app.get("/api/runner", async () => ({ status: options.runner?.status() ?? IDLE }));
1024
- const control = (action) => async function handler(request, reply) {
1025
- const { runner } = options;
1026
- if (!runner) {
1027
- return sendApiError(
1028
- reply,
1029
- 501,
1030
- "runner_unavailable",
1031
- "The autonomous runner is not available"
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
- let taskId;
1035
- if (action === "start" && request.body !== void 0 && request.body !== null) {
1036
- const parsed = runnerStartSchema.safeParse(request.body);
1037
- if (!parsed.success) return invalid(reply, parsed.error);
1038
- taskId = parsed.data.taskId;
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
- if (action === "start") await runner.start(taskId === void 0 ? {} : { taskId });
1042
- else await runner[action]();
1043
- } catch (error) {
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
- audit.record({
1051
- actor: "api",
1052
- action: `runner.${action}`,
1053
- ...taskId !== void 0 ? { details: { taskId } } : {}
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.writeHead(200, {
1153
- ...inherited,
1154
- "content-type": "text/event-stream; charset=utf-8",
1155
- "cache-control": "no-cache, no-transform",
1156
- connection: "keep-alive",
1157
- "x-accel-buffering": "no"
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
- return sendOpenAIError(reply, status, body);
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
- } catch (err) {
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
- yield "data: [DONE]\n\n";
1201
- }
1202
- function registerV1Routes(app, engine) {
1203
- app.get("/v1/models", async () => ({ object: "list", data: await engine.router.listModels() }));
1204
- app.post("/v1/chat/completions", async (request, reply) => {
1205
- const parsed = chatRequestSchema.safeParse(request.body);
1206
- if (!parsed.success) {
1207
- return sendOpenAIError(reply, 400, {
1208
- message: z6.prettifyError(parsed.error),
1209
- type: "invalid_request_error",
1210
- code: "invalid_body"
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
- let result;
1231
- try {
1232
- result = await engine.router.stream(body, routeOptions);
1233
- } catch (err) {
1234
- return sendRoutingError(reply, err);
1330
+ if (/^[A-Za-z]$/.test(piece)) {
1331
+ pendingDrive = piece;
1332
+ continue;
1235
1333
  }
1236
- setRoutingHeaders(reply, result.meta);
1237
- reply.header("content-type", "text/event-stream; charset=utf-8").header("cache-control", "no-cache, no-transform").header("x-accel-buffering", "no");
1238
- return reply.send(Readable.from(sseFrames(result.chunks)));
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 loggerOption(logger) {
1273
- if (!logger) return false;
1274
- return {
1275
- ...typeof logger === "object" ? logger : {},
1276
- serializers: {
1277
- req(req) {
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
- async function buildGateway(engine, options = {}) {
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
- app.addContentTypeParser("application/json", { parseAs: "string" }, (_request, body, done) => {
1329
- const text = typeof body === "string" ? body : body.toString("utf8");
1330
- if (text.trim() === "") return done(null, void 0);
1331
- try {
1332
- done(null, JSON.parse(text));
1333
- } catch (err) {
1334
- const error = Object.assign(err instanceof Error ? err : new Error("Invalid JSON"), {
1335
- statusCode: 400
1336
- });
1337
- done(error, void 0);
1338
- }
1339
- });
1340
- app.setErrorHandler((error, request, reply) => {
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
- app.setNotFoundHandler((request, reply) => {
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 LOOPBACK = /* @__PURE__ */ new Set(["127.0.0.1", "::1", "localhost"]);
1380
- async function startGateway(engine, options = {}) {
1381
- const app = await buildGateway(engine, options);
1382
- const host = options.host ?? engine.config.server.host;
1383
- const port = options.port ?? engine.config.server.port;
1384
- if (!engine.config.server.authToken && !LOOPBACK.has(host)) {
1385
- app.log.warn(
1386
- `Listening on ${host} without server.authToken: anyone on the network can use your accounts.`
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
- await app.listen({ host, port });
1390
- return app;
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-PIIJ3RSI.js.map
1614
+ //# sourceMappingURL=chunk-FYPMA55O.js.map