@app-vitals/agent-lib 0.0.1 → 0.1.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 (44) hide show
  1. package/admin-types.ts +4097 -0
  2. package/chat-notify.ts +41 -0
  3. package/chat-notify.unit.test.ts +88 -0
  4. package/claim-ttl.ts +12 -0
  5. package/claim-ttl.unit.test.ts +26 -0
  6. package/claude-timeout.ts +10 -0
  7. package/claude-timeout.unit.test.ts +8 -0
  8. package/cli-flags.ts +83 -0
  9. package/cli-flags.unit.test.ts +137 -0
  10. package/clone-plan.ts +73 -0
  11. package/clone-plan.unit.test.ts +69 -0
  12. package/default-account.ts +5 -0
  13. package/default-account.unit.test.ts +8 -0
  14. package/default-agent-env.ts +15 -0
  15. package/default-agent-env.unit.test.ts +19 -0
  16. package/github-login.ts +20 -0
  17. package/github-login.unit.test.ts +78 -0
  18. package/graceful-shutdown.ts +143 -0
  19. package/graceful-shutdown.unit.test.ts +236 -0
  20. package/http.ts +14 -0
  21. package/http.unit.test.ts +42 -0
  22. package/org-repo.ts +9 -0
  23. package/org-repo.unit.test.ts +42 -0
  24. package/package-exports.unit.test.ts +21 -0
  25. package/package.json +36 -4
  26. package/pg-sslmode.ts +66 -0
  27. package/pg-sslmode.unit.test.ts +74 -0
  28. package/pool-observability.ts +209 -0
  29. package/pool-observability.unit.test.ts +233 -0
  30. package/pricing.ts +98 -0
  31. package/pricing.unit.test.ts +214 -0
  32. package/progress-phases.ts +58 -0
  33. package/progress-phases.unit.test.ts +50 -0
  34. package/request-context.ts +23 -0
  35. package/request-context.unit.test.ts +51 -0
  36. package/secret-env-vars.ts +41 -0
  37. package/secret-env-vars.unit.test.ts +62 -0
  38. package/sentry.ts +201 -0
  39. package/sentry.unit.test.ts +294 -0
  40. package/task-store-types.ts +3256 -0
  41. package/task-store-types.unit.test.ts +24 -0
  42. package/web/toolbar.ts +244 -0
  43. package/web/toolbar.unit.test.ts +243 -0
  44. package/README.md +0 -3
@@ -0,0 +1,78 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { isGithubLogin } from "./github-login.ts";
3
+
4
+ describe("isGithubLogin — valid strings", () => {
5
+ test("returns true for a simple login", () => {
6
+ expect(isGithubLogin("octocat")).toBe(true);
7
+ });
8
+
9
+ test("returns true for a login with hyphens and numbers", () => {
10
+ expect(isGithubLogin("my-org-1")).toBe(true);
11
+ });
12
+
13
+ test("returns true for a single character login", () => {
14
+ expect(isGithubLogin("a")).toBe(true);
15
+ });
16
+
17
+ test("returns true for a login at the max length of 39 characters", () => {
18
+ expect(isGithubLogin("a".repeat(39))).toBe(true);
19
+ });
20
+
21
+ test("returns true for app/renovate bot login", () => {
22
+ expect(isGithubLogin("app/renovate")).toBe(true);
23
+ });
24
+
25
+ test("returns true for app/dependabot bot login", () => {
26
+ expect(isGithubLogin("app/dependabot")).toBe(true);
27
+ });
28
+
29
+ test("returns true for app/<slug> with hyphens", () => {
30
+ expect(isGithubLogin("app/my-bot-name")).toBe(true);
31
+ });
32
+ });
33
+
34
+ describe("isGithubLogin — invalid strings", () => {
35
+ test("returns false for an empty string", () => {
36
+ expect(isGithubLogin("")).toBe(false);
37
+ });
38
+
39
+ test("returns false for a login over 39 characters", () => {
40
+ expect(isGithubLogin("a".repeat(40))).toBe(false);
41
+ });
42
+
43
+ test("returns false for a login with a leading hyphen", () => {
44
+ expect(isGithubLogin("-octocat")).toBe(false);
45
+ });
46
+
47
+ test("returns false for a login with a trailing hyphen", () => {
48
+ expect(isGithubLogin("octocat-")).toBe(false);
49
+ });
50
+
51
+ test("returns false for a login with consecutive hyphens", () => {
52
+ expect(isGithubLogin("octo--cat")).toBe(false);
53
+ });
54
+
55
+ test("returns false for a login containing an underscore", () => {
56
+ expect(isGithubLogin("octo_cat")).toBe(false);
57
+ });
58
+
59
+ test("returns false for a login containing a space", () => {
60
+ expect(isGithubLogin("octo cat")).toBe(false);
61
+ });
62
+
63
+ test("returns false for a login containing a slash", () => {
64
+ expect(isGithubLogin("octo/cat")).toBe(false);
65
+ });
66
+
67
+ test("returns false for app/ with no slug", () => {
68
+ expect(isGithubLogin("app/")).toBe(false);
69
+ });
70
+
71
+ test("returns false for app/<slug> with leading hyphen in slug", () => {
72
+ expect(isGithubLogin("app/-bad")).toBe(false);
73
+ });
74
+
75
+ test("returns false for app/<slug> with double slash", () => {
76
+ expect(isGithubLogin("app//x")).toBe(false);
77
+ });
78
+ });
@@ -0,0 +1,143 @@
1
+ /**
2
+ * lib/graceful-shutdown.ts
3
+ *
4
+ * Wires SIGTERM/SIGINT into a coordinated shutdown for a `Bun.serve()`-backed
5
+ * service: stop accepting new connections (letting in-flight requests
6
+ * finish), run ordered cleanup (e.g. `prisma.$disconnect()`), then exit.
7
+ *
8
+ * A hard deadline forces a non-graceful `server.stop(true)` + `exit(1)` so a
9
+ * stuck cleanup fn can never outlive the pod's terminationGracePeriod (30s in
10
+ * the Helm chart — see DBE-1.x/DBE-3.1, out of scope here).
11
+ *
12
+ * Every I/O-adjacent collaborator — the server, the signal source, the exit
13
+ * call, and the deadline timer — is injectable so this is unit-testable with
14
+ * fakes: no real signals sent, no real waiting, no `mock.module()` / global
15
+ * overrides (see CLAUDE.md's test isolation rule).
16
+ */
17
+
18
+ /** Below the pod's terminationGracePeriod (30s) so a stuck cleanup can't outlive it. */
19
+ const DEFAULT_HARD_DEADLINE_MS = 20_000;
20
+
21
+ /** Mirrors Bun.serve()'s return value: stop() waits for in-flight requests, stop(true) force-closes. */
22
+ interface ShutdownServer {
23
+ stop(closeActiveConnections?: boolean): unknown;
24
+ }
25
+
26
+ /** Run in order on shutdown; a throw is logged and does not block later cleanup fns or the final exit. */
27
+ type CleanupFn = () => Promise<void>;
28
+
29
+ /** Narrowed to the one method this module needs — satisfied by the real `process`. */
30
+ interface SignalSource {
31
+ on(event: "SIGTERM" | "SIGINT", listener: () => void): unknown;
32
+ }
33
+
34
+ interface GracefulShutdownLogger {
35
+ log: (...args: unknown[]) => void;
36
+ error: (...args: unknown[]) => void;
37
+ }
38
+
39
+ interface GracefulShutdownOptions {
40
+ /** The Bun.serve() return value (or anything shaped like it). */
41
+ server: ShutdownServer;
42
+ /** Cleanup fns run sequentially, in order, after the server stops accepting new work. */
43
+ cleanup: CleanupFn[];
44
+ /** Forces stop(true) + exit(1) if the graceful path hasn't finished by then. Default 20s. */
45
+ hardDeadlineMs?: number;
46
+ /** Defaults to the real `process.exit`. */
47
+ exit?: (code: number) => void;
48
+ /** Defaults to the real global `setTimeout`. Injectable so tests can trigger the deadline manually. */
49
+ setTimeoutFn?: (fn: () => void, ms: number) => unknown;
50
+ /** Defaults to the real global `clearTimeout`. */
51
+ clearTimeoutFn?: (handle: unknown) => void;
52
+ /** Defaults to the real `process`. Injectable so tests can fire signals without touching the real process. */
53
+ process?: SignalSource;
54
+ logger?: GracefulShutdownLogger;
55
+ /** Used only to prefix log lines, e.g. "admin", "task-store", "chat". */
56
+ serviceName?: string;
57
+ }
58
+
59
+ interface GracefulShutdown {
60
+ /** True once a shutdown signal has been received — gate readiness probes on this. */
61
+ isShuttingDown: () => boolean;
62
+ }
63
+
64
+ export function registerGracefulShutdown(
65
+ options: GracefulShutdownOptions,
66
+ ): GracefulShutdown {
67
+ const {
68
+ server,
69
+ cleanup,
70
+ hardDeadlineMs = DEFAULT_HARD_DEADLINE_MS,
71
+ exit = process.exit.bind(process),
72
+ // Wrapped (rather than assigned the raw global) so the interface can
73
+ // stay in terms of `unknown` for the timer handle without TypeScript
74
+ // narrowing these bindings to the global setTimeout/clearTimeout
75
+ // overloads' own parameter types.
76
+ setTimeoutFn = (fn: () => void, ms: number): unknown => setTimeout(fn, ms),
77
+ clearTimeoutFn = (handle: unknown): void =>
78
+ clearTimeout(handle as Parameters<typeof clearTimeout>[0]),
79
+ process: signalSource = process,
80
+ logger = console,
81
+ serviceName = "service",
82
+ } = options;
83
+
84
+ let shuttingDown = false;
85
+ let settled = false;
86
+
87
+ // Declared via `let` (not inline in the setTimeoutFn call) so a fake
88
+ // setTimeoutFn that invokes its callback synchronously never hits a
89
+ // temporal-dead-zone reference to this variable.
90
+ let deadlineHandle: unknown;
91
+
92
+ const finish = (code: number) => {
93
+ if (settled) return;
94
+ settled = true;
95
+ clearTimeoutFn(deadlineHandle);
96
+ exit(code);
97
+ };
98
+
99
+ const handleSignal = (signal: string) => {
100
+ if (shuttingDown) return;
101
+ shuttingDown = true;
102
+
103
+ logger.log(`[${serviceName}] received ${signal}, shutting down gracefully`);
104
+
105
+ deadlineHandle = setTimeoutFn(() => {
106
+ logger.error(
107
+ `[${serviceName}] graceful shutdown exceeded ${hardDeadlineMs}ms — forcing stop`,
108
+ );
109
+ try {
110
+ server.stop(true);
111
+ } catch (err) {
112
+ logger.error(`[${serviceName}] error force-stopping server:`, err);
113
+ }
114
+ finish(1);
115
+ }, hardDeadlineMs);
116
+
117
+ void (async () => {
118
+ try {
119
+ await server.stop();
120
+ } catch (err) {
121
+ logger.error(`[${serviceName}] error stopping server:`, err);
122
+ }
123
+
124
+ for (const fn of cleanup) {
125
+ try {
126
+ await fn();
127
+ } catch (err) {
128
+ logger.error(`[${serviceName}] cleanup fn failed:`, err);
129
+ }
130
+ }
131
+
132
+ logger.log(`[${serviceName}] graceful shutdown complete`);
133
+ finish(0);
134
+ })();
135
+ };
136
+
137
+ signalSource.on("SIGTERM", () => handleSignal("SIGTERM"));
138
+ signalSource.on("SIGINT", () => handleSignal("SIGINT"));
139
+
140
+ return {
141
+ isShuttingDown: () => shuttingDown,
142
+ };
143
+ }
@@ -0,0 +1,236 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { registerGracefulShutdown } from "./graceful-shutdown.ts";
3
+
4
+ /** Records signal listeners without touching the real `process` object. */
5
+ function fakeSignalSource() {
6
+ const listeners: Record<string, () => void> = {};
7
+ return {
8
+ on(event: "SIGTERM" | "SIGINT", listener: () => void) {
9
+ listeners[event] = listener;
10
+ },
11
+ listeners,
12
+ };
13
+ }
14
+
15
+ /** Fake server.stop() — records the `closeActiveConnections` arg per call and resolves immediately. */
16
+ function fakeServer(calls: string[]) {
17
+ return {
18
+ stop(closeActiveConnections?: boolean) {
19
+ calls.push(closeActiveConnections ? "stop(true)" : "stop()");
20
+ return Promise.resolve();
21
+ },
22
+ };
23
+ }
24
+
25
+ /** Fake setTimeout — never fires on its own; the test triggers it manually via `fire()`. */
26
+ function fakeTimeout() {
27
+ let scheduled: { fn: () => void; ms: number } | undefined;
28
+ return {
29
+ setTimeoutFn: (fn: () => void, ms: number) => {
30
+ scheduled = { fn, ms };
31
+ return "handle";
32
+ },
33
+ clearTimeoutFn: () => {
34
+ scheduled = undefined;
35
+ },
36
+ fire: () => scheduled?.fn(),
37
+ ms: () => scheduled?.ms,
38
+ };
39
+ }
40
+
41
+ /** Fake process.exit() that also resolves a promise so tests can await shutdown completion. */
42
+ function fakeExit() {
43
+ const calls: number[] = [];
44
+ let resolve!: () => void;
45
+ const settled = new Promise<void>((r) => {
46
+ resolve = r;
47
+ });
48
+ return {
49
+ exit: (code: number) => {
50
+ calls.push(code);
51
+ resolve();
52
+ },
53
+ calls,
54
+ settled,
55
+ };
56
+ }
57
+
58
+ const NOOP_LOGGER = { log: () => {}, error: () => {} };
59
+
60
+ /**
61
+ * Builds a fresh set of fakes and registers a shutdown handler against them.
62
+ * Every test shares this same wiring — only `cleanup` and `hardDeadlineMs`
63
+ * vary — so the fixture construction lives here once instead of being
64
+ * repeated per test.
65
+ */
66
+ function harness(
67
+ overrides: {
68
+ cleanup?: Array<() => Promise<void>>;
69
+ hardDeadlineMs?: number;
70
+ } = {},
71
+ ) {
72
+ const calls: string[] = [];
73
+ const server = fakeServer(calls);
74
+ const signal = fakeSignalSource();
75
+ const timeout = fakeTimeout();
76
+ const exit = fakeExit();
77
+
78
+ const handle = registerGracefulShutdown({
79
+ server,
80
+ cleanup: overrides.cleanup ?? [async () => {}],
81
+ ...(overrides.hardDeadlineMs !== undefined
82
+ ? { hardDeadlineMs: overrides.hardDeadlineMs }
83
+ : {}),
84
+ process: signal,
85
+ setTimeoutFn: timeout.setTimeoutFn,
86
+ clearTimeoutFn: timeout.clearTimeoutFn,
87
+ exit: exit.exit,
88
+ logger: NOOP_LOGGER,
89
+ });
90
+
91
+ return { calls, signal, timeout, exit, handle };
92
+ }
93
+
94
+ describe("registerGracefulShutdown", () => {
95
+ test("calls server.stop() before running cleanup fns", async () => {
96
+ const { calls, signal, exit } = harness({
97
+ cleanup: [
98
+ async () => {
99
+ calls.push("cleanup:0");
100
+ },
101
+ ],
102
+ });
103
+
104
+ signal.listeners.SIGTERM();
105
+ await exit.settled;
106
+
107
+ expect(calls).toEqual(["stop()", "cleanup:0"]);
108
+ });
109
+
110
+ test("runs cleanup fns in the given order", async () => {
111
+ const { calls, signal, exit } = harness({
112
+ cleanup: [
113
+ async () => {
114
+ calls.push("cleanup:a");
115
+ },
116
+ async () => {
117
+ calls.push("cleanup:b");
118
+ },
119
+ async () => {
120
+ calls.push("cleanup:c");
121
+ },
122
+ ],
123
+ });
124
+
125
+ signal.listeners.SIGTERM();
126
+ await exit.settled;
127
+
128
+ expect(calls).toEqual(["stop()", "cleanup:a", "cleanup:b", "cleanup:c"]);
129
+ });
130
+
131
+ test("exits with code 0 on clean successful shutdown", async () => {
132
+ const { signal, exit } = harness();
133
+
134
+ signal.listeners.SIGTERM();
135
+ await exit.settled;
136
+
137
+ expect(exit.calls).toEqual([0]);
138
+ });
139
+
140
+ test("hard deadline forces server.stop(true) and exit(1) when cleanup never resolves", async () => {
141
+ const { calls, signal, timeout, exit } = harness({
142
+ cleanup: [
143
+ () =>
144
+ new Promise<void>(() => {
145
+ // never resolves — simulates a stuck cleanup fn
146
+ }),
147
+ ],
148
+ hardDeadlineMs: 20_000,
149
+ });
150
+
151
+ signal.listeners.SIGTERM();
152
+
153
+ // Let the graceful stop() + cleanup kick-off settle onto the stuck promise
154
+ // before manually firing the deadline (no real waiting involved).
155
+ await Promise.resolve();
156
+ await Promise.resolve();
157
+
158
+ expect(timeout.ms()).toBe(20_000);
159
+ timeout.fire();
160
+ await exit.settled;
161
+
162
+ expect(calls).toContain("stop(true)");
163
+ expect(exit.calls).toEqual([1]);
164
+ });
165
+
166
+ test("a second SIGTERM after the first is a no-op", async () => {
167
+ const { calls, signal, exit } = harness({
168
+ cleanup: [
169
+ async () => {
170
+ calls.push("cleanup:0");
171
+ },
172
+ ],
173
+ });
174
+
175
+ signal.listeners.SIGTERM();
176
+ await exit.settled;
177
+
178
+ signal.listeners.SIGTERM();
179
+ // Flush microtasks so a (bugged) second invocation would have had a chance to run.
180
+ await Promise.resolve();
181
+ await Promise.resolve();
182
+
183
+ expect(calls).toEqual(["stop()", "cleanup:0"]);
184
+ expect(exit.calls).toEqual([0]);
185
+ });
186
+
187
+ test("SIGINT triggers the same shutdown sequence as SIGTERM", async () => {
188
+ const { calls, signal, exit } = harness({
189
+ cleanup: [
190
+ async () => {
191
+ calls.push("cleanup:0");
192
+ },
193
+ ],
194
+ });
195
+
196
+ signal.listeners.SIGINT();
197
+ await exit.settled;
198
+
199
+ expect(calls).toEqual(["stop()", "cleanup:0"]);
200
+ expect(exit.calls).toEqual([0]);
201
+ });
202
+
203
+ test("exposes a readable shuttingDown flag, set synchronously on signal", async () => {
204
+ const { signal, exit, handle } = harness();
205
+
206
+ expect(handle.isShuttingDown()).toBe(false);
207
+
208
+ signal.listeners.SIGTERM();
209
+
210
+ // True immediately, before the async stop/cleanup chain has settled.
211
+ expect(handle.isShuttingDown()).toBe(true);
212
+
213
+ await exit.settled;
214
+ expect(handle.isShuttingDown()).toBe(true);
215
+ });
216
+
217
+ test("a cleanup fn that throws does not prevent later cleanup fns or process.exit", async () => {
218
+ const { calls, signal, exit } = harness({
219
+ cleanup: [
220
+ async () => {
221
+ calls.push("cleanup:a");
222
+ throw new Error("boom");
223
+ },
224
+ async () => {
225
+ calls.push("cleanup:b");
226
+ },
227
+ ],
228
+ });
229
+
230
+ signal.listeners.SIGTERM();
231
+ await exit.settled;
232
+
233
+ expect(calls).toEqual(["stop()", "cleanup:a", "cleanup:b"]);
234
+ expect(exit.calls).toEqual([0]);
235
+ });
236
+ });
package/http.ts ADDED
@@ -0,0 +1,14 @@
1
+ /** Parses a request body as a JSON object, returning `{}` on any error or non-object shape. */
2
+ export async function readJson(c: {
3
+ req: { json: () => Promise<unknown> };
4
+ }): Promise<Record<string, unknown>> {
5
+ try {
6
+ const body = await c.req.json();
7
+ if (body && typeof body === "object" && !Array.isArray(body)) {
8
+ return body as Record<string, unknown>;
9
+ }
10
+ return {};
11
+ } catch {
12
+ return {};
13
+ }
14
+ }
@@ -0,0 +1,42 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { readJson } from "./http.ts";
3
+
4
+ describe("readJson — valid bodies", () => {
5
+ test("returns the parsed object when the body is a plain object", async () => {
6
+ const c = { req: { json: async () => ({ foo: "bar" }) } };
7
+ expect(await readJson(c)).toEqual({ foo: "bar" });
8
+ });
9
+
10
+ test("returns an empty object when the body is an empty object", async () => {
11
+ const c = { req: { json: async () => ({}) } };
12
+ expect(await readJson(c)).toEqual({});
13
+ });
14
+ });
15
+
16
+ describe("readJson — invalid bodies", () => {
17
+ test("returns {} when the body is an array", async () => {
18
+ const c = { req: { json: async () => [1, 2, 3] } };
19
+ expect(await readJson(c)).toEqual({});
20
+ });
21
+
22
+ test("returns {} when the body is a string primitive", async () => {
23
+ const c = { req: { json: async () => "not an object" } };
24
+ expect(await readJson(c)).toEqual({});
25
+ });
26
+
27
+ test("returns {} when the body is null", async () => {
28
+ const c = { req: { json: async () => null } };
29
+ expect(await readJson(c)).toEqual({});
30
+ });
31
+
32
+ test("returns {} when req.json() throws", async () => {
33
+ const c = {
34
+ req: {
35
+ json: async () => {
36
+ throw new Error("invalid JSON");
37
+ },
38
+ },
39
+ };
40
+ expect(await readJson(c)).toEqual({});
41
+ });
42
+ });
package/org-repo.ts ADDED
@@ -0,0 +1,9 @@
1
+ /** Returns true when `s` is a valid "org/repo" string with non-empty org and repo parts. */
2
+ export function isOrgRepo(s: string): boolean {
3
+ const parts = s.split("/");
4
+ return (
5
+ parts.length === 2 &&
6
+ parts[0].trim().length > 0 &&
7
+ parts[1].trim().length > 0
8
+ );
9
+ }
@@ -0,0 +1,42 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { isOrgRepo } from "./org-repo.ts";
3
+
4
+ describe("isOrgRepo — valid strings", () => {
5
+ test("returns true for a simple org/repo string", () => {
6
+ expect(isOrgRepo("app-vitals/shipwright")).toBe(true);
7
+ });
8
+
9
+ test("returns true when org and repo contain hyphens and numbers", () => {
10
+ expect(isOrgRepo("my-org-1/my-repo-2")).toBe(true);
11
+ });
12
+ });
13
+
14
+ describe("isOrgRepo — invalid strings", () => {
15
+ test("returns false for a string with no slash", () => {
16
+ expect(isOrgRepo("shipwright")).toBe(false);
17
+ });
18
+
19
+ test("returns false for a string with more than one slash", () => {
20
+ expect(isOrgRepo("app-vitals/shipwright/extra")).toBe(false);
21
+ });
22
+
23
+ test("returns false when the org part is empty", () => {
24
+ expect(isOrgRepo("/shipwright")).toBe(false);
25
+ });
26
+
27
+ test("returns false when the repo part is empty", () => {
28
+ expect(isOrgRepo("app-vitals/")).toBe(false);
29
+ });
30
+
31
+ test("returns false when the org part is only whitespace", () => {
32
+ expect(isOrgRepo(" /shipwright")).toBe(false);
33
+ });
34
+
35
+ test("returns false when the repo part is only whitespace", () => {
36
+ expect(isOrgRepo("app-vitals/ ")).toBe(false);
37
+ });
38
+
39
+ test("returns false for an empty string", () => {
40
+ expect(isOrgRepo("")).toBe(false);
41
+ });
42
+ });
@@ -0,0 +1,21 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { existsSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import pkg from "./package.json" with { type: "json" };
5
+
6
+ describe("lib/package.json exports", () => {
7
+ for (const [subpath, target] of Object.entries(pkg.exports)) {
8
+ if (target.includes("*")) continue;
9
+ test(`${subpath} target exists on disk`, () => {
10
+ expect(existsSync(join(import.meta.dir, target))).toBe(true);
11
+ });
12
+ }
13
+
14
+ test("every non-wildcard export imports cleanly", async () => {
15
+ for (const [, target] of Object.entries(pkg.exports)) {
16
+ if (target.includes("*")) continue;
17
+ const mod = await import(join(import.meta.dir, target));
18
+ expect(typeof mod).toBe("object");
19
+ }
20
+ });
21
+ });
package/package.json CHANGED
@@ -1,8 +1,40 @@
1
1
  {
2
2
  "name": "@app-vitals/agent-lib",
3
- "version": "0.0.1",
4
- "description": "Placeholder reserving the package name. The real release is published from github.com/app-vitals/agent.",
3
+ "version": "0.1.1",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "git+https://github.com/app-vitals/agent.git",
7
+ "directory": "lib"
8
+ },
5
9
  "license": "MIT",
6
- "repository": {"type": "git", "url": "git+https://github.com/app-vitals/agent.git"},
7
- "publishConfig": {"access": "public"}
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "type": "module",
14
+ "exports": {
15
+ "./admin-types": "./admin-types.ts",
16
+ "./chat-notify": "./chat-notify.ts",
17
+ "./claim-ttl": "./claim-ttl.ts",
18
+ "./claude-timeout": "./claude-timeout.ts",
19
+ "./cli-flags": "./cli-flags.ts",
20
+ "./clone-plan": "./clone-plan.ts",
21
+ "./github-login": "./github-login.ts",
22
+ "./graceful-shutdown": "./graceful-shutdown.ts",
23
+ "./http": "./http.ts",
24
+ "./org-repo": "./org-repo.ts",
25
+ "./pg-sslmode": "./pg-sslmode.ts",
26
+ "./pool-observability": "./pool-observability.ts",
27
+ "./pricing": "./pricing.ts",
28
+ "./progress-phases": "./progress-phases.ts",
29
+ "./request-context": "./request-context.ts",
30
+ "./secret-env-vars": "./secret-env-vars.ts",
31
+ "./default-account": "./default-account.ts",
32
+ "./default-agent-env": "./default-agent-env.ts",
33
+ "./sentry": "./sentry.ts",
34
+ "./task-store-types": "./task-store-types.ts",
35
+ "./web/*": "./web/*"
36
+ },
37
+ "dependencies": {
38
+ "@sentry/bun": "^10.63.0"
39
+ }
8
40
  }