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
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
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,
|
|
@@ -183,7 +183,7 @@ function ProviderCombobox({ value, onChange, inputClass }) {
|
|
|
183
183
|
// Unified add secret dialog (manual + OAuth modes)
|
|
184
184
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
185
185
|
|
|
186
|
-
function AddSecretDialog({ open, onAdd, onCancel, onOAuthSuccess }) {
|
|
186
|
+
function AddSecretDialog({ open, onAdd, onCancel, onOAuthSuccess, editingSecret }) {
|
|
187
187
|
// Shared state
|
|
188
188
|
const [mode, setMode] = useState('manual');
|
|
189
189
|
const [name, setName] = useState('');
|
|
@@ -209,8 +209,8 @@ function AddSecretDialog({ open, onAdd, onCancel, onOAuthSuccess }) {
|
|
|
209
209
|
// Reset all state on open
|
|
210
210
|
useEffect(() => {
|
|
211
211
|
if (open) {
|
|
212
|
-
setMode('manual');
|
|
213
|
-
setName('');
|
|
212
|
+
setMode(editingSecret ? 'oauth' : 'manual');
|
|
213
|
+
setName(editingSecret?.key || '');
|
|
214
214
|
setError(null);
|
|
215
215
|
// Manual
|
|
216
216
|
setValue('');
|
|
@@ -224,7 +224,7 @@ function AddSecretDialog({ open, onAdd, onCancel, onOAuthSuccess }) {
|
|
|
224
224
|
setStatus('form');
|
|
225
225
|
setCopied(false);
|
|
226
226
|
setRedirectUri(`${window.location.origin}/api/oauth/callback`);
|
|
227
|
-
setTimeout(() => nameRef.current?.focus(), 50);
|
|
227
|
+
if (!editingSecret) setTimeout(() => nameRef.current?.focus(), 50);
|
|
228
228
|
}
|
|
229
229
|
return () => {
|
|
230
230
|
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
@@ -344,7 +344,7 @@ function AddSecretDialog({ open, onAdd, onCancel, onOAuthSuccess }) {
|
|
|
344
344
|
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';
|
|
345
345
|
|
|
346
346
|
return (
|
|
347
|
-
<Dialog open={open} onClose={onCancel} title=
|
|
347
|
+
<Dialog open={open} onClose={onCancel} title={editingSecret ? 'Re-authorize Secret' : 'Add Secret'}>
|
|
348
348
|
{status === 'success' ? (
|
|
349
349
|
<div className="flex items-center justify-center gap-2 py-8 text-green-500">
|
|
350
350
|
<CheckIcon size={20} />
|
|
@@ -359,31 +359,33 @@ function AddSecretDialog({ open, onAdd, onCancel, onOAuthSuccess }) {
|
|
|
359
359
|
) : (
|
|
360
360
|
<>
|
|
361
361
|
<div className="space-y-3">
|
|
362
|
-
{/* Mode toggle */}
|
|
363
|
-
|
|
364
|
-
<
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
362
|
+
{/* Mode toggle — hidden when re-authorizing */}
|
|
363
|
+
{!editingSecret && (
|
|
364
|
+
<div className="flex rounded-md border border-border overflow-hidden">
|
|
365
|
+
<button
|
|
366
|
+
type="button"
|
|
367
|
+
onClick={() => handleModeChange('manual')}
|
|
368
|
+
className={`flex-1 px-3 py-1.5 text-xs font-medium transition-colors ${
|
|
369
|
+
mode === 'manual'
|
|
370
|
+
? 'bg-foreground text-background'
|
|
371
|
+
: 'text-muted-foreground hover:text-foreground hover:bg-accent'
|
|
372
|
+
}`}
|
|
373
|
+
>
|
|
374
|
+
Manual
|
|
375
|
+
</button>
|
|
376
|
+
<button
|
|
377
|
+
type="button"
|
|
378
|
+
onClick={() => handleModeChange('oauth')}
|
|
379
|
+
className={`flex-1 px-3 py-1.5 text-xs font-medium transition-colors ${
|
|
380
|
+
mode === 'oauth'
|
|
381
|
+
? 'bg-foreground text-background'
|
|
382
|
+
: 'text-muted-foreground hover:text-foreground hover:bg-accent'
|
|
383
|
+
}`}
|
|
384
|
+
>
|
|
385
|
+
OAuth
|
|
386
|
+
</button>
|
|
387
|
+
</div>
|
|
388
|
+
)}
|
|
387
389
|
|
|
388
390
|
{/* Secret name — shared */}
|
|
389
391
|
<div>
|
|
@@ -394,7 +396,8 @@ function AddSecretDialog({ open, onAdd, onCancel, onOAuthSuccess }) {
|
|
|
394
396
|
value={name}
|
|
395
397
|
onChange={(e) => setName(e.target.value.toUpperCase().replace(/[^A-Z0-9_]/g, ''))}
|
|
396
398
|
placeholder={mode === 'manual' ? 'e.g. GOOGLE_SERVICE_ACCOUNT_KEY' : 'e.g. GOOGLE_OAUTH_TOKEN'}
|
|
397
|
-
className={inputClass}
|
|
399
|
+
className={`${inputClass}${editingSecret ? ' text-muted-foreground bg-muted' : ''}`}
|
|
400
|
+
readOnly={!!editingSecret}
|
|
398
401
|
onKeyDown={(e) => e.key === 'Enter' && mode === 'manual' && handleSave()}
|
|
399
402
|
/>
|
|
400
403
|
</div>
|
|
@@ -522,6 +525,53 @@ function AddSecretDialog({ open, onAdd, onCancel, onOAuthSuccess }) {
|
|
|
522
525
|
);
|
|
523
526
|
}
|
|
524
527
|
|
|
528
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
529
|
+
// OAuth secret row — Re-authorize instead of inline edit
|
|
530
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
531
|
+
|
|
532
|
+
function OAuthSecretRow({ secret, onReauthorize, onDelete }) {
|
|
533
|
+
const [confirmDelete, setConfirmDelete] = useState(false);
|
|
534
|
+
|
|
535
|
+
const handleDelete = async () => {
|
|
536
|
+
if (!confirmDelete) {
|
|
537
|
+
setConfirmDelete(true);
|
|
538
|
+
setTimeout(() => setConfirmDelete(false), 3000);
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
await onDelete();
|
|
542
|
+
setConfirmDelete(false);
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
return (
|
|
546
|
+
<div className="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between py-3">
|
|
547
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
548
|
+
<span className="text-sm font-medium font-mono">{secret.key}</span>
|
|
549
|
+
<span className="text-xs text-muted-foreground">OAuth</span>
|
|
550
|
+
<StatusBadge isSet={secret.isSet} />
|
|
551
|
+
</div>
|
|
552
|
+
<div className="flex items-center gap-1.5 shrink-0 self-start sm:self-auto">
|
|
553
|
+
<button
|
|
554
|
+
onClick={onReauthorize}
|
|
555
|
+
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"
|
|
556
|
+
>
|
|
557
|
+
Re-authorize
|
|
558
|
+
</button>
|
|
559
|
+
<button
|
|
560
|
+
onClick={handleDelete}
|
|
561
|
+
className={`rounded-md p-1.5 text-xs border transition-colors ${
|
|
562
|
+
confirmDelete
|
|
563
|
+
? 'border-destructive text-destructive hover:bg-destructive/10'
|
|
564
|
+
: 'border-border text-muted-foreground hover:text-destructive hover:border-destructive'
|
|
565
|
+
}`}
|
|
566
|
+
title={confirmDelete ? 'Click again to confirm' : 'Delete'}
|
|
567
|
+
>
|
|
568
|
+
<TrashIcon size={12} />
|
|
569
|
+
</button>
|
|
570
|
+
</div>
|
|
571
|
+
</div>
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
|
|
525
575
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
526
576
|
// Jobs page
|
|
527
577
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -530,6 +580,7 @@ export function JobsPage() {
|
|
|
530
580
|
const [secrets, setSecrets] = useState([]);
|
|
531
581
|
const [loading, setLoading] = useState(true);
|
|
532
582
|
const [showAdd, setShowAdd] = useState(false);
|
|
583
|
+
const [reauthorizing, setReauthorizing] = useState(null);
|
|
533
584
|
|
|
534
585
|
const loadSecrets = async () => {
|
|
535
586
|
try {
|
|
@@ -591,6 +642,13 @@ export function JobsPage() {
|
|
|
591
642
|
onCancel={() => setShowAdd(false)}
|
|
592
643
|
onOAuthSuccess={loadSecrets}
|
|
593
644
|
/>
|
|
645
|
+
<AddSecretDialog
|
|
646
|
+
open={!!reauthorizing}
|
|
647
|
+
onAdd={handleAdd}
|
|
648
|
+
onCancel={() => setReauthorizing(null)}
|
|
649
|
+
onOAuthSuccess={loadSecrets}
|
|
650
|
+
editingSecret={reauthorizing}
|
|
651
|
+
/>
|
|
594
652
|
{secrets.length === 0 ? (
|
|
595
653
|
<EmptyState
|
|
596
654
|
message="No job secrets configured yet."
|
|
@@ -600,7 +658,14 @@ export function JobsPage() {
|
|
|
600
658
|
) : (
|
|
601
659
|
<div className="rounded-lg border bg-card p-4">
|
|
602
660
|
<div className="divide-y divide-border">
|
|
603
|
-
{secrets.map((s) => (
|
|
661
|
+
{secrets.map((s) => s.secretType === 'oauth2' ? (
|
|
662
|
+
<OAuthSecretRow
|
|
663
|
+
key={s.key}
|
|
664
|
+
secret={s}
|
|
665
|
+
onReauthorize={() => setReauthorizing(s)}
|
|
666
|
+
onDelete={() => handleDelete(s.key)}
|
|
667
|
+
/>
|
|
668
|
+
) : (
|
|
604
669
|
<SecretRow
|
|
605
670
|
key={s.key}
|
|
606
671
|
label={s.key}
|
package/lib/code/code-page.js
CHANGED
|
@@ -4,6 +4,7 @@ import { useState, useCallback, useEffect, useRef } from "react";
|
|
|
4
4
|
import dynamic from "next/dynamic";
|
|
5
5
|
import { DndContext, closestCenter, PointerSensor, useSensor, useSensors } from "@dnd-kit/core";
|
|
6
6
|
import { SortableContext, horizontalListSortingStrategy, useSortable, arrayMove } from "@dnd-kit/sortable";
|
|
7
|
+
import { restrictToHorizontalAxis } from "@dnd-kit/modifiers";
|
|
7
8
|
import { CSS } from "@dnd-kit/utilities";
|
|
8
9
|
import { AppSidebar } from "../chat/components/app-sidebar.js";
|
|
9
10
|
import { SidebarProvider, SidebarInset } from "../chat/components/ui/sidebar.js";
|
|
@@ -276,7 +277,7 @@ function CodePage({ session, codeWorkspaceId }) {
|
|
|
276
277
|
closeTitle: "Close session"
|
|
277
278
|
}
|
|
278
279
|
),
|
|
279
|
-
/* @__PURE__ */ jsx(DndContext, { sensors, collisionDetection: closestCenter, onDragEnd: handleDragEnd, children: /* @__PURE__ */ jsx(SortableContext, { items: dynamicTabIds, strategy: horizontalListSortingStrategy, children: tabs.slice(1).map((tab) => /* @__PURE__ */ jsx(
|
|
280
|
+
/* @__PURE__ */ jsx(DndContext, { sensors, collisionDetection: closestCenter, onDragEnd: handleDragEnd, modifiers: [restrictToHorizontalAxis], autoScroll: false, children: /* @__PURE__ */ jsx(SortableContext, { items: dynamicTabIds, strategy: horizontalListSortingStrategy, children: tabs.slice(1).map((tab) => /* @__PURE__ */ jsx(
|
|
280
281
|
SortableTab,
|
|
281
282
|
{
|
|
282
283
|
tab,
|
package/lib/code/code-page.jsx
CHANGED
|
@@ -4,6 +4,7 @@ import { useState, useCallback, useEffect, useRef } from 'react';
|
|
|
4
4
|
import dynamic from 'next/dynamic';
|
|
5
5
|
import { DndContext, closestCenter, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
|
|
6
6
|
import { SortableContext, horizontalListSortingStrategy, useSortable, arrayMove } from '@dnd-kit/sortable';
|
|
7
|
+
import { restrictToHorizontalAxis } from '@dnd-kit/modifiers';
|
|
7
8
|
import { CSS } from '@dnd-kit/utilities';
|
|
8
9
|
import { AppSidebar } from '../chat/components/app-sidebar.js';
|
|
9
10
|
import { SidebarProvider, SidebarInset } from '../chat/components/ui/sidebar.js';
|
|
@@ -321,7 +322,7 @@ export default function CodePage({ session, codeWorkspaceId }) {
|
|
|
321
322
|
/>
|
|
322
323
|
|
|
323
324
|
{/* Dynamic tabs — draggable */}
|
|
324
|
-
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
|
325
|
+
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd} modifiers={[restrictToHorizontalAxis]} autoScroll={false}>
|
|
325
326
|
<SortableContext items={dynamicTabIds} strategy={horizontalListSortingStrategy}>
|
|
326
327
|
{tabs.slice(1).map((tab) => (
|
|
327
328
|
<SortableTab
|
|
@@ -6,7 +6,6 @@ import { FitAddon } from "@xterm/addon-fit";
|
|
|
6
6
|
import { SearchAddon } from "@xterm/addon-search";
|
|
7
7
|
import { WebLinksAddon } from "@xterm/addon-web-links";
|
|
8
8
|
import { SerializeAddon } from "@xterm/addon-serialize";
|
|
9
|
-
import { ClipboardAddon } from "@xterm/addon-clipboard";
|
|
10
9
|
import "@xterm/xterm/css/xterm.css";
|
|
11
10
|
import { SpinnerIcon, MicIcon } from "../chat/components/icons.js";
|
|
12
11
|
import { COMMAND_LABELS, CommandOutputDialog } from "../chat/components/code-mode-toggle.js";
|
|
@@ -171,7 +170,6 @@ function TerminalView({ codeWorkspaceId, wsPath, isActive = true, showToolbar =
|
|
|
171
170
|
term.loadAddon(searchAddon);
|
|
172
171
|
term.loadAddon(webLinksAddon);
|
|
173
172
|
term.loadAddon(serializeAddon);
|
|
174
|
-
term.loadAddon(new ClipboardAddon());
|
|
175
173
|
termRef.current = term;
|
|
176
174
|
fitAddonRef.current = fitAddon;
|
|
177
175
|
term.open(containerRef.current);
|
|
@@ -453,6 +451,11 @@ function TerminalView({ codeWorkspaceId, wsPath, isActive = true, showToolbar =
|
|
|
453
451
|
color: #ef4444 !important;
|
|
454
452
|
background: rgba(239,68,68,0.12) !important;
|
|
455
453
|
}
|
|
454
|
+
@media (max-width: 767px) {
|
|
455
|
+
.code-toolbar-right {
|
|
456
|
+
overflow-x: auto;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
456
459
|
.code-voice-dialog {
|
|
457
460
|
position: absolute;
|
|
458
461
|
bottom: 100%;
|
|
@@ -620,7 +623,7 @@ function TerminalView({ codeWorkspaceId, wsPath, isActive = true, showToolbar =
|
|
|
620
623
|
]
|
|
621
624
|
}
|
|
622
625
|
) }),
|
|
623
|
-
/* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 6
|
|
626
|
+
/* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 6 }, className: "code-toolbar-right scrollbar-hide", children: [
|
|
624
627
|
voiceAvailable && /* @__PURE__ */ jsxs("div", { ref: voiceDialogRef, style: { position: "relative" }, children: [
|
|
625
628
|
/* @__PURE__ */ jsx(
|
|
626
629
|
"button",
|
|
@@ -6,7 +6,6 @@ import { FitAddon } from '@xterm/addon-fit';
|
|
|
6
6
|
import { SearchAddon } from '@xterm/addon-search';
|
|
7
7
|
import { WebLinksAddon } from '@xterm/addon-web-links';
|
|
8
8
|
import { SerializeAddon } from '@xterm/addon-serialize';
|
|
9
|
-
import { ClipboardAddon } from '@xterm/addon-clipboard';
|
|
10
9
|
import '@xterm/xterm/css/xterm.css';
|
|
11
10
|
import { SpinnerIcon, MicIcon } from '../chat/components/icons.js';
|
|
12
11
|
import { COMMAND_LABELS, CommandOutputDialog } from '../chat/components/code-mode-toggle.js';
|
|
@@ -204,7 +203,6 @@ export default function TerminalView({ codeWorkspaceId, wsPath, isActive = true,
|
|
|
204
203
|
term.loadAddon(searchAddon);
|
|
205
204
|
term.loadAddon(webLinksAddon);
|
|
206
205
|
term.loadAddon(serializeAddon);
|
|
207
|
-
term.loadAddon(new ClipboardAddon());
|
|
208
206
|
|
|
209
207
|
termRef.current = term;
|
|
210
208
|
fitAddonRef.current = fitAddon;
|
|
@@ -524,6 +522,11 @@ export default function TerminalView({ codeWorkspaceId, wsPath, isActive = true,
|
|
|
524
522
|
color: #ef4444 !important;
|
|
525
523
|
background: rgba(239,68,68,0.12) !important;
|
|
526
524
|
}
|
|
525
|
+
@media (max-width: 767px) {
|
|
526
|
+
.code-toolbar-right {
|
|
527
|
+
overflow-x: auto;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
527
530
|
.code-voice-dialog {
|
|
528
531
|
position: absolute;
|
|
529
532
|
bottom: 100%;
|
|
@@ -694,7 +697,7 @@ export default function TerminalView({ codeWorkspaceId, wsPath, isActive = true,
|
|
|
694
697
|
{themeLabel}
|
|
695
698
|
</button>
|
|
696
699
|
</div>
|
|
697
|
-
<div style={{ display: 'flex', alignItems: 'center', gap: 6
|
|
700
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }} className="code-toolbar-right scrollbar-hide">
|
|
698
701
|
{voiceAvailable && (
|
|
699
702
|
<div ref={voiceDialogRef} style={{ position: 'relative' }}>
|
|
700
703
|
<button
|
package/lib/db/api-keys.js
CHANGED
|
@@ -32,15 +32,25 @@ function _ensureCache() {
|
|
|
32
32
|
if (_cache !== null) return _cache;
|
|
33
33
|
|
|
34
34
|
const db = getDb();
|
|
35
|
+
const ONE_HOUR = 60 * 60 * 1000;
|
|
36
|
+
const cutoff = Date.now() - ONE_HOUR;
|
|
37
|
+
|
|
35
38
|
const rows = db
|
|
36
39
|
.select()
|
|
37
40
|
.from(settings)
|
|
38
41
|
.where(eq(settings.type, 'api_key'))
|
|
39
42
|
.all();
|
|
40
43
|
|
|
41
|
-
|
|
44
|
+
const jobKeyRows = db
|
|
45
|
+
.select()
|
|
46
|
+
.from(settings)
|
|
47
|
+
.where(eq(settings.type, 'agent_job_api_key'))
|
|
48
|
+
.all()
|
|
49
|
+
.filter(r => r.lastUsedAt !== null ? r.lastUsedAt > cutoff : r.createdAt > cutoff);
|
|
50
|
+
|
|
51
|
+
_cache = [...rows, ...jobKeyRows].map((row) => {
|
|
42
52
|
const parsed = JSON.parse(row.value);
|
|
43
|
-
return { id: row.id, keyHash: parsed.key_hash };
|
|
53
|
+
return { id: row.id, keyHash: parsed.key_hash, type: row.type };
|
|
44
54
|
});
|
|
45
55
|
|
|
46
56
|
return _cache;
|
|
@@ -181,6 +191,29 @@ export function verifyApiKey(rawKey) {
|
|
|
181
191
|
return null;
|
|
182
192
|
}
|
|
183
193
|
|
|
194
|
+
/**
|
|
195
|
+
* Create a per-job API key for an agent job container.
|
|
196
|
+
* Stored as type 'agent_job_api_key'. Valid for 1 hour since last use.
|
|
197
|
+
* @param {string} jobId - Agent job ID (stored as key column for traceability)
|
|
198
|
+
* @returns {{ key: string }} The raw API key to inject into the container
|
|
199
|
+
*/
|
|
200
|
+
export function createAgentJobApiKey(jobId) {
|
|
201
|
+
const db = getDb();
|
|
202
|
+
const key = generateApiKey();
|
|
203
|
+
const keyHash = hashApiKey(key);
|
|
204
|
+
const now = Date.now();
|
|
205
|
+
db.insert(settings).values({
|
|
206
|
+
id: randomUUID(),
|
|
207
|
+
type: 'agent_job_api_key',
|
|
208
|
+
key: jobId,
|
|
209
|
+
value: JSON.stringify({ key_hash: keyHash }),
|
|
210
|
+
createdAt: now,
|
|
211
|
+
updatedAt: now,
|
|
212
|
+
}).run();
|
|
213
|
+
invalidateApiKeyCache();
|
|
214
|
+
return { key };
|
|
215
|
+
}
|
|
216
|
+
|
|
184
217
|
/**
|
|
185
218
|
* Backfill lastUsedAt column from JSON value for existing api_key rows.
|
|
186
219
|
* Idempotent — only processes rows that still have last_used_at in JSON.
|
package/lib/db/config.js
CHANGED
|
@@ -272,11 +272,23 @@ export function deleteAgentJobSecret(key) {
|
|
|
272
272
|
export function listAgentJobSecrets() {
|
|
273
273
|
const db = getDb();
|
|
274
274
|
const rows = db
|
|
275
|
-
.select({ key: settings.key, updatedAt: settings.updatedAt })
|
|
275
|
+
.select({ key: settings.key, value: settings.value, updatedAt: settings.updatedAt })
|
|
276
276
|
.from(settings)
|
|
277
277
|
.where(eq(settings.type, 'agent_job_secret'))
|
|
278
278
|
.all();
|
|
279
|
-
return rows.map((r) =>
|
|
279
|
+
return rows.map((r) => {
|
|
280
|
+
let secretType = 'manual';
|
|
281
|
+
try {
|
|
282
|
+
const decrypted = decrypt(JSON.parse(r.value));
|
|
283
|
+
try {
|
|
284
|
+
const parsed = JSON.parse(decrypted);
|
|
285
|
+
if (parsed.type === 'oauth2' || parsed.type === 'oauth_token') {
|
|
286
|
+
secretType = parsed.type;
|
|
287
|
+
}
|
|
288
|
+
} catch {}
|
|
289
|
+
} catch {}
|
|
290
|
+
return { key: r.key, isSet: true, updatedAt: r.updatedAt, secretType };
|
|
291
|
+
});
|
|
280
292
|
}
|
|
281
293
|
|
|
282
294
|
/**
|
|
@@ -292,13 +304,37 @@ export function getAllAgentJobSecrets() {
|
|
|
292
304
|
.all();
|
|
293
305
|
return rows.map((r) => {
|
|
294
306
|
try {
|
|
295
|
-
|
|
296
|
-
|
|
307
|
+
const decrypted = decrypt(JSON.parse(r.value));
|
|
308
|
+
try {
|
|
309
|
+
const parsed = JSON.parse(decrypted);
|
|
310
|
+
if (parsed.type === 'oauth2' || parsed.type === 'oauth_token') {
|
|
311
|
+
return { key: r.key, value: null };
|
|
312
|
+
}
|
|
313
|
+
} catch {}
|
|
314
|
+
return { key: r.key, value: decrypted };
|
|
315
|
+
} catch (err) {
|
|
316
|
+
console.warn(`[secrets] failed to decrypt agent secret "${r.key}":`, err.message);
|
|
297
317
|
return null;
|
|
298
318
|
}
|
|
299
319
|
}).filter(Boolean);
|
|
300
320
|
}
|
|
301
321
|
|
|
322
|
+
/**
|
|
323
|
+
* Get a single agent job secret, decrypted. Returns the raw stored string (may be JSON).
|
|
324
|
+
* @param {string} key
|
|
325
|
+
* @returns {string|null}
|
|
326
|
+
*/
|
|
327
|
+
export function getAgentJobSecretRaw(key) {
|
|
328
|
+
const db = getDb();
|
|
329
|
+
const row = db
|
|
330
|
+
.select()
|
|
331
|
+
.from(settings)
|
|
332
|
+
.where(and(eq(settings.type, 'agent_job_secret'), eq(settings.key, key)))
|
|
333
|
+
.get();
|
|
334
|
+
if (!row) return null;
|
|
335
|
+
try { return decrypt(JSON.parse(row.value)); } catch { return null; }
|
|
336
|
+
}
|
|
337
|
+
|
|
302
338
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
303
339
|
// Migration: import env vars to DB on first run
|
|
304
340
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import cron from 'node-cron';
|
|
2
|
+
import { eq } from 'drizzle-orm';
|
|
3
|
+
import { getDb } from './db/index.js';
|
|
4
|
+
import { settings } from './db/schema.js';
|
|
5
|
+
import { invalidateApiKeyCache } from './db/api-keys.js';
|
|
6
|
+
|
|
7
|
+
const ONE_HOUR = 60 * 60 * 1000;
|
|
8
|
+
|
|
9
|
+
function cleanExpiredAgentJobKeys() {
|
|
10
|
+
try {
|
|
11
|
+
const db = getDb();
|
|
12
|
+
const cutoff = Date.now() - ONE_HOUR;
|
|
13
|
+
const rows = db
|
|
14
|
+
.select({ id: settings.id, lastUsedAt: settings.lastUsedAt, createdAt: settings.createdAt })
|
|
15
|
+
.from(settings)
|
|
16
|
+
.where(eq(settings.type, 'agent_job_api_key'))
|
|
17
|
+
.all();
|
|
18
|
+
const expiredIds = rows
|
|
19
|
+
.filter(r => r.lastUsedAt !== null ? r.lastUsedAt < cutoff : r.createdAt < cutoff)
|
|
20
|
+
.map(r => r.id);
|
|
21
|
+
if (expiredIds.length > 0) {
|
|
22
|
+
for (const id of expiredIds) {
|
|
23
|
+
db.delete(settings).where(eq(settings.id, id)).run();
|
|
24
|
+
}
|
|
25
|
+
invalidateApiKeyCache();
|
|
26
|
+
console.log(`[maintenance] Deleted ${expiredIds.length} expired agent job key(s)`);
|
|
27
|
+
}
|
|
28
|
+
} catch (err) {
|
|
29
|
+
console.error('[maintenance] cleanExpiredAgentJobKeys failed:', err);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function startMaintenanceCron() {
|
|
34
|
+
cron.schedule('0 * * * *', cleanExpiredAgentJobKeys);
|
|
35
|
+
}
|
package/lib/oauth/helper.js
CHANGED
|
@@ -56,3 +56,37 @@ export async function exchangeCodeForToken({ code, clientId, clientSecret, token
|
|
|
56
56
|
|
|
57
57
|
return data;
|
|
58
58
|
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Refresh an OAuth2 access token using a refresh token.
|
|
62
|
+
*
|
|
63
|
+
* POSTs to the provider's token endpoint with grant_type=refresh_token.
|
|
64
|
+
* Returns the full JSON response (new access_token, possibly new refresh_token, etc.).
|
|
65
|
+
*/
|
|
66
|
+
export async function refreshOAuthToken({ refreshToken, clientId, clientSecret, tokenUrl }) {
|
|
67
|
+
const body = new URLSearchParams({
|
|
68
|
+
grant_type: 'refresh_token',
|
|
69
|
+
refresh_token: refreshToken,
|
|
70
|
+
client_id: clientId,
|
|
71
|
+
client_secret: clientSecret,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const response = await fetch(tokenUrl, {
|
|
75
|
+
method: 'POST',
|
|
76
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
77
|
+
body: body.toString(),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const data = await response.json();
|
|
81
|
+
|
|
82
|
+
if (!response.ok) {
|
|
83
|
+
const errorMsg = data.error_description || data.error || 'Token refresh failed';
|
|
84
|
+
throw new Error(errorMsg);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!data.access_token) {
|
|
88
|
+
throw new Error('No access_token in refresh response');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return data;
|
|
92
|
+
}
|
|
@@ -3,6 +3,7 @@ import { z } from 'zod';
|
|
|
3
3
|
import { githubApi } from './github.js';
|
|
4
4
|
import { createModel } from '../ai/model.js';
|
|
5
5
|
import { getConfig } from '../config.js';
|
|
6
|
+
import { createAgentJobApiKey } from '../db/api-keys.js';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Generate a short descriptive title for an agent job using the LLM.
|
|
@@ -92,6 +93,7 @@ async function createAgentJob(agentJobDescription, options = {}) {
|
|
|
92
93
|
|
|
93
94
|
// 6. Launch Docker container locally (fire-and-forget with async cleanup)
|
|
94
95
|
const repoSlug = `${GH_OWNER}/${GH_REPO}`;
|
|
96
|
+
const { key: agentJobToken } = createAgentJobApiKey(agentJobId);
|
|
95
97
|
launchAgentJobContainer({
|
|
96
98
|
agentJobId,
|
|
97
99
|
repo: repoSlug,
|
|
@@ -100,6 +102,7 @@ async function createAgentJob(agentJobDescription, options = {}) {
|
|
|
100
102
|
description: agentJobDescription,
|
|
101
103
|
codingAgent: options.agentBackend,
|
|
102
104
|
llmModel: options.llmModel,
|
|
105
|
+
agentJobToken,
|
|
103
106
|
}).catch(err => {
|
|
104
107
|
console.error(`[agent-job] Failed to launch container for ${agentJobId}:`, err.message);
|
|
105
108
|
});
|
package/lib/tools/docker.js
CHANGED
|
@@ -227,7 +227,7 @@ async function runInteractiveContainer({ containerName, repo, branch, codingAgen
|
|
|
227
227
|
const { getAllAgentJobSecrets } = await import('../db/config.js');
|
|
228
228
|
const jobSecrets = getAllAgentJobSecrets();
|
|
229
229
|
for (const { key, value } of jobSecrets) {
|
|
230
|
-
if (!env.some(e => e.startsWith(`${key}=`))) {
|
|
230
|
+
if (value !== null && !env.some(e => e.startsWith(`${key}=`))) {
|
|
231
231
|
env.push(`${key}=${value}`);
|
|
232
232
|
}
|
|
233
233
|
}
|
|
@@ -434,7 +434,7 @@ async function runHeadlessContainer({ containerName, repo, branch, featureBranch
|
|
|
434
434
|
const { getAllAgentJobSecrets } = await import('../db/config.js');
|
|
435
435
|
const jobSecrets = getAllAgentJobSecrets();
|
|
436
436
|
for (const { key, value } of jobSecrets) {
|
|
437
|
-
if (!env.some(e => e.startsWith(`${key}=`))) {
|
|
437
|
+
if (value !== null && !env.some(e => e.startsWith(`${key}=`))) {
|
|
438
438
|
env.push(`${key}=${value}`);
|
|
439
439
|
}
|
|
440
440
|
}
|
|
@@ -851,7 +851,7 @@ async function removeVolume(name) {
|
|
|
851
851
|
* @param {string} [options.llmModel] - Model override
|
|
852
852
|
* @returns {Promise<{containerId: string, containerName: string, volumeName: string}>}
|
|
853
853
|
*/
|
|
854
|
-
async function runAgentJobContainer({ agentJobId, repo, branch, title, description, codingAgent, llmModel }) {
|
|
854
|
+
async function runAgentJobContainer({ agentJobId, repo, branch, title, description, codingAgent, llmModel, agentJobToken }) {
|
|
855
855
|
const agent = codingAgent || getConfig('CODING_AGENT') || 'claude-code';
|
|
856
856
|
const version = process.env.THEPOPEBOT_VERSION;
|
|
857
857
|
const image = `stephengpope/thepopebot:coding-agent-${agent}-${version}`;
|
|
@@ -885,11 +885,18 @@ async function runAgentJobContainer({ agentJobId, repo, branch, title, descripti
|
|
|
885
885
|
env.push(`GH_TOKEN=${ghToken}`);
|
|
886
886
|
}
|
|
887
887
|
|
|
888
|
-
// Inject
|
|
888
|
+
// Inject APP_URL so agents can call back to the event handler
|
|
889
|
+
const appUrl = getConfig('APP_URL');
|
|
890
|
+
if (appUrl) env.push(`APP_URL=${appUrl}`);
|
|
891
|
+
|
|
892
|
+
// Inject per-job API token for agent-secrets skill
|
|
893
|
+
if (agentJobToken) env.push(`AGENT_JOB_TOKEN=${agentJobToken}`);
|
|
894
|
+
|
|
895
|
+
// Inject agent job secrets (plain secrets as env vars; oauth types are null — agent must fetch via get)
|
|
889
896
|
const { getAllAgentJobSecrets } = await import('../db/config.js');
|
|
890
897
|
const jobSecrets = getAllAgentJobSecrets();
|
|
891
898
|
for (const { key, value } of jobSecrets) {
|
|
892
|
-
if (!env.some(e => e.startsWith(`${key}=`))) {
|
|
899
|
+
if (value !== null && !env.some(e => e.startsWith(`${key}=`))) {
|
|
893
900
|
env.push(`${key}=${value}`);
|
|
894
901
|
}
|
|
895
902
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thepopebot",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.75-beta.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Create autonomous AI agents with a two-layer architecture: Next.js Event Handler + Docker Agent.",
|
|
6
6
|
"bin": {
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"./middleware": "./lib/auth/middleware.js",
|
|
33
33
|
"./oauth": "./lib/oauth/helper.js",
|
|
34
34
|
"./oauth/providers": "./lib/oauth/providers.js",
|
|
35
|
+
"./maintenance": "./lib/maintenance.js",
|
|
35
36
|
"./package.json": "./package.json"
|
|
36
37
|
},
|
|
37
38
|
"files": [
|
|
@@ -68,6 +69,7 @@
|
|
|
68
69
|
"@ai-sdk/react": "^2.0.0",
|
|
69
70
|
"@clack/prompts": "^0.10.0",
|
|
70
71
|
"@dnd-kit/core": "^6.3.1",
|
|
72
|
+
"@dnd-kit/modifiers": "^9.0.0",
|
|
71
73
|
"@dnd-kit/sortable": "^10.0.0",
|
|
72
74
|
"@grammyjs/parse-mode": "^2.2.0",
|
|
73
75
|
"@langchain/anthropic": "^1.3.17",
|
|
@@ -77,7 +79,6 @@
|
|
|
77
79
|
"@langchain/langgraph-checkpoint-sqlite": "^1.0.1",
|
|
78
80
|
"@langchain/openai": "^1.2.7",
|
|
79
81
|
"@monaco-editor/react": "^4.7.0",
|
|
80
|
-
"@xterm/addon-clipboard": "^0.2.0",
|
|
81
82
|
"@xterm/addon-fit": "^0.10.0",
|
|
82
83
|
"@xterm/addon-search": "^0.15.0",
|
|
83
84
|
"@xterm/addon-serialize": "^0.13.0",
|