@chrysb/alphaclaw 0.5.3 → 0.5.4-beta.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.
- package/lib/public/js/components/agent-send-modal.js +11 -25
- package/lib/public/js/components/doctor/index.js +6 -5
- package/lib/public/js/components/file-tree.js +26 -1
- package/lib/public/js/components/file-viewer/constants.js +2 -0
- package/lib/public/js/components/file-viewer/index.js +1 -0
- package/lib/public/js/components/file-viewer/markdown-split-view.js +2 -1
- package/lib/public/js/components/file-viewer/use-file-viewer.js +24 -6
- package/lib/public/js/components/file-viewer/utils.js +19 -0
- package/lib/public/js/components/google/gmail-setup-wizard.js +117 -50
- package/lib/public/js/components/google/index.js +45 -44
- package/lib/public/js/components/google/use-gmail-watch.js +2 -2
- package/lib/public/js/components/icons.js +13 -0
- package/lib/public/js/components/models-tab/index.js +5 -3
- package/lib/public/js/components/models-tab/provider-auth-card.js +1 -1
- package/lib/public/js/components/onboarding/welcome-form-step.js +9 -1
- package/lib/public/js/components/session-select-field.js +72 -0
- package/lib/public/js/components/webhooks.js +114 -44
- package/lib/public/js/components/welcome/use-welcome.js +41 -20
- package/lib/public/js/hooks/use-destination-session-selection.js +85 -0
- package/lib/public/js/hooks/useAgentSessions.js +14 -0
- package/lib/public/js/lib/api.js +10 -4
- package/lib/public/js/lib/clipboard.js +40 -0
- package/lib/public/js/lib/model-config.js +2 -2
- package/lib/server/auth-profiles.js +3 -0
- package/lib/server/constants.js +2 -2
- package/lib/server/db/usage/pricing.js +1 -1
- package/lib/server/doctor/prompt.js +32 -10
- package/lib/server/gmail-watch.js +49 -22
- package/lib/server/onboarding/github.js +1 -1
- package/lib/server/routes/gmail.js +5 -1
- package/lib/server/routes/models.js +84 -1
- package/lib/server/routes/system.js +28 -26
- package/lib/server/routes/webhooks.js +2 -2
- package/lib/server/webhooks.js +32 -4
- package/package.json +1 -1
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from "https://esm.sh/preact/hooks";
|
|
2
|
+
import { useAgentSessions } from "./useAgentSessions.js";
|
|
3
|
+
|
|
4
|
+
export const kNoDestinationSessionValue = "__none__";
|
|
5
|
+
|
|
6
|
+
export const kDestinationSessionFilter = (sessionRow) => {
|
|
7
|
+
const key = String(sessionRow?.key || "").toLowerCase();
|
|
8
|
+
return key.includes(":direct:") || key.includes(":group:");
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const getDestinationFromSession = (sessionRow = null) => {
|
|
12
|
+
const channel = String(sessionRow?.replyChannel || "").trim();
|
|
13
|
+
const to = String(sessionRow?.replyTo || "").trim();
|
|
14
|
+
if (!channel || !to) return null;
|
|
15
|
+
return { channel, to };
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const useDestinationSessionSelection = ({
|
|
19
|
+
enabled = false,
|
|
20
|
+
resetKey = "",
|
|
21
|
+
} = {}) => {
|
|
22
|
+
const [manualSessionKey, setManualSessionKey] = useState("");
|
|
23
|
+
const [hasManualSelection, setHasManualSelection] = useState(false);
|
|
24
|
+
const {
|
|
25
|
+
sessions,
|
|
26
|
+
selectedSessionKey,
|
|
27
|
+
setSelectedSessionKey,
|
|
28
|
+
loading,
|
|
29
|
+
error,
|
|
30
|
+
} = useAgentSessions({
|
|
31
|
+
enabled,
|
|
32
|
+
filter: kDestinationSessionFilter,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (!enabled) return;
|
|
37
|
+
setManualSessionKey("");
|
|
38
|
+
setHasManualSelection(false);
|
|
39
|
+
}, [enabled, resetKey]);
|
|
40
|
+
|
|
41
|
+
const preferredSessionKey = useMemo(() => {
|
|
42
|
+
const matchingPreferredSession = sessions.find(
|
|
43
|
+
(sessionRow) =>
|
|
44
|
+
String(sessionRow?.key || "") === String(selectedSessionKey || "").trim(),
|
|
45
|
+
);
|
|
46
|
+
return String(
|
|
47
|
+
matchingPreferredSession?.key || sessions[0]?.key || "",
|
|
48
|
+
).trim();
|
|
49
|
+
}, [sessions, selectedSessionKey]);
|
|
50
|
+
|
|
51
|
+
const effectiveSessionKey = hasManualSelection
|
|
52
|
+
? manualSessionKey
|
|
53
|
+
: preferredSessionKey;
|
|
54
|
+
|
|
55
|
+
const selectedSession = useMemo(
|
|
56
|
+
() =>
|
|
57
|
+
sessions.find(
|
|
58
|
+
(sessionRow) =>
|
|
59
|
+
String(sessionRow?.key || "") === String(effectiveSessionKey || "").trim(),
|
|
60
|
+
) || null,
|
|
61
|
+
[effectiveSessionKey, sessions],
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const selectedDestination = useMemo(
|
|
65
|
+
() => getDestinationFromSession(selectedSession),
|
|
66
|
+
[selectedSession],
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const setDestinationSessionKey = useCallback((key) => {
|
|
70
|
+
const normalizedKey = String(key || "");
|
|
71
|
+
setManualSessionKey(normalizedKey);
|
|
72
|
+
setHasManualSelection(true);
|
|
73
|
+
setSelectedSessionKey(normalizedKey);
|
|
74
|
+
}, [setSelectedSessionKey]);
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
sessions,
|
|
78
|
+
loading,
|
|
79
|
+
error,
|
|
80
|
+
destinationSessionKey: effectiveSessionKey,
|
|
81
|
+
setDestinationSessionKey,
|
|
82
|
+
selectedDestinationSession: selectedSession,
|
|
83
|
+
selectedDestination,
|
|
84
|
+
};
|
|
85
|
+
};
|
|
@@ -119,6 +119,20 @@ export const useAgentSessions = ({ enabled = false, filter } = {}) => {
|
|
|
119
119
|
[allSessions, filter],
|
|
120
120
|
);
|
|
121
121
|
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
if (!enabled) return;
|
|
124
|
+
if (sessions.length === 0) {
|
|
125
|
+
if (selectedSessionKey) setSelectedSessionKeyState("");
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const hasSelectedSession = sessions.some(
|
|
129
|
+
(row) => String(row?.key || "") === String(selectedSessionKey || ""),
|
|
130
|
+
);
|
|
131
|
+
if (hasSelectedSession) return;
|
|
132
|
+
const preferred = pickPreferredSession(sessions, readLastSessionKey());
|
|
133
|
+
setSelectedSessionKeyState(String(preferred?.key || ""));
|
|
134
|
+
}, [enabled, sessions, selectedSessionKey]);
|
|
135
|
+
|
|
122
136
|
const selectedSession = useMemo(
|
|
123
137
|
() => sessions.find((row) => String(row?.key || "") === selectedSessionKey) || null,
|
|
124
138
|
[sessions, selectedSessionKey],
|
package/lib/public/js/lib/api.js
CHANGED
|
@@ -163,11 +163,14 @@ export const saveGmailConfig = async ({
|
|
|
163
163
|
return parseJsonOrThrow(res, "Could not save Gmail watch config");
|
|
164
164
|
};
|
|
165
165
|
|
|
166
|
-
export const startGmailWatch = async (accountId) => {
|
|
166
|
+
export const startGmailWatch = async (accountId, { destination = null } = {}) => {
|
|
167
167
|
const res = await authFetch("/api/gmail/watch/start", {
|
|
168
168
|
method: "POST",
|
|
169
169
|
headers: { "Content-Type": "application/json" },
|
|
170
|
-
body: JSON.stringify({
|
|
170
|
+
body: JSON.stringify({
|
|
171
|
+
accountId: String(accountId || ""),
|
|
172
|
+
...(destination ? { destination } : {}),
|
|
173
|
+
}),
|
|
171
174
|
});
|
|
172
175
|
return parseJsonOrThrow(res, "Could not start Gmail watch");
|
|
173
176
|
};
|
|
@@ -655,11 +658,14 @@ export async function fetchWebhookDetail(name) {
|
|
|
655
658
|
return parseJsonOrThrow(res, "Could not load webhook detail");
|
|
656
659
|
}
|
|
657
660
|
|
|
658
|
-
export async function createWebhook(name) {
|
|
661
|
+
export async function createWebhook(name, { destination = null } = {}) {
|
|
659
662
|
const res = await authFetch("/api/webhooks", {
|
|
660
663
|
method: "POST",
|
|
661
664
|
headers: { "Content-Type": "application/json" },
|
|
662
|
-
body: JSON.stringify({
|
|
665
|
+
body: JSON.stringify({
|
|
666
|
+
name,
|
|
667
|
+
...(destination ? { destination } : {}),
|
|
668
|
+
}),
|
|
663
669
|
});
|
|
664
670
|
return parseJsonOrThrow(res, "Could not create webhook");
|
|
665
671
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export const copyTextToClipboard = async (value) => {
|
|
2
|
+
const text = String(value || "");
|
|
3
|
+
if (!text) return false;
|
|
4
|
+
|
|
5
|
+
try {
|
|
6
|
+
if (navigator?.clipboard?.writeText) {
|
|
7
|
+
await navigator.clipboard.writeText(text);
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
} catch {}
|
|
11
|
+
|
|
12
|
+
let fallbackElement = null;
|
|
13
|
+
let appendedFallbackElement = false;
|
|
14
|
+
try {
|
|
15
|
+
if (
|
|
16
|
+
!document?.createElement ||
|
|
17
|
+
!document?.body?.appendChild ||
|
|
18
|
+
!document?.body?.removeChild ||
|
|
19
|
+
typeof document.execCommand !== "function"
|
|
20
|
+
) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
fallbackElement = document.createElement("textarea");
|
|
25
|
+
fallbackElement.value = text;
|
|
26
|
+
fallbackElement.setAttribute("readonly", "");
|
|
27
|
+
fallbackElement.style.position = "fixed";
|
|
28
|
+
fallbackElement.style.opacity = "0";
|
|
29
|
+
document.body.appendChild(fallbackElement);
|
|
30
|
+
appendedFallbackElement = true;
|
|
31
|
+
fallbackElement.select();
|
|
32
|
+
return document.execCommand("copy");
|
|
33
|
+
} catch {
|
|
34
|
+
return false;
|
|
35
|
+
} finally {
|
|
36
|
+
if (fallbackElement && appendedFallbackElement) {
|
|
37
|
+
document.body.removeChild(fallbackElement);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
};
|
|
@@ -22,8 +22,8 @@ export const kFeaturedModelDefs = [
|
|
|
22
22
|
preferredKeys: ["openai-codex/gpt-5.3-codex", "openai-codex/gpt-5.2-codex"],
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
|
-
label: "Gemini 3",
|
|
26
|
-
preferredKeys: ["google/gemini-3-pro-preview", "google/gemini-3-flash-preview"],
|
|
25
|
+
label: "Gemini 3.1 Pro",
|
|
26
|
+
preferredKeys: ["google/gemini-3.1-pro-preview", "google/gemini-3-flash-preview"],
|
|
27
27
|
},
|
|
28
28
|
];
|
|
29
29
|
|
|
@@ -41,6 +41,8 @@ const credentialMode = (credential) => {
|
|
|
41
41
|
const getEnvVarForApiKeyProvider = (provider) =>
|
|
42
42
|
kApiKeyEnvVarByProvider[String(provider || "").trim()] || "";
|
|
43
43
|
|
|
44
|
+
const listApiKeyProviders = () => Object.keys(kApiKeyEnvVarByProvider);
|
|
45
|
+
|
|
44
46
|
const getDefaultProfileIdForApiKeyProvider = (provider) => {
|
|
45
47
|
const normalized = String(provider || "").trim();
|
|
46
48
|
return normalized ? `${normalized}:default` : "";
|
|
@@ -339,6 +341,7 @@ const createAuthProfiles = () => {
|
|
|
339
341
|
upsertApiKeyProfileForEnvVar,
|
|
340
342
|
removeApiKeyProfileForEnvVar,
|
|
341
343
|
getEnvVarForApiKeyProvider,
|
|
344
|
+
listApiKeyProviders,
|
|
342
345
|
getDefaultProfileIdForApiKeyProvider,
|
|
343
346
|
getModelConfig,
|
|
344
347
|
setModelConfig,
|
package/lib/server/constants.js
CHANGED
|
@@ -118,9 +118,9 @@ const kFallbackOnboardingModels = [
|
|
|
118
118
|
label: "OpenAI GPT-5.1 Codex",
|
|
119
119
|
},
|
|
120
120
|
{
|
|
121
|
-
key: "google/gemini-3-pro-preview",
|
|
121
|
+
key: "google/gemini-3.1-pro-preview",
|
|
122
122
|
provider: "google",
|
|
123
|
-
label: "Gemini 3 Pro
|
|
123
|
+
label: "Gemini 3.1 Pro",
|
|
124
124
|
},
|
|
125
125
|
{
|
|
126
126
|
key: "google/gemini-3-flash-preview",
|
|
@@ -13,7 +13,7 @@ const kGlobalModelPricing = {
|
|
|
13
13
|
"gpt-4.1": { input: 2.0, output: 8.0 },
|
|
14
14
|
"gpt-4o": { input: 2.5, output: 10.0 },
|
|
15
15
|
"gpt-4o-mini": { input: 0.15, output: 0.6 },
|
|
16
|
-
"gemini-3-pro-preview": { input: 2.0, output: 12.0 },
|
|
16
|
+
"gemini-3.1-pro-preview": { input: 2.0, output: 12.0 },
|
|
17
17
|
"gemini-3-flash-preview": { input: 0.5, output: 3.0 },
|
|
18
18
|
"gemini-2.0-flash": { input: 0.1, output: 0.4 },
|
|
19
19
|
};
|
|
@@ -12,17 +12,37 @@ const renderList = (items = []) =>
|
|
|
12
12
|
const renderContextFileList = (files = []) =>
|
|
13
13
|
files.map((file) => `\`${file.path}\``).join(", ");
|
|
14
14
|
|
|
15
|
-
const
|
|
15
|
+
const renderHistoricalCards = (cards = []) => {
|
|
16
16
|
if (!cards.length) return "";
|
|
17
|
-
const
|
|
18
|
-
(card) =>
|
|
19
|
-
|
|
20
|
-
(card
|
|
21
|
-
|
|
17
|
+
const dismissedLines = cards
|
|
18
|
+
.filter((card) => card?.status === "dismissed")
|
|
19
|
+
.map(
|
|
20
|
+
(card) =>
|
|
21
|
+
`- [${card.status}] ${card.title}` +
|
|
22
|
+
(card.category ? ` (${card.category})` : ""),
|
|
23
|
+
);
|
|
24
|
+
const fixedLines = cards
|
|
25
|
+
.filter((card) => card?.status === "fixed")
|
|
26
|
+
.map(
|
|
27
|
+
(card) =>
|
|
28
|
+
`- [${card.status}] ${card.title}` +
|
|
29
|
+
(card.category ? ` (${card.category})` : ""),
|
|
30
|
+
);
|
|
31
|
+
const sections = [];
|
|
32
|
+
if (dismissedLines.length) {
|
|
33
|
+
sections.push(
|
|
34
|
+
`Previously dismissed findings (do not re-suggest these):\n${dismissedLines.join("\n")}`,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
if (fixedLines.length) {
|
|
38
|
+
sections.push(
|
|
39
|
+
`Previou findings marked as fixed (context only; re-suggest them if they are still present):\n${fixedLines.join("\n")}`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
if (!sections.length) return "";
|
|
22
43
|
return `
|
|
23
44
|
|
|
24
|
-
|
|
25
|
-
${lines.join("\n")}
|
|
45
|
+
${sections.join("\n\n")}
|
|
26
46
|
`;
|
|
27
47
|
};
|
|
28
48
|
|
|
@@ -115,13 +135,15 @@ Return exactly this JSON shape:
|
|
|
115
135
|
]
|
|
116
136
|
}
|
|
117
137
|
|
|
118
|
-
${
|
|
138
|
+
${renderHistoricalCards(resolvedCards)}Constraints:
|
|
119
139
|
- Maximum 12 cards
|
|
120
140
|
- Use relative paths in evidence and targetPaths
|
|
121
141
|
- Include startLine (and optionally endLine) in evidence and targetPaths when the finding relates to a specific section of a file
|
|
122
142
|
- targetPaths items can be strings or objects with { path, startLine? }
|
|
123
143
|
- Do not include duplicate cards
|
|
124
|
-
- Do not re-suggest findings that appear in the "Previously
|
|
144
|
+
- Do not re-suggest findings that appear in the "Previously dismissed" list above
|
|
145
|
+
- Previously fixed findings may be re-suggested if the underlying issue is still present
|
|
146
|
+
- If a previously fixed finding is still present, you may call that out in the summary or card wording
|
|
125
147
|
- Do not create cards for healthy default-template behavior
|
|
126
148
|
- Do not create cards whose primary recommendation is to refactor AlphaClaw-managed file structure
|
|
127
149
|
- fixPrompt must only reference files the agent can edit. Never suggest editing files listed in "AlphaClaw locked/managed paths" above — they are managed by AlphaClaw, so manual edits would be lost.
|
|
@@ -61,6 +61,47 @@ const parseProjectIdFromTopicPath = (topicPath = "") => {
|
|
|
61
61
|
return match?.[1] ? String(match[1]) : "";
|
|
62
62
|
};
|
|
63
63
|
|
|
64
|
+
const normalizeDestination = (destination = null) => {
|
|
65
|
+
if (!destination || typeof destination !== "object") return null;
|
|
66
|
+
const channel = String(destination?.channel || "").trim();
|
|
67
|
+
const to = String(destination?.to || "").trim();
|
|
68
|
+
if (!channel && !to) return null;
|
|
69
|
+
if (!channel || !to) {
|
|
70
|
+
throw new Error("destination.channel and destination.to are required");
|
|
71
|
+
}
|
|
72
|
+
return { channel, to };
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const buildGmailTransformSource = (destination = null) => {
|
|
76
|
+
const normalizedDestination = normalizeDestination(destination);
|
|
77
|
+
return [
|
|
78
|
+
"export default async function transform(payload) {",
|
|
79
|
+
" const data = payload?.payload || payload || {};",
|
|
80
|
+
" const messages = Array.isArray(data.messages) ? data.messages : [];",
|
|
81
|
+
" const first = messages[0] || {};",
|
|
82
|
+
" const from = String(first.from || \"unknown sender\").trim();",
|
|
83
|
+
" const subject = String(first.subject || \"(no subject)\").trim();",
|
|
84
|
+
" const snippet = String(first.snippet || \"\").trim();",
|
|
85
|
+
" return {",
|
|
86
|
+
" message: `New email from ${from}\\nSubject: ${subject}\\n${snippet}`.trim(),",
|
|
87
|
+
" messages,",
|
|
88
|
+
' name: "Gmail",',
|
|
89
|
+
' wakeMode: "now",',
|
|
90
|
+
...(normalizedDestination
|
|
91
|
+
? [
|
|
92
|
+
` channel: ${JSON.stringify(normalizedDestination.channel)},`,
|
|
93
|
+
` to: ${JSON.stringify(normalizedDestination.to)},`,
|
|
94
|
+
]
|
|
95
|
+
: []),
|
|
96
|
+
" };",
|
|
97
|
+
"}",
|
|
98
|
+
"",
|
|
99
|
+
].join("\n");
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const getGmailTransformAbsolutePath = (constants) =>
|
|
103
|
+
path.join(constants.OPENCLAW_DIR, "hooks/transforms/gmail/gmail-transform.mjs");
|
|
104
|
+
|
|
64
105
|
const ensureTopicPathForClient = ({
|
|
65
106
|
state,
|
|
66
107
|
client,
|
|
@@ -209,7 +250,7 @@ const createGmailWatchService = ({
|
|
|
209
250
|
return { token, changed: true };
|
|
210
251
|
};
|
|
211
252
|
|
|
212
|
-
const ensureHooksPreset = () => {
|
|
253
|
+
const ensureHooksPreset = ({ destination = null } = {}) => {
|
|
213
254
|
const configPath = path.join(constants.OPENCLAW_DIR, "openclaw.json");
|
|
214
255
|
if (!fs.existsSync(configPath)) {
|
|
215
256
|
throw new Error("openclaw.json not found. Complete onboarding first.");
|
|
@@ -253,23 +294,7 @@ const createGmailWatchService = ({
|
|
|
253
294
|
wakeMode: "now",
|
|
254
295
|
transform: { module: gmailTransformModulePath },
|
|
255
296
|
},
|
|
256
|
-
transformSource:
|
|
257
|
-
"export default async function transform(payload) {",
|
|
258
|
-
" const data = payload?.payload || payload || {};",
|
|
259
|
-
" const messages = Array.isArray(data.messages) ? data.messages : [];",
|
|
260
|
-
" const first = messages[0] || {};",
|
|
261
|
-
" const from = String(first.from || \"unknown sender\").trim();",
|
|
262
|
-
" const subject = String(first.subject || \"(no subject)\").trim();",
|
|
263
|
-
" const snippet = String(first.snippet || \"\").trim();",
|
|
264
|
-
" return {",
|
|
265
|
-
" message: `New email from ${from}\\nSubject: ${subject}\\n${snippet}`.trim(),",
|
|
266
|
-
" messages,",
|
|
267
|
-
' name: "Gmail",',
|
|
268
|
-
' wakeMode: "now",',
|
|
269
|
-
" };",
|
|
270
|
-
"}",
|
|
271
|
-
"",
|
|
272
|
-
].join("\n"),
|
|
297
|
+
transformSource: buildGmailTransformSource(destination),
|
|
273
298
|
});
|
|
274
299
|
const webhookAfter = fs.readFileSync(configPath, "utf8");
|
|
275
300
|
if (webhookBefore !== webhookAfter) {
|
|
@@ -278,9 +303,9 @@ const createGmailWatchService = ({
|
|
|
278
303
|
return { changed };
|
|
279
304
|
};
|
|
280
305
|
|
|
281
|
-
const ensureHookWiring = () => {
|
|
306
|
+
const ensureHookWiring = ({ destination = null } = {}) => {
|
|
282
307
|
const webhook = ensureWebhookToken();
|
|
283
|
-
const hooks = ensureHooksPreset();
|
|
308
|
+
const hooks = ensureHooksPreset({ destination });
|
|
284
309
|
const changed = webhook.changed || hooks.changed;
|
|
285
310
|
if (changed) markRestartRequired("gmail-watch");
|
|
286
311
|
return { webhookToken: webhook.token, changed };
|
|
@@ -341,6 +366,7 @@ const createGmailWatchService = ({
|
|
|
341
366
|
const pushEndpoint = `${baseUrl}/gmail-pubsub?token=${encodeURIComponent(
|
|
342
367
|
String(push.token || ""),
|
|
343
368
|
)}`;
|
|
369
|
+
const transformExists = fs.existsSync(getGmailTransformAbsolutePath(constants));
|
|
344
370
|
const commands =
|
|
345
371
|
projectId && push.token
|
|
346
372
|
? {
|
|
@@ -358,6 +384,7 @@ const createGmailWatchService = ({
|
|
|
358
384
|
subscriptionName,
|
|
359
385
|
pushEndpoint,
|
|
360
386
|
commands,
|
|
387
|
+
transformExists,
|
|
361
388
|
configured: Boolean(topicPath && push.token && projectId),
|
|
362
389
|
};
|
|
363
390
|
};
|
|
@@ -448,7 +475,7 @@ const createGmailWatchService = ({
|
|
|
448
475
|
};
|
|
449
476
|
};
|
|
450
477
|
|
|
451
|
-
const startWatch = async ({ accountId, req }) => {
|
|
478
|
+
const startWatch = async ({ accountId, req, destination = null }) => {
|
|
452
479
|
let state = readState();
|
|
453
480
|
const account = getGoogleAccountById(state, accountId);
|
|
454
481
|
if (!account) throw new Error("Google account not found");
|
|
@@ -466,7 +493,7 @@ const createGmailWatchService = ({
|
|
|
466
493
|
state = ensuredTopic.state;
|
|
467
494
|
const topicPath = ensuredTopic.topicPath;
|
|
468
495
|
|
|
469
|
-
const { webhookToken } = ensureHookWiring();
|
|
496
|
+
const { webhookToken } = ensureHookWiring({ destination });
|
|
470
497
|
const watchStart = await runGogForAccount({
|
|
471
498
|
account,
|
|
472
499
|
command:
|
|
@@ -109,7 +109,7 @@ const verifyGithubRepoForOnboarding = async ({
|
|
|
109
109
|
return {
|
|
110
110
|
ok: false,
|
|
111
111
|
status: 400,
|
|
112
|
-
error: `Repository "${repoUrl}" already exists and is not empty.
|
|
112
|
+
error: `Repository "${repoUrl}" already exists and is not empty. To import, use "Import existing setup" instead.`,
|
|
113
113
|
};
|
|
114
114
|
}
|
|
115
115
|
const commitCheckDetails = await parseGithubErrorMessage(commitsRes);
|
|
@@ -60,7 +60,11 @@ const registerGmailRoutes = ({
|
|
|
60
60
|
try {
|
|
61
61
|
const accountId = String(req.body?.accountId || "").trim();
|
|
62
62
|
if (!accountId) return res.status(400).json({ ok: false, error: "accountId is required" });
|
|
63
|
-
const result = await gmailWatchService.startWatch({
|
|
63
|
+
const result = await gmailWatchService.startWatch({
|
|
64
|
+
accountId,
|
|
65
|
+
req,
|
|
66
|
+
destination: req.body?.destination || null,
|
|
67
|
+
});
|
|
64
68
|
const snapshot = await getRestartSnapshot();
|
|
65
69
|
return res.json({
|
|
66
70
|
...result,
|
|
@@ -34,6 +34,62 @@ const registerModelRoutes = ({
|
|
|
34
34
|
return next;
|
|
35
35
|
};
|
|
36
36
|
|
|
37
|
+
const readEnvVarMap = () => {
|
|
38
|
+
if (typeof readEnvFile !== "function") return new Map();
|
|
39
|
+
return new Map(
|
|
40
|
+
(readEnvFile() || []).map((entry) => [
|
|
41
|
+
String(entry?.key || "").trim(),
|
|
42
|
+
String(entry?.value || "").trim(),
|
|
43
|
+
]),
|
|
44
|
+
);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const buildEnvBackedProfiles = (agentId) => {
|
|
48
|
+
const envMap = readEnvVarMap();
|
|
49
|
+
const providers = authProfiles.listApiKeyProviders?.() || [];
|
|
50
|
+
return providers.flatMap((provider) => {
|
|
51
|
+
const envKey = authProfiles.getEnvVarForApiKeyProvider?.(provider);
|
|
52
|
+
const envValue = String(envMap.get(envKey) || "").trim();
|
|
53
|
+
if (!envKey || !envValue) return [];
|
|
54
|
+
const profileId =
|
|
55
|
+
authProfiles.getDefaultProfileIdForApiKeyProvider?.(provider) ||
|
|
56
|
+
`${provider}:default`;
|
|
57
|
+
return [
|
|
58
|
+
{
|
|
59
|
+
id: profileId,
|
|
60
|
+
type: "api_key",
|
|
61
|
+
provider,
|
|
62
|
+
key: envValue,
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const mergeProfilesWithEnvFallback = (profiles, agentId) => {
|
|
69
|
+
const mergedProfiles = Array.isArray(profiles) ? [...profiles] : [];
|
|
70
|
+
const profileIndexById = new Map(
|
|
71
|
+
mergedProfiles.map((profile, index) => [profile?.id, index]),
|
|
72
|
+
);
|
|
73
|
+
for (const envProfile of buildEnvBackedProfiles(agentId)) {
|
|
74
|
+
const existingIndex = profileIndexById.get(envProfile.id);
|
|
75
|
+
if (existingIndex === undefined) {
|
|
76
|
+
profileIndexById.set(envProfile.id, mergedProfiles.length);
|
|
77
|
+
mergedProfiles.push(envProfile);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const existingProfile = mergedProfiles[existingIndex] || {};
|
|
81
|
+
const existingValue = String(
|
|
82
|
+
existingProfile?.key || existingProfile?.token || existingProfile?.access || "",
|
|
83
|
+
).trim();
|
|
84
|
+
if (existingValue) continue;
|
|
85
|
+
mergedProfiles[existingIndex] = {
|
|
86
|
+
...existingProfile,
|
|
87
|
+
...envProfile,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return mergedProfiles;
|
|
91
|
+
};
|
|
92
|
+
|
|
37
93
|
const syncEnvVarsForProfiles = (profiles) => {
|
|
38
94
|
if (
|
|
39
95
|
!Array.isArray(profiles) ||
|
|
@@ -62,6 +118,28 @@ const registerModelRoutes = ({
|
|
|
62
118
|
reloadEnv();
|
|
63
119
|
};
|
|
64
120
|
|
|
121
|
+
const syncProfilesFromEnvVars = (agentId) => {
|
|
122
|
+
if (
|
|
123
|
+
typeof readEnvFile !== "function" ||
|
|
124
|
+
typeof authProfiles.upsertApiKeyProfileForEnvVar !== "function" ||
|
|
125
|
+
typeof authProfiles.removeApiKeyProfileForEnvVar !== "function"
|
|
126
|
+
) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const envMap = readEnvVarMap();
|
|
130
|
+
const providers = authProfiles.listApiKeyProviders?.() || [];
|
|
131
|
+
for (const provider of providers) {
|
|
132
|
+
const envKey = authProfiles.getEnvVarForApiKeyProvider?.(provider);
|
|
133
|
+
if (!envKey) continue;
|
|
134
|
+
const envValue = String(envMap.get(envKey) || "").trim();
|
|
135
|
+
if (!envValue) {
|
|
136
|
+
authProfiles.removeApiKeyProfileForEnvVar(provider, agentId);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
authProfiles.upsertApiKeyProfileForEnvVar(provider, envValue, agentId);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
65
143
|
// ── Existing CLI-backed catalog/status routes ──
|
|
66
144
|
|
|
67
145
|
app.get("/api/models", async (req, res) => {
|
|
@@ -135,7 +213,10 @@ const registerModelRoutes = ({
|
|
|
135
213
|
try {
|
|
136
214
|
const { primary, configuredModels } = authProfiles.getModelConfig();
|
|
137
215
|
const agentId = req.query.agentId || undefined;
|
|
138
|
-
const profiles =
|
|
216
|
+
const profiles = mergeProfilesWithEnvFallback(
|
|
217
|
+
authProfiles.listProfiles(agentId),
|
|
218
|
+
agentId,
|
|
219
|
+
);
|
|
139
220
|
const store = authProfiles.loadAuthStore(agentId);
|
|
140
221
|
res.json({
|
|
141
222
|
ok: true,
|
|
@@ -179,6 +260,8 @@ const registerModelRoutes = ({
|
|
|
179
260
|
syncEnvVarsForProfiles(profiles);
|
|
180
261
|
}
|
|
181
262
|
|
|
263
|
+
syncProfilesFromEnvVars(agentId);
|
|
264
|
+
|
|
182
265
|
if (authOrder && typeof authOrder === "object") {
|
|
183
266
|
for (const [provider, order] of Object.entries(authOrder)) {
|
|
184
267
|
if (Array.isArray(order)) {
|
|
@@ -107,29 +107,7 @@ const registerSystemRoutes = ({
|
|
|
107
107
|
String(entry?.value || ""),
|
|
108
108
|
]),
|
|
109
109
|
);
|
|
110
|
-
const providers = [
|
|
111
|
-
"anthropic",
|
|
112
|
-
"openai",
|
|
113
|
-
"google",
|
|
114
|
-
"opencode",
|
|
115
|
-
"openrouter",
|
|
116
|
-
"zai",
|
|
117
|
-
"vercel-ai-gateway",
|
|
118
|
-
"kilocode",
|
|
119
|
-
"xai",
|
|
120
|
-
"mistral",
|
|
121
|
-
"cerebras",
|
|
122
|
-
"moonshot",
|
|
123
|
-
"kimi-coding",
|
|
124
|
-
"volcengine",
|
|
125
|
-
"byteplus",
|
|
126
|
-
"synthetic",
|
|
127
|
-
"minimax",
|
|
128
|
-
"voyage",
|
|
129
|
-
"groq",
|
|
130
|
-
"deepgram",
|
|
131
|
-
"vllm",
|
|
132
|
-
];
|
|
110
|
+
const providers = authProfiles.listApiKeyProviders?.() || [];
|
|
133
111
|
for (const provider of providers) {
|
|
134
112
|
const envKey = authProfiles.getEnvVarForApiKeyProvider?.(provider);
|
|
135
113
|
if (!envKey) continue;
|
|
@@ -141,6 +119,30 @@ const registerSystemRoutes = ({
|
|
|
141
119
|
authProfiles.upsertApiKeyProfileForEnvVar(provider, value);
|
|
142
120
|
}
|
|
143
121
|
};
|
|
122
|
+
const getSessionReplyTarget = (sessionKey = "") => {
|
|
123
|
+
const key = String(sessionKey || "");
|
|
124
|
+
const telegramDirectMatch = key.match(/:telegram:direct:([^:]+)$/);
|
|
125
|
+
if (telegramDirectMatch) {
|
|
126
|
+
return {
|
|
127
|
+
replyChannel: "telegram",
|
|
128
|
+
replyTo: String(telegramDirectMatch[1] || ""),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
const telegramTopicMatch = key.match(
|
|
132
|
+
/:telegram:group:([^:]+):topic:([^:]+)$/,
|
|
133
|
+
);
|
|
134
|
+
if (telegramTopicMatch) {
|
|
135
|
+
return {
|
|
136
|
+
replyChannel: "telegram",
|
|
137
|
+
replyTo: `${String(telegramTopicMatch[1] || "")}:${String(telegramTopicMatch[2] || "")}`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
replyChannel: "",
|
|
142
|
+
replyTo: "",
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
|
|
144
146
|
const listSendableAgentSessions = async () => {
|
|
145
147
|
const result = await clawCmd("sessions --json", { quiet: true });
|
|
146
148
|
if (!result.ok) {
|
|
@@ -163,14 +165,14 @@ const registerSystemRoutes = ({
|
|
|
163
165
|
})
|
|
164
166
|
.map((sessionRow) => {
|
|
165
167
|
const key = String(sessionRow?.key || "");
|
|
166
|
-
const
|
|
168
|
+
const replyTarget = getSessionReplyTarget(key);
|
|
167
169
|
return {
|
|
168
170
|
key,
|
|
169
171
|
sessionId: String(sessionRow?.sessionId || ""),
|
|
170
172
|
updatedAt: Number(sessionRow?.updatedAt) || 0,
|
|
171
173
|
label: buildSessionLabel(sessionRow),
|
|
172
|
-
replyChannel:
|
|
173
|
-
replyTo:
|
|
174
|
+
replyChannel: replyTarget.replyChannel,
|
|
175
|
+
replyTo: replyTarget.replyTo,
|
|
174
176
|
};
|
|
175
177
|
})
|
|
176
178
|
.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
@@ -136,9 +136,9 @@ const registerWebhookRoutes = ({
|
|
|
136
136
|
|
|
137
137
|
app.post("/api/webhooks", async (req, res) => {
|
|
138
138
|
try {
|
|
139
|
-
const { name: rawName } = req.body || {};
|
|
139
|
+
const { name: rawName, destination = null } = req.body || {};
|
|
140
140
|
const name = validateWebhookName(rawName);
|
|
141
|
-
const webhook = createWebhook({ fs, constants, name });
|
|
141
|
+
const webhook = createWebhook({ fs, constants, name, destination });
|
|
142
142
|
const baseUrl = getBaseUrl(req);
|
|
143
143
|
const urls = buildWebhookUrls({ baseUrl, name });
|
|
144
144
|
const syncWarning = await runWebhookGitSync("create", name);
|