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.
Files changed (120) hide show
  1. package/README.md +1 -1
  2. package/api/CLAUDE.md +1 -1
  3. package/api/index.js +5 -12
  4. package/bin/CLAUDE.md +1 -1
  5. package/bin/cli.js +329 -14
  6. package/bin/docker-build.js +5 -0
  7. package/bin/managed-paths.js +0 -7
  8. package/bin/sync.js +84 -0
  9. package/config/CLAUDE.md +1 -29
  10. package/config/instrumentation.js +1 -1
  11. package/lib/CLAUDE.md +3 -3
  12. package/lib/ai/CLAUDE.md +24 -3
  13. package/lib/ai/agent.js +8 -5
  14. package/lib/ai/async-channel.js +51 -0
  15. package/lib/ai/headless-stream.js +3 -0
  16. package/lib/ai/index.js +149 -173
  17. package/lib/ai/line-mappers.js +72 -9
  18. package/lib/ai/tools.js +40 -28
  19. package/lib/chat/actions.js +34 -6
  20. package/lib/chat/api.js +17 -1
  21. package/lib/chat/components/chat-header.js +4 -0
  22. package/lib/chat/components/chat-header.jsx +4 -0
  23. package/lib/chat/components/chat-input.js +1 -0
  24. package/lib/chat/components/chat-input.jsx +1 -0
  25. package/lib/chat/components/chat.js +9 -1
  26. package/lib/chat/components/chat.jsx +15 -2
  27. package/lib/chat/components/chats-page.js +3 -3
  28. package/lib/chat/components/chats-page.jsx +4 -6
  29. package/lib/chat/components/crons-page.js +1 -1
  30. package/lib/chat/components/crons-page.jsx +1 -1
  31. package/lib/chat/components/message.js +12 -4
  32. package/lib/chat/components/message.jsx +17 -4
  33. package/lib/chat/components/settings-chat-page.js +2 -1
  34. package/lib/chat/components/settings-chat-page.jsx +4 -1
  35. package/lib/chat/components/settings-coding-agents-page.js +139 -1
  36. package/lib/chat/components/settings-coding-agents-page.jsx +160 -0
  37. package/lib/chat/components/settings-jobs-page.js +13 -2
  38. package/lib/chat/components/settings-jobs-page.jsx +15 -1
  39. package/lib/chat/components/settings-secrets-layout.js +1 -1
  40. package/lib/chat/components/settings-secrets-layout.jsx +1 -1
  41. package/lib/chat/components/sidebar-history-item.js +3 -3
  42. package/lib/chat/components/sidebar-history-item.jsx +4 -6
  43. package/lib/chat/components/triggers-page.js +1 -1
  44. package/lib/chat/components/triggers-page.jsx +1 -1
  45. package/lib/cluster/actions.js +4 -4
  46. package/lib/cluster/execute.js +3 -1
  47. package/lib/code/actions.js +34 -11
  48. package/lib/code/code-page.js +40 -40
  49. package/lib/code/code-page.jsx +36 -36
  50. package/lib/code/port-forwards.js +17 -3
  51. package/lib/code/terminal-view.js +16 -0
  52. package/lib/code/terminal-view.jsx +18 -0
  53. package/lib/config.js +4 -0
  54. package/lib/cron.js +3 -3
  55. package/lib/db/api-keys.js +22 -61
  56. package/lib/db/config.js +23 -0
  57. package/lib/db/index.js +3 -1
  58. package/lib/maintenance.js +34 -11
  59. package/lib/paths.js +1 -38
  60. package/lib/tools/create-agent-job.js +0 -4
  61. package/lib/tools/docker.js +23 -16
  62. package/lib/triggers.js +4 -3
  63. package/lib/utils/render-md.js +3 -1
  64. package/package.json +2 -1
  65. package/setup/setup-ssl.mjs +414 -0
  66. package/templates/.github/workflows/rebuild-event-handler.yml +3 -0
  67. package/templates/.github/workflows/upgrade-event-handler.yml +1 -1
  68. package/templates/.gitignore.template +7 -3
  69. package/templates/.tmp/CLAUDE.md.template +5 -0
  70. package/templates/CLAUDE.md +3 -2
  71. package/templates/CLAUDE.md.template +24 -357
  72. package/templates/agent-job/CLAUDE.md.template +57 -0
  73. package/templates/agent-job/CRONS.json +16 -0
  74. package/templates/{config/agent-job → agent-job}/SOUL.md +3 -3
  75. package/templates/agent-job/SYSTEM.md +60 -0
  76. package/templates/agents/CLAUDE.md.template +54 -0
  77. package/templates/data/CLAUDE.md.template +5 -0
  78. package/templates/docker-compose.custom.yml +41 -62
  79. package/templates/docker-compose.yml +14 -21
  80. package/templates/event-handler/CLAUDE.md.template +0 -0
  81. package/templates/logs/CLAUDE.md.template +5 -0
  82. package/templates/skills/CLAUDE.md.template +57 -32
  83. package/templates/skills/active/.gitkeep +0 -0
  84. package/templates/skills/library/agent-job-secrets/SKILL.md +23 -0
  85. package/templates/skills/library/agent-job-secrets/agent-job-secrets.js +62 -0
  86. package/templates/.pi/extensions/env-sanitizer/index.ts +0 -48
  87. package/templates/.pi/extensions/env-sanitizer/package.json +0 -5
  88. package/templates/README.md +0 -75
  89. package/templates/config/CLAUDE.md.template +0 -40
  90. package/templates/config/CRONS.json +0 -56
  91. package/templates/config/agent-job/AGENT_JOB.md +0 -30
  92. package/templates/cron/CLAUDE.md.template +0 -24
  93. package/templates/docker-compose.litellm.yml +0 -82
  94. package/templates/docs/CLAUDE.md.template +0 -12
  95. package/templates/docs/CLI.md +0 -59
  96. package/templates/docs/CLUSTERS.md +0 -151
  97. package/templates/docs/CONFIGURATION.md +0 -181
  98. package/templates/docs/CRONS_AND_TRIGGERS.md +0 -132
  99. package/templates/docs/GETTING_STARTED.md +0 -64
  100. package/templates/docs/SECURITY.md +0 -61
  101. package/templates/docs/SKILLS.md +0 -113
  102. package/templates/docs/UPGRADING.md +0 -92
  103. package/templates/skills/LICENSE +0 -21
  104. package/templates/skills/README.md +0 -117
  105. package/templates/skills/agent-job-secrets/SKILL.md +0 -25
  106. package/templates/skills/agent-job-secrets/agent-job-secrets.js +0 -66
  107. package/templates/traefik-dynamic.yml.example +0 -7
  108. package/templates/triggers/CLAUDE.md.template +0 -41
  109. /package/templates/{config → agent-job}/HEARTBEAT.md +0 -0
  110. /package/templates/{cron → data}/.gitkeep +0 -0
  111. /package/templates/{logs → data/clusters}/.gitkeep +0 -0
  112. /package/templates/{triggers → data/db}/.gitkeep +0 -0
  113. /package/templates/{config/agent-job → event-handler}/SUMMARY.md +0 -0
  114. /package/templates/{config → event-handler}/TRIGGERS.json +0 -0
  115. /package/templates/{config → event-handler}/agent-chat/SYSTEM.md +0 -0
  116. /package/templates/{config/cluster → event-handler/clusters}/ROLE.md +0 -0
  117. /package/templates/{config/cluster → event-handler/clusters}/SYSTEM.md +0 -0
  118. /package/templates/{config → event-handler}/code-chat/SYSTEM.md +0 -0
  119. /package/templates/{config → event-handler}/litellm/main.yaml +0 -0
  120. /package/templates/skills/{playwright-cli → library/playwright-cli}/SKILL.md +0 -0
@@ -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) }) }),
@@ -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
- * Routes use *.localhost for local dev. No TLS on forwarded ports.
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 host = `${workspaceId.slice(0, 8)}-${port}.localhost`;
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(' - web');
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 { cronsFile, cronDir } from './paths.js';
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 = cronsFile;
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: cronDir });
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) {
@@ -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 all cached hashes.
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 cached = _ensureCache();
124
+ const db = getDb();
168
125
 
169
- if (!cached || cached.length === 0) return null;
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 entry of cached) {
174
- const a = Buffer.from(entry.keyHash, 'hex');
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 column directly (non-blocking)
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, entry.id))
144
+ .where(eq(settings.id, row.id))
183
145
  .run();
184
- } catch {
185
- // Non-fatal: last_used_at is informational
146
+ } catch (err) {
147
+ console.error('[api-keys] Failed to update last_used_at:', err.message);
186
148
  }
187
- return entry;
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-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)
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(jobId) {
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: jobId,
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 { thepopebotDb, PROJECT_ROOT } from '../paths.js';
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
  /**
@@ -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 ONE_HOUR = 60 * 60 * 1000;
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() - ONE_HOUR;
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
- const expiredIds = rows
19
- .filter(r => r.lastUsedAt !== null ? r.lastUsedAt < cutoff : r.createdAt < cutoff)
20
- .map(r => r.id);
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
- invalidateApiKeyCache();
26
- console.log(`[maintenance] Deleted ${expiredIds.length} expired agent job key(s)`);
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 * * * *', cleanExpiredAgentJobKeys);
57
+ cron.schedule('0 * * * *', runMaintenance);
35
58
  }