thepopebot 1.2.75-beta.2 → 1.2.75-beta.21
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 +1 -1
- package/api/CLAUDE.md +1 -1
- package/api/index.js +5 -12
- package/bin/CLAUDE.md +1 -1
- package/bin/cli.js +329 -14
- package/bin/docker-build.js +5 -0
- package/bin/managed-paths.js +0 -7
- package/bin/sync.js +84 -0
- package/config/CLAUDE.md +1 -29
- package/config/instrumentation.js +1 -1
- package/lib/CLAUDE.md +3 -3
- package/lib/ai/CLAUDE.md +24 -3
- package/lib/ai/agent.js +8 -5
- package/lib/ai/async-channel.js +51 -0
- package/lib/ai/headless-stream.js +3 -0
- package/lib/ai/index.js +149 -173
- package/lib/ai/line-mappers.js +72 -9
- package/lib/ai/tools.js +40 -28
- package/lib/chat/actions.js +34 -6
- package/lib/chat/api.js +17 -1
- package/lib/chat/components/chat-header.js +4 -0
- package/lib/chat/components/chat-header.jsx +4 -0
- package/lib/chat/components/chat-input.js +1 -0
- package/lib/chat/components/chat-input.jsx +1 -0
- package/lib/chat/components/chat.js +9 -1
- package/lib/chat/components/chat.jsx +15 -2
- package/lib/chat/components/chats-page.js +3 -3
- package/lib/chat/components/chats-page.jsx +4 -6
- package/lib/chat/components/crons-page.js +1 -1
- package/lib/chat/components/crons-page.jsx +1 -1
- package/lib/chat/components/message.js +12 -4
- package/lib/chat/components/message.jsx +17 -4
- package/lib/chat/components/settings-chat-page.js +2 -1
- package/lib/chat/components/settings-chat-page.jsx +4 -1
- package/lib/chat/components/settings-coding-agents-page.js +139 -1
- package/lib/chat/components/settings-coding-agents-page.jsx +160 -0
- package/lib/chat/components/settings-jobs-page.js +13 -2
- package/lib/chat/components/settings-jobs-page.jsx +15 -1
- package/lib/chat/components/settings-secrets-layout.js +1 -1
- package/lib/chat/components/settings-secrets-layout.jsx +1 -1
- package/lib/chat/components/sidebar-history-item.js +3 -3
- package/lib/chat/components/sidebar-history-item.jsx +4 -6
- package/lib/chat/components/triggers-page.js +1 -1
- package/lib/chat/components/triggers-page.jsx +1 -1
- package/lib/cluster/actions.js +4 -4
- package/lib/cluster/execute.js +3 -1
- package/lib/code/actions.js +34 -11
- package/lib/code/code-page.js +40 -40
- package/lib/code/code-page.jsx +36 -36
- package/lib/code/port-forwards.js +17 -3
- package/lib/code/terminal-view.js +16 -0
- package/lib/code/terminal-view.jsx +18 -0
- package/lib/config.js +4 -0
- package/lib/cron.js +3 -3
- package/lib/db/api-keys.js +22 -61
- package/lib/db/config.js +23 -0
- package/lib/db/index.js +3 -1
- package/lib/maintenance.js +34 -11
- package/lib/paths.js +1 -38
- package/lib/tools/create-agent-job.js +0 -4
- package/lib/tools/docker.js +23 -16
- package/lib/triggers.js +4 -3
- package/lib/utils/render-md.js +3 -1
- package/package.json +2 -1
- package/setup/setup-ssl.mjs +414 -0
- package/templates/.github/workflows/rebuild-event-handler.yml +3 -0
- package/templates/.github/workflows/upgrade-event-handler.yml +1 -1
- package/templates/.gitignore.template +7 -3
- package/templates/.tmp/CLAUDE.md.template +5 -0
- package/templates/CLAUDE.md +3 -2
- package/templates/CLAUDE.md.template +24 -357
- package/templates/agent-job/CLAUDE.md.template +57 -0
- package/templates/agent-job/CRONS.json +16 -0
- package/templates/{config/agent-job → agent-job}/SOUL.md +3 -3
- package/templates/agent-job/SYSTEM.md +60 -0
- package/templates/agents/CLAUDE.md.template +54 -0
- package/templates/data/CLAUDE.md.template +5 -0
- package/templates/docker-compose.custom.yml +41 -62
- package/templates/docker-compose.yml +14 -21
- package/templates/event-handler/CLAUDE.md.template +0 -0
- package/templates/logs/CLAUDE.md.template +5 -0
- package/templates/skills/CLAUDE.md.template +57 -32
- package/templates/skills/active/.gitkeep +0 -0
- package/templates/skills/library/agent-job-secrets/SKILL.md +23 -0
- package/templates/skills/library/agent-job-secrets/agent-job-secrets.js +62 -0
- package/templates/.pi/extensions/env-sanitizer/index.ts +0 -48
- package/templates/.pi/extensions/env-sanitizer/package.json +0 -5
- package/templates/README.md +0 -75
- package/templates/config/CLAUDE.md.template +0 -40
- package/templates/config/CRONS.json +0 -56
- package/templates/config/agent-job/AGENT_JOB.md +0 -30
- package/templates/cron/CLAUDE.md.template +0 -24
- package/templates/docker-compose.litellm.yml +0 -82
- package/templates/docs/CLAUDE.md.template +0 -12
- package/templates/docs/CLI.md +0 -59
- package/templates/docs/CLUSTERS.md +0 -151
- package/templates/docs/CONFIGURATION.md +0 -181
- package/templates/docs/CRONS_AND_TRIGGERS.md +0 -132
- package/templates/docs/GETTING_STARTED.md +0 -64
- package/templates/docs/SECURITY.md +0 -61
- package/templates/docs/SKILLS.md +0 -113
- package/templates/docs/UPGRADING.md +0 -92
- package/templates/skills/LICENSE +0 -21
- package/templates/skills/README.md +0 -117
- package/templates/skills/agent-job-secrets/SKILL.md +0 -25
- package/templates/skills/agent-job-secrets/agent-job-secrets.js +0 -66
- package/templates/traefik-dynamic.yml.example +0 -7
- package/templates/triggers/CLAUDE.md.template +0 -41
- /package/templates/{config → agent-job}/HEARTBEAT.md +0 -0
- /package/templates/{cron → data}/.gitkeep +0 -0
- /package/templates/{logs → data/clusters}/.gitkeep +0 -0
- /package/templates/{triggers → data/db}/.gitkeep +0 -0
- /package/templates/{config/agent-job → event-handler}/SUMMARY.md +0 -0
- /package/templates/{config → event-handler}/TRIGGERS.json +0 -0
- /package/templates/{config → event-handler}/agent-chat/SYSTEM.md +0 -0
- /package/templates/{config/cluster → event-handler/clusters}/ROLE.md +0 -0
- /package/templates/{config/cluster → event-handler/clusters}/SYSTEM.md +0 -0
- /package/templates/{config → event-handler}/code-chat/SYSTEM.md +0 -0
- /package/templates/{config → event-handler}/litellm/main.yaml +0 -0
- /package/templates/skills/{playwright-cli → library/playwright-cli}/SKILL.md +0 -0
package/lib/code/code-page.js
CHANGED
|
@@ -299,6 +299,45 @@ function CodePage({ session, codeWorkspaceId }) {
|
|
|
299
299
|
/* @__PURE__ */ jsx(SpinnerIcon, { size: 12 }),
|
|
300
300
|
/* @__PURE__ */ jsx("span", { children: "Editor..." })
|
|
301
301
|
] }),
|
|
302
|
+
portForwards.length > 0 && /* @__PURE__ */ jsx("div", { className: "flex items-center gap-1", children: portForwards.map((pf) => /* @__PURE__ */ jsxs(
|
|
303
|
+
"div",
|
|
304
|
+
{
|
|
305
|
+
className: "flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium font-mono text-muted-foreground shrink-0 whitespace-nowrap rounded-t-md border border-b-0 border-emerald-500/30 bg-emerald-500/5",
|
|
306
|
+
children: [
|
|
307
|
+
/* @__PURE__ */ jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-emerald-500" }),
|
|
308
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
309
|
+
":",
|
|
310
|
+
pf.port
|
|
311
|
+
] }),
|
|
312
|
+
/* @__PURE__ */ jsx(
|
|
313
|
+
"button",
|
|
314
|
+
{
|
|
315
|
+
className: "hover:text-emerald-400 transition-colors",
|
|
316
|
+
onClick: () => window.open(pf.url, "_blank"),
|
|
317
|
+
title: `Open ${pf.url}`,
|
|
318
|
+
children: /* @__PURE__ */ jsxs("svg", { width: "10", height: "10", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
319
|
+
/* @__PURE__ */ jsx("path", { d: "M7 3H3v10h10V9" }),
|
|
320
|
+
/* @__PURE__ */ jsx("path", { d: "M10 2h4v4" }),
|
|
321
|
+
/* @__PURE__ */ jsx("path", { d: "M14 2L7 9" })
|
|
322
|
+
] })
|
|
323
|
+
}
|
|
324
|
+
),
|
|
325
|
+
/* @__PURE__ */ jsx(
|
|
326
|
+
"button",
|
|
327
|
+
{
|
|
328
|
+
className: "hover:text-destructive transition-colors",
|
|
329
|
+
onClick: () => handleStopPort(pf.port),
|
|
330
|
+
title: "Stop forwarding",
|
|
331
|
+
children: /* @__PURE__ */ jsxs("svg", { width: "10", height: "10", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", children: [
|
|
332
|
+
/* @__PURE__ */ jsx("line", { x1: "4", y1: "4", x2: "12", y2: "12" }),
|
|
333
|
+
/* @__PURE__ */ jsx("line", { x1: "12", y1: "4", x2: "4", y2: "12" })
|
|
334
|
+
] })
|
|
335
|
+
}
|
|
336
|
+
)
|
|
337
|
+
]
|
|
338
|
+
},
|
|
339
|
+
pf.port
|
|
340
|
+
)) }),
|
|
302
341
|
/* @__PURE__ */ jsx("div", { className: "self-stretch my-1.5 mx-1 md:mx-4 w-px bg-border shrink-0" }),
|
|
303
342
|
/* @__PURE__ */ jsx(
|
|
304
343
|
"button",
|
|
@@ -341,46 +380,7 @@ function CodePage({ session, codeWorkspaceId }) {
|
|
|
341
380
|
title: "Forward a port",
|
|
342
381
|
children: "+ Port"
|
|
343
382
|
}
|
|
344
|
-
)
|
|
345
|
-
portForwards.length > 0 && /* @__PURE__ */ jsx("div", { className: "flex items-center gap-1 ml-auto", children: portForwards.map((pf) => /* @__PURE__ */ jsxs(
|
|
346
|
-
"div",
|
|
347
|
-
{
|
|
348
|
-
className: "flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium font-mono text-muted-foreground shrink-0 whitespace-nowrap rounded-t-md border border-b-0 border-emerald-500/30 bg-emerald-500/5",
|
|
349
|
-
children: [
|
|
350
|
-
/* @__PURE__ */ jsx("div", { className: "w-1.5 h-1.5 rounded-full bg-emerald-500" }),
|
|
351
|
-
/* @__PURE__ */ jsxs("span", { children: [
|
|
352
|
-
":",
|
|
353
|
-
pf.port
|
|
354
|
-
] }),
|
|
355
|
-
/* @__PURE__ */ jsx(
|
|
356
|
-
"button",
|
|
357
|
-
{
|
|
358
|
-
className: "hover:text-emerald-400 transition-colors",
|
|
359
|
-
onClick: () => window.open(pf.url, "_blank"),
|
|
360
|
-
title: `Open ${pf.url}`,
|
|
361
|
-
children: /* @__PURE__ */ jsxs("svg", { width: "10", height: "10", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
362
|
-
/* @__PURE__ */ jsx("path", { d: "M7 3H3v10h10V9" }),
|
|
363
|
-
/* @__PURE__ */ jsx("path", { d: "M10 2h4v4" }),
|
|
364
|
-
/* @__PURE__ */ jsx("path", { d: "M14 2L7 9" })
|
|
365
|
-
] })
|
|
366
|
-
}
|
|
367
|
-
),
|
|
368
|
-
/* @__PURE__ */ jsx(
|
|
369
|
-
"button",
|
|
370
|
-
{
|
|
371
|
-
className: "hover:text-destructive transition-colors",
|
|
372
|
-
onClick: () => handleStopPort(pf.port),
|
|
373
|
-
title: "Stop forwarding",
|
|
374
|
-
children: /* @__PURE__ */ jsxs("svg", { width: "10", height: "10", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", children: [
|
|
375
|
-
/* @__PURE__ */ jsx("line", { x1: "4", y1: "4", x2: "12", y2: "12" }),
|
|
376
|
-
/* @__PURE__ */ jsx("line", { x1: "12", y1: "4", x2: "4", y2: "12" })
|
|
377
|
-
] })
|
|
378
|
-
}
|
|
379
|
-
)
|
|
380
|
-
]
|
|
381
|
-
},
|
|
382
|
-
pf.port
|
|
383
|
-
)) })
|
|
383
|
+
)
|
|
384
384
|
] }),
|
|
385
385
|
/* @__PURE__ */ jsxs("div", { style: { position: "relative", flex: 1, minHeight: 0, display: "flex", flexDirection: "column" }, children: [
|
|
386
386
|
showDiff && /* @__PURE__ */ jsx("div", { style: { position: "absolute", inset: 0, zIndex: 20, display: "flex", flexDirection: "column" }, children: /* @__PURE__ */ jsx(DiffViewer, { workspaceId: codeWorkspaceId, diffStats, onClose: () => setShowDiff(false) }) }),
|
package/lib/code/code-page.jsx
CHANGED
|
@@ -356,6 +356,42 @@ export default function CodePage({ session, codeWorkspaceId }) {
|
|
|
356
356
|
</div>
|
|
357
357
|
)}
|
|
358
358
|
|
|
359
|
+
{/* Active port forwards */}
|
|
360
|
+
{portForwards.length > 0 && (
|
|
361
|
+
<div className="flex items-center gap-1">
|
|
362
|
+
{portForwards.map((pf) => (
|
|
363
|
+
<div
|
|
364
|
+
key={pf.port}
|
|
365
|
+
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium font-mono text-muted-foreground shrink-0 whitespace-nowrap rounded-t-md border border-b-0 border-emerald-500/30 bg-emerald-500/5"
|
|
366
|
+
>
|
|
367
|
+
<div className="w-1.5 h-1.5 rounded-full bg-emerald-500" />
|
|
368
|
+
<span>:{pf.port}</span>
|
|
369
|
+
<button
|
|
370
|
+
className="hover:text-emerald-400 transition-colors"
|
|
371
|
+
onClick={() => window.open(pf.url, '_blank')}
|
|
372
|
+
title={`Open ${pf.url}`}
|
|
373
|
+
>
|
|
374
|
+
<svg width="10" height="10" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
375
|
+
<path d="M7 3H3v10h10V9" />
|
|
376
|
+
<path d="M10 2h4v4" />
|
|
377
|
+
<path d="M14 2L7 9" />
|
|
378
|
+
</svg>
|
|
379
|
+
</button>
|
|
380
|
+
<button
|
|
381
|
+
className="hover:text-destructive transition-colors"
|
|
382
|
+
onClick={() => handleStopPort(pf.port)}
|
|
383
|
+
title="Stop forwarding"
|
|
384
|
+
>
|
|
385
|
+
<svg width="10" height="10" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
|
|
386
|
+
<line x1="4" y1="4" x2="12" y2="12" />
|
|
387
|
+
<line x1="12" y1="4" x2="4" y2="12" />
|
|
388
|
+
</svg>
|
|
389
|
+
</button>
|
|
390
|
+
</div>
|
|
391
|
+
))}
|
|
392
|
+
</div>
|
|
393
|
+
)}
|
|
394
|
+
|
|
359
395
|
{/* Divider between real tabs and + buttons */}
|
|
360
396
|
<div className="self-stretch my-1.5 mx-1 md:mx-4 w-px bg-border shrink-0" />
|
|
361
397
|
|
|
@@ -392,42 +428,6 @@ export default function CodePage({ session, codeWorkspaceId }) {
|
|
|
392
428
|
>
|
|
393
429
|
+ Port
|
|
394
430
|
</button>
|
|
395
|
-
|
|
396
|
-
{/* Active port forwards */}
|
|
397
|
-
{portForwards.length > 0 && (
|
|
398
|
-
<div className="flex items-center gap-1 ml-auto">
|
|
399
|
-
{portForwards.map((pf) => (
|
|
400
|
-
<div
|
|
401
|
-
key={pf.port}
|
|
402
|
-
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium font-mono text-muted-foreground shrink-0 whitespace-nowrap rounded-t-md border border-b-0 border-emerald-500/30 bg-emerald-500/5"
|
|
403
|
-
>
|
|
404
|
-
<div className="w-1.5 h-1.5 rounded-full bg-emerald-500" />
|
|
405
|
-
<span>:{pf.port}</span>
|
|
406
|
-
<button
|
|
407
|
-
className="hover:text-emerald-400 transition-colors"
|
|
408
|
-
onClick={() => window.open(pf.url, '_blank')}
|
|
409
|
-
title={`Open ${pf.url}`}
|
|
410
|
-
>
|
|
411
|
-
<svg width="10" height="10" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
412
|
-
<path d="M7 3H3v10h10V9" />
|
|
413
|
-
<path d="M10 2h4v4" />
|
|
414
|
-
<path d="M14 2L7 9" />
|
|
415
|
-
</svg>
|
|
416
|
-
</button>
|
|
417
|
-
<button
|
|
418
|
-
className="hover:text-destructive transition-colors"
|
|
419
|
-
onClick={() => handleStopPort(pf.port)}
|
|
420
|
-
title="Stop forwarding"
|
|
421
|
-
>
|
|
422
|
-
<svg width="10" height="10" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
|
|
423
|
-
<line x1="4" y1="4" x2="12" y2="12" />
|
|
424
|
-
<line x1="12" y1="4" x2="4" y2="12" />
|
|
425
|
-
</svg>
|
|
426
|
-
</button>
|
|
427
|
-
</div>
|
|
428
|
-
))}
|
|
429
|
-
</div>
|
|
430
|
-
)}
|
|
431
431
|
</div>
|
|
432
432
|
|
|
433
433
|
{/* Tab content panels — all mounted, hidden via display */}
|
|
@@ -98,7 +98,10 @@ export function clearWorkspaceForwards(workspaceId) {
|
|
|
98
98
|
|
|
99
99
|
/**
|
|
100
100
|
* Write Traefik dynamic config as YAML for all active port forwards.
|
|
101
|
-
*
|
|
101
|
+
*
|
|
102
|
+
* When SSL_DOMAIN is set (custom compose with Let's Encrypt), routes use
|
|
103
|
+
* *.SSL_DOMAIN on the websecure entrypoint with the letsencrypt cert resolver.
|
|
104
|
+
* Otherwise, routes use *.localhost on the web entrypoint (local dev).
|
|
102
105
|
*
|
|
103
106
|
* MUST be YAML, not JSON — Traefik's file provider treats backticks in
|
|
104
107
|
* JSON as Go template delimiters, silently breaking Host() rules.
|
|
@@ -125,14 +128,25 @@ function writeTraefikConfig() {
|
|
|
125
128
|
return;
|
|
126
129
|
}
|
|
127
130
|
|
|
131
|
+
const sslDomain = process.env.SSL_DOMAIN;
|
|
132
|
+
const entrypoint = sslDomain ? 'websecure' : 'web';
|
|
133
|
+
|
|
128
134
|
const lines = ['http:', ' routers:'];
|
|
129
135
|
for (const { workspaceId, port } of entries) {
|
|
130
136
|
const key = `wksp-${workspaceId}-${port}`;
|
|
131
|
-
const
|
|
137
|
+
const subdomain = `${workspaceId.slice(0, 8)}-${port}`;
|
|
138
|
+
const host = sslDomain ? `${subdomain}.${sslDomain}` : `${subdomain}.localhost`;
|
|
132
139
|
lines.push(` ${key}:`);
|
|
133
140
|
lines.push(' rule: Host(`' + host + '`)');
|
|
134
141
|
lines.push(' entryPoints:');
|
|
135
|
-
lines.push(
|
|
142
|
+
lines.push(` - ${entrypoint}`);
|
|
143
|
+
if (sslDomain) {
|
|
144
|
+
lines.push(' tls:');
|
|
145
|
+
lines.push(' certResolver: letsencrypt');
|
|
146
|
+
lines.push(' domains:');
|
|
147
|
+
lines.push(` - main: ${sslDomain}`);
|
|
148
|
+
lines.push(` sans: "*.${sslDomain}"`);
|
|
149
|
+
}
|
|
136
150
|
lines.push(` service: ${key}`);
|
|
137
151
|
}
|
|
138
152
|
lines.push(' services:');
|
|
@@ -54,6 +54,7 @@ function TerminalView({ codeWorkspaceId, wsPath, isActive = true, showToolbar =
|
|
|
54
54
|
const [voiceText, setVoiceText] = useState("");
|
|
55
55
|
const [partialText, setPartialText] = useState("");
|
|
56
56
|
const voiceDialogRef = useRef(null);
|
|
57
|
+
const voiceTextareaRef = useRef(null);
|
|
57
58
|
const volumeRef = useRef(0);
|
|
58
59
|
const { voiceAvailable, isConnecting, isRecording, startRecording, stopRecording } = useVoiceInput({
|
|
59
60
|
getToken: getVoiceToken,
|
|
@@ -318,6 +319,11 @@ function TerminalView({ codeWorkspaceId, wsPath, isActive = true, showToolbar =
|
|
|
318
319
|
document.addEventListener("keydown", handler);
|
|
319
320
|
return () => document.removeEventListener("keydown", handler);
|
|
320
321
|
}, [voiceDialogOpen, closeVoiceDialog]);
|
|
322
|
+
useEffect(() => {
|
|
323
|
+
if (voiceTextareaRef.current) {
|
|
324
|
+
voiceTextareaRef.current.scrollTop = voiceTextareaRef.current.scrollHeight;
|
|
325
|
+
}
|
|
326
|
+
}, [voiceText, partialText]);
|
|
321
327
|
const themeIcon = termTheme === "light" ? /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", children: [
|
|
322
328
|
/* @__PURE__ */ jsx("circle", { cx: "8", cy: "8", r: "3" }),
|
|
323
329
|
/* @__PURE__ */ jsx("line", { x1: "8", y1: "1", x2: "8", y2: "3" }),
|
|
@@ -638,6 +644,7 @@ function TerminalView({ codeWorkspaceId, wsPath, isActive = true, showToolbar =
|
|
|
638
644
|
/* @__PURE__ */ jsx(
|
|
639
645
|
"textarea",
|
|
640
646
|
{
|
|
647
|
+
ref: voiceTextareaRef,
|
|
641
648
|
rows: 6,
|
|
642
649
|
value: voiceText + (partialText ? (voiceText && !voiceText.endsWith(" ") ? " " : "") + partialText : ""),
|
|
643
650
|
onChange: (e) => {
|
|
@@ -675,6 +682,15 @@ function TerminalView({ codeWorkspaceId, wsPath, isActive = true, showToolbar =
|
|
|
675
682
|
] })
|
|
676
683
|
] })
|
|
677
684
|
] }),
|
|
685
|
+
diffStats?.currentBranch && /* @__PURE__ */ jsx(
|
|
686
|
+
"button",
|
|
687
|
+
{
|
|
688
|
+
className: "code-toolbar-btn",
|
|
689
|
+
style: { cursor: "default", opacity: 0.7, maxWidth: 140, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
|
|
690
|
+
title: diffStats.currentBranch,
|
|
691
|
+
children: diffStats.currentBranch
|
|
692
|
+
}
|
|
693
|
+
),
|
|
678
694
|
/* @__PURE__ */ jsx(
|
|
679
695
|
ToolbarCommandButton,
|
|
680
696
|
{
|
|
@@ -64,6 +64,7 @@ export default function TerminalView({ codeWorkspaceId, wsPath, isActive = true,
|
|
|
64
64
|
const [voiceText, setVoiceText] = useState('');
|
|
65
65
|
const [partialText, setPartialText] = useState('');
|
|
66
66
|
const voiceDialogRef = useRef(null);
|
|
67
|
+
const voiceTextareaRef = useRef(null);
|
|
67
68
|
const volumeRef = useRef(0);
|
|
68
69
|
|
|
69
70
|
const { voiceAvailable, isConnecting, isRecording, startRecording, stopRecording } = useVoiceInput({
|
|
@@ -378,6 +379,13 @@ export default function TerminalView({ codeWorkspaceId, wsPath, isActive = true,
|
|
|
378
379
|
return () => document.removeEventListener('keydown', handler);
|
|
379
380
|
}, [voiceDialogOpen, closeVoiceDialog]);
|
|
380
381
|
|
|
382
|
+
// Auto-scroll voice textarea to bottom as text grows
|
|
383
|
+
useEffect(() => {
|
|
384
|
+
if (voiceTextareaRef.current) {
|
|
385
|
+
voiceTextareaRef.current.scrollTop = voiceTextareaRef.current.scrollHeight;
|
|
386
|
+
}
|
|
387
|
+
}, [voiceText, partialText]);
|
|
388
|
+
|
|
381
389
|
const themeIcon = termTheme === 'light' ? (
|
|
382
390
|
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
|
|
383
391
|
<circle cx="8" cy="8" r="3" />
|
|
@@ -710,6 +718,7 @@ export default function TerminalView({ codeWorkspaceId, wsPath, isActive = true,
|
|
|
710
718
|
{voiceDialogOpen && (
|
|
711
719
|
<div className="code-voice-dialog">
|
|
712
720
|
<textarea
|
|
721
|
+
ref={voiceTextareaRef}
|
|
713
722
|
rows={6}
|
|
714
723
|
value={voiceText + (partialText ? (voiceText && !voiceText.endsWith(' ') ? ' ' : '') + partialText : '')}
|
|
715
724
|
onChange={(e) => { setVoiceText(e.target.value); setPartialText(''); }}
|
|
@@ -741,6 +750,15 @@ export default function TerminalView({ codeWorkspaceId, wsPath, isActive = true,
|
|
|
741
750
|
)}
|
|
742
751
|
</div>
|
|
743
752
|
)}
|
|
753
|
+
{diffStats?.currentBranch && (
|
|
754
|
+
<button
|
|
755
|
+
className="code-toolbar-btn"
|
|
756
|
+
style={{ cursor: 'default', opacity: 0.7, maxWidth: 140, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}
|
|
757
|
+
title={diffStats.currentBranch}
|
|
758
|
+
>
|
|
759
|
+
{diffStats.currentBranch}
|
|
760
|
+
</button>
|
|
761
|
+
)}
|
|
744
762
|
<ToolbarCommandButton
|
|
745
763
|
codeWorkspaceId={codeWorkspaceId}
|
|
746
764
|
diffStats={diffStats}
|
package/lib/config.js
CHANGED
|
@@ -54,6 +54,9 @@ const CONFIG_KEYS = new Set([
|
|
|
54
54
|
'CODING_AGENT_OPENCODE_ENABLED',
|
|
55
55
|
'CODING_AGENT_OPENCODE_PROVIDER',
|
|
56
56
|
'CODING_AGENT_OPENCODE_MODEL',
|
|
57
|
+
'CODING_AGENT_KIMI_CLI_ENABLED',
|
|
58
|
+
'CODING_AGENT_KIMI_CLI_PROVIDER',
|
|
59
|
+
'CODING_AGENT_KIMI_CLI_MODEL',
|
|
57
60
|
]);
|
|
58
61
|
|
|
59
62
|
// Default values
|
|
@@ -69,6 +72,7 @@ const DEFAULTS = {
|
|
|
69
72
|
CODING_AGENT_CODEX_CLI_ENABLED: 'false',
|
|
70
73
|
CODING_AGENT_CODEX_CLI_AUTH: 'api-key',
|
|
71
74
|
CODING_AGENT_OPENCODE_ENABLED: 'false',
|
|
75
|
+
CODING_AGENT_KIMI_CLI_ENABLED: 'false',
|
|
72
76
|
};
|
|
73
77
|
|
|
74
78
|
// In-memory cache on globalThis to survive Next.js webpack chunk duplication.
|
package/lib/cron.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import cron from 'node-cron';
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
|
-
import {
|
|
4
|
+
import { PROJECT_ROOT } from './paths.js';
|
|
5
5
|
import { executeAction } from './actions.js';
|
|
6
6
|
|
|
7
7
|
function getInstalledVersion() {
|
|
@@ -231,7 +231,7 @@ function startBuiltinCrons() {
|
|
|
231
231
|
* @returns {Array} - Array of scheduled cron tasks
|
|
232
232
|
*/
|
|
233
233
|
function loadCrons() {
|
|
234
|
-
const cronFile =
|
|
234
|
+
const cronFile = path.join(PROJECT_ROOT, 'agent-job/CRONS.json');
|
|
235
235
|
|
|
236
236
|
console.log('\n--- Cron Jobs ---');
|
|
237
237
|
|
|
@@ -255,7 +255,7 @@ function loadCrons() {
|
|
|
255
255
|
|
|
256
256
|
const task = cron.schedule(schedule, async () => {
|
|
257
257
|
try {
|
|
258
|
-
const result = await executeAction(cronEntry, { cwd:
|
|
258
|
+
const result = await executeAction(cronEntry, { cwd: PROJECT_ROOT });
|
|
259
259
|
console.log(`[CRON] ${name}: ${result || 'ran'}`);
|
|
260
260
|
console.log(`[CRON] ${name}: completed!`);
|
|
261
261
|
} catch (err) {
|
package/lib/db/api-keys.js
CHANGED
|
@@ -5,9 +5,6 @@ import { settings } from './schema.js';
|
|
|
5
5
|
|
|
6
6
|
const KEY_PREFIX = 'tpb_';
|
|
7
7
|
|
|
8
|
-
// In-memory cache: array of { id, keyHash } or null (not loaded)
|
|
9
|
-
let _cache = null;
|
|
10
|
-
|
|
11
8
|
/**
|
|
12
9
|
* Generate a new API key: tpb_ + 64 hex chars (32 random bytes).
|
|
13
10
|
* @returns {string}
|
|
@@ -25,44 +22,6 @@ export function hashApiKey(key) {
|
|
|
25
22
|
return createHash('sha256').update(key).digest('hex');
|
|
26
23
|
}
|
|
27
24
|
|
|
28
|
-
/**
|
|
29
|
-
* Lazy-load all API key hashes into the in-memory cache.
|
|
30
|
-
*/
|
|
31
|
-
function _ensureCache() {
|
|
32
|
-
if (_cache !== null) return _cache;
|
|
33
|
-
|
|
34
|
-
const db = getDb();
|
|
35
|
-
const ONE_HOUR = 60 * 60 * 1000;
|
|
36
|
-
const cutoff = Date.now() - ONE_HOUR;
|
|
37
|
-
|
|
38
|
-
const rows = db
|
|
39
|
-
.select()
|
|
40
|
-
.from(settings)
|
|
41
|
-
.where(eq(settings.type, 'api_key'))
|
|
42
|
-
.all();
|
|
43
|
-
|
|
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) => {
|
|
52
|
-
const parsed = JSON.parse(row.value);
|
|
53
|
-
return { id: row.id, keyHash: parsed.key_hash, type: row.type };
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
return _cache;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Clear the in-memory cache (call after create/delete).
|
|
61
|
-
*/
|
|
62
|
-
export function invalidateApiKeyCache() {
|
|
63
|
-
_cache = null;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
25
|
/**
|
|
67
26
|
* Create a new named API key.
|
|
68
27
|
* @param {string} name - Human-readable name for the key
|
|
@@ -89,7 +48,6 @@ export function createApiKeyRecord(name, createdBy) {
|
|
|
89
48
|
};
|
|
90
49
|
|
|
91
50
|
db.insert(settings).values(record).run();
|
|
92
|
-
invalidateApiKeyCache();
|
|
93
51
|
|
|
94
52
|
return {
|
|
95
53
|
key,
|
|
@@ -143,7 +101,6 @@ export function getApiKey() {
|
|
|
143
101
|
export function deleteApiKeyById(id) {
|
|
144
102
|
const db = getDb();
|
|
145
103
|
db.delete(settings).where(eq(settings.id, id)).run();
|
|
146
|
-
invalidateApiKeyCache();
|
|
147
104
|
}
|
|
148
105
|
|
|
149
106
|
/**
|
|
@@ -152,11 +109,11 @@ export function deleteApiKeyById(id) {
|
|
|
152
109
|
export function deleteApiKey() {
|
|
153
110
|
const db = getDb();
|
|
154
111
|
db.delete(settings).where(eq(settings.type, 'api_key')).run();
|
|
155
|
-
invalidateApiKeyCache();
|
|
156
112
|
}
|
|
157
113
|
|
|
158
114
|
/**
|
|
159
|
-
* Verify a raw API key against
|
|
115
|
+
* Verify a raw API key against stored hashes.
|
|
116
|
+
* Queries the database directly on each call (SQLite is in-process, no caching needed).
|
|
160
117
|
* @param {string} rawKey - Raw API key from request header
|
|
161
118
|
* @returns {object|null} Record if valid, null otherwise
|
|
162
119
|
*/
|
|
@@ -164,27 +121,32 @@ export function verifyApiKey(rawKey) {
|
|
|
164
121
|
if (!rawKey || !rawKey.startsWith(KEY_PREFIX)) return null;
|
|
165
122
|
|
|
166
123
|
const keyHash = hashApiKey(rawKey);
|
|
167
|
-
const
|
|
124
|
+
const db = getDb();
|
|
168
125
|
|
|
169
|
-
|
|
126
|
+
const rows = [
|
|
127
|
+
...db.select().from(settings).where(eq(settings.type, 'api_key')).all(),
|
|
128
|
+
...db.select().from(settings).where(eq(settings.type, 'agent_job_api_key')).all(),
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
if (rows.length === 0) return null;
|
|
170
132
|
|
|
171
133
|
const b = Buffer.from(keyHash, 'hex');
|
|
172
134
|
|
|
173
|
-
for (const
|
|
174
|
-
const
|
|
135
|
+
for (const row of rows) {
|
|
136
|
+
const parsed = JSON.parse(row.value);
|
|
137
|
+
const a = Buffer.from(parsed.key_hash, 'hex');
|
|
175
138
|
if (a.length === b.length && timingSafeEqual(a, b)) {
|
|
176
|
-
// Update last_used_at
|
|
139
|
+
// Update last_used_at
|
|
177
140
|
try {
|
|
178
|
-
const db = getDb();
|
|
179
141
|
const now = Date.now();
|
|
180
142
|
db.update(settings)
|
|
181
143
|
.set({ lastUsedAt: now, updatedAt: now })
|
|
182
|
-
.where(eq(settings.id,
|
|
144
|
+
.where(eq(settings.id, row.id))
|
|
183
145
|
.run();
|
|
184
|
-
} catch {
|
|
185
|
-
|
|
146
|
+
} catch (err) {
|
|
147
|
+
console.error('[api-keys] Failed to update last_used_at:', err.message);
|
|
186
148
|
}
|
|
187
|
-
return
|
|
149
|
+
return { id: row.id, keyHash: parsed.key_hash, type: row.type };
|
|
188
150
|
}
|
|
189
151
|
}
|
|
190
152
|
|
|
@@ -192,12 +154,12 @@ export function verifyApiKey(rawKey) {
|
|
|
192
154
|
}
|
|
193
155
|
|
|
194
156
|
/**
|
|
195
|
-
* Create a per-
|
|
196
|
-
* Stored as type 'agent_job_api_key'
|
|
197
|
-
* @param {string}
|
|
157
|
+
* Create a per-container API key for agent secret access.
|
|
158
|
+
* Stored as type 'agent_job_api_key' with the container name in the key column.
|
|
159
|
+
* @param {string} containerName - Docker container name (used for cleanup)
|
|
198
160
|
* @returns {{ key: string }} The raw API key to inject into the container
|
|
199
161
|
*/
|
|
200
|
-
export function createAgentJobApiKey(
|
|
162
|
+
export function createAgentJobApiKey(containerName) {
|
|
201
163
|
const db = getDb();
|
|
202
164
|
const key = generateApiKey();
|
|
203
165
|
const keyHash = hashApiKey(key);
|
|
@@ -205,12 +167,11 @@ export function createAgentJobApiKey(jobId) {
|
|
|
205
167
|
db.insert(settings).values({
|
|
206
168
|
id: randomUUID(),
|
|
207
169
|
type: 'agent_job_api_key',
|
|
208
|
-
key:
|
|
170
|
+
key: containerName,
|
|
209
171
|
value: JSON.stringify({ key_hash: keyHash }),
|
|
210
172
|
createdAt: now,
|
|
211
173
|
updatedAt: now,
|
|
212
174
|
}).run();
|
|
213
|
-
invalidateApiKeyCache();
|
|
214
175
|
return { key };
|
|
215
176
|
}
|
|
216
177
|
|
package/lib/db/config.js
CHANGED
|
@@ -254,6 +254,29 @@ export function setAgentJobSecret(key, value, userId) {
|
|
|
254
254
|
.run();
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
+
/**
|
|
258
|
+
* Get OAuth credentials stored with an agent job secret.
|
|
259
|
+
* Returns { clientId, clientSecret, tokenUrl } if the secret is oauth2, null otherwise.
|
|
260
|
+
* @param {string} key
|
|
261
|
+
*/
|
|
262
|
+
export function getAgentJobSecretOAuthCredentials(key) {
|
|
263
|
+
const db = getDb();
|
|
264
|
+
const row = db
|
|
265
|
+
.select({ value: settings.value })
|
|
266
|
+
.from(settings)
|
|
267
|
+
.where(and(eq(settings.type, 'agent_job_secret'), eq(settings.key, key)))
|
|
268
|
+
.get();
|
|
269
|
+
if (!row) return null;
|
|
270
|
+
try {
|
|
271
|
+
const decrypted = decrypt(JSON.parse(row.value));
|
|
272
|
+
const parsed = JSON.parse(decrypted);
|
|
273
|
+
if (parsed.type === 'oauth2' && parsed.clientId && parsed.clientSecret) {
|
|
274
|
+
return { clientId: parsed.clientId, clientSecret: parsed.clientSecret, tokenUrl: parsed.tokenUrl };
|
|
275
|
+
}
|
|
276
|
+
} catch {}
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
|
|
257
280
|
/**
|
|
258
281
|
* Delete an agent job secret.
|
|
259
282
|
* @param {string} key
|
package/lib/db/index.js
CHANGED
|
@@ -3,10 +3,12 @@ import path from 'path';
|
|
|
3
3
|
import Database from 'better-sqlite3';
|
|
4
4
|
import { drizzle } from 'drizzle-orm/better-sqlite3';
|
|
5
5
|
import { migrate } from 'drizzle-orm/better-sqlite3/migrator';
|
|
6
|
-
import {
|
|
6
|
+
import { PROJECT_ROOT } from '../paths.js';
|
|
7
7
|
import * as schema from './schema.js';
|
|
8
8
|
import { backfillLastUsedAt } from './api-keys.js';
|
|
9
9
|
|
|
10
|
+
const thepopebotDb = process.env.DATABASE_PATH || path.join(PROJECT_ROOT, 'data/db/thepopebot.sqlite');
|
|
11
|
+
|
|
10
12
|
let _db = null;
|
|
11
13
|
|
|
12
14
|
/**
|
package/lib/maintenance.js
CHANGED
|
@@ -2,34 +2,57 @@ import cron from 'node-cron';
|
|
|
2
2
|
import { eq } from 'drizzle-orm';
|
|
3
3
|
import { getDb } from './db/index.js';
|
|
4
4
|
import { settings } from './db/schema.js';
|
|
5
|
-
import { invalidateApiKeyCache } from './db/api-keys.js';
|
|
6
5
|
|
|
7
|
-
const
|
|
6
|
+
const TWENTY_FOUR_HOURS = 24 * 60 * 60 * 1000;
|
|
8
7
|
|
|
9
|
-
function cleanExpiredAgentJobKeys() {
|
|
8
|
+
async function cleanExpiredAgentJobKeys() {
|
|
10
9
|
try {
|
|
11
10
|
const db = getDb();
|
|
12
|
-
const cutoff = Date.now() -
|
|
11
|
+
const cutoff = Date.now() - TWENTY_FOUR_HOURS;
|
|
13
12
|
const rows = db
|
|
14
|
-
.select({ id: settings.id, lastUsedAt: settings.lastUsedAt, createdAt: settings.createdAt })
|
|
13
|
+
.select({ id: settings.id, key: settings.key, lastUsedAt: settings.lastUsedAt, createdAt: settings.createdAt })
|
|
15
14
|
.from(settings)
|
|
16
15
|
.where(eq(settings.type, 'agent_job_api_key'))
|
|
17
16
|
.all();
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
|
|
18
|
+
// Filter to candidates not used in the last 24 hours
|
|
19
|
+
const candidates = rows.filter(r =>
|
|
20
|
+
(r.lastUsedAt !== null ? r.lastUsedAt : r.createdAt) < cutoff
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
if (candidates.length === 0) {
|
|
24
|
+
console.log(`[maintenance] No expired agent job keys (${rows.length} active)`);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Check if the container still exists for each candidate
|
|
29
|
+
const { inspectContainer } = await import('./tools/docker.js');
|
|
30
|
+
const expiredIds = [];
|
|
31
|
+
for (const r of candidates) {
|
|
32
|
+
const info = await inspectContainer(r.key);
|
|
33
|
+
if (!info) {
|
|
34
|
+
expiredIds.push(r.id);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
21
38
|
if (expiredIds.length > 0) {
|
|
22
39
|
for (const id of expiredIds) {
|
|
23
40
|
db.delete(settings).where(eq(settings.id, id)).run();
|
|
24
41
|
}
|
|
25
|
-
|
|
26
|
-
|
|
42
|
+
console.log(`[maintenance] Deleted ${expiredIds.length} orphaned agent job key(s)`);
|
|
43
|
+
} else {
|
|
44
|
+
console.log(`[maintenance] ${candidates.length} candidate(s) checked, all containers still running`);
|
|
27
45
|
}
|
|
28
46
|
} catch (err) {
|
|
29
47
|
console.error('[maintenance] cleanExpiredAgentJobKeys failed:', err);
|
|
30
48
|
}
|
|
31
49
|
}
|
|
32
50
|
|
|
51
|
+
async function runMaintenance() {
|
|
52
|
+
console.log('[maintenance] Running maintenance...');
|
|
53
|
+
await cleanExpiredAgentJobKeys();
|
|
54
|
+
}
|
|
55
|
+
|
|
33
56
|
export function startMaintenanceCron() {
|
|
34
|
-
cron.schedule('0 * * * *',
|
|
57
|
+
cron.schedule('0 * * * *', runMaintenance);
|
|
35
58
|
}
|