@chrysb/alphaclaw 0.8.0 → 0.8.1-beta.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 (37) hide show
  1. package/lib/public/js/app.js +100 -83
  2. package/lib/public/js/components/agents-tab/agent-pairing-section.js +47 -12
  3. package/lib/public/js/components/channels.js +14 -17
  4. package/lib/public/js/components/envars.js +42 -6
  5. package/lib/public/js/components/features.js +6 -12
  6. package/lib/public/js/components/general/use-general-tab.js +10 -5
  7. package/lib/public/js/components/google/use-gmail-watch.js +22 -18
  8. package/lib/public/js/components/google/use-google-accounts.js +23 -23
  9. package/lib/public/js/components/models-tab/use-models.js +20 -4
  10. package/lib/public/js/components/nodes-tab/connected-nodes/user-connected-nodes.js +2 -2
  11. package/lib/public/js/components/nodes-tab/use-nodes-tab.js +13 -9
  12. package/lib/public/js/components/routes/webhooks-route.js +1 -1
  13. package/lib/public/js/components/webhooks/create-webhook-modal/index.js +176 -0
  14. package/lib/public/js/components/webhooks/helpers.js +106 -0
  15. package/lib/public/js/components/webhooks/index.js +148 -0
  16. package/lib/public/js/components/webhooks/request-history/index.js +241 -0
  17. package/lib/public/js/components/webhooks/request-history/use-request-history.js +167 -0
  18. package/lib/public/js/components/webhooks/webhook-detail/index.js +374 -0
  19. package/lib/public/js/components/webhooks/webhook-detail/use-webhook-detail.js +261 -0
  20. package/lib/public/js/components/webhooks/webhook-list/index.js +96 -0
  21. package/lib/public/js/components/webhooks/webhook-list/use-webhook-list.js +30 -0
  22. package/lib/public/js/hooks/use-app-shell-controller.js +59 -6
  23. package/lib/public/js/hooks/use-cached-fetch.js +63 -0
  24. package/lib/public/js/hooks/usePolling.js +45 -7
  25. package/lib/public/js/lib/api-cache.js +88 -0
  26. package/lib/public/js/lib/api.js +64 -1
  27. package/lib/server/db/webhooks/index.js +144 -0
  28. package/lib/server/db/webhooks/schema.js +13 -0
  29. package/lib/server/init/register-server-routes.js +21 -0
  30. package/lib/server/oauth-callback-middleware.js +34 -0
  31. package/lib/server/routes/proxy.js +2 -0
  32. package/lib/server/routes/system.js +50 -2
  33. package/lib/server/routes/webhooks.js +126 -18
  34. package/lib/server/webhook-middleware.js +6 -1
  35. package/lib/server.js +12 -0
  36. package/package.json +1 -1
  37. package/lib/public/js/components/webhooks.js +0 -1259
@@ -0,0 +1,241 @@
1
+ import { h } from "https://esm.sh/preact";
2
+ import { useMemo } from "https://esm.sh/preact/hooks";
3
+ import htm from "https://esm.sh/htm";
4
+ import { sendAgentMessage } from "../../../lib/api.js";
5
+ import { ActionButton } from "../../action-button.js";
6
+ import { AgentSendModal } from "../../agent-send-modal.js";
7
+ import { showToast } from "../../toast.js";
8
+ import {
9
+ buildWebhookDebugMessage,
10
+ formatBytes,
11
+ formatLastReceived,
12
+ getRequestStatusTone,
13
+ jsonPretty,
14
+ kStatusFilters,
15
+ } from "../helpers.js";
16
+ import { useRequestHistory } from "./use-request-history.js";
17
+
18
+ const html = htm.bind(h);
19
+
20
+ export const RequestHistory = ({
21
+ selectedHookName = "",
22
+ effectiveAuthMode = "headers",
23
+ webhookUrl = "",
24
+ webhookUrlWithQueryToken = "",
25
+ bearerTokenValue = "",
26
+ selectedWebhook = null,
27
+ }) => {
28
+ const { state, actions } = useRequestHistory({
29
+ selectedHookName,
30
+ effectiveAuthMode,
31
+ webhookUrl,
32
+ webhookUrlWithQueryToken,
33
+ bearerTokenValue,
34
+ });
35
+
36
+ const {
37
+ requests,
38
+ statusFilter,
39
+ expandedRows,
40
+ replayingRequestId,
41
+ debugLoadingRequestId,
42
+ debugRequest,
43
+ } = state;
44
+
45
+ const debugAgentMessage = useMemo(
46
+ () =>
47
+ buildWebhookDebugMessage({
48
+ hookName: selectedHookName,
49
+ webhook: selectedWebhook,
50
+ request: debugRequest,
51
+ }),
52
+ [debugRequest, selectedHookName, selectedWebhook],
53
+ );
54
+
55
+ return html`
56
+ <div class="bg-surface border border-border rounded-xl p-4 space-y-3">
57
+ <div class="flex items-center justify-between gap-3">
58
+ <h3 class="card-label">Request history</h3>
59
+ <div class="flex items-center gap-2">
60
+ ${kStatusFilters.map(
61
+ (filter) => html`
62
+ <button
63
+ class="text-xs px-2 py-1 rounded border ${statusFilter === filter
64
+ ? "border-cyan-400 text-cyan-200 bg-cyan-400/10"
65
+ : "border-border text-gray-400 hover:text-gray-200"}"
66
+ onclick=${() => actions.handleSetStatusFilter(filter)}
67
+ >
68
+ ${filter}
69
+ </button>
70
+ `,
71
+ )}
72
+ </div>
73
+ </div>
74
+
75
+ ${requests.length === 0
76
+ ? html`<p class="text-sm text-gray-500">No requests logged yet.</p>`
77
+ : html`
78
+ <div class="ac-history-list">
79
+ ${requests.map((item) => {
80
+ const statusTone = getRequestStatusTone(item.status);
81
+ return html`
82
+ <details
83
+ class="ac-history-item"
84
+ open=${expandedRows.has(item.id)}
85
+ ontoggle=${(e) =>
86
+ actions.handleRequestRowToggle(item.id, !!e.currentTarget?.open)}
87
+ >
88
+ <summary class="ac-history-summary">
89
+ <div class="ac-history-summary-row">
90
+ <span class="inline-flex items-center gap-2 min-w-0">
91
+ <span class="ac-history-toggle shrink-0" aria-hidden="true"
92
+ >▸</span
93
+ >
94
+ <span class="truncate text-xs text-gray-300">
95
+ ${formatLastReceived(item.createdAt)}
96
+ </span>
97
+ </span>
98
+ <span class="inline-flex items-center gap-2 shrink-0">
99
+ <span class="text-xs text-gray-500"
100
+ >${formatBytes(item.payloadSize)}</span
101
+ >
102
+ <span class=${`text-xs font-medium ${statusTone.textClass}`}
103
+ >${item.gatewayStatus || "n/a"}</span
104
+ >
105
+ <span class="inline-flex items-center">
106
+ <span
107
+ class=${`h-2.5 w-2.5 rounded-full ${statusTone.dotClass}`}
108
+ title=${item.status || "unknown"}
109
+ aria-label=${item.status || "unknown"}
110
+ ></span>
111
+ </span>
112
+ </span>
113
+ </div>
114
+ </summary>
115
+ ${expandedRows.has(item.id)
116
+ ? html`
117
+ <div class="ac-history-body space-y-3">
118
+ <div>
119
+ <p class="text-[11px] text-gray-500 mb-1">Headers</p>
120
+ <pre
121
+ class="text-xs bg-black/30 border border-border rounded p-2 overflow-auto"
122
+ >
123
+ ${jsonPretty(item.headers)}</pre
124
+ >
125
+ <div class="mt-2 flex justify-start gap-2">
126
+ <button
127
+ class="h-7 text-xs px-2.5 rounded-lg ac-btn-secondary"
128
+ onclick=${() =>
129
+ actions.handleCopyRequestField(
130
+ jsonPretty(item.headers),
131
+ "Headers",
132
+ )}
133
+ >
134
+ Copy
135
+ </button>
136
+ </div>
137
+ </div>
138
+ <div>
139
+ <p class="text-[11px] text-gray-500 mb-1">
140
+ Payload ${item.payloadTruncated ? "(truncated)" : ""}
141
+ </p>
142
+ <pre
143
+ class="text-xs bg-black/30 border border-border rounded p-2 overflow-auto"
144
+ >
145
+ ${jsonPretty(item.payload)}</pre
146
+ >
147
+ <div class="mt-2 flex justify-start gap-2">
148
+ <button
149
+ class="h-7 text-xs px-2.5 rounded-lg ac-btn-secondary"
150
+ onclick=${() =>
151
+ actions.handleCopyRequestField(
152
+ item.payload,
153
+ "Payload",
154
+ )}
155
+ >
156
+ Copy
157
+ </button>
158
+ <button
159
+ class="h-7 text-xs px-2.5 rounded-lg ac-btn-secondary disabled:opacity-60"
160
+ onclick=${() => actions.handleReplayRequest(item)}
161
+ disabled=${item.payloadTruncated ||
162
+ replayingRequestId === item.id}
163
+ title=${item.payloadTruncated
164
+ ? "Cannot replay truncated payload"
165
+ : "Replay this payload"}
166
+ >
167
+ ${replayingRequestId === item.id
168
+ ? "Replaying..."
169
+ : "Replay"}
170
+ </button>
171
+ </div>
172
+ </div>
173
+ <div>
174
+ <p class="text-[11px] text-gray-500 mb-1">
175
+ Gateway response (${item.gatewayStatus || "n/a"})
176
+ </p>
177
+ <pre
178
+ class="text-xs bg-black/30 border border-border rounded p-2 overflow-auto"
179
+ >
180
+ ${jsonPretty(item.gatewayBody)}</pre
181
+ >
182
+ <div class="mt-2 flex justify-start gap-2">
183
+ <button
184
+ class="h-7 text-xs px-2.5 rounded-lg ac-btn-secondary"
185
+ onclick=${() =>
186
+ actions.handleCopyRequestField(
187
+ item.gatewayBody,
188
+ "Gateway response",
189
+ )}
190
+ >
191
+ Copy
192
+ </button>
193
+ ${item.status === "error"
194
+ ? html`<${ActionButton}
195
+ onClick=${() =>
196
+ actions.handleAskAgentToDebug(item)}
197
+ loading=${debugLoadingRequestId === item.id}
198
+ tone="primary"
199
+ size="sm"
200
+ idleLabel="Ask agent to debug"
201
+ loadingLabel="Loading..."
202
+ className="h-7 px-2.5"
203
+ />`
204
+ : null}
205
+ </div>
206
+ </div>
207
+ </div>
208
+ `
209
+ : null}
210
+ </details>
211
+ `;
212
+ })}
213
+ </div>
214
+ `}
215
+ <${AgentSendModal}
216
+ visible=${!!debugRequest}
217
+ title="Ask agent to debug"
218
+ messageLabel="Debug request"
219
+ messageRows=${12}
220
+ initialMessage=${debugAgentMessage}
221
+ resetKey=${String(debugRequest?.id || "")}
222
+ submitLabel="Send debug request"
223
+ loadingLabel="Sending..."
224
+ onClose=${() => actions.setDebugRequest(null)}
225
+ onSubmit=${async ({ selectedSessionKey, message }) => {
226
+ try {
227
+ await sendAgentMessage({
228
+ message,
229
+ sessionKey: selectedSessionKey,
230
+ });
231
+ showToast("Debug request sent to agent", "success");
232
+ return true;
233
+ } catch (err) {
234
+ showToast(err.message || "Could not send debug request", "error");
235
+ return false;
236
+ }
237
+ }}
238
+ />
239
+ </div>
240
+ `;
241
+ };
@@ -0,0 +1,167 @@
1
+ import { useCallback, useMemo, useState } from "https://esm.sh/preact/hooks";
2
+ import {
3
+ fetchWebhookRequest,
4
+ fetchWebhookRequests,
5
+ } from "../../../lib/api.js";
6
+ import { usePolling } from "../../../hooks/usePolling.js";
7
+ import { showToast } from "../../toast.js";
8
+
9
+ export const useRequestHistory = ({
10
+ selectedHookName = "",
11
+ effectiveAuthMode = "headers",
12
+ webhookUrl = "",
13
+ webhookUrlWithQueryToken = "",
14
+ bearerTokenValue = "",
15
+ }) => {
16
+ const [statusFilter, setStatusFilter] = useState("all");
17
+ const [expandedRows, setExpandedRows] = useState(() => new Set());
18
+ const [replayingRequestId, setReplayingRequestId] = useState(null);
19
+ const [debugLoadingRequestId, setDebugLoadingRequestId] = useState(null);
20
+ const [debugRequest, setDebugRequest] = useState(null);
21
+
22
+ const requestsPoll = usePolling(
23
+ async () => {
24
+ if (!selectedHookName) return { requests: [] };
25
+ const data = await fetchWebhookRequests(selectedHookName, {
26
+ limit: 25,
27
+ offset: 0,
28
+ status: statusFilter,
29
+ });
30
+ return data;
31
+ },
32
+ 5000,
33
+ { enabled: !!selectedHookName },
34
+ );
35
+
36
+ const requests = requestsPoll.data?.requests || [];
37
+
38
+ const handleRequestRowToggle = useCallback((id, isOpen) => {
39
+ setExpandedRows((prev) => {
40
+ const next = new Set(prev);
41
+ if (isOpen) next.add(id);
42
+ else next.delete(id);
43
+ return next;
44
+ });
45
+ }, []);
46
+
47
+ const handleSetStatusFilter = useCallback(
48
+ (filter) => {
49
+ setStatusFilter(filter);
50
+ setExpandedRows(new Set());
51
+ setTimeout(() => requestsPoll.refresh(), 0);
52
+ },
53
+ [requestsPoll.refresh],
54
+ );
55
+
56
+ const resetState = useCallback(() => {
57
+ setStatusFilter("all");
58
+ setExpandedRows(new Set());
59
+ setDebugRequest(null);
60
+ setDebugLoadingRequestId(null);
61
+ setReplayingRequestId(null);
62
+ }, []);
63
+
64
+ const handleCopyRequestField = useCallback(async (value, label) => {
65
+ try {
66
+ await navigator.clipboard.writeText(String(value || ""));
67
+ showToast(`${label} copied`, "success");
68
+ } catch {
69
+ showToast(
70
+ `Could not copy ${String(label || "value").toLowerCase()}`,
71
+ "error",
72
+ );
73
+ }
74
+ }, []);
75
+
76
+ const requestUrl = useMemo(() => {
77
+ return effectiveAuthMode === "query" ? webhookUrlWithQueryToken : webhookUrl;
78
+ }, [effectiveAuthMode, webhookUrl, webhookUrlWithQueryToken]);
79
+
80
+ const requestHeaders = useMemo(() => {
81
+ const headers = { "Content-Type": "application/json" };
82
+ if (effectiveAuthMode === "headers") {
83
+ headers.Authorization = bearerTokenValue;
84
+ }
85
+ return headers;
86
+ }, [bearerTokenValue, effectiveAuthMode]);
87
+
88
+ const handleReplayRequest = useCallback(
89
+ async (item) => {
90
+ if (!item || replayingRequestId === item.id) return;
91
+ if (item.payloadTruncated) {
92
+ showToast("Cannot replay a truncated payload", "warning");
93
+ return;
94
+ }
95
+ setReplayingRequestId(item.id);
96
+ try {
97
+ const response = await fetch(requestUrl, {
98
+ method: "POST",
99
+ headers: requestHeaders,
100
+ body: String(item.payload || ""),
101
+ });
102
+ const bodyText = await response.text();
103
+ let body = null;
104
+ try {
105
+ body = bodyText ? JSON.parse(bodyText) : null;
106
+ } catch {
107
+ body = null;
108
+ }
109
+ const errorMessage =
110
+ body?.ok === false
111
+ ? body?.error || "Webhook rejected"
112
+ : !response.ok
113
+ ? body?.error || bodyText || `HTTP ${response.status}`
114
+ : "";
115
+ if (errorMessage) {
116
+ showToast(`Replay failed: ${errorMessage}`, "error");
117
+ return;
118
+ }
119
+ showToast("Request replayed", "success");
120
+ setTimeout(() => requestsPoll.refresh(), 0);
121
+ } catch (err) {
122
+ showToast(err.message || "Could not replay request", "error");
123
+ } finally {
124
+ setReplayingRequestId(null);
125
+ }
126
+ },
127
+ [replayingRequestId, requestHeaders, requestUrl, requestsPoll.refresh],
128
+ );
129
+
130
+ const handleAskAgentToDebug = useCallback(
131
+ async (item) => {
132
+ if (!selectedHookName || !item?.id || debugLoadingRequestId === item.id)
133
+ return;
134
+ try {
135
+ setDebugLoadingRequestId(item.id);
136
+ const data = await fetchWebhookRequest(selectedHookName, item.id);
137
+ setDebugRequest(data?.request || item);
138
+ } catch (err) {
139
+ showToast(err.message || "Could not load webhook request details", "error");
140
+ } finally {
141
+ setDebugLoadingRequestId(null);
142
+ }
143
+ },
144
+ [debugLoadingRequestId, selectedHookName],
145
+ );
146
+
147
+ return {
148
+ state: {
149
+ requests,
150
+ statusFilter,
151
+ expandedRows,
152
+ replayingRequestId,
153
+ debugLoadingRequestId,
154
+ debugRequest,
155
+ },
156
+ actions: {
157
+ refreshRequests: requestsPoll.refresh,
158
+ handleRequestRowToggle,
159
+ handleSetStatusFilter,
160
+ handleReplayRequest,
161
+ handleCopyRequestField,
162
+ handleAskAgentToDebug,
163
+ setDebugRequest,
164
+ resetState,
165
+ },
166
+ };
167
+ };