auggy 0.3.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 (121) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/LICENSE +201 -0
  3. package/README.md +161 -0
  4. package/package.json +76 -0
  5. package/src/agent-card.ts +39 -0
  6. package/src/agent.ts +283 -0
  7. package/src/agentmail-client.ts +138 -0
  8. package/src/augments/bash/index.ts +463 -0
  9. package/src/augments/bash/skill/SKILL.md +156 -0
  10. package/src/augments/budgets/budget-store.ts +513 -0
  11. package/src/augments/budgets/index.ts +134 -0
  12. package/src/augments/budgets/preamble.ts +93 -0
  13. package/src/augments/budgets/types.ts +89 -0
  14. package/src/augments/file-memory/index.ts +71 -0
  15. package/src/augments/filesystem/index.ts +533 -0
  16. package/src/augments/filesystem/skill/SKILL.md +142 -0
  17. package/src/augments/filesystem/skill/references/mount-permissions.md +81 -0
  18. package/src/augments/layered-memory/extractor/buffer.ts +56 -0
  19. package/src/augments/layered-memory/extractor/frequency.ts +79 -0
  20. package/src/augments/layered-memory/extractor/inject-handler.ts +103 -0
  21. package/src/augments/layered-memory/extractor/parse.ts +75 -0
  22. package/src/augments/layered-memory/extractor/prompt.md +26 -0
  23. package/src/augments/layered-memory/index.ts +757 -0
  24. package/src/augments/layered-memory/skill/SKILL.md +153 -0
  25. package/src/augments/layered-memory/storage/migrations/README.md +16 -0
  26. package/src/augments/layered-memory/storage/migrations/supabase-add-fact-fields.sql +9 -0
  27. package/src/augments/layered-memory/storage/sqlite-store.ts +352 -0
  28. package/src/augments/layered-memory/storage/supabase-store.ts +263 -0
  29. package/src/augments/layered-memory/storage/types.ts +98 -0
  30. package/src/augments/link/index.ts +489 -0
  31. package/src/augments/link/translate.ts +261 -0
  32. package/src/augments/notify/adapters/agentmail.ts +70 -0
  33. package/src/augments/notify/adapters/telegram.ts +60 -0
  34. package/src/augments/notify/adapters/webhook.ts +55 -0
  35. package/src/augments/notify/index.ts +284 -0
  36. package/src/augments/notify/skill/SKILL.md +150 -0
  37. package/src/augments/org-context/index.ts +721 -0
  38. package/src/augments/org-context/skill/SKILL.md +96 -0
  39. package/src/augments/skills/index.ts +103 -0
  40. package/src/augments/supabase-memory/index.ts +151 -0
  41. package/src/augments/telegram-transport/index.ts +312 -0
  42. package/src/augments/telegram-transport/polling.ts +55 -0
  43. package/src/augments/telegram-transport/webhook.ts +56 -0
  44. package/src/augments/turn-control/index.ts +61 -0
  45. package/src/augments/turn-control/skill/SKILL.md +155 -0
  46. package/src/augments/visitor-auth/email-validation.ts +66 -0
  47. package/src/augments/visitor-auth/index.ts +779 -0
  48. package/src/augments/visitor-auth/rate-limiter.ts +90 -0
  49. package/src/augments/visitor-auth/skill/SKILL.md +55 -0
  50. package/src/augments/visitor-auth/storage/sqlite-store.ts +398 -0
  51. package/src/augments/visitor-auth/storage/types.ts +164 -0
  52. package/src/augments/visitor-auth/types.ts +123 -0
  53. package/src/augments/visitor-auth/verify-page.ts +179 -0
  54. package/src/augments/web-fetch/index.ts +331 -0
  55. package/src/augments/web-fetch/skill/SKILL.md +100 -0
  56. package/src/cli/agent-index.ts +289 -0
  57. package/src/cli/augment-catalog.ts +320 -0
  58. package/src/cli/augment-resolver.ts +597 -0
  59. package/src/cli/commands/add-skill.ts +194 -0
  60. package/src/cli/commands/add.ts +87 -0
  61. package/src/cli/commands/chat.ts +207 -0
  62. package/src/cli/commands/create.ts +462 -0
  63. package/src/cli/commands/dev.ts +139 -0
  64. package/src/cli/commands/eval.ts +180 -0
  65. package/src/cli/commands/ls.ts +66 -0
  66. package/src/cli/commands/remove.ts +95 -0
  67. package/src/cli/commands/restart.ts +40 -0
  68. package/src/cli/commands/start.ts +123 -0
  69. package/src/cli/commands/status.ts +104 -0
  70. package/src/cli/commands/stop.ts +84 -0
  71. package/src/cli/commands/visitors-revoke.ts +155 -0
  72. package/src/cli/commands/visitors.ts +101 -0
  73. package/src/cli/config-parser.ts +1034 -0
  74. package/src/cli/engine-resolver.ts +68 -0
  75. package/src/cli/index.ts +178 -0
  76. package/src/cli/model-picker.ts +89 -0
  77. package/src/cli/pid-registry.ts +146 -0
  78. package/src/cli/plist-generator.ts +117 -0
  79. package/src/cli/resolve-config.ts +56 -0
  80. package/src/cli/scaffold-skills.ts +158 -0
  81. package/src/cli/scaffold.ts +291 -0
  82. package/src/cli/skill-frontmatter.ts +51 -0
  83. package/src/cli/skill-validator.ts +151 -0
  84. package/src/cli/types.ts +228 -0
  85. package/src/cli/yaml-helpers.ts +66 -0
  86. package/src/engines/_shared/cost.ts +55 -0
  87. package/src/engines/_shared/schema-normalize.ts +75 -0
  88. package/src/engines/anthropic/pricing.ts +117 -0
  89. package/src/engines/anthropic.ts +483 -0
  90. package/src/engines/openai/pricing.ts +67 -0
  91. package/src/engines/openai.ts +446 -0
  92. package/src/engines/openrouter/pricing.ts +83 -0
  93. package/src/engines/openrouter.ts +185 -0
  94. package/src/helpers.ts +24 -0
  95. package/src/http.ts +387 -0
  96. package/src/index.ts +165 -0
  97. package/src/kernel/capability-table.ts +172 -0
  98. package/src/kernel/context-allocator.ts +161 -0
  99. package/src/kernel/history-manager.ts +198 -0
  100. package/src/kernel/lifecycle-manager.ts +106 -0
  101. package/src/kernel/output-validator.ts +35 -0
  102. package/src/kernel/preamble.ts +23 -0
  103. package/src/kernel/route-collector.ts +97 -0
  104. package/src/kernel/timeout.ts +21 -0
  105. package/src/kernel/tool-selector.ts +47 -0
  106. package/src/kernel/trace-emitter.ts +66 -0
  107. package/src/kernel/transport-queue.ts +147 -0
  108. package/src/kernel/turn-loop.ts +1148 -0
  109. package/src/memory/context-synthesis.ts +83 -0
  110. package/src/memory/memory-bus.ts +61 -0
  111. package/src/memory/registry.ts +80 -0
  112. package/src/memory/tools.ts +320 -0
  113. package/src/memory/types.ts +8 -0
  114. package/src/parts.ts +30 -0
  115. package/src/scaffold-templates/identity.md +31 -0
  116. package/src/telegram-client.ts +145 -0
  117. package/src/tokenizer.ts +14 -0
  118. package/src/transports/ag-ui-events.ts +253 -0
  119. package/src/transports/visitor-token.ts +82 -0
  120. package/src/transports/web-transport.ts +948 -0
  121. package/src/types.ts +1009 -0
@@ -0,0 +1,261 @@
1
+ /**
2
+ * Translation layer between @auggy/link's wire types and augment-1's runtime
3
+ * types. Kept here (separate from index.ts) so the field mapping is exercised
4
+ * in isolation by the unit tests.
5
+ *
6
+ * Field map summary (audit reference):
7
+ *
8
+ * link.Participant.type ("agent" | "human") → PeerIdentity.kind
9
+ * link.Participant.trust ("creator"|"agent"|"public") → PeerIdentity.trustLevel
10
+ * (identical alphabet — string passes through)
11
+ * link.Participant.id (UUID) → PeerIdentity.id
12
+ * link.Participant.org_id (optional) → PeerIdentity.orgId
13
+ *
14
+ * link.HandlerContext.parts → InboundMessage.parts (text-only at v0.1;
15
+ * link's metadata is dropped because
16
+ * augment-1's Part is bare)
17
+ * link.HandlerContext.task_id → TurnTrigger.taskId / InboundMessage.taskId
18
+ * link.HandlerContext.idempotency_key → InboundMessage.metadata.idempotency_key
19
+ * link.HandlerContext.request_id → InboundMessage.metadata.request_id
20
+ * link.HandlerContext.parent_task_id → InboundMessage.metadata.parent_task_id
21
+ * link.HandlerContext.received_at (ISO) → InboundMessage.timestamp (epoch ms)
22
+ *
23
+ * v0.1 augment intentionally returns ONLY MessageOutcome (sync answer) or
24
+ * ErrorOutcome. TaskCreateOutcome / TaskContinueOutcome are deferred until
25
+ * augment-1's turn loop grows long-running task semantics — see ADR-022
26
+ * sequencing.
27
+ */
28
+
29
+ import type {
30
+ HandlerContext as LinkHandlerContext,
31
+ HandlerOutcome as LinkHandlerOutcome,
32
+ Part as LinkPart,
33
+ Participant as LinkParticipant,
34
+ } from "@auggy/link";
35
+
36
+ import type {
37
+ InboundMessage,
38
+ Part as Augment1Part,
39
+ PeerIdentity,
40
+ TurnResult,
41
+ TurnTrigger,
42
+ } from "../../types";
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // Participant ↔ PeerIdentity
46
+ // ---------------------------------------------------------------------------
47
+
48
+ /**
49
+ * Translate a verified link Participant (returned by BearerAuthProvider) to an
50
+ * augment-1 PeerIdentity. The trust alphabet is shared, so `trust` passes
51
+ * through unchanged.
52
+ *
53
+ * `sourceAugment` is the augment-1 runtime name of the link augment instance
54
+ * (defaults to "link", but operators can rename it in agent.yaml; the caller
55
+ * passes the configured value).
56
+ */
57
+ export function participantToPeerIdentity(p: LinkParticipant, sourceAugment: string): PeerIdentity {
58
+ // Participant.type is "agent" | "human"; augment-1's PeerKind admits the
59
+ // same two values plus "system"|"anonymous" (not produced by link).
60
+ const kind: PeerIdentity["kind"] = p.type;
61
+
62
+ // Participant.trust ∈ {creator, agent, public}; PeerIdentity.trustLevel ∈
63
+ // the same set. Identity passes through.
64
+ const identity: PeerIdentity = {
65
+ id: p.id,
66
+ kind,
67
+ trustLevel: p.trust,
68
+ sourceAugment,
69
+ };
70
+ if (p.org_id !== undefined) {
71
+ identity.orgId = p.org_id;
72
+ }
73
+ return identity;
74
+ }
75
+
76
+ // ---------------------------------------------------------------------------
77
+ // Part ↔ Part (text-only at v0.1)
78
+ // ---------------------------------------------------------------------------
79
+
80
+ /**
81
+ * Convert an inbound link Part to an augment-1 Part. v0.1 is text-only on
82
+ * both sides; link's optional `metadata` is dropped because augment-1's
83
+ * text Part doesn't carry metadata.
84
+ *
85
+ * The narrow union on link's side (`Part = TextPart` at v0.1) guarantees
86
+ * the discriminator check below is exhaustive today; adding non-text parts
87
+ * to link in v0.2 will flag this function as a TS coverage gap.
88
+ */
89
+ export function linkPartToAugment1Part(p: LinkPart): Augment1Part {
90
+ if (p.kind === "text") {
91
+ return { kind: "text", text: p.text };
92
+ }
93
+ // Unreachable at v0.1 (link's Part union is text-only). Throw rather than
94
+ // silently coerce — the runtime should surface schema drift loudly.
95
+ throw new Error(
96
+ `link: unsupported inbound Part kind "${(p as { kind: string }).kind}" — v0.1 augment is text-only`,
97
+ );
98
+ }
99
+
100
+ /**
101
+ * Convert an outbound augment-1 Part to a link Part. v0.1 is text-only;
102
+ * file/data parts are silently dropped (caller filters before calling).
103
+ * Returns `null` for non-text parts so the caller can prune them.
104
+ */
105
+ export function augment1PartToLinkPart(p: Augment1Part): LinkPart | null {
106
+ if (p.kind === "text") {
107
+ return { kind: "text", text: p.text };
108
+ }
109
+ // file/data: dropped at v0.1.
110
+ return null;
111
+ }
112
+
113
+ // ---------------------------------------------------------------------------
114
+ // HandlerContext → TurnTrigger
115
+ // ---------------------------------------------------------------------------
116
+
117
+ /**
118
+ * Build the metadata object for InboundMessage from the link HandlerContext.
119
+ * Only includes keys that are actually present so downstream consumers can
120
+ * distinguish "absent" from "empty".
121
+ */
122
+ function buildInboundMetadata(ctx: LinkHandlerContext): Record<string, unknown> {
123
+ const md: Record<string, unknown> = { request_id: ctx.request_id };
124
+ if (ctx.idempotency_key !== undefined) {
125
+ md.idempotency_key = ctx.idempotency_key;
126
+ }
127
+ if (ctx.parent_task_id !== undefined) {
128
+ md.parent_task_id = ctx.parent_task_id;
129
+ }
130
+ return md;
131
+ }
132
+
133
+ /**
134
+ * Parse the ISO-8601 `received_at` to epoch ms. Falls back to `Date.now()`
135
+ * when the timestamp is unparseable (defense-in-depth — link's schema
136
+ * enforces ISO-8601 at the wire, but a corrupted store could produce
137
+ * NaN).
138
+ */
139
+ function parseReceivedAtMs(receivedAt: string): number {
140
+ const ms = Date.parse(receivedAt);
141
+ return Number.isFinite(ms) ? ms : Date.now();
142
+ }
143
+
144
+ /**
145
+ * Build an augment-1 TurnTrigger from a verified link HandlerContext.
146
+ *
147
+ * Two pieces of state come from the augment (not the wire):
148
+ * - sourceAugment: the configured instance name in agent.yaml (e.g. "link"
149
+ * or "link-mesh"). Used as both `peer.sourceAugment` and `trigger.source`.
150
+ * - threadId: derived by the caller. For initial sends, the link augment
151
+ * uses `link-<participantId>` so a given peer's traffic threads stably
152
+ * across requests. For task resumptions, the caller passes the existing
153
+ * threadId so history doesn't fragment.
154
+ *
155
+ * `turnId` is freshly minted on every call; augment-1's kernel owns turn
156
+ * ids, not link's request id.
157
+ */
158
+ export function handlerContextToTrigger(
159
+ ctx: LinkHandlerContext,
160
+ sourceAugment: string,
161
+ threadId: string,
162
+ ): TurnTrigger {
163
+ const peer = participantToPeerIdentity(ctx.from, sourceAugment);
164
+ const parts = ctx.parts.map(linkPartToAugment1Part);
165
+ const timestamp = parseReceivedAtMs(ctx.received_at);
166
+ const inbound: InboundMessage = {
167
+ parts,
168
+ sourceAugment,
169
+ peer,
170
+ timestamp,
171
+ metadata: buildInboundMetadata(ctx),
172
+ };
173
+ if (ctx.task_id !== undefined) {
174
+ inbound.taskId = ctx.task_id;
175
+ }
176
+ const trigger: TurnTrigger = {
177
+ type: "message",
178
+ turnId: crypto.randomUUID(),
179
+ threadId,
180
+ timestamp,
181
+ source: sourceAugment,
182
+ peer,
183
+ payload: inbound,
184
+ };
185
+ if (ctx.task_id !== undefined) {
186
+ trigger.taskId = ctx.task_id;
187
+ }
188
+ return trigger;
189
+ }
190
+
191
+ // ---------------------------------------------------------------------------
192
+ // TurnResult → HandlerOutcome
193
+ // ---------------------------------------------------------------------------
194
+
195
+ /**
196
+ * JSON-RPC INTERNAL_ERROR code per @auggy/link's `INTERNAL_ERROR` constant
197
+ * (mirrored here to avoid a runtime import the test harness has to mock).
198
+ * link's protocol/errors module surfaces this same value.
199
+ */
200
+ const INTERNAL_ERROR_CODE = -32603;
201
+
202
+ /**
203
+ * Translate an augment-1 TurnResult to a link HandlerOutcome.
204
+ *
205
+ * v0.1 augment policy:
206
+ * - On rejected/failed → ErrorOutcome with INTERNAL_ERROR. `errorResponse`
207
+ * becomes the message; falls back to `turn <status>` when empty.
208
+ * - On completed with response → MessageOutcome with the text parts
209
+ * (file/data parts dropped because link is text-only at v0.1).
210
+ * - Defensive: completed-without-response should not happen for a turn
211
+ * that admitted via this transport, but if it does, surface as
212
+ * ErrorOutcome rather than silently emitting an empty message
213
+ * (an empty `parts: []` would fail link's MessageOutcome validation).
214
+ */
215
+ export function turnResultToHandlerOutcome(result: TurnResult): LinkHandlerOutcome {
216
+ if (result.status === "rejected" || result.status === "failed") {
217
+ return {
218
+ kind: "error",
219
+ code: INTERNAL_ERROR_CODE,
220
+ message: result.errorResponse ?? `turn ${result.status}`,
221
+ };
222
+ }
223
+
224
+ // Other non-"completed" states (canceled, input-required, auth-required,
225
+ // working) shouldn't appear as a sync TurnResult from the kernel, but if
226
+ // they do, treat them as errors so callers don't silently miss them.
227
+ if (result.status !== "completed") {
228
+ return {
229
+ kind: "error",
230
+ code: INTERNAL_ERROR_CODE,
231
+ message: `unexpected turn status "${result.status}" for link sync flow`,
232
+ };
233
+ }
234
+
235
+ const response = result.response;
236
+ if (!response || response.parts.length === 0) {
237
+ return {
238
+ kind: "error",
239
+ code: INTERNAL_ERROR_CODE,
240
+ message: "turn completed without a response",
241
+ };
242
+ }
243
+
244
+ const linkParts: LinkPart[] = [];
245
+ for (const part of response.parts) {
246
+ const translated = augment1PartToLinkPart(part);
247
+ if (translated) linkParts.push(translated);
248
+ }
249
+ if (linkParts.length === 0) {
250
+ return {
251
+ kind: "error",
252
+ code: INTERNAL_ERROR_CODE,
253
+ message: "turn response had no text parts (link is text-only at v0.1)",
254
+ };
255
+ }
256
+
257
+ return {
258
+ kind: "message",
259
+ parts: linkParts,
260
+ };
261
+ }
@@ -0,0 +1,70 @@
1
+ import { createAgentMailClient } from "../../../agentmail-client";
2
+ import type { AgentMailClient } from "../../../agentmail-client";
3
+ import type {
4
+ NotifyAdapter,
5
+ NotifyDestination,
6
+ NotifyPayload,
7
+ NotifyDeliveryResult,
8
+ AgentMailNotifyDestination,
9
+ } from "../../../types";
10
+
11
+ export interface CreateAgentMailAdapterOptions {
12
+ /** Test-only client override; production constructs from destination's apiKey. */
13
+ clientFactory?: (apiKey: string, baseUrl?: string) => AgentMailClient;
14
+ }
15
+
16
+ export function createAgentMailAdapter(opts: CreateAgentMailAdapterOptions = {}): NotifyAdapter {
17
+ const factory =
18
+ opts.clientFactory ??
19
+ ((apiKey, baseUrl) => createAgentMailClient({ apiKey, apiBaseUrl: baseUrl }));
20
+ const cache = new Map<string, AgentMailClient>();
21
+
22
+ function getClient(apiKey: string, baseUrl?: string): AgentMailClient {
23
+ const cacheKey = `${apiKey}:${baseUrl ?? ""}`;
24
+ let client = cache.get(cacheKey);
25
+ if (!client) {
26
+ client = factory(apiKey, baseUrl);
27
+ cache.set(cacheKey, client);
28
+ }
29
+ return client;
30
+ }
31
+
32
+ function formatBody(payload: NotifyPayload): string {
33
+ const lines = [payload.summary];
34
+ if (payload.reason) lines.push("", `Reason: ${payload.reason}`);
35
+ if (payload.visitor) lines.push(`Visitor: ${payload.visitor}`);
36
+ return lines.join("\n");
37
+ }
38
+
39
+ return {
40
+ async deliver(
41
+ destination: NotifyDestination,
42
+ payload: NotifyPayload,
43
+ ): Promise<NotifyDeliveryResult> {
44
+ if (destination.transport !== "agentmail") {
45
+ return {
46
+ status: "failed",
47
+ detail: `agentMailAdapter received non-agentmail destination: ${destination.transport}`,
48
+ };
49
+ }
50
+ const dest = destination as AgentMailNotifyDestination;
51
+ const client = getClient(dest.apiKey, dest.apiBaseUrl);
52
+ const subject = `${dest.subjectPrefix ?? ""}${payload.summary}`;
53
+ try {
54
+ const result = await client.send({
55
+ inboxId: dest.inboxId,
56
+ to: Array.isArray(dest.to) ? dest.to : [dest.to],
57
+ subject,
58
+ text: formatBody(payload),
59
+ labels: dest.labels,
60
+ });
61
+ if (result.status === "sent") {
62
+ return { status: "sent" };
63
+ }
64
+ return { status: "failed", detail: result.detail };
65
+ } catch (err) {
66
+ return { status: "failed", detail: `agentmail error: ${(err as Error).message}` };
67
+ }
68
+ },
69
+ };
70
+ }
@@ -0,0 +1,60 @@
1
+ import { createTelegramBotClient } from "../../../telegram-client";
2
+ import type { TelegramBotClient } from "../../../telegram-client";
3
+ import type {
4
+ NotifyAdapter,
5
+ NotifyDestination,
6
+ NotifyPayload,
7
+ NotifyDeliveryResult,
8
+ TelegramNotifyDestination,
9
+ } from "../../../types";
10
+
11
+ export interface CreateTelegramAdapterOptions {
12
+ /** Override the client factory for testing. */
13
+ clientFactory?: (botToken: string) => TelegramBotClient;
14
+ }
15
+
16
+ export function createTelegramAdapter(opts: CreateTelegramAdapterOptions = {}): NotifyAdapter {
17
+ const factory =
18
+ opts.clientFactory ?? ((botToken: string) => createTelegramBotClient({ botToken }));
19
+ const cache = new Map<string, TelegramBotClient>();
20
+
21
+ function getClient(botToken: string): TelegramBotClient {
22
+ let client = cache.get(botToken);
23
+ if (!client) {
24
+ client = factory(botToken);
25
+ cache.set(botToken, client);
26
+ }
27
+ return client;
28
+ }
29
+
30
+ function formatText(payload: NotifyPayload): string {
31
+ const lines = [`*${payload.summary}*`];
32
+ if (payload.reason) lines.push(`_Reason:_ ${payload.reason}`);
33
+ if (payload.visitor) lines.push(`_Visitor:_ ${payload.visitor}`);
34
+ return lines.join("\n");
35
+ }
36
+
37
+ return {
38
+ async deliver(
39
+ destination: NotifyDestination,
40
+ payload: NotifyPayload,
41
+ ): Promise<NotifyDeliveryResult> {
42
+ if (destination.transport !== "telegram") {
43
+ return {
44
+ status: "failed",
45
+ detail: `telegramAdapter received non-telegram destination: ${destination.transport}`,
46
+ };
47
+ }
48
+ const dest = destination as TelegramNotifyDestination;
49
+ try {
50
+ const client = getClient(dest.botToken);
51
+ await client.sendMessage(dest.chatId, formatText(payload), {
52
+ parseMode: dest.parseMode ?? "Markdown",
53
+ });
54
+ return { status: "sent" };
55
+ } catch (err) {
56
+ return { status: "failed", detail: `telegram error: ${(err as Error).message}` };
57
+ }
58
+ },
59
+ };
60
+ }
@@ -0,0 +1,55 @@
1
+ import { createHttpClient } from "../../../http";
2
+ import type { HttpClient } from "../../../http";
3
+ import type {
4
+ NotifyAdapter,
5
+ NotifyDestination,
6
+ NotifyPayload,
7
+ NotifyDeliveryResult,
8
+ WebhookNotifyDestination,
9
+ } from "../../../types";
10
+
11
+ export interface CreateWebhookAdapterOptions {
12
+ client?: Pick<HttpClient, "post">;
13
+ }
14
+
15
+ export function createWebhookAdapter(opts: CreateWebhookAdapterOptions = {}): NotifyAdapter {
16
+ const http =
17
+ opts.client ?? createHttpClient({ timeoutMs: 10_000, userAgent: "auggy-notify-webhook/0.1" });
18
+
19
+ return {
20
+ async deliver(
21
+ destination: NotifyDestination,
22
+ payload: NotifyPayload,
23
+ ): Promise<NotifyDeliveryResult> {
24
+ if (destination.transport !== "webhook") {
25
+ return {
26
+ status: "failed",
27
+ detail: `webhookAdapter received non-webhook destination: ${destination.transport}`,
28
+ };
29
+ }
30
+ const dest = destination as WebhookNotifyDestination;
31
+ const body = JSON.stringify({
32
+ summary: payload.summary,
33
+ ...(payload.reason ? { reason: payload.reason } : {}),
34
+ ...(payload.visitor ? { visitor: payload.visitor } : {}),
35
+ channel: "notify",
36
+ });
37
+
38
+ try {
39
+ const res = await http.post(dest.url, {
40
+ headers: { "content-type": "application/json", ...(dest.headers ?? {}) },
41
+ body,
42
+ });
43
+ if (res.status < 200 || res.status >= 300) {
44
+ return {
45
+ status: "failed",
46
+ detail: `webhook ${dest.url} returned ${res.status}: ${res.body.slice(0, 200)}`,
47
+ };
48
+ }
49
+ return { status: "sent" };
50
+ } catch (err) {
51
+ return { status: "failed", detail: `webhook ${dest.url} error: ${(err as Error).message}` };
52
+ }
53
+ },
54
+ };
55
+ }