thepopebot 1.2.70-beta.9 → 1.2.70

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 CHANGED
@@ -1,6 +1,4 @@
1
- # thepopebot
2
-
3
- ## Why thepopebot?
1
+ # Why thepopebot?
4
2
 
5
3
  **The repository IS the agent** — Every action your agent takes is a git commit. You can see exactly what it did, when, and why. If it screws up, revert it. Want to clone your agent? Fork the repo — code, personality, scheduled jobs, full history, all of it goes with your fork.
6
4
 
@@ -126,7 +124,26 @@ npm install thepopebot@latest
126
124
  npx thepopebot init
127
125
  ```
128
126
 
129
- For most people, that's it — `init` handles everything. It updates your project files, runs `npm install`, and updates `THEPOPEBOT_VERSION` in your local `.env`.
127
+ For most people, that's it — `init` handles everything. It updates your project files, runs `npm install`, and updates `THEPOPEBOT_VERSION` in your local `.env`. See [Understanding `init`](#understanding-init) below for details on what this updates and how to handle custom changes.
128
+
129
+ **3. Rebuild for local dev**
130
+
131
+ ```bash
132
+ npm run build
133
+ ```
134
+
135
+ **4. Commit and push**
136
+
137
+ ```bash
138
+ git add -A && git commit -m "upgrade thepopebot to vX.X.X"
139
+ git push
140
+ ```
141
+
142
+ Pushing to `main` triggers the `rebuild-event-handler.yml` workflow on your server. It detects the version change, runs `thepopebot init`, updates `THEPOPEBOT_VERSION` in the server's `.env`, pulls the new Docker image, restarts the container, rebuilds `.next`, and reloads PM2 — no manual `docker compose` needed.
143
+
144
+ > **Upgrade failed?** See [Recovering from a Failed Upgrade](docs/UPGRADE.md#recovering-from-a-failed-upgrade).
145
+
146
+ ### Understanding `init`
130
147
 
131
148
  #### How your project is structured
132
149
 
@@ -136,7 +153,7 @@ When you ran `thepopebot init` the first time, it scaffolded a project folder wi
136
153
 
137
154
  | Files | What they do |
138
155
  |-------|-------------|
139
- | `config/SOUL.md`, `CHATBOT.md`, `AGENT.md`, etc. | Your agent's personality, behavior, and prompts |
156
+ | `config/SOUL.md`, `EVENT_HANDLER.md`, `AGENT.md`, etc. | Your agent's personality, behavior, and prompts |
140
157
  | `config/CRONS.json`, `TRIGGERS.json` | Your scheduled jobs and webhook triggers |
141
158
  | `app/` | Next.js pages and UI components |
142
159
  | `docker/job/` | The Dockerfile for your agent's job container |
@@ -180,21 +197,6 @@ If you've made custom changes to managed files (e.g., added extra steps to a Git
180
197
  npx thepopebot init --no-managed
181
198
  ```
182
199
 
183
- **3. Rebuild for local dev**
184
-
185
- ```bash
186
- npm run build
187
- ```
188
-
189
- **4. Commit and push**
190
-
191
- ```bash
192
- git add -A && git commit -m "upgrade thepopebot to vX.X.X"
193
- git push
194
- ```
195
-
196
- Pushing to `main` triggers the `rebuild-event-handler.yml` workflow on your server. It detects the version change, runs `thepopebot init`, updates `THEPOPEBOT_VERSION` in the server's `.env`, pulls the new Docker image, restarts the container, rebuilds `.next`, and reloads PM2 — no manual `docker compose` needed.
197
-
198
200
  ---
199
201
 
200
202
  ## CLI Commands
package/lib/ai/agent.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { createReactAgent } from '@langchain/langgraph/prebuilt';
2
2
  import { SystemMessage } from '@langchain/core/messages';
3
3
  import { createModel } from './model.js';
4
- import { createJobTool, getJobStatusTool } from './tools.js';
4
+ import { createJobTool, getJobStatusTool, getSystemTechnicalSpecsTool, getPiSkillCreationGuideTool } from './tools.js';
5
5
  import { SqliteSaver } from '@langchain/langgraph-checkpoint-sqlite';
6
- import { chatbotMd, thepopebotDb } from '../paths.js';
6
+ import { eventHandlerMd, thepopebotDb } from '../paths.js';
7
7
  import { render_md } from '../utils/render-md.js';
8
8
 
9
9
  let _agent = null;
@@ -16,14 +16,14 @@ let _agent = null;
16
16
  export async function getAgent() {
17
17
  if (!_agent) {
18
18
  const model = await createModel();
19
- const tools = [createJobTool, getJobStatusTool];
19
+ const tools = [createJobTool, getJobStatusTool, getSystemTechnicalSpecsTool, getPiSkillCreationGuideTool];
20
20
  const checkpointer = SqliteSaver.fromConnString(thepopebotDb);
21
21
 
22
22
  _agent = createReactAgent({
23
23
  llm: model,
24
24
  tools,
25
25
  checkpointSaver: checkpointer,
26
- prompt: (state) => [new SystemMessage(render_md(chatbotMd)), ...state.messages],
26
+ prompt: (state) => [new SystemMessage(render_md(eventHandlerMd)), ...state.messages],
27
27
  });
28
28
  }
29
29
  return _agent;
package/lib/ai/index.js CHANGED
@@ -149,24 +149,45 @@ async function* chatStream(threadId, message, attachments = [], options = {}) {
149
149
  for await (const event of stream) {
150
150
  // streamMode: 'messages' yields [message, metadata] tuples
151
151
  const msg = Array.isArray(event) ? event[0] : event;
152
- const isAI = msg._getType?.() === 'ai';
153
- if (!isAI) continue;
154
-
155
- // Content can be a string or an array of content blocks
156
- let text = '';
157
- if (typeof msg.content === 'string') {
158
- text = msg.content;
159
- } else if (Array.isArray(msg.content)) {
160
- text = msg.content
161
- .filter((b) => b.type === 'text' && b.text)
162
- .map((b) => b.text)
163
- .join('');
164
- }
165
-
166
- if (text) {
167
- fullText += text;
168
- yield text;
152
+ const msgType = msg._getType?.();
153
+
154
+ if (msgType === 'ai') {
155
+ // Tool calls AIMessage.tool_calls is an array of { id, name, args }
156
+ if (msg.tool_calls?.length > 0) {
157
+ for (const tc of msg.tool_calls) {
158
+ yield {
159
+ type: 'tool-call',
160
+ toolCallId: tc.id,
161
+ toolName: tc.name,
162
+ args: tc.args,
163
+ };
164
+ }
165
+ }
166
+
167
+ // Text content (wrapped in structured object)
168
+ let text = '';
169
+ if (typeof msg.content === 'string') {
170
+ text = msg.content;
171
+ } else if (Array.isArray(msg.content)) {
172
+ text = msg.content
173
+ .filter((b) => b.type === 'text' && b.text)
174
+ .map((b) => b.text)
175
+ .join('');
176
+ }
177
+
178
+ if (text) {
179
+ fullText += text;
180
+ yield { type: 'text', text };
181
+ }
182
+ } else if (msgType === 'tool') {
183
+ // Tool result — ToolMessage has tool_call_id and content
184
+ yield {
185
+ type: 'tool-result',
186
+ toolCallId: msg.tool_call_id,
187
+ result: msg.content,
188
+ };
169
189
  }
190
+ // Skip other message types (human, system)
170
191
  }
171
192
 
172
193
  // Save assistant response to DB
@@ -192,7 +213,7 @@ async function autoTitle(threadId, firstMessage) {
192
213
  const chat = getChatById(threadId);
193
214
  if (!chat || chat.title !== 'New Chat') return;
194
215
 
195
- const model = await createModel();
216
+ const model = await createModel({ maxTokens: 250 });
196
217
  const response = await model.invoke([
197
218
  ['system', 'Generate a short (3-6 word) title for this chat based on the user\'s first message. Return ONLY the title, nothing else.'],
198
219
  ['human', firstMessage],
package/lib/ai/tools.js CHANGED
@@ -1,7 +1,9 @@
1
+ import fs from 'fs';
1
2
  import { tool } from '@langchain/core/tools';
2
3
  import { z } from 'zod';
3
4
  import { createJob } from '../tools/create-job.js';
4
5
  import { getJobStatus } from '../tools/github.js';
6
+ import { claudeMd, skillGuidePath } from '../paths.js';
5
7
 
6
8
  const createJobTool = tool(
7
9
  async ({ job_description }) => {
@@ -15,7 +17,7 @@ const createJobTool = tool(
15
17
  {
16
18
  name: 'create_job',
17
19
  description:
18
- 'Create an autonomous job for thepopebot to execute. Use this tool liberally - if the user asks for ANY task to be done, create a job. Jobs can handle code changes, file updates, research tasks, web scraping, data analysis, or anything requiring autonomous work. When the user explicitly asks for a job, ALWAYS use this tool. Returns the job ID and branch name.',
20
+ 'Create an autonomous job that runs a Docker agent in a container. The Docker agent has full filesystem access, web search, browser automation, and other abilities. The job description you provide becomes the Docker agent\'s task prompt. Returns the job ID and branch name.',
19
21
  schema: z.object({
20
22
  job_description: z
21
23
  .string()
@@ -46,4 +48,36 @@ const getJobStatusTool = tool(
46
48
  }
47
49
  );
48
50
 
49
- export { createJobTool, getJobStatusTool };
51
+ const getSystemTechnicalSpecsTool = tool(
52
+ async () => {
53
+ try {
54
+ return fs.readFileSync(claudeMd, 'utf8');
55
+ } catch {
56
+ return 'No technical documentation found (CLAUDE.md not present in project root).';
57
+ }
58
+ },
59
+ {
60
+ name: 'get_system_technical_specs',
61
+ description:
62
+ 'Read the system architecture and technical documentation (CLAUDE.md). Use this when you need to understand how the system itself works — the event handler, Docker agent, API routes, database, cron/trigger configuration, GitHub Actions, deployment, or file structure. Use this before planning jobs that modify system configuration or infrastructure. NOT for Pi skill creation (use get_pi_skill_creation_guide for that).',
63
+ schema: z.object({}),
64
+ }
65
+ );
66
+
67
+ const getPiSkillCreationGuideTool = tool(
68
+ async () => {
69
+ try {
70
+ return fs.readFileSync(skillGuidePath, 'utf8');
71
+ } catch {
72
+ return 'Skill guide not found.';
73
+ }
74
+ },
75
+ {
76
+ name: 'get_pi_skill_creation_guide',
77
+ description:
78
+ 'Load the guide for creating, modifying, and understanding Pi agent skills (pi-skills). Use this when the user wants to create a new skill, asks how skills work, wants to modify an existing skill, or when you need to understand the skill format (SKILL.md, {baseDir}, activation, testing). This is about Pi skills specifically — the lightweight bash/Node.js wrappers that extend what the Docker agent can do. NOT for understanding the system architecture (use get_system_technical_specs for that).',
79
+ schema: z.object({}),
80
+ }
81
+ );
82
+
83
+ export { createJobTool, getJobStatusTool, getSystemTechnicalSpecsTool, getPiSkillCreationGuideTool };
@@ -23,7 +23,27 @@ export const middleware = auth((req) => {
23
23
 
24
24
  // Everything else requires auth
25
25
  if (!req.auth) {
26
- return NextResponse.redirect(new URL('/login', req.url));
26
+ const response = NextResponse.redirect(new URL('/login', req.url));
27
+
28
+ // Clear stale session cookies that can't be decrypted (e.g. after AUTH_SECRET rotation
29
+ // or container restart). Auth.js clears these internally in route handlers via
30
+ // sessionStore.clean(), but NOT in middleware — so the bad cookie loops forever.
31
+ // Only session-token cookies are cleared; csrf-token and callback-url are left intact.
32
+ const cookieNames = Object.keys(req.cookies.getAll().reduce((acc, c) => { acc[c.name] = true; return acc; }, {}));
33
+ const staleSessionCookies = cookieNames.filter(name =>
34
+ name === 'authjs.session-token' ||
35
+ name === '__Secure-authjs.session-token' ||
36
+ /^authjs\.session-token\.\d+$/.test(name) ||
37
+ /^__Secure-authjs\.session-token\.\d+$/.test(name)
38
+ );
39
+
40
+ if (staleSessionCookies.length > 0) {
41
+ for (const name of staleSessionCookies) {
42
+ response.cookies.set(name, '', { maxAge: 0, path: '/' });
43
+ }
44
+ }
45
+
46
+ return response;
27
47
  }
28
48
  });
29
49
 
package/lib/chat/api.js CHANGED
@@ -81,17 +81,46 @@ export async function POST(request) {
81
81
  // Signal start of assistant message
82
82
  writer.write({ type: 'start' });
83
83
 
84
- const textId = crypto.randomUUID();
85
84
  let textStarted = false;
85
+ let textId = crypto.randomUUID();
86
86
 
87
87
  for await (const chunk of chunks) {
88
- if (!textStarted) {
89
- writer.write({ type: 'text-start', id: textId });
90
- textStarted = true;
88
+ if (chunk.type === 'text') {
89
+ if (!textStarted) {
90
+ textId = crypto.randomUUID();
91
+ writer.write({ type: 'text-start', id: textId });
92
+ textStarted = true;
93
+ }
94
+ writer.write({ type: 'text-delta', id: textId, delta: chunk.text });
95
+
96
+ } else if (chunk.type === 'tool-call') {
97
+ // Close any open text block before tool events
98
+ if (textStarted) {
99
+ writer.write({ type: 'text-end', id: textId });
100
+ textStarted = false;
101
+ }
102
+ writer.write({
103
+ type: 'tool-input-start',
104
+ toolCallId: chunk.toolCallId,
105
+ toolName: chunk.toolName,
106
+ });
107
+ writer.write({
108
+ type: 'tool-input-available',
109
+ toolCallId: chunk.toolCallId,
110
+ toolName: chunk.toolName,
111
+ input: chunk.args,
112
+ });
113
+
114
+ } else if (chunk.type === 'tool-result') {
115
+ writer.write({
116
+ type: 'tool-output-available',
117
+ toolCallId: chunk.toolCallId,
118
+ output: chunk.result,
119
+ });
91
120
  }
92
- writer.write({ type: 'text-delta', id: textId, delta: chunk });
93
121
  }
94
122
 
123
+ // Close final text block if still open
95
124
  if (textStarted) {
96
125
  writer.write({ type: 'text-end', id: textId });
97
126
  }
@@ -42,7 +42,7 @@ function AppSidebar({ user }) {
42
42
  /* @__PURE__ */ jsxs(SidebarHeader, { children: [
43
43
  /* @__PURE__ */ jsxs("div", { className: collapsed ? "flex justify-center" : "flex items-center justify-between", children: [
44
44
  !collapsed && /* @__PURE__ */ jsxs("span", { className: "px-2 font-semibold text-lg", children: [
45
- "The Pope Bot",
45
+ "ThePopeBot",
46
46
  version && /* @__PURE__ */ jsxs("span", { className: "text-[11px] font-normal text-muted-foreground", children: [
47
47
  " v",
48
48
  version
@@ -49,7 +49,7 @@ export function AppSidebar({ user }) {
49
49
  {/* Top row: brand name + toggle icon (open) or just toggle icon (collapsed) */}
50
50
  <div className={collapsed ? 'flex justify-center' : 'flex items-center justify-between'}>
51
51
  {!collapsed && (
52
- <span className="px-2 font-semibold text-lg">The Pope Bot{version && <span className="text-[11px] font-normal text-muted-foreground"> v{version}</span>}</span>
52
+ <span className="px-2 font-semibold text-lg">ThePopeBot{version && <span className="text-[11px] font-normal text-muted-foreground"> v{version}</span>}</span>
53
53
  )}
54
54
  <Tooltip>
55
55
  <TooltipTrigger asChild>
@@ -209,7 +209,7 @@ function PaperclipIcon({ size = 16 }) {
209
209
  }
210
210
  );
211
211
  }
212
- function XIcon({ size = 16 }) {
212
+ function XIcon({ size = 16, className = "" }) {
213
213
  return /* @__PURE__ */ jsxs(
214
214
  "svg",
215
215
  {
@@ -222,6 +222,7 @@ function XIcon({ size = 16 }) {
222
222
  strokeLinejoin: "round",
223
223
  width: size,
224
224
  height: size,
225
+ className,
225
226
  children: [
226
227
  /* @__PURE__ */ jsx("path", { d: "M18 6 6 18" }),
227
228
  /* @__PURE__ */ jsx("path", { d: "m6 6 12 12" })
@@ -311,7 +312,7 @@ function RefreshIcon({ size = 16 }) {
311
312
  }
312
313
  );
313
314
  }
314
- function ChevronDownIcon({ size = 16 }) {
315
+ function ChevronDownIcon({ size = 16, className = "" }) {
315
316
  return /* @__PURE__ */ jsx(
316
317
  "svg",
317
318
  {
@@ -324,6 +325,7 @@ function ChevronDownIcon({ size = 16 }) {
324
325
  strokeLinejoin: "round",
325
326
  width: size,
326
327
  height: size,
328
+ className,
327
329
  children: /* @__PURE__ */ jsx("path", { d: "m6 9 6 6 6-6" })
328
330
  }
329
331
  );
@@ -389,7 +391,7 @@ function CopyIcon({ size = 16 }) {
389
391
  }
390
392
  );
391
393
  }
392
- function CheckIcon({ size = 16 }) {
394
+ function CheckIcon({ size = 16, className = "" }) {
393
395
  return /* @__PURE__ */ jsx(
394
396
  "svg",
395
397
  {
@@ -402,6 +404,7 @@ function CheckIcon({ size = 16 }) {
402
404
  strokeLinejoin: "round",
403
405
  width: size,
404
406
  height: size,
407
+ className,
405
408
  children: /* @__PURE__ */ jsx("path", { d: "M20 6 9 17l-5-5" })
406
409
  }
407
410
  );
@@ -635,6 +638,24 @@ function LifeBuoyIcon({ size = 16 }) {
635
638
  }
636
639
  );
637
640
  }
641
+ function WrenchIcon({ size = 16, className = "" }) {
642
+ return /* @__PURE__ */ jsx(
643
+ "svg",
644
+ {
645
+ xmlns: "http://www.w3.org/2000/svg",
646
+ viewBox: "0 0 24 24",
647
+ fill: "none",
648
+ stroke: "currentColor",
649
+ strokeWidth: 2,
650
+ strokeLinecap: "round",
651
+ strokeLinejoin: "round",
652
+ width: size,
653
+ height: size,
654
+ className,
655
+ children: /* @__PURE__ */ jsx("path", { d: "M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z" })
656
+ }
657
+ );
658
+ }
638
659
  function LogOutIcon({ size = 16 }) {
639
660
  return /* @__PURE__ */ jsxs(
640
661
  "svg",
@@ -688,6 +709,7 @@ export {
688
709
  SunIcon,
689
710
  SwarmIcon,
690
711
  TrashIcon,
712
+ WrenchIcon,
691
713
  XIcon,
692
714
  ZapIcon
693
715
  };
@@ -205,7 +205,7 @@ export function PaperclipIcon({ size = 16 }) {
205
205
  );
206
206
  }
207
207
 
208
- export function XIcon({ size = 16 }) {
208
+ export function XIcon({ size = 16, className = '' }) {
209
209
  return (
210
210
  <svg
211
211
  xmlns="http://www.w3.org/2000/svg"
@@ -217,6 +217,7 @@ export function XIcon({ size = 16 }) {
217
217
  strokeLinejoin="round"
218
218
  width={size}
219
219
  height={size}
220
+ className={className}
220
221
  >
221
222
  <path d="M18 6 6 18" />
222
223
  <path d="m6 6 12 12" />
@@ -304,7 +305,7 @@ export function RefreshIcon({ size = 16 }) {
304
305
  );
305
306
  }
306
307
 
307
- export function ChevronDownIcon({ size = 16 }) {
308
+ export function ChevronDownIcon({ size = 16, className = '' }) {
308
309
  return (
309
310
  <svg
310
311
  xmlns="http://www.w3.org/2000/svg"
@@ -316,6 +317,7 @@ export function ChevronDownIcon({ size = 16 }) {
316
317
  strokeLinejoin="round"
317
318
  width={size}
318
319
  height={size}
320
+ className={className}
319
321
  >
320
322
  <path d="m6 9 6 6 6-6" />
321
323
  </svg>
@@ -380,7 +382,7 @@ export function CopyIcon({ size = 16 }) {
380
382
  );
381
383
  }
382
384
 
383
- export function CheckIcon({ size = 16 }) {
385
+ export function CheckIcon({ size = 16, className = '' }) {
384
386
  return (
385
387
  <svg
386
388
  xmlns="http://www.w3.org/2000/svg"
@@ -392,6 +394,7 @@ export function CheckIcon({ size = 16 }) {
392
394
  strokeLinejoin="round"
393
395
  width={size}
394
396
  height={size}
397
+ className={className}
395
398
  >
396
399
  <path d="M20 6 9 17l-5-5" />
397
400
  </svg>
@@ -626,6 +629,25 @@ export function LifeBuoyIcon({ size = 16 }) {
626
629
  );
627
630
  }
628
631
 
632
+ export function WrenchIcon({ size = 16, className = '' }) {
633
+ return (
634
+ <svg
635
+ xmlns="http://www.w3.org/2000/svg"
636
+ viewBox="0 0 24 24"
637
+ fill="none"
638
+ stroke="currentColor"
639
+ strokeWidth={2}
640
+ strokeLinecap="round"
641
+ strokeLinejoin="round"
642
+ width={size}
643
+ height={size}
644
+ className={className}
645
+ >
646
+ <path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z" />
647
+ </svg>
648
+ );
649
+ }
650
+
629
651
  export function LogOutIcon({ size = 16 }) {
630
652
  return (
631
653
  <svg