agentinbox-cli 0.0.0 → 0.1.0-nightly.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +312 -0
  3. package/dist/audit-CQe-_dPQ.js +2 -0
  4. package/dist/audit-DETqygzG.js +65 -0
  5. package/dist/audit-DETqygzG.js.map +1 -0
  6. package/dist/auth-BniddcRD.js +447 -0
  7. package/dist/auth-BniddcRD.js.map +1 -0
  8. package/dist/auth-C2v7bEmQ.js +2 -0
  9. package/dist/bus-Cz0flS37.js +12 -0
  10. package/dist/bus-Cz0flS37.js.map +1 -0
  11. package/dist/bus-OKIGut72.js +2 -0
  12. package/dist/cli.js +141 -0
  13. package/dist/cli.js.map +1 -0
  14. package/dist/config-CIvgMqLx.js +2 -0
  15. package/dist/config-v975gxv4.js +44 -0
  16. package/dist/config-v975gxv4.js.map +1 -0
  17. package/dist/crypto-DYZ4R7c2.js +74 -0
  18. package/dist/crypto-DYZ4R7c2.js.map +1 -0
  19. package/dist/db-CfoMO5zW.js +2 -0
  20. package/dist/db-DdagFmUY.js +247 -0
  21. package/dist/db-DdagFmUY.js.map +1 -0
  22. package/dist/errors-CJLUPllS.js +38 -0
  23. package/dist/errors-CJLUPllS.js.map +1 -0
  24. package/dist/instance-DZMV78rI.js +2 -0
  25. package/dist/instance-Dy2jeyZc.js +174 -0
  26. package/dist/instance-Dy2jeyZc.js.map +1 -0
  27. package/dist/server-CWqKeCGn.js +981 -0
  28. package/dist/server-CWqKeCGn.js.map +1 -0
  29. package/dist/service-63pdSmhO.js +332 -0
  30. package/dist/service-63pdSmhO.js.map +1 -0
  31. package/dist/settings-Dq2ETFsd.js +98 -0
  32. package/dist/settings-Dq2ETFsd.js.map +1 -0
  33. package/dist/settings-qtn5WTBM.js +2 -0
  34. package/dist/setup-token-Bkwk84l7.js +47 -0
  35. package/dist/setup-token-Bkwk84l7.js.map +1 -0
  36. package/dist/skill/SKILL.md +226 -0
  37. package/dist/skill-BWWtinsE.js +2 -0
  38. package/dist/skill-WKbeyNct.js +43 -0
  39. package/dist/skill-WKbeyNct.js.map +1 -0
  40. package/dist/urls-CNYhbeUQ.js +2 -0
  41. package/dist/urls-DmnonkJo.js +22 -0
  42. package/dist/urls-DmnonkJo.js.map +1 -0
  43. package/dist/web/assets/_agentId-il4y7OCG.js +1 -0
  44. package/dist/web/assets/_app-BIO8WIYB.js +1 -0
  45. package/dist/web/assets/_app-wlLL4MrD.js +63 -0
  46. package/dist/web/assets/_webhookId-CiwDrkY4.js +1 -0
  47. package/dist/web/assets/account-status-at9Bv3_T.js +1 -0
  48. package/dist/web/assets/accounts-B4rfPwCV.js +1 -0
  49. package/dist/web/assets/accounts-list-e--JWxHH.js +1 -0
  50. package/dist/web/assets/activity-zrckFhPk.js +1 -0
  51. package/dist/web/assets/agent-create-form-DaWPR6kx.js +1 -0
  52. package/dist/web/assets/agents-D4gPq4dt.js +1 -0
  53. package/dist/web/assets/approvals-DaoZ6wuf.js +2 -0
  54. package/dist/web/assets/arrow-left-DVJV4Zx7.js +1 -0
  55. package/dist/web/assets/arrow-right-4PFX6MSS.js +1 -0
  56. package/dist/web/assets/audit-bAdLe-px.js +1 -0
  57. package/dist/web/assets/audit-bits-DQHo74i6.js +1 -0
  58. package/dist/web/assets/bot-DVw15lBn.js +1 -0
  59. package/dist/web/assets/card-EQTpqntn.js +1 -0
  60. package/dist/web/assets/checkbox-DDZZurgK.js +1 -0
  61. package/dist/web/assets/chevron-down-892ylB4R.js +1 -0
  62. package/dist/web/assets/chevron-right-CbBKS6-T.js +1 -0
  63. package/dist/web/assets/chips-input-Btdc_f7b.js +1 -0
  64. package/dist/web/assets/circle-x-Bi4IGwky.js +1 -0
  65. package/dist/web/assets/common-h6596gEC.js +1 -0
  66. package/dist/web/assets/connect-host-Co31qIYr.js +1 -0
  67. package/dist/web/assets/data-table-DsKrM62X.js +1 -0
  68. package/dist/web/assets/dialog-CgZr27yw.js +1 -0
  69. package/dist/web/assets/dist-B0e3Ju3r.js +1 -0
  70. package/dist/web/assets/empty-CE_WZGEC.js +1 -0
  71. package/dist/web/assets/es2015-DPaldoxG.js +41 -0
  72. package/dist/web/assets/event-row-C0qRfWuV.js +1 -0
  73. package/dist/web/assets/events-Svyij7mp.js +1 -0
  74. package/dist/web/assets/eye-off-DjpaihHf.js +1 -0
  75. package/dist/web/assets/field-CcyMn3ey.js +1 -0
  76. package/dist/web/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  77. package/dist/web/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  78. package/dist/web/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  79. package/dist/web/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  80. package/dist/web/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
  81. package/dist/web/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
  82. package/dist/web/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
  83. package/dist/web/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
  84. package/dist/web/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
  85. package/dist/web/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
  86. package/dist/web/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  87. package/dist/web/assets/get-started-Cxfw97sJ.js +1 -0
  88. package/dist/web/assets/grants-BilXCDps.js +1 -0
  89. package/dist/web/assets/index-DgIgw66p.css +2 -0
  90. package/dist/web/assets/index-TbxC5ZgP.js +10 -0
  91. package/dist/web/assets/input-CMhmmHiH.js +1 -0
  92. package/dist/web/assets/input-group-DRs3TNaT.js +1 -0
  93. package/dist/web/assets/key-round-CVIZRCLM.js +1 -0
  94. package/dist/web/assets/labels-C_Pwium9.js +1 -0
  95. package/dist/web/assets/login-DUJia7lg.js +1 -0
  96. package/dist/web/assets/mail-CAVmuESo.js +1 -0
  97. package/dist/web/assets/mutations-D_GluJ0j.js +1 -0
  98. package/dist/web/assets/paperclip-QFjM4y6n.js +1 -0
  99. package/dist/web/assets/password-input-_m9Ycphv.js +1 -0
  100. package/dist/web/assets/pencil-Bf1JhsG0.js +1 -0
  101. package/dist/web/assets/play-DiPk49JH.js +1 -0
  102. package/dist/web/assets/plus-CETmK0SB.js +1 -0
  103. package/dist/web/assets/preload-helper-BFuRUfdt.js +1 -0
  104. package/dist/web/assets/progress-3ZOsbLSK.js +6 -0
  105. package/dist/web/assets/queries-a3M5_loa.js +2 -0
  106. package/dist/web/assets/refresh-cw-DOn9kD7g.js +1 -0
  107. package/dist/web/assets/search-gcYjitzV.js +1 -0
  108. package/dist/web/assets/select-_tULk-vs.js +1 -0
  109. package/dist/web/assets/send-BhQwhuJo.js +1 -0
  110. package/dist/web/assets/settings-Dl4wb4f0.js +1 -0
  111. package/dist/web/assets/settings-form-D21eKBh-.js +1 -0
  112. package/dist/web/assets/setup-DYnNhnfH.js +1 -0
  113. package/dist/web/assets/sheet--2lKU0jN.js +1 -0
  114. package/dist/web/assets/shield-alert-CbFgd06X.js +1 -0
  115. package/dist/web/assets/stamp-CH2OvjH2.js +1 -0
  116. package/dist/web/assets/stream-6OwrgVZw.js +1 -0
  117. package/dist/web/assets/tabs-CiAUEj_y.js +1 -0
  118. package/dist/web/assets/tag-CP72Kk6Y.js +1 -0
  119. package/dist/web/assets/test-connection-F2tMowbv.js +1 -0
  120. package/dist/web/assets/textarea-CCokBZIw.js +1 -0
  121. package/dist/web/assets/toggle-group-D1pC7sJr.js +1 -0
  122. package/dist/web/assets/trash-DxdMNg8B.js +1 -0
  123. package/dist/web/assets/use-sign-out-BuzlTb0Q.js +1 -0
  124. package/dist/web/assets/useInfiniteQuery-DE6WChlb.js +1 -0
  125. package/dist/web/assets/useMatch-BKUnm54_.js +1 -0
  126. package/dist/web/assets/useMutation-DGl4PaZ1.js +1 -0
  127. package/dist/web/assets/useQuery-DAhf1YDH.js +1 -0
  128. package/dist/web/assets/useSelector-D70eKNYs.js +1 -0
  129. package/dist/web/assets/webhook-BWT75M78.js +1 -0
  130. package/dist/web/assets/webhook-sheet-4U_6Rw8I.js +34 -0
  131. package/dist/web/assets/webhooks-C1JvTI88.js +1 -0
  132. package/dist/web/assets/with-selector-CkNyXYMW.js +1 -0
  133. package/dist/web/assets/x-BHjDLXNO.js +1 -0
  134. package/dist/web/favicon.svg +1 -0
  135. package/dist/web/index.html +31 -0
  136. package/package.json +94 -1
@@ -0,0 +1,981 @@
1
+ #!/usr/bin/env node
2
+ import { t as Db } from "./db-DdagFmUY.js";
3
+ import { t as AuditLog } from "./audit-DETqygzG.js";
4
+ import { a as errorMessage, i as conflict, n as PolicyDenied, r as badRequest, t as HttpError } from "./errors-CJLUPllS.js";
5
+ import { a as verifyPasswordAsync, n as hashPasswordAsync, r as safeEqual, t as Crypto } from "./crypto-DYZ4R7c2.js";
6
+ import { a as adminAuth, c as csrfGuard, d as hasSession, f as isClaimed, g as requireAdmin, h as rejection, l as currentSetupToken, m as lockedOut, n as MAX_PASSWORD_LENGTH, o as checkSecret, s as clientIp, u as endSession, v as sessionToken, y as startSession } from "./auth-BniddcRD.js";
7
+ import { a as setupTokenPath, r as readSetupToken, t as deleteSetupToken } from "./setup-token-Bkwk84l7.js";
8
+ import { t as createBus } from "./bus-Cz0flS37.js";
9
+ import { n as loadConfig } from "./config-v975gxv4.js";
10
+ import { c as NPM_PACKAGE, i as cliCommand, n as assertMasterKeyUsable, t as acquireInstanceLock } from "./instance-Dy2jeyZc.js";
11
+ import { r as loopbackUrl, t as consoleUrls } from "./urls-DmnonkJo.js";
12
+ import { i as renderSkill } from "./skill-WKbeyNct.js";
13
+ import { t as SettingsStore } from "./settings-Dq2ETFsd.js";
14
+ import { existsSync, readFileSync, statSync } from "node:fs";
15
+ import { homedir } from "node:os";
16
+ import { extname, join, resolve, sep } from "node:path";
17
+ import pc from "picocolors";
18
+ import { createHash } from "node:crypto";
19
+ import { serve } from "@hono/node-server";
20
+ import open from "open";
21
+ import { serveStatic } from "@hono/node-server/serve-static";
22
+ import { Hono } from "hono";
23
+ import { bodyLimit } from "hono/body-limit";
24
+ import { HTTPException } from "hono/http-exception";
25
+ import { z } from "zod";
26
+ //#region server/api/util.ts
27
+ /**
28
+ * Console API routers are `(ctx) => Hono<AppEnv>` factories with paths relative to /api
29
+ * (e.g. "/accounts/:id"). server/app.ts mounts them behind the admin auth middleware and turns
30
+ * thrown HttpError / PolicyDenied into ApiError JSON responses.
31
+ */
32
+ const router = () => new Hono();
33
+ /** Parses and validates a JSON body; throws HttpError(400) with a readable message. */
34
+ async function readBody(c, schema) {
35
+ let raw;
36
+ try {
37
+ const text = await c.req.text();
38
+ raw = text ? JSON.parse(text) : {};
39
+ } catch {
40
+ throw badRequest("Request body must be valid JSON");
41
+ }
42
+ const parsed = schema.safeParse(raw);
43
+ if (!parsed.success) throw badRequest(z.prettifyError(parsed.error));
44
+ return parsed.data;
45
+ }
46
+ /** Audit fields for console (admin) actions: spread into ctx.audit.record. */
47
+ const ADMIN_ACTOR = {
48
+ actorType: "admin",
49
+ actorId: null
50
+ };
51
+ //#endregion
52
+ //#region server/api/accounts.ts
53
+ function accountsApi(_ctx) {
54
+ return router();
55
+ }
56
+ //#endregion
57
+ //#region server/api/agents.ts
58
+ function agentsApi(_ctx) {
59
+ return router();
60
+ }
61
+ //#endregion
62
+ //#region server/api/approvals.ts
63
+ function approvalsApi(_ctx) {
64
+ return router();
65
+ }
66
+ //#endregion
67
+ //#region server/api/audit.ts
68
+ function auditApi(_ctx) {
69
+ return router();
70
+ }
71
+ //#endregion
72
+ //#region server/api/setup.ts
73
+ const ClaimBody = z.object({
74
+ token: z.string().trim().min(1, "Setup token is required").max(200),
75
+ password: z.string().max(MAX_PASSWORD_LENGTH, `Password must be at most ${MAX_PASSWORD_LENGTH} characters`)
76
+ });
77
+ /** Unauthenticated: first-run status and the one-time claim. Mounted before the admin guard. */
78
+ function setupPublicApi(ctx) {
79
+ const auth = adminAuth(ctx);
80
+ return router().get("/setup/status", (c) => {
81
+ c.header("Cache-Control", "no-store");
82
+ return c.json({
83
+ claimed: isClaimed(ctx),
84
+ authenticated: hasSession(c, ctx)
85
+ });
86
+ }).post("/setup/claim", async (c) => {
87
+ const ip = clientIp(c, ctx.config.trustProxy);
88
+ const banned = auth.limiter.bannedMinutes(ip);
89
+ if (banned) return lockedOut(c, banned);
90
+ const body = await readBody(c, ClaimBody);
91
+ if (isClaimed(ctx)) throw conflict("This instance is already claimed", "Sign in with the admin password instead.");
92
+ const expected = readSetupToken(ctx.config.dataDir);
93
+ if (!expected) throw new HttpError(403, "invalid_token", "The setup link has expired", `Restart agentinbox or run \`${cliCommand("reset-password")}\` for a new link.`);
94
+ const check = await checkSecret(ctx, ip, `claim:${body.token}`, () => safeEqual(body.token, expected), { global: false });
95
+ if (!check.ok) {
96
+ const rejected = rejection(c, check);
97
+ if (rejected) return rejected;
98
+ ctx.audit.record({
99
+ actorType: "admin",
100
+ action: "admin.claim",
101
+ outcome: "denied",
102
+ reason: check.nowBanned ? "lockout" : "invalid_token",
103
+ detail: { ip }
104
+ });
105
+ throw new HttpError(403, "invalid_token", "Invalid setup token", "Use the exact setup link printed by agentinbox on startup.");
106
+ }
107
+ checkPassword(body.password);
108
+ const hash = await hashPasswordAsync(body.password);
109
+ if (isClaimed(ctx)) throw conflict("This instance is already claimed", "Sign in with the admin password instead.");
110
+ ctx.settings.setState("admin.passwordHash", hash);
111
+ deleteSetupToken(ctx.config.dataDir);
112
+ auth.limiter.reset();
113
+ startSession(c, ctx);
114
+ ctx.audit.record({
115
+ actorType: "admin",
116
+ action: "admin.claim",
117
+ outcome: "ok",
118
+ detail: { ip }
119
+ });
120
+ return c.body(null, 204);
121
+ });
122
+ }
123
+ /** Authenticated: the setup checklist. */
124
+ function setupApi(ctx) {
125
+ return router().get("/setup/state", (c) => c.json(setupState(ctx)));
126
+ }
127
+ function checkPassword(password) {
128
+ if (password.length < 10) throw badRequest(`Password must be at least 10 characters`);
129
+ if (password.length > 1024) throw badRequest(`Password must be at most ${MAX_PASSWORD_LENGTH} characters`);
130
+ }
131
+ /** Composes the setup checklist. Reads the stored state directly if a service is unavailable. */
132
+ function setupState(ctx) {
133
+ const claimed = isClaimed(ctx);
134
+ const client = attempt(ctx, () => ctx.google.clientView()) ?? storedClientView(ctx);
135
+ const pubsub = attempt(ctx, () => ctx.google.pubsubState()) ?? storedPubsub(ctx);
136
+ const count = (table) => ctx.db.get(`SELECT COUNT(*) AS n FROM ${table}`)?.n ?? 0;
137
+ const accountsConnected = count("accounts");
138
+ const googleClientConfigured = !!client.clientId && client.hasSecret;
139
+ return {
140
+ claimed,
141
+ googleClientConfigured,
142
+ gcpProjectId: client.projectId,
143
+ googleAppType: client.appType,
144
+ pubsub,
145
+ accountsConnected,
146
+ agentsCreated: count("agents"),
147
+ complete: claimed && googleClientConfigured && accountsConnected > 0
148
+ };
149
+ }
150
+ function attempt(ctx, fn) {
151
+ try {
152
+ return fn();
153
+ } catch (err) {
154
+ ctx.log.debug("setup state: falling back to stored state", { error: err instanceof Error ? err.message : String(err) });
155
+ return null;
156
+ }
157
+ }
158
+ function storedClientView(ctx) {
159
+ const s = ctx.settings.getState("google.client");
160
+ return {
161
+ clientId: s?.clientId || null,
162
+ hasSecret: !!s?.clientSecretEnc,
163
+ projectId: s?.projectId || null,
164
+ appType: s?.appType ?? null
165
+ };
166
+ }
167
+ function storedPubsub(ctx) {
168
+ const s = ctx.settings.getState("pubsub");
169
+ return {
170
+ mode: ctx.settings.get().ingestMode,
171
+ topic: s?.topic ?? null,
172
+ subscription: s?.subscription ?? null,
173
+ lastError: s?.lastError ?? null,
174
+ lastMessageAt: s?.lastMessageAt ?? null
175
+ };
176
+ }
177
+ //#endregion
178
+ //#region server/api/auth.ts
179
+ const password = (what) => z.string().max(MAX_PASSWORD_LENGTH, `${what} must be at most ${MAX_PASSWORD_LENGTH} characters`);
180
+ const LoginBody = z.object({ password: password("Password").min(1, "Password is required") });
181
+ const PasswordBody = z.object({
182
+ current: password("Current password"),
183
+ next: password("New password")
184
+ });
185
+ /** Unauthenticated: sign in and out. Mounted before the admin guard. */
186
+ function authPublicApi(ctx) {
187
+ const auth = adminAuth(ctx);
188
+ return router().post("/auth/login", async (c) => {
189
+ const ip = clientIp(c, ctx.config.trustProxy);
190
+ const banned = auth.limiter.bannedMinutes(ip);
191
+ if (banned) return lockedOut(c, banned);
192
+ const body = await readBody(c, LoginBody);
193
+ const hash = ctx.settings.getState("admin.passwordHash");
194
+ if (!hash) throw conflict("This instance has not been claimed yet", "Open the setup link printed by agentinbox on startup.");
195
+ const check = await checkSecret(ctx, ip, body.password, () => verifyPasswordAsync(body.password, hash));
196
+ if (!check.ok) {
197
+ const rejected = rejection(c, check);
198
+ if (rejected) return rejected;
199
+ ctx.audit.record({
200
+ ...ADMIN_ACTOR,
201
+ action: "admin.login",
202
+ outcome: "denied",
203
+ reason: check.nowBanned ? "lockout" : "bad_password",
204
+ detail: { ip }
205
+ });
206
+ throw new HttpError(401, "invalid_password", "Wrong password");
207
+ }
208
+ startSession(c, ctx);
209
+ ctx.audit.record({
210
+ ...ADMIN_ACTOR,
211
+ action: "admin.login",
212
+ outcome: "ok",
213
+ detail: { ip }
214
+ });
215
+ return c.body(null, 204);
216
+ }).post("/auth/logout", (c) => {
217
+ endSession(c, ctx);
218
+ return c.body(null, 204);
219
+ });
220
+ }
221
+ /** Authenticated: change the admin password. Revokes every other session. */
222
+ function authApi(ctx) {
223
+ const auth = adminAuth(ctx);
224
+ return router().post("/auth/password", async (c) => {
225
+ const ip = clientIp(c, ctx.config.trustProxy);
226
+ const banned = auth.limiter.bannedMinutes(ip);
227
+ if (banned) return lockedOut(c, banned);
228
+ const body = await readBody(c, PasswordBody);
229
+ const hash = ctx.settings.getState("admin.passwordHash");
230
+ const check = await checkSecret(ctx, ip, body.current, async () => !!hash && await verifyPasswordAsync(body.current, hash));
231
+ if (!check.ok) {
232
+ const rejected = rejection(c, check);
233
+ if (rejected) return rejected;
234
+ ctx.audit.record({
235
+ ...ADMIN_ACTOR,
236
+ action: "admin.password_change",
237
+ outcome: "denied",
238
+ reason: check.nowBanned ? "lockout" : "bad_password",
239
+ detail: { ip }
240
+ });
241
+ throw new HttpError(400, "invalid_password", "Current password is wrong");
242
+ }
243
+ checkPassword(body.next);
244
+ ctx.settings.setState("admin.passwordHash", await hashPasswordAsync(body.next));
245
+ const revoked = auth.sessions.deleteAll(sessionToken(c));
246
+ auth.limiter.reset();
247
+ ctx.audit.record({
248
+ ...ADMIN_ACTOR,
249
+ action: "admin.password_change",
250
+ outcome: "ok",
251
+ detail: {
252
+ ip,
253
+ revokedSessions: revoked
254
+ }
255
+ });
256
+ return c.body(null, 204);
257
+ });
258
+ }
259
+ //#endregion
260
+ //#region server/api/events.ts
261
+ function eventsApi(_ctx) {
262
+ return router();
263
+ }
264
+ //#endregion
265
+ //#region server/api/google.ts
266
+ function googleApi(_ctx) {
267
+ return router();
268
+ }
269
+ //#endregion
270
+ //#region server/api/oauth.ts
271
+ function oauthRoutes(_ctx) {
272
+ return router();
273
+ }
274
+ //#endregion
275
+ //#region server/api/overview.ts
276
+ function overviewApi(_ctx) {
277
+ return router();
278
+ }
279
+ //#endregion
280
+ //#region server/api/settings.ts
281
+ /** Shape check only; ranges and URL rules are enforced by SettingsStore.update. */
282
+ const SettingsPatch = z.object({
283
+ publicUrl: z.string().trim(),
284
+ ingestMode: z.enum(["pubsub", "poll"]),
285
+ pollIntervalSeconds: z.number(),
286
+ maxBodyChars: z.number(),
287
+ retentionDays: z.number(),
288
+ approvalTtlHours: z.number(),
289
+ webhookAllowlist: z.array(z.string())
290
+ }).partial().strict();
291
+ function settingsApi(ctx) {
292
+ return router().get("/settings", (c) => c.json(ctx.settings.get())).patch("/settings", async (c) => {
293
+ const patch = await readBody(c, SettingsPatch);
294
+ const before = ctx.settings.get();
295
+ const next = ctx.settings.update(patch);
296
+ const changed = Object.keys(next).filter((k) => JSON.stringify(before[k]) !== JSON.stringify(next[k]));
297
+ if (changed.length) ctx.audit.record({
298
+ ...ADMIN_ACTOR,
299
+ action: "settings.update",
300
+ outcome: "ok",
301
+ detail: { changed }
302
+ });
303
+ return c.json(next);
304
+ });
305
+ }
306
+ //#endregion
307
+ //#region server/api/system.ts
308
+ const DEFAULT_HOST = "0.0.0.0";
309
+ const LATEST_TTL_MS = 18e5;
310
+ const LATEST_RETRY_MS = 3e5;
311
+ /**
312
+ * Public health check for uptime monitors: 200 when healthy, 503 when degraded (an account needs
313
+ * re-auth or is erroring, or ingest reports an error). Only states and the version, never secrets.
314
+ */
315
+ function healthRoutes(ctx) {
316
+ const handler = (c) => {
317
+ const body = health(ctx);
318
+ c.header("Cache-Control", "no-store");
319
+ return c.json(body, body.status === "ok" ? 200 : 503);
320
+ };
321
+ return router().get("/health", handler).get("/health/", handler).get("/api/health", handler).get("/api/health/", handler);
322
+ }
323
+ function health(ctx) {
324
+ const counts = ctx.db.get("SELECT COUNT(*) AS total, COALESCE(SUM(status IN ('needs_reauth', 'error')), 0) AS attention FROM accounts") ?? {
325
+ total: 0,
326
+ attention: 0
327
+ };
328
+ let ingestError = false;
329
+ if (counts.total > 0) try {
330
+ ingestError = !!ctx.ingest.status().lastError;
331
+ } catch {}
332
+ let setupComplete = false;
333
+ try {
334
+ setupComplete = setupState(ctx).complete;
335
+ } catch {}
336
+ return {
337
+ status: counts.attention > 0 || ingestError ? "degraded" : "ok",
338
+ version: ctx.config.version,
339
+ setupComplete
340
+ };
341
+ }
342
+ /** Authenticated: instance facts and the update notice. */
343
+ function systemApi(ctx) {
344
+ return router().get("/system", (c) => {
345
+ let ingest;
346
+ try {
347
+ ingest = ctx.ingest.status();
348
+ } catch (err) {
349
+ ingest = {
350
+ mode: ctx.settings.get().ingestMode,
351
+ running: false,
352
+ lastPullAt: null,
353
+ lastError: errorMessage(err)
354
+ };
355
+ }
356
+ return c.json({
357
+ version: ctx.config.version,
358
+ nodeVersion: process.versions.node,
359
+ dataDir: ctx.config.dataDir,
360
+ service: ctx.config.service,
361
+ addresses: consoleUrls(ctx.config.host, ctx.config.port),
362
+ publicUrl: ctx.settings.publicUrl(),
363
+ ingest
364
+ });
365
+ }).get("/system/latest", async (c) => {
366
+ const latest = await latestVersion(ctx);
367
+ const current = ctx.config.version;
368
+ return c.json({
369
+ current,
370
+ latest,
371
+ updateAvailable: !!latest && compareVersions(latest, current) > 0,
372
+ command: upgradeCommand(ctx.config)
373
+ });
374
+ });
375
+ }
376
+ let latestCache;
377
+ /** The npm `latest` dist-tag, cached for 30 minutes (5 after a failed check). */
378
+ async function latestVersion(ctx) {
379
+ if (latestCache && Date.now() - latestCache.at < (latestCache.version ? LATEST_TTL_MS : LATEST_RETRY_MS)) return latestCache.version;
380
+ let version = null;
381
+ try {
382
+ const res = await fetch(`https://registry.npmjs.org/${NPM_PACKAGE}/latest`, { signal: AbortSignal.timeout(5e3) });
383
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
384
+ version = (await res.json()).version ?? null;
385
+ } catch (err) {
386
+ ctx.log.debug("Update check failed", { error: errorMessage(err) });
387
+ }
388
+ latestCache = {
389
+ at: Date.now(),
390
+ version
391
+ };
392
+ return version;
393
+ }
394
+ /**
395
+ * Semver precedence: numeric core, then a pre-release sorts before its release, and pre-release
396
+ * identifiers compare numerically when both are numbers ("nightly.10" > "nightly.9").
397
+ */
398
+ function compareVersions(a, b) {
399
+ const parse = (v) => {
400
+ const clean = v.trim().replace(/^v/, "").split("+")[0];
401
+ const dash = clean.indexOf("-");
402
+ return {
403
+ parts: (dash === -1 ? clean : clean.slice(0, dash)).split(".").map((n) => Number.parseInt(n, 10) || 0),
404
+ pre: dash === -1 ? null : clean.slice(dash + 1).split(".")
405
+ };
406
+ };
407
+ const x = parse(a);
408
+ const y = parse(b);
409
+ for (let i = 0; i < Math.max(x.parts.length, y.parts.length); i++) {
410
+ const d = (x.parts[i] ?? 0) - (y.parts[i] ?? 0);
411
+ if (d) return Math.sign(d);
412
+ }
413
+ if (!x.pre || !y.pre) return x.pre === y.pre ? 0 : x.pre ? -1 : 1;
414
+ for (let i = 0; i < Math.max(x.pre.length, y.pre.length); i++) {
415
+ const p = x.pre[i];
416
+ const q = y.pre[i];
417
+ if (p === void 0) return -1;
418
+ if (q === void 0) return 1;
419
+ const pn = /^\d+$/.test(p);
420
+ const qn = /^\d+$/.test(q);
421
+ if (pn && qn) {
422
+ const d = Number(p) - Number(q);
423
+ if (d) return Math.sign(d);
424
+ } else if (pn !== qn) return pn ? -1 : 1;
425
+ else if (p !== q) return p < q ? -1 : 1;
426
+ }
427
+ return 0;
428
+ }
429
+ const DEFAULT_DATA_DIR = () => resolve(join(homedir(), ".agentinbox"));
430
+ /** Runs, or reinstalls as a service, the latest version with this instance's options. */
431
+ function upgradeCommand(config) {
432
+ const args = [
433
+ ...config.trustProxy ? ["TRUST_PROXY=1"] : [],
434
+ "npx",
435
+ "-y",
436
+ NPM_PACKAGE
437
+ ];
438
+ if (config.service) args.push("service", "install");
439
+ if (config.dataDir !== DEFAULT_DATA_DIR()) args.push("--data", config.dataDir);
440
+ if (config.port !== 8425) args.push("--port", String(config.port));
441
+ if (config.host !== DEFAULT_HOST) args.push("--host", config.host);
442
+ return args.map(shellQuote).join(" ");
443
+ }
444
+ const shellQuote = (a) => /^[\w@%+=:,./-]+$/.test(a) ? a : `'${a.replace(/'/g, `'\\''`)}'`;
445
+ //#endregion
446
+ //#region server/api/webhooks.ts
447
+ function webhooksApi(_ctx) {
448
+ return router();
449
+ }
450
+ //#endregion
451
+ //#region server/csp.ts
452
+ /**
453
+ * Content Security Policy. Inline scripts run only when their exact content is known up front:
454
+ * the console's index.html (hashed when the file is read) and scripts registered here by routes
455
+ * that render HTML. Never hash scripts out of a response body: that would bless injected markup.
456
+ *
457
+ * A server-rendered page with an inline script (e.g. the OAuth callback page) must keep that
458
+ * script static, call `registerInlineScript(source)` once at module load with the exact source,
459
+ * and pass per-request data in a `<script type="application/json" id="…">` block, which CSP
460
+ * doesn't execute and the static script reads with JSON.parse.
461
+ */
462
+ const registered = /* @__PURE__ */ new Set();
463
+ /** "sha256-…" source for a script's exact text (what goes between <script> and <\/script>). */
464
+ const scriptHash = (source) => `sha256-${createHash("sha256").update(source).digest("base64")}`;
465
+ function contentSecurityPolicy(extraHashes = []) {
466
+ return [
467
+ "default-src 'self'",
468
+ `script-src 'self'${[.../* @__PURE__ */ new Set([...registered, ...extraHashes])].map((h) => ` '${h}'`).join("")}`,
469
+ "style-src 'self' 'unsafe-inline'",
470
+ "img-src 'self' data:",
471
+ "connect-src 'self'",
472
+ "object-src 'none'",
473
+ "base-uri 'none'",
474
+ "frame-ancestors 'none'",
475
+ "form-action 'self'"
476
+ ].join("; ");
477
+ }
478
+ /** Hashes of the executable inline scripts in a trusted document (the built index.html). */
479
+ function inlineScriptHashes(html) {
480
+ const hashes = /* @__PURE__ */ new Set();
481
+ for (const m of html.matchAll(/<script\b([^>]*)>([\s\S]*?)<\/script\s*>/gi)) {
482
+ const attrs = m[1];
483
+ if (/\bsrc\s*=/i.test(attrs) || !m[2]) continue;
484
+ const type = /\btype\s*=\s*["']?([^"'\s>]+)/i.exec(attrs)?.[1]?.toLowerCase();
485
+ if (type && type !== "module" && type !== "text/javascript" && type !== "application/javascript") continue;
486
+ hashes.add(scriptHash(m[2]));
487
+ }
488
+ return [...hashes];
489
+ }
490
+ //#endregion
491
+ //#region server/mcp/index.ts
492
+ function mcpRoutes(_ctx) {
493
+ return router();
494
+ }
495
+ //#endregion
496
+ //#region server/app.ts
497
+ /**
498
+ * dist/web relative to the running module: the bundle (dist/cli.js) and source runs
499
+ * (server/app.ts via tsx) both resolve to <package root>/dist/web.
500
+ */
501
+ const DEFAULT_WEB_DIR = resolve(import.meta.dirname, "..", "dist", "web");
502
+ const API_BODY_LIMIT = 1048576;
503
+ const AUTH_BODY_LIMIT = 16384;
504
+ const AUTH_PATHS = [
505
+ "/api/setup/claim",
506
+ "/api/auth/login",
507
+ "/api/auth/password"
508
+ ];
509
+ const MCP_BODY_LIMIT = 4194304;
510
+ const AGENT_BODY_LIMIT = 65536;
511
+ /** Extensions served as files; any other path (even with a dot, e.g. /accounts/me@example.com) is a console route. */
512
+ const ASSET_EXTENSIONS = /* @__PURE__ */ new Set([
513
+ ".js",
514
+ ".mjs",
515
+ ".css",
516
+ ".map",
517
+ ".json",
518
+ ".webmanifest",
519
+ ".txt",
520
+ ".xml",
521
+ ".png",
522
+ ".jpg",
523
+ ".jpeg",
524
+ ".gif",
525
+ ".webp",
526
+ ".avif",
527
+ ".svg",
528
+ ".ico",
529
+ ".woff",
530
+ ".woff2",
531
+ ".ttf",
532
+ ".otf",
533
+ ".wasm"
534
+ ]);
535
+ /** Paths the console's index.html fallback must never answer. */
536
+ const SERVER_PREFIXES = [
537
+ "/api",
538
+ "/mcp",
539
+ "/oauth",
540
+ "/skill.md",
541
+ "/health"
542
+ ];
543
+ const isServerPath = (path) => SERVER_PREFIXES.some((p) => path === p || path.startsWith(p + "/"));
544
+ function createApp(ctx, opts = {}) {
545
+ const app = new Hono();
546
+ currentSetupToken(ctx);
547
+ let consoleHashes = () => [];
548
+ app.use("*", async (c, next) => {
549
+ await next();
550
+ try {
551
+ c.res.headers.set("Content-Security-Policy", contentSecurityPolicy(consoleHashes()));
552
+ c.res.headers.set("X-Frame-Options", "DENY");
553
+ c.res.headers.set("X-Content-Type-Options", "nosniff");
554
+ c.res.headers.set("Referrer-Policy", "no-referrer");
555
+ } catch {}
556
+ });
557
+ app.onError((err, c) => errorResponse(ctx, err, c));
558
+ app.notFound((c) => {
559
+ if (c.req.path === "/api" || c.req.path.startsWith("/api/")) return apiError(c, 404, "not_found", "No such API route");
560
+ return c.text("Not found", 404);
561
+ });
562
+ const tooLarge = (c) => apiError(c, 413, "payload_too_large", "Request body is too large");
563
+ const limit = (maxSize) => bodyLimit({
564
+ maxSize,
565
+ onError: tooLarge
566
+ });
567
+ for (const path of AUTH_PATHS) app.use(path, limit(AUTH_BODY_LIMIT));
568
+ app.use("/api/*", limit(API_BODY_LIMIT));
569
+ app.use("/mcp", limit(MCP_BODY_LIMIT));
570
+ app.use("/mcp/*", limit(MCP_BODY_LIMIT));
571
+ app.use("/agent/*", limit(AGENT_BODY_LIMIT));
572
+ app.use("/api/*", csrfGuard());
573
+ app.route("/", healthRoutes(ctx));
574
+ app.route("/api", setupPublicApi(ctx));
575
+ app.route("/api", authPublicApi(ctx));
576
+ app.get("/skill.md", (c) => {
577
+ c.header("Content-Type", "text/markdown; charset=utf-8");
578
+ c.header("Cache-Control", "no-cache");
579
+ return c.body(renderSkill(ctx.settings.publicUrl()));
580
+ });
581
+ app.route("/", oauthRoutes(ctx));
582
+ app.route("/", mcpRoutes(ctx));
583
+ app.use("/api/*", requireAdmin(ctx));
584
+ for (const api of [
585
+ setupApi,
586
+ authApi,
587
+ settingsApi,
588
+ systemApi,
589
+ googleApi,
590
+ accountsApi,
591
+ agentsApi,
592
+ approvalsApi,
593
+ webhooksApi,
594
+ eventsApi,
595
+ auditApi,
596
+ overviewApi
597
+ ]) app.route("/api", api(ctx));
598
+ if (ctx.config.dev) app.get("*", (c) => isServerPath(c.req.path) ? c.notFound() : devPage(c));
599
+ else consoleHashes = serveConsole(app, opts.webDir ?? DEFAULT_WEB_DIR);
600
+ return app;
601
+ }
602
+ /** Serves the built console; returns the CSP hashes of index.html's inline scripts (re-read when the file changes). */
603
+ function serveConsole(app, webDir) {
604
+ const index = join(webDir, "index.html");
605
+ if (!existsSync(index)) {
606
+ app.get("*", (c) => isServerPath(c.req.path) ? c.notFound() : missingBuildPage(c, webDir));
607
+ return () => [];
608
+ }
609
+ let cached = {
610
+ mtimeMs: -1,
611
+ hashes: []
612
+ };
613
+ const hashes = () => {
614
+ try {
615
+ const { mtimeMs } = statSync(index);
616
+ if (mtimeMs !== cached.mtimeMs) cached = {
617
+ mtimeMs,
618
+ hashes: inlineScriptHashes(readFileSync(index, "utf8"))
619
+ };
620
+ } catch {}
621
+ return cached.hashes;
622
+ };
623
+ const assets = serveStatic({ root: webDir });
624
+ const assetsDir = join(webDir, "assets") + sep;
625
+ app.use("*", async (c, next) => {
626
+ if (isServerPath(c.req.path)) return next();
627
+ const res = await assets(c, next);
628
+ if (res instanceof Response) {
629
+ const hashed = join(webDir, c.req.path).startsWith(assetsDir);
630
+ res.headers.set("Cache-Control", hashed ? "public, max-age=31536000, immutable" : "no-cache");
631
+ }
632
+ return res;
633
+ });
634
+ app.get("*", (c) => {
635
+ const path = c.req.path;
636
+ if (isServerPath(path) || path.startsWith("/assets/") || ASSET_EXTENSIONS.has(extname(path).toLowerCase())) return c.notFound();
637
+ c.header("Cache-Control", "no-cache");
638
+ return c.html(readFileSync(index, "utf8"));
639
+ });
640
+ return hashes;
641
+ }
642
+ function errorResponse(ctx, err, c) {
643
+ if (err instanceof HttpError) return c.json(err.toJSON(), err.status);
644
+ if (err instanceof PolicyDenied) return apiError(c, 403, "policy_denied", err.message);
645
+ if (err instanceof z.ZodError) return apiError(c, 400, "bad_request", z.prettifyError(err));
646
+ if (err instanceof HTTPException) return err.res ? err.getResponse() : apiError(c, err.status, "http_error", err.message || "Request failed");
647
+ if (err instanceof SyntaxError && /JSON/i.test(err.message)) return apiError(c, 400, "bad_request", "Request body must be valid JSON");
648
+ ctx.log.error(`${c.req.method} ${c.req.path} failed`, {
649
+ error: err.message,
650
+ stack: err.stack
651
+ });
652
+ return apiError(c, 500, "internal", "Something went wrong. Check the agentinbox logs for details.");
653
+ }
654
+ function apiError(c, status, code, message) {
655
+ return c.json({ error: {
656
+ code,
657
+ message
658
+ } }, status);
659
+ }
660
+ const page = (title, body) => `<!doctype html>
661
+ <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
662
+ <title>${title}</title>
663
+ <style>
664
+ :root { color-scheme: light dark; font-family: ui-sans-serif, system-ui, sans-serif; }
665
+ body { margin: 0; min-height: 100vh; display: grid; place-items: center; background: Canvas; color: CanvasText; }
666
+ main { max-width: 34rem; padding: 2rem; line-height: 1.55; }
667
+ h1 { font-size: 1.25rem; margin: 0 0 .75rem; }
668
+ code { font-family: ui-monospace, monospace; padding: .1rem .35rem; border-radius: .3rem; background: color-mix(in srgb, CanvasText 10%, transparent); }
669
+ p { margin: .5rem 0; opacity: .85; }
670
+ </style></head><body><main>${body}</main></body></html>`;
671
+ const escapeHtml = (s) => s.replace(/[&<>"]/g, (ch) => `&#${ch.charCodeAt(0)};`);
672
+ function missingBuildPage(c, webDir) {
673
+ c.header("Cache-Control", "no-store");
674
+ return c.html(page("agentinbox: console not built", `<h1>The web console hasn't been built</h1>
675
+ <p>agentinbox is running, but there is no console build in <code>${escapeHtml(webDir)}</code>.</p>
676
+ <p>From a source checkout, run <code>pnpm build</code> and restart, or use <code>pnpm dev</code> for development.</p>
677
+ <p>The API (<code>/api</code>), MCP endpoint (<code>/mcp</code>) and <code>/health</code> work as usual.</p>`), 503);
678
+ }
679
+ function devPage(c) {
680
+ const url = new URL(c.req.url);
681
+ const target = `http://localhost:5173${url.pathname}${url.search}`;
682
+ return c.html(page("agentinbox (dev)", `<h1>agentinbox is running in dev mode</h1>
683
+ <p>The console is served by Vite: open <a href="${escapeHtml(target)}">${escapeHtml(target)}</a>.</p>`));
684
+ }
685
+ //#endregion
686
+ //#region server/maintenance.ts
687
+ const INTERVAL_MS = 36e5;
688
+ const FIRST_RUN_MS = 6e4;
689
+ /** Prunes data older than Settings.retentionDays and expired admin sessions. Never throws. */
690
+ function runMaintenance(ctx) {
691
+ const log = ctx.log.child("maintenance");
692
+ const before = (/* @__PURE__ */ new Date(Date.now() - ctx.settings.get().retentionDays * 864e5)).toISOString();
693
+ const step = (name, fn) => {
694
+ try {
695
+ return fn();
696
+ } catch (err) {
697
+ log.warn(`Pruning ${name} failed`, { error: errorMessage(err) });
698
+ return 0;
699
+ }
700
+ };
701
+ const result = {
702
+ deliveries: step("deliveries", () => ctx.webhooks.prune(before)),
703
+ events: step("events", () => ctx.events.prune(before)),
704
+ audit: step("audit log", () => ctx.audit.prune(before)),
705
+ sessions: step("sessions", () => adminAuth(ctx).sessions.pruneExpired())
706
+ };
707
+ if (Object.values(result).some((n) => n > 0)) log.info("Pruned old data", { ...result });
708
+ return result;
709
+ }
710
+ /** Runs maintenance shortly after start, then hourly. Timers are unref'd. Returns a stop function. */
711
+ function startMaintenance(ctx) {
712
+ const run = () => {
713
+ try {
714
+ runMaintenance(ctx);
715
+ } catch (err) {
716
+ ctx.log.warn("Maintenance failed", { error: errorMessage(err) });
717
+ }
718
+ };
719
+ const first = setTimeout(run, FIRST_RUN_MS).unref();
720
+ const timer = setInterval(run, INTERVAL_MS).unref();
721
+ return () => {
722
+ clearTimeout(first);
723
+ clearInterval(timer);
724
+ };
725
+ }
726
+ //#endregion
727
+ //#region server/util/log.ts
728
+ const order = {
729
+ debug: 10,
730
+ info: 20,
731
+ warn: 30,
732
+ error: 40
733
+ };
734
+ const min = order[process.env.AGENTINBOX_LOG_LEVEL ?? "info"] ?? 20;
735
+ const color = {
736
+ debug: pc.gray,
737
+ info: pc.cyan,
738
+ warn: pc.yellow,
739
+ error: pc.red
740
+ };
741
+ function createLogger(scope = "agentinbox") {
742
+ const emit = (level, msg, meta) => {
743
+ if (order[level] < min) return;
744
+ const ts = (/* @__PURE__ */ new Date()).toISOString().slice(11, 19);
745
+ const extra = meta && Object.keys(meta).length ? " " + pc.gray(JSON.stringify(meta)) : "";
746
+ const line = `${pc.gray(ts)} ${color[level](level.padEnd(5))} ${pc.bold(scope)} ${msg}${extra}`;
747
+ (level === "error" || level === "warn" ? process.stderr : process.stdout).write(line + "\n");
748
+ };
749
+ return {
750
+ debug: (m, x) => emit("debug", m, x),
751
+ info: (m, x) => emit("info", m, x),
752
+ warn: (m, x) => emit("warn", m, x),
753
+ error: (m, x) => emit("error", m, x),
754
+ child: (s) => createLogger(`${scope}:${s}`)
755
+ };
756
+ }
757
+ //#endregion
758
+ //#region server/context.ts
759
+ /** Start order. Policy and events first (no I/O), ingest last so everything it emits into is running. */
760
+ const SERVICE_ORDER = [
761
+ "policy",
762
+ "accounts",
763
+ "events",
764
+ "webhooks",
765
+ "mail",
766
+ "google",
767
+ "ingest"
768
+ ];
769
+ function createCore(config) {
770
+ const db = new Db(config.dbPath);
771
+ const bus = createBus();
772
+ return {
773
+ config,
774
+ db,
775
+ crypto: Crypto.load(config.masterKeyPath),
776
+ log: createLogger(),
777
+ bus,
778
+ settings: new SettingsStore(db, bus, config),
779
+ audit: new AuditLog(db, bus)
780
+ };
781
+ }
782
+ //#endregion
783
+ //#region server/util/stub.ts
784
+ /**
785
+ * TEMPORARY scaffolding: a service whose methods throw "not implemented". Lets every domain compile
786
+ * and run its own tests before the others land. Removed at integration.
787
+ */
788
+ function stubService(name) {
789
+ return new Proxy({}, { get(_target, prop) {
790
+ if (prop === "start" || prop === "stop" || prop === "then" || typeof prop === "symbol") return void 0;
791
+ return () => {
792
+ throw new Error(`${name}.${String(prop)} is not implemented yet`);
793
+ };
794
+ } });
795
+ }
796
+ //#endregion
797
+ //#region server/events/index.ts
798
+ function createEventsService(_ctx) {
799
+ return stubService("events");
800
+ }
801
+ //#endregion
802
+ //#region server/google/index.ts
803
+ function createGoogleService(_ctx) {
804
+ return stubService("google");
805
+ }
806
+ function createAccountsService(_ctx) {
807
+ return stubService("accounts");
808
+ }
809
+ //#endregion
810
+ //#region server/ingest/index.ts
811
+ function createIngestService(_ctx) {
812
+ return stubService("ingest");
813
+ }
814
+ //#endregion
815
+ //#region server/mail/index.ts
816
+ function createMailService(_ctx) {
817
+ return stubService("mail");
818
+ }
819
+ //#endregion
820
+ //#region server/policy/index.ts
821
+ function createPolicyService(_ctx) {
822
+ return stubService("policy");
823
+ }
824
+ //#endregion
825
+ //#region server/webhooks/index.ts
826
+ function createWebhooksService(_ctx) {
827
+ return stubService("webhooks");
828
+ }
829
+ //#endregion
830
+ //#region server/wire.ts
831
+ const FACTORIES = {
832
+ accounts: createAccountsService,
833
+ google: createGoogleService,
834
+ ingest: createIngestService,
835
+ events: createEventsService,
836
+ policy: createPolicyService,
837
+ mail: createMailService,
838
+ webhooks: createWebhooksService
839
+ };
840
+ /** Builds the full context. Nothing runs until `startServices`. */
841
+ function createAppContext(config, overrides = {}) {
842
+ const ctx = createCore(config);
843
+ for (const key of SERVICE_ORDER) ctx[key] = (overrides[key] ?? FACTORIES[key])(ctx);
844
+ return ctx;
845
+ }
846
+ async function startServices(ctx) {
847
+ for (const key of SERVICE_ORDER) await ctx[key].start?.();
848
+ }
849
+ /** Stops services in reverse order, then closes the database. Errors are logged, not thrown. */
850
+ async function stopServices(ctx) {
851
+ for (const key of [...SERVICE_ORDER].reverse()) try {
852
+ await ctx[key].stop?.();
853
+ } catch (err) {
854
+ ctx.log.error(`Failed to stop ${key}`, { error: err instanceof Error ? err.message : String(err) });
855
+ }
856
+ ctx.db.close();
857
+ }
858
+ //#endregion
859
+ //#region server/server.ts
860
+ const SHUTDOWN_TIMEOUT_MS = 1e4;
861
+ /**
862
+ * Takes the data directory's instance lock, checks the master key against stored secrets, builds
863
+ * the context, binds the port, then starts services (so a second instance never runs ingest
864
+ * against the same data). Use `port: 0` in tests; `config.port` is updated to the bound port.
865
+ */
866
+ async function startServer(opts) {
867
+ const { config } = opts;
868
+ const lock = acquireInstanceLock(config);
869
+ let ctx;
870
+ let server;
871
+ try {
872
+ assertMasterKeyUsable(config);
873
+ ctx = createAppContext(config, opts.overrides);
874
+ } catch (err) {
875
+ lock.release();
876
+ throw err;
877
+ }
878
+ try {
879
+ server = await listen(createApp(ctx, { webDir: opts.webDir }).fetch, config.host, config.port);
880
+ } catch (err) {
881
+ ctx.db.close();
882
+ lock.release();
883
+ throw friendlyListenError(err, config.port);
884
+ }
885
+ config.port = server.address().port;
886
+ lock.update(config.port);
887
+ let stopMaintenance = () => {};
888
+ let closing;
889
+ const close = () => closing ??= (async () => {
890
+ stopMaintenance();
891
+ await new Promise((resolve) => {
892
+ server.close(() => resolve());
893
+ server.closeAllConnections?.();
894
+ });
895
+ await stopServices(ctx);
896
+ lock.release();
897
+ })();
898
+ try {
899
+ await startServices(ctx);
900
+ } catch (err) {
901
+ await close();
902
+ throw err;
903
+ }
904
+ stopMaintenance = startMaintenance(ctx);
905
+ return {
906
+ url: loopbackUrl(config.host, config.port),
907
+ ctx,
908
+ server,
909
+ close
910
+ };
911
+ }
912
+ function listen(fetch, hostname, port) {
913
+ return new Promise((resolve, reject) => {
914
+ const server = serve({
915
+ fetch,
916
+ hostname,
917
+ port
918
+ }, () => {
919
+ server.off("error", reject);
920
+ resolve(server);
921
+ });
922
+ server.once("error", reject);
923
+ });
924
+ }
925
+ function friendlyListenError(err, port) {
926
+ const code = err?.code;
927
+ if (code === "EADDRINUSE") return /* @__PURE__ */ new Error(`Port ${port} is already in use (is agentinbox already running?). Try --port <port>.`);
928
+ if (code === "EACCES") return /* @__PURE__ */ new Error(`Not allowed to listen on port ${port}. Use a port above 1024, e.g. --port 8425.`);
929
+ if (code === "EADDRNOTAVAIL") return /* @__PURE__ */ new Error(`Can't bind to that address. Check --host.`);
930
+ return err instanceof Error ? err : new Error(String(err));
931
+ }
932
+ /** `agentinbox start`: runs in the foreground until SIGINT/SIGTERM. */
933
+ async function runForeground(opts) {
934
+ const config = loadConfig(opts, opts.version);
935
+ const running = await startServer({ config });
936
+ printBanner(running.ctx);
937
+ const token = currentSetupToken(running.ctx);
938
+ if (token && opts.open && process.stdout.isTTY && !config.dev && !config.service) openBrowser(`${consoleUrls(config.host, config.port)[0]}/setup?token=${token}`);
939
+ for (const signal of ["SIGINT", "SIGTERM"]) process.once(signal, () => {
940
+ console.log(pc.dim(`\n Stopping agentinbox…`));
941
+ setTimeout(() => {
942
+ console.error(`agentinbox did not stop within ${SHUTDOWN_TIMEOUT_MS / 1e3} s; exiting anyway.`);
943
+ process.exit(1);
944
+ }, SHUTDOWN_TIMEOUT_MS).unref();
945
+ running.close().then(() => process.exit(0), (err) => {
946
+ console.error(errorMessage(err));
947
+ process.exit(1);
948
+ });
949
+ });
950
+ }
951
+ function printBanner(ctx) {
952
+ const { config } = ctx;
953
+ const urls = config.dev ? ["http://localhost:5173"] : consoleUrls(config.host, config.port);
954
+ const token = currentSetupToken(ctx);
955
+ const out = [
956
+ "",
957
+ ` ${pc.bold("agentinbox")} ${pc.dim(`v${config.version} · data in ${config.dataDir}`)}`,
958
+ ""
959
+ ];
960
+ if (token && config.service) out.push(` ${pc.yellow("Not claimed yet.")} The setup link is ${urls[0]}/setup?token=<token>, with the token in ${setupTokenPath(config.dataDir)}.`, ` ${pc.dim(`\`${cliCommand("service install")}\` prints the full link.`)}`);
961
+ else if (token) {
962
+ out.push(` ${pc.yellow("First run:")} open this link to claim the instance and finish setup:`, "");
963
+ for (const url of urls) out.push(` ${pc.cyan(`${url}/setup?token=${token}`)}`);
964
+ } else {
965
+ out.push(` ${pc.dim("Console")} ${pc.cyan(urls[0])}`);
966
+ for (const url of urls.slice(1)) out.push(` ${pc.cyan(url)}`);
967
+ out.push(` ${pc.dim("MCP")} ${pc.cyan(`${ctx.settings.publicUrl()}/mcp`)}`);
968
+ }
969
+ if (config.dev) out.push("", pc.dim(` Dev mode: API on :${config.port}, console via Vite on :5173.`));
970
+ console.log(out.join("\n") + "\n");
971
+ }
972
+ function openBrowser(url) {
973
+ if (process.platform === "linux" && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) return;
974
+ try {
975
+ open(url).then((child) => child.on("error", () => {}), () => {});
976
+ } catch {}
977
+ }
978
+ //#endregion
979
+ export { runForeground };
980
+
981
+ //# sourceMappingURL=server-CWqKeCGn.js.map