thepopebot 1.2.74 → 1.2.75-beta.2
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/README.md +11 -0
- package/api/CLAUDE.md +2 -0
- package/api/index.js +95 -3
- package/bin/cli.js +1 -1
- package/bin/managed-paths.js +1 -1
- package/bin/sync.js +7 -2
- package/config/instrumentation.js +4 -0
- package/lib/chat/actions.js +4 -4
- package/lib/chat/components/chat-input.js +2 -2
- package/lib/chat/components/chat-input.jsx +2 -2
- package/lib/chat/components/settings-chat-page.js +41 -37
- package/lib/chat/components/settings-chat-page.jsx +39 -37
- package/lib/chat/components/settings-jobs-page.js +68 -10
- package/lib/chat/components/settings-jobs-page.jsx +99 -34
- package/lib/code/code-page.js +2 -1
- package/lib/code/code-page.jsx +2 -1
- package/lib/code/terminal-view.js +6 -3
- package/lib/code/terminal-view.jsx +6 -3
- package/lib/db/api-keys.js +35 -2
- package/lib/db/config.js +40 -4
- package/lib/maintenance.js +35 -0
- package/lib/oauth/helper.js +34 -0
- package/lib/tools/create-agent-job.js +3 -0
- package/lib/tools/docker.js +12 -5
- package/package.json +3 -2
- package/templates/docker-compose.custom.yml +1 -0
- package/templates/docker-compose.litellm.yml +1 -0
- package/templates/docker-compose.yml +2 -0
- package/templates/skills/agent-job-secrets/SKILL.md +25 -0
- package/templates/skills/agent-job-secrets/agent-job-secrets.js +66 -0
- package/templates/skills/playwright-cli/SKILL.md +294 -0
- package/templates/skills/brave-search/SKILL.md +0 -79
- package/templates/skills/brave-search/content.js +0 -86
- package/templates/skills/brave-search/package-lock.json +0 -621
- package/templates/skills/brave-search/package.json +0 -14
- package/templates/skills/brave-search/search.js +0 -199
- package/templates/skills/browser-tools/SKILL.md +0 -196
- package/templates/skills/browser-tools/browser-content.js +0 -103
- package/templates/skills/browser-tools/browser-cookies.js +0 -35
- package/templates/skills/browser-tools/browser-eval.js +0 -53
- package/templates/skills/browser-tools/browser-hn-scraper.js +0 -108
- package/templates/skills/browser-tools/browser-nav.js +0 -44
- package/templates/skills/browser-tools/browser-pick.js +0 -162
- package/templates/skills/browser-tools/browser-screenshot.js +0 -34
- package/templates/skills/browser-tools/browser-start.js +0 -87
- package/templates/skills/browser-tools/package-lock.json +0 -2556
- package/templates/skills/browser-tools/package.json +0 -19
- package/templates/skills/get-secret/SKILL.md +0 -34
- package/templates/skills/get-secret/get-secret.js +0 -33
- package/templates/skills/google-docs/SKILL.md +0 -23
- package/templates/skills/google-docs/create.sh +0 -69
- package/templates/skills/google-drive/SKILL.md +0 -47
- package/templates/skills/google-drive/delete.sh +0 -47
- package/templates/skills/google-drive/download.sh +0 -50
- package/templates/skills/google-drive/list.sh +0 -41
- package/templates/skills/google-drive/upload.sh +0 -76
- package/templates/skills/kie-ai/SKILL.md +0 -38
- package/templates/skills/kie-ai/generate-image.sh +0 -77
- package/templates/skills/kie-ai/generate-video.sh +0 -69
- package/templates/skills/youtube-transcript/SKILL.md +0 -41
- package/templates/skills/youtube-transcript/package-lock.json +0 -24
- package/templates/skills/youtube-transcript/package.json +0 -8
- package/templates/skills/youtube-transcript/transcript.js +0 -84
package/README.md
CHANGED
|
@@ -192,6 +192,17 @@ See [Different Models](docs/RUNNING_DIFFERENT_MODELS.md) for the full provider r
|
|
|
192
192
|
|
|
193
193
|
---
|
|
194
194
|
|
|
195
|
+
## Known Issues
|
|
196
|
+
|
|
197
|
+
### Windows: `SQLITE_IOERR_SHMOPEN`
|
|
198
|
+
|
|
199
|
+
SQLite can't create or open its shared-memory (`.shm`) file. Common causes:
|
|
200
|
+
|
|
201
|
+
- **Antivirus** (Windows Defender, etc.) locking the database files — add your project folder to the exclusion list
|
|
202
|
+
- **Cloud-synced folders** (OneDrive, Dropbox, Google Drive) — move your project to a non-synced directory like `C:\Projects\`
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
195
206
|
## Docs
|
|
196
207
|
|
|
197
208
|
| Document | Description |
|
package/api/CLAUDE.md
CHANGED
|
@@ -25,6 +25,8 @@ Browser-facing data fetching uses **fetch route handlers** colocated with pages
|
|
|
25
25
|
|--------|------|------|---------|
|
|
26
26
|
| GET | `/api/ping` | None | Health check |
|
|
27
27
|
| POST | `/api/create-agent-job` | `x-api-key` | Create agent job |
|
|
28
|
+
| GET | `/api/get-agent-job-secret` | `x-api-key` | Get an agent job secret; oauth2 credentials return only the access_token (auto-refreshed) |
|
|
29
|
+
| POST | `/api/set-agent-job-secret` | `x-api-key` | Set/update an agent job secret (for agents to persist rotated credentials) |
|
|
28
30
|
| GET | `/api/agent-jobs/status` | `x-api-key` | Agent job status (query: `?agent_job_id=`) |
|
|
29
31
|
| POST | `/api/telegram/webhook` | Telegram webhook secret | Telegram message handler |
|
|
30
32
|
| POST | `/api/telegram/register` | `x-api-key` | Register bot token + webhook URL |
|
package/api/index.js
CHANGED
|
@@ -11,6 +11,9 @@ import { getConfig } from '../lib/config.js';
|
|
|
11
11
|
import { parseOAuthState, exchangeCodeForToken } from '../lib/oauth/helper.js';
|
|
12
12
|
import { setAgentJobSecret } from '../lib/db/config.js';
|
|
13
13
|
|
|
14
|
+
// ── Per-key lock for OAuth token refresh ────────────────────────────
|
|
15
|
+
const _refreshLocks = new Map();
|
|
16
|
+
|
|
14
17
|
// Bot token — resolved from DB/env, can be overridden by /telegram/register
|
|
15
18
|
let telegramBotToken = null;
|
|
16
19
|
|
|
@@ -104,6 +107,81 @@ async function handleCreateAgentJob(request) {
|
|
|
104
107
|
}
|
|
105
108
|
}
|
|
106
109
|
|
|
110
|
+
async function handleGetAgentSecret(request) {
|
|
111
|
+
const record = verifyApiKey(request.headers.get('x-api-key'));
|
|
112
|
+
if (record.type !== 'agent_job_api_key') {
|
|
113
|
+
return Response.json({ error: 'Forbidden' }, { status: 403 });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const key = new URL(request.url).searchParams.get('key');
|
|
117
|
+
if (!key) return Response.json({ error: 'Missing key' }, { status: 400 });
|
|
118
|
+
|
|
119
|
+
const { getAgentJobSecretRaw, setAgentJobSecret: saveSecret } = await import('../lib/db/config.js');
|
|
120
|
+
const raw = getAgentJobSecretRaw(key);
|
|
121
|
+
if (!raw) return Response.json({ error: 'Not found' }, { status: 404 });
|
|
122
|
+
|
|
123
|
+
let parsed;
|
|
124
|
+
try {
|
|
125
|
+
parsed = JSON.parse(raw);
|
|
126
|
+
} catch {
|
|
127
|
+
// Plain string
|
|
128
|
+
return Response.json({ value: raw });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (parsed.type === 'oauth2') {
|
|
132
|
+
// Serialize refresh per key — prevents concurrent requests from racing on token rotation
|
|
133
|
+
if (!_refreshLocks.has(key)) _refreshLocks.set(key, Promise.resolve());
|
|
134
|
+
let release;
|
|
135
|
+
const gate = new Promise((r) => { release = r; });
|
|
136
|
+
const prev = _refreshLocks.get(key);
|
|
137
|
+
_refreshLocks.set(key, gate);
|
|
138
|
+
await prev;
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
// Re-read after acquiring lock — previous request may have already refreshed
|
|
142
|
+
const freshRaw = getAgentJobSecretRaw(key);
|
|
143
|
+
const freshParsed = freshRaw ? JSON.parse(freshRaw) : parsed;
|
|
144
|
+
|
|
145
|
+
const { refreshOAuthToken } = await import('../lib/oauth/helper.js');
|
|
146
|
+
const newToken = await refreshOAuthToken({
|
|
147
|
+
refreshToken: freshParsed.token.refresh_token,
|
|
148
|
+
clientId: freshParsed.clientId,
|
|
149
|
+
clientSecret: freshParsed.clientSecret,
|
|
150
|
+
tokenUrl: freshParsed.tokenUrl,
|
|
151
|
+
});
|
|
152
|
+
// Persist updated token (refresh token may have rotated)
|
|
153
|
+
saveSecret(key, JSON.stringify({ ...freshParsed, token: { ...freshParsed.token, ...newToken } }), 'refresh');
|
|
154
|
+
return Response.json({ value: newToken.access_token });
|
|
155
|
+
} catch (err) {
|
|
156
|
+
console.error(`[secrets] OAuth refresh failed for "${key}":`, err.message);
|
|
157
|
+
return Response.json({ error: `OAuth refresh failed: ${err.message}` }, { status: 502 });
|
|
158
|
+
} finally {
|
|
159
|
+
release();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (parsed.type === 'oauth_token') {
|
|
163
|
+
return Response.json({ value: JSON.stringify(parsed.token) });
|
|
164
|
+
}
|
|
165
|
+
// Unknown structured value — return raw
|
|
166
|
+
return Response.json({ value: raw });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function handleSetAgentSecret(request) {
|
|
170
|
+
const record = verifyApiKey(request.headers.get('x-api-key'));
|
|
171
|
+
if (record.type !== 'agent_job_api_key') {
|
|
172
|
+
return Response.json({ error: 'Forbidden' }, { status: 403 });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const body = await request.json();
|
|
176
|
+
const { key, value } = body;
|
|
177
|
+
if (!key || typeof value !== 'string') {
|
|
178
|
+
return Response.json({ error: 'Missing key or value' }, { status: 400 });
|
|
179
|
+
}
|
|
180
|
+
const { setAgentJobSecret } = await import('../lib/db/config.js');
|
|
181
|
+
setAgentJobSecret(key, value, 'agent');
|
|
182
|
+
return Response.json({ success: true });
|
|
183
|
+
}
|
|
184
|
+
|
|
107
185
|
async function handleTelegramRegister(request) {
|
|
108
186
|
const body = await request.json();
|
|
109
187
|
const { bot_token, webhook_url } = body;
|
|
@@ -248,9 +326,21 @@ async function handleOAuthCallback(request) {
|
|
|
248
326
|
redirectUri,
|
|
249
327
|
});
|
|
250
328
|
|
|
251
|
-
// Save
|
|
252
|
-
const
|
|
253
|
-
|
|
329
|
+
// Save token with typed wrapper so the API can auto-refresh on fetch
|
|
330
|
+
const secretType = state.secretType || 'oauth2';
|
|
331
|
+
let stored;
|
|
332
|
+
if (secretType === 'oauth_token') {
|
|
333
|
+
stored = JSON.stringify({ type: 'oauth_token', token: tokenData });
|
|
334
|
+
} else {
|
|
335
|
+
stored = JSON.stringify({
|
|
336
|
+
type: 'oauth2',
|
|
337
|
+
token: tokenData,
|
|
338
|
+
clientId: state.clientId,
|
|
339
|
+
clientSecret: state.clientSecret,
|
|
340
|
+
tokenUrl: state.tokenUrl,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
setAgentJobSecret(state.secretName, stored, 'oauth');
|
|
254
344
|
|
|
255
345
|
return oauthResultPage(true, state.secretName);
|
|
256
346
|
} catch (err) {
|
|
@@ -315,6 +405,7 @@ async function POST(request) {
|
|
|
315
405
|
// Route to handler
|
|
316
406
|
switch (routePath) {
|
|
317
407
|
case '/create-agent-job': return handleCreateAgentJob(request);
|
|
408
|
+
case '/set-agent-job-secret': return handleSetAgentSecret(request);
|
|
318
409
|
case '/telegram/webhook': return handleTelegramWebhook(request);
|
|
319
410
|
case '/telegram/register': return handleTelegramRegister(request);
|
|
320
411
|
case '/github/webhook': return handleGithubWebhook(request);
|
|
@@ -333,6 +424,7 @@ async function GET(request) {
|
|
|
333
424
|
switch (routePath) {
|
|
334
425
|
case '/ping': return Response.json({ message: 'Pong!' });
|
|
335
426
|
case '/agent-jobs/status': return handleAgentJobStatus(request);
|
|
427
|
+
case '/get-agent-job-secret': return handleGetAgentSecret(request);
|
|
336
428
|
case '/oauth/callback': return handleOAuthCallback(request);
|
|
337
429
|
default: return Response.json({ error: 'Not found' }, { status: 404 });
|
|
338
430
|
}
|
package/bin/cli.js
CHANGED
|
@@ -262,7 +262,7 @@ async function init() {
|
|
|
262
262
|
}
|
|
263
263
|
|
|
264
264
|
// Create default skill activation symlinks
|
|
265
|
-
const defaultSkills = ['
|
|
265
|
+
const defaultSkills = ['agent-job-secrets'];
|
|
266
266
|
const activeDir = path.join(cwd, 'skills', 'active');
|
|
267
267
|
fs.mkdirSync(activeDir, { recursive: true });
|
|
268
268
|
for (const skill of defaultSkills) {
|
package/bin/managed-paths.js
CHANGED
package/bin/sync.js
CHANGED
|
@@ -277,8 +277,7 @@ function buildDockerImage(projectPath) {
|
|
|
277
277
|
fs.cpSync(webSrc, webDest, { recursive: true });
|
|
278
278
|
|
|
279
279
|
try {
|
|
280
|
-
|
|
281
|
-
execSync(`docker build --no-cache -f - -t ${imageTag} .`, {
|
|
280
|
+
execSync(`docker build -f - -t ${imageTag} .`, {
|
|
282
281
|
input: dockerfile,
|
|
283
282
|
stdio: ['pipe', 'inherit', 'inherit'],
|
|
284
283
|
cwd: projectPath,
|
|
@@ -287,6 +286,12 @@ function buildDockerImage(projectPath) {
|
|
|
287
286
|
fs.rmSync(webDest, { recursive: true, force: true });
|
|
288
287
|
}
|
|
289
288
|
|
|
289
|
+
// Clean up dangling images from previous builds
|
|
290
|
+
try {
|
|
291
|
+
execSync('docker image prune -f', { stdio: 'ignore' });
|
|
292
|
+
} catch {}
|
|
293
|
+
|
|
294
|
+
|
|
290
295
|
// Update THEPOPEBOT_VERSION in .env
|
|
291
296
|
const envPath = path.join(projectPath, '.env');
|
|
292
297
|
if (fs.existsSync(envPath)) {
|
|
@@ -70,5 +70,9 @@ export async function register() {
|
|
|
70
70
|
const { startClusterRuntime } = await import('../lib/cluster/runtime.js');
|
|
71
71
|
startClusterRuntime();
|
|
72
72
|
|
|
73
|
+
// Start internal maintenance cron (cleanup expired agent job keys, etc.)
|
|
74
|
+
const { startMaintenanceCron } = await import('../lib/maintenance.js');
|
|
75
|
+
startMaintenanceCron();
|
|
76
|
+
|
|
73
77
|
console.log('thepopebot initialized');
|
|
74
78
|
}
|
package/lib/chat/actions.js
CHANGED
|
@@ -915,10 +915,10 @@ export async function getChatSettings() {
|
|
|
915
915
|
const customProviders = getCustomProviders();
|
|
916
916
|
|
|
917
917
|
// Get active config values
|
|
918
|
-
const activeProvider = getConfigValue('LLM_PROVIDER') ||
|
|
919
|
-
const activeModel = getConfigValue('LLM_MODEL') ||
|
|
920
|
-
const maxTokens = getConfigValue('LLM_MAX_TOKENS') ||
|
|
921
|
-
const agentBackend = getConfigValue('AGENT_BACKEND') ||
|
|
918
|
+
const activeProvider = getConfigValue('LLM_PROVIDER') || '';
|
|
919
|
+
const activeModel = getConfigValue('LLM_MODEL') || '';
|
|
920
|
+
const maxTokens = getConfigValue('LLM_MAX_TOKENS') || '4096';
|
|
921
|
+
const agentBackend = getConfigValue('AGENT_BACKEND') || '';
|
|
922
922
|
|
|
923
923
|
return {
|
|
924
924
|
builtinProviders: BUILTIN_PROVIDERS,
|
|
@@ -87,7 +87,7 @@ function ChatInput({ input, setInput, onSubmit, status, stop, files, setFiles, d
|
|
|
87
87
|
const textarea = textareaRef.current;
|
|
88
88
|
if (!textarea) return;
|
|
89
89
|
textarea.style.height = "auto";
|
|
90
|
-
textarea.style.height = `${
|
|
90
|
+
textarea.style.height = `${textarea.scrollHeight}px`;
|
|
91
91
|
}, []);
|
|
92
92
|
useEffect(() => {
|
|
93
93
|
adjustHeight();
|
|
@@ -213,7 +213,7 @@ function ChatInput({ input, setInput, onSubmit, status, stop, files, setFiles, d
|
|
|
213
213
|
className: cn(
|
|
214
214
|
"w-full resize-none bg-transparent px-2 py-1.5 text-sm text-foreground",
|
|
215
215
|
"placeholder:text-muted-foreground focus:outline-none",
|
|
216
|
-
"max-h-[200px]"
|
|
216
|
+
"min-h-[84px] md:min-h-0 max-h-[40vh] md:max-h-[200px]"
|
|
217
217
|
),
|
|
218
218
|
disabled: isStreaming
|
|
219
219
|
}
|
|
@@ -69,7 +69,7 @@ export function ChatInput({ input, setInput, onSubmit, status, stop, files, setF
|
|
|
69
69
|
const textarea = textareaRef.current;
|
|
70
70
|
if (!textarea) return;
|
|
71
71
|
textarea.style.height = 'auto';
|
|
72
|
-
textarea.style.height = `${
|
|
72
|
+
textarea.style.height = `${textarea.scrollHeight}px`;
|
|
73
73
|
}, []);
|
|
74
74
|
|
|
75
75
|
useEffect(() => {
|
|
@@ -229,7 +229,7 @@ export function ChatInput({ input, setInput, onSubmit, status, stop, files, setF
|
|
|
229
229
|
className={cn(
|
|
230
230
|
'w-full resize-none bg-transparent px-2 py-1.5 text-sm text-foreground',
|
|
231
231
|
'placeholder:text-muted-foreground focus:outline-none',
|
|
232
|
-
'max-h-[200px]'
|
|
232
|
+
'min-h-[84px] md:min-h-0 max-h-[40vh] md:max-h-[200px]'
|
|
233
233
|
)}
|
|
234
234
|
disabled={isStreaming}
|
|
235
235
|
/>
|
|
@@ -57,11 +57,30 @@ function ActiveConfig({ settings, onSave }) {
|
|
|
57
57
|
const [saved, setSaved] = useState(false);
|
|
58
58
|
const initialized = useRef(false);
|
|
59
59
|
const saveTimer = useRef(null);
|
|
60
|
+
const availableProviders = [];
|
|
61
|
+
if (settings?.builtinProviders && settings?.credentialStatuses) {
|
|
62
|
+
const statusMap = new Map(settings.credentialStatuses.map((s) => [s.key, s.isSet]));
|
|
63
|
+
for (const [slug, prov] of Object.entries(settings.builtinProviders)) {
|
|
64
|
+
const hasKey = prov.credentials.some((c) => statusMap.get(c.key));
|
|
65
|
+
if (hasKey) {
|
|
66
|
+
availableProviders.push({ slug, name: prov.name, models: prov.models });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (settings?.customProviders) {
|
|
71
|
+
for (const cp of settings.customProviders) {
|
|
72
|
+
availableProviders.push({ slug: cp.key, name: cp.name, models: cp.models.map((m) => ({ id: m, name: m })) });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
60
75
|
useEffect(() => {
|
|
61
76
|
if (settings?.active) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
77
|
+
const prov = settings.active.provider || availableProviders[0]?.slug || "";
|
|
78
|
+
const resolved = availableProviders.find((p) => p.slug === prov);
|
|
79
|
+
const models = resolved?.models || [];
|
|
80
|
+
const def = models.find((m) => m.default);
|
|
81
|
+
setProvider(prov);
|
|
82
|
+
setModel(settings.active.model || def?.id || models[0]?.id || "");
|
|
83
|
+
setModelText(settings.active.model || def?.id || models[0]?.id || "");
|
|
65
84
|
setMaxTokens(settings.active.maxTokens || "4096");
|
|
66
85
|
setTimeout(() => {
|
|
67
86
|
initialized.current = true;
|
|
@@ -82,21 +101,6 @@ function ActiveConfig({ settings, onSave }) {
|
|
|
82
101
|
if (saveTimer.current) clearTimeout(saveTimer.current);
|
|
83
102
|
saveTimer.current = setTimeout(() => doSave(p, m, mt, ws), 800);
|
|
84
103
|
}, [doSave]);
|
|
85
|
-
const availableProviders = [];
|
|
86
|
-
if (settings?.builtinProviders && settings?.credentialStatuses) {
|
|
87
|
-
const statusMap = new Map(settings.credentialStatuses.map((s) => [s.key, s.isSet]));
|
|
88
|
-
for (const [slug, prov] of Object.entries(settings.builtinProviders)) {
|
|
89
|
-
const hasKey = prov.credentials.some((c) => statusMap.get(c.key));
|
|
90
|
-
if (hasKey) {
|
|
91
|
-
availableProviders.push({ slug, name: prov.name, models: prov.models });
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
if (settings?.customProviders) {
|
|
96
|
-
for (const cp of settings.customProviders) {
|
|
97
|
-
availableProviders.push({ slug: cp.key, name: cp.name, models: cp.models.map((m) => ({ id: m, name: m })) });
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
104
|
const selectedProvider = availableProviders.find((p) => p.slug === provider);
|
|
101
105
|
const handleProviderChange = (slug) => {
|
|
102
106
|
setProvider(slug);
|
|
@@ -129,28 +133,28 @@ function ActiveConfig({ settings, onSave }) {
|
|
|
129
133
|
}
|
|
130
134
|
};
|
|
131
135
|
return /* @__PURE__ */ jsxs("div", { className: "rounded-lg border bg-card p-4", children: [
|
|
132
|
-
(saving || saved) && /* @__PURE__ */ jsxs("div", { className: "flex justify-end mb-2", children: [
|
|
133
|
-
saving && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: "Saving..." }),
|
|
134
|
-
saved && /* @__PURE__ */ jsxs("span", { className: "text-xs text-green-500 inline-flex items-center gap-1", children: [
|
|
135
|
-
/* @__PURE__ */ jsx(CheckIcon, { size: 12 }),
|
|
136
|
-
" Saved"
|
|
137
|
-
] })
|
|
138
|
-
] }),
|
|
139
136
|
/* @__PURE__ */ jsxs("div", { className: "divide-y divide-border", children: [
|
|
140
137
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between py-3 first:pt-0", children: [
|
|
141
138
|
/* @__PURE__ */ jsx("label", { className: "text-sm font-medium shrink-0", children: "Provider" }),
|
|
142
|
-
/* @__PURE__ */ jsxs(
|
|
143
|
-
"
|
|
144
|
-
{
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
139
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
140
|
+
saving && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: "Saving..." }),
|
|
141
|
+
saved && /* @__PURE__ */ jsxs("span", { className: "text-xs text-green-500 inline-flex items-center gap-1", children: [
|
|
142
|
+
/* @__PURE__ */ jsx(CheckIcon, { size: 12 }),
|
|
143
|
+
" Saved"
|
|
144
|
+
] }),
|
|
145
|
+
/* @__PURE__ */ jsxs(
|
|
146
|
+
"select",
|
|
147
|
+
{
|
|
148
|
+
value: provider,
|
|
149
|
+
onChange: (e) => handleProviderChange(e.target.value),
|
|
150
|
+
className: "w-48 rounded-md border border-border bg-background px-3 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-foreground",
|
|
151
|
+
children: [
|
|
152
|
+
availableProviders.map((p) => /* @__PURE__ */ jsx("option", { value: p.slug, children: p.name }, p.slug)),
|
|
153
|
+
availableProviders.length === 0 && /* @__PURE__ */ jsx("option", { value: "", disabled: true, children: "No providers configured" })
|
|
154
|
+
]
|
|
155
|
+
}
|
|
156
|
+
)
|
|
157
|
+
] })
|
|
154
158
|
] }),
|
|
155
159
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between py-3", children: [
|
|
156
160
|
/* @__PURE__ */ jsx("label", { className: "text-sm font-medium shrink-0", children: "Model" }),
|
|
@@ -73,11 +73,31 @@ function ActiveConfig({ settings, onSave }) {
|
|
|
73
73
|
const initialized = useRef(false);
|
|
74
74
|
const saveTimer = useRef(null);
|
|
75
75
|
|
|
76
|
+
const availableProviders = [];
|
|
77
|
+
if (settings?.builtinProviders && settings?.credentialStatuses) {
|
|
78
|
+
const statusMap = new Map(settings.credentialStatuses.map((s) => [s.key, s.isSet]));
|
|
79
|
+
for (const [slug, prov] of Object.entries(settings.builtinProviders)) {
|
|
80
|
+
const hasKey = prov.credentials.some((c) => statusMap.get(c.key));
|
|
81
|
+
if (hasKey) {
|
|
82
|
+
availableProviders.push({ slug, name: prov.name, models: prov.models });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (settings?.customProviders) {
|
|
87
|
+
for (const cp of settings.customProviders) {
|
|
88
|
+
availableProviders.push({ slug: cp.key, name: cp.name, models: cp.models.map((m) => ({ id: m, name: m })) });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
76
92
|
useEffect(() => {
|
|
77
93
|
if (settings?.active) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
94
|
+
const prov = settings.active.provider || availableProviders[0]?.slug || '';
|
|
95
|
+
const resolved = availableProviders.find((p) => p.slug === prov);
|
|
96
|
+
const models = resolved?.models || [];
|
|
97
|
+
const def = models.find((m) => m.default);
|
|
98
|
+
setProvider(prov);
|
|
99
|
+
setModel(settings.active.model || def?.id || models[0]?.id || '');
|
|
100
|
+
setModelText(settings.active.model || def?.id || models[0]?.id || '');
|
|
81
101
|
setMaxTokens(settings.active.maxTokens || '4096');
|
|
82
102
|
setTimeout(() => { initialized.current = true; }, 100);
|
|
83
103
|
}
|
|
@@ -99,22 +119,6 @@ function ActiveConfig({ settings, onSave }) {
|
|
|
99
119
|
saveTimer.current = setTimeout(() => doSave(p, m, mt, ws), 800);
|
|
100
120
|
}, [doSave]);
|
|
101
121
|
|
|
102
|
-
const availableProviders = [];
|
|
103
|
-
if (settings?.builtinProviders && settings?.credentialStatuses) {
|
|
104
|
-
const statusMap = new Map(settings.credentialStatuses.map((s) => [s.key, s.isSet]));
|
|
105
|
-
for (const [slug, prov] of Object.entries(settings.builtinProviders)) {
|
|
106
|
-
const hasKey = prov.credentials.some((c) => statusMap.get(c.key));
|
|
107
|
-
if (hasKey) {
|
|
108
|
-
availableProviders.push({ slug, name: prov.name, models: prov.models });
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
if (settings?.customProviders) {
|
|
113
|
-
for (const cp of settings.customProviders) {
|
|
114
|
-
availableProviders.push({ slug: cp.key, name: cp.name, models: cp.models.map((m) => ({ id: m, name: m })) });
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
122
|
const selectedProvider = availableProviders.find((p) => p.slug === provider);
|
|
119
123
|
|
|
120
124
|
const handleProviderChange = (slug) => {
|
|
@@ -156,27 +160,25 @@ function ActiveConfig({ settings, onSave }) {
|
|
|
156
160
|
|
|
157
161
|
return (
|
|
158
162
|
<div className="rounded-lg border bg-card p-4">
|
|
159
|
-
{(saving || saved) && (
|
|
160
|
-
<div className="flex justify-end mb-2">
|
|
161
|
-
{saving && <span className="text-xs text-muted-foreground">Saving...</span>}
|
|
162
|
-
{saved && <span className="text-xs text-green-500 inline-flex items-center gap-1"><CheckIcon size={12} /> Saved</span>}
|
|
163
|
-
</div>
|
|
164
|
-
)}
|
|
165
163
|
<div className="divide-y divide-border">
|
|
166
164
|
<div className="flex items-center justify-between py-3 first:pt-0">
|
|
167
165
|
<label className="text-sm font-medium shrink-0">Provider</label>
|
|
168
|
-
<
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
166
|
+
<div className="flex items-center gap-2">
|
|
167
|
+
{saving && <span className="text-xs text-muted-foreground">Saving...</span>}
|
|
168
|
+
{saved && <span className="text-xs text-green-500 inline-flex items-center gap-1"><CheckIcon size={12} /> Saved</span>}
|
|
169
|
+
<select
|
|
170
|
+
value={provider}
|
|
171
|
+
onChange={(e) => handleProviderChange(e.target.value)}
|
|
172
|
+
className="w-48 rounded-md border border-border bg-background px-3 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-foreground"
|
|
173
|
+
>
|
|
174
|
+
{availableProviders.map((p) => (
|
|
175
|
+
<option key={p.slug} value={p.slug}>{p.name}</option>
|
|
176
|
+
))}
|
|
177
|
+
{availableProviders.length === 0 && (
|
|
178
|
+
<option value="" disabled>No providers configured</option>
|
|
179
|
+
)}
|
|
180
|
+
</select>
|
|
181
|
+
</div>
|
|
180
182
|
</div>
|
|
181
183
|
|
|
182
184
|
<div className="flex items-center justify-between py-3">
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useState, useEffect, useRef, useCallback } from "react";
|
|
4
|
-
import { PlusIcon, CopyIcon, CheckIcon, SpinnerIcon } from "./icons.js";
|
|
5
|
-
import { SecretRow, Dialog, EmptyState } from "./settings-shared.js";
|
|
4
|
+
import { PlusIcon, CopyIcon, CheckIcon, SpinnerIcon, TrashIcon } from "./icons.js";
|
|
5
|
+
import { SecretRow, StatusBadge, Dialog, EmptyState } from "./settings-shared.js";
|
|
6
6
|
import { OAUTH_PROVIDERS } from "../../oauth/providers.js";
|
|
7
7
|
import {
|
|
8
8
|
getAgentJobSecrets,
|
|
@@ -137,7 +137,7 @@ function ProviderCombobox({ value, onChange, inputClass }) {
|
|
|
137
137
|
open && filtered.length === 0 && query && /* @__PURE__ */ jsx("div", { className: "absolute z-10 mt-1 w-full rounded-md border border-border bg-background shadow-lg px-3 py-2 text-sm text-muted-foreground", children: "No providers found" })
|
|
138
138
|
] });
|
|
139
139
|
}
|
|
140
|
-
function AddSecretDialog({ open, onAdd, onCancel, onOAuthSuccess }) {
|
|
140
|
+
function AddSecretDialog({ open, onAdd, onCancel, onOAuthSuccess, editingSecret }) {
|
|
141
141
|
const [mode, setMode] = useState("manual");
|
|
142
142
|
const [name, setName] = useState("");
|
|
143
143
|
const [error, setError] = useState(null);
|
|
@@ -156,8 +156,8 @@ function AddSecretDialog({ open, onAdd, onCancel, onOAuthSuccess }) {
|
|
|
156
156
|
const timeoutRef = useRef(null);
|
|
157
157
|
useEffect(() => {
|
|
158
158
|
if (open) {
|
|
159
|
-
setMode("manual");
|
|
160
|
-
setName("");
|
|
159
|
+
setMode(editingSecret ? "oauth" : "manual");
|
|
160
|
+
setName(editingSecret?.key || "");
|
|
161
161
|
setError(null);
|
|
162
162
|
setValue("");
|
|
163
163
|
setShowValue(false);
|
|
@@ -169,7 +169,7 @@ function AddSecretDialog({ open, onAdd, onCancel, onOAuthSuccess }) {
|
|
|
169
169
|
setStatus("form");
|
|
170
170
|
setCopied(false);
|
|
171
171
|
setRedirectUri(`${window.location.origin}/api/oauth/callback`);
|
|
172
|
-
setTimeout(() => nameRef.current?.focus(), 50);
|
|
172
|
+
if (!editingSecret) setTimeout(() => nameRef.current?.focus(), 50);
|
|
173
173
|
}
|
|
174
174
|
return () => {
|
|
175
175
|
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
@@ -262,7 +262,7 @@ function AddSecretDialog({ open, onAdd, onCancel, onOAuthSuccess }) {
|
|
|
262
262
|
}, 5 * 60 * 1e3);
|
|
263
263
|
};
|
|
264
264
|
const inputClass = "w-full rounded-md border border-border bg-background px-3 py-1.5 text-sm font-mono focus:outline-none focus:ring-1 focus:ring-foreground";
|
|
265
|
-
return /* @__PURE__ */ jsx(Dialog, { open, onClose: onCancel, title: "Add Secret", children: status === "success" ? /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-center gap-2 py-8 text-green-500", children: [
|
|
265
|
+
return /* @__PURE__ */ jsx(Dialog, { open, onClose: onCancel, title: editingSecret ? "Re-authorize Secret" : "Add Secret", children: status === "success" ? /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-center gap-2 py-8 text-green-500", children: [
|
|
266
266
|
/* @__PURE__ */ jsx(CheckIcon, { size: 20 }),
|
|
267
267
|
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: "Token saved!" })
|
|
268
268
|
] }) : status === "waiting" ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center gap-3 py-8", children: [
|
|
@@ -271,7 +271,7 @@ function AddSecretDialog({ open, onAdd, onCancel, onOAuthSuccess }) {
|
|
|
271
271
|
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "Complete the login in the popup window." })
|
|
272
272
|
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
273
273
|
/* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
|
|
274
|
-
/* @__PURE__ */ jsxs("div", { className: "flex rounded-md border border-border overflow-hidden", children: [
|
|
274
|
+
!editingSecret && /* @__PURE__ */ jsxs("div", { className: "flex rounded-md border border-border overflow-hidden", children: [
|
|
275
275
|
/* @__PURE__ */ jsx(
|
|
276
276
|
"button",
|
|
277
277
|
{
|
|
@@ -301,7 +301,8 @@ function AddSecretDialog({ open, onAdd, onCancel, onOAuthSuccess }) {
|
|
|
301
301
|
value: name,
|
|
302
302
|
onChange: (e) => setName(e.target.value.toUpperCase().replace(/[^A-Z0-9_]/g, "")),
|
|
303
303
|
placeholder: mode === "manual" ? "e.g. GOOGLE_SERVICE_ACCOUNT_KEY" : "e.g. GOOGLE_OAUTH_TOKEN",
|
|
304
|
-
className: inputClass
|
|
304
|
+
className: `${inputClass}${editingSecret ? " text-muted-foreground bg-muted" : ""}`,
|
|
305
|
+
readOnly: !!editingSecret,
|
|
305
306
|
onKeyDown: (e) => e.key === "Enter" && mode === "manual" && handleSave()
|
|
306
307
|
}
|
|
307
308
|
)
|
|
@@ -439,10 +440,49 @@ function AddSecretDialog({ open, onAdd, onCancel, onOAuthSuccess }) {
|
|
|
439
440
|
] })
|
|
440
441
|
] }) });
|
|
441
442
|
}
|
|
443
|
+
function OAuthSecretRow({ secret, onReauthorize, onDelete }) {
|
|
444
|
+
const [confirmDelete, setConfirmDelete] = useState(false);
|
|
445
|
+
const handleDelete = async () => {
|
|
446
|
+
if (!confirmDelete) {
|
|
447
|
+
setConfirmDelete(true);
|
|
448
|
+
setTimeout(() => setConfirmDelete(false), 3e3);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
await onDelete();
|
|
452
|
+
setConfirmDelete(false);
|
|
453
|
+
};
|
|
454
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between py-3", children: [
|
|
455
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 min-w-0", children: [
|
|
456
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium font-mono", children: secret.key }),
|
|
457
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: "OAuth" }),
|
|
458
|
+
/* @__PURE__ */ jsx(StatusBadge, { isSet: secret.isSet })
|
|
459
|
+
] }),
|
|
460
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 shrink-0 self-start sm:self-auto", children: [
|
|
461
|
+
/* @__PURE__ */ jsx(
|
|
462
|
+
"button",
|
|
463
|
+
{
|
|
464
|
+
onClick: onReauthorize,
|
|
465
|
+
className: "rounded-md px-2.5 py-1.5 text-xs font-medium border border-border text-muted-foreground hover:bg-accent hover:text-foreground transition-colors",
|
|
466
|
+
children: "Re-authorize"
|
|
467
|
+
}
|
|
468
|
+
),
|
|
469
|
+
/* @__PURE__ */ jsx(
|
|
470
|
+
"button",
|
|
471
|
+
{
|
|
472
|
+
onClick: handleDelete,
|
|
473
|
+
className: `rounded-md p-1.5 text-xs border transition-colors ${confirmDelete ? "border-destructive text-destructive hover:bg-destructive/10" : "border-border text-muted-foreground hover:text-destructive hover:border-destructive"}`,
|
|
474
|
+
title: confirmDelete ? "Click again to confirm" : "Delete",
|
|
475
|
+
children: /* @__PURE__ */ jsx(TrashIcon, { size: 12 })
|
|
476
|
+
}
|
|
477
|
+
)
|
|
478
|
+
] })
|
|
479
|
+
] });
|
|
480
|
+
}
|
|
442
481
|
function JobsPage() {
|
|
443
482
|
const [secrets, setSecrets] = useState([]);
|
|
444
483
|
const [loading, setLoading] = useState(true);
|
|
445
484
|
const [showAdd, setShowAdd] = useState(false);
|
|
485
|
+
const [reauthorizing, setReauthorizing] = useState(null);
|
|
446
486
|
const loadSecrets = async () => {
|
|
447
487
|
try {
|
|
448
488
|
const result = await getAgentJobSecrets();
|
|
@@ -501,6 +541,16 @@ function JobsPage() {
|
|
|
501
541
|
onOAuthSuccess: loadSecrets
|
|
502
542
|
}
|
|
503
543
|
),
|
|
544
|
+
/* @__PURE__ */ jsx(
|
|
545
|
+
AddSecretDialog,
|
|
546
|
+
{
|
|
547
|
+
open: !!reauthorizing,
|
|
548
|
+
onAdd: handleAdd,
|
|
549
|
+
onCancel: () => setReauthorizing(null),
|
|
550
|
+
onOAuthSuccess: loadSecrets,
|
|
551
|
+
editingSecret: reauthorizing
|
|
552
|
+
}
|
|
553
|
+
),
|
|
504
554
|
secrets.length === 0 ? /* @__PURE__ */ jsx(
|
|
505
555
|
EmptyState,
|
|
506
556
|
{
|
|
@@ -508,7 +558,15 @@ function JobsPage() {
|
|
|
508
558
|
actionLabel: "Add secret",
|
|
509
559
|
onAction: () => setShowAdd(true)
|
|
510
560
|
}
|
|
511
|
-
) : /* @__PURE__ */ jsx("div", { className: "rounded-lg border bg-card p-4", children: /* @__PURE__ */ jsx("div", { className: "divide-y divide-border", children: secrets.map((s) => /* @__PURE__ */ jsx(
|
|
561
|
+
) : /* @__PURE__ */ jsx("div", { className: "rounded-lg border bg-card p-4", children: /* @__PURE__ */ jsx("div", { className: "divide-y divide-border", children: secrets.map((s) => s.secretType === "oauth2" ? /* @__PURE__ */ jsx(
|
|
562
|
+
OAuthSecretRow,
|
|
563
|
+
{
|
|
564
|
+
secret: s,
|
|
565
|
+
onReauthorize: () => setReauthorizing(s),
|
|
566
|
+
onDelete: () => handleDelete(s.key)
|
|
567
|
+
},
|
|
568
|
+
s.key
|
|
569
|
+
) : /* @__PURE__ */ jsx(
|
|
512
570
|
SecretRow,
|
|
513
571
|
{
|
|
514
572
|
label: s.key,
|