thepopebot 1.2.57 → 1.2.59

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
@@ -200,6 +200,22 @@ Pushing to `main` triggers the `rebuild-event-handler.yml` workflow on your serv
200
200
 
201
201
  ---
202
202
 
203
+ ## Template File Conventions
204
+
205
+ The `templates/` directory contains files scaffolded into user projects by `thepopebot init`. Two naming conventions handle files that npm or AI tools would otherwise misinterpret:
206
+
207
+ **`.template` suffix** — Files ending in `.template` are scaffolded with the suffix stripped. This is used for files that npm mangles (`.gitignore`) or that AI tools would pick up as real project docs (`CLAUDE.md`).
208
+
209
+ | In `templates/` | Scaffolded as |
210
+ |-----------------|---------------|
211
+ | `.gitignore.template` | `.gitignore` |
212
+ | `CLAUDE.md.template` | `CLAUDE.md` |
213
+ | `api/CLAUDE.md.template` | `api/CLAUDE.md` |
214
+
215
+ **`CLAUDE.md` exclusion** — The scaffolding walker skips any file named `CLAUDE.md` (without the `.template` suffix). This is a safety net so a bare `CLAUDE.md` accidentally added to `templates/` never gets copied into user projects where AI tools would confuse it with real project instructions.
216
+
217
+ ---
218
+
203
219
  ## Docs
204
220
 
205
221
  | Document | Description |
package/api/index.js CHANGED
@@ -161,12 +161,9 @@ async function processChannelMessage(adapter, normalized) {
161
161
  async function handleGithubWebhook(request) {
162
162
  const { GH_WEBHOOK_SECRET } = process.env;
163
163
 
164
- // Validate webhook secret (timing-safe)
165
- if (GH_WEBHOOK_SECRET) {
166
- const headerSecret = request.headers.get('x-github-webhook-secret-token');
167
- if (!safeCompare(headerSecret, GH_WEBHOOK_SECRET)) {
168
- return Response.json({ error: 'Unauthorized' }, { status: 401 });
169
- }
164
+ // Validate webhook secret (timing-safe, required)
165
+ if (!GH_WEBHOOK_SECRET || !safeCompare(request.headers.get('x-github-webhook-secret-token'), GH_WEBHOOK_SECRET)) {
166
+ return Response.json({ error: 'Unauthorized' }, { status: 401 });
170
167
  }
171
168
 
172
169
  const payload = await request.json();
package/bin/cli.js CHANGED
@@ -25,6 +25,26 @@ function isManaged(relPath) {
25
25
  return MANAGED_PATHS.some(p => relPath === p || relPath.startsWith(p));
26
26
  }
27
27
 
28
+ // Files that must never be scaffolded directly (use .template suffix instead).
29
+ const EXCLUDED_FILENAMES = ['CLAUDE.md'];
30
+
31
+ // Files ending in .template are scaffolded with the suffix stripped.
32
+ // e.g. .gitignore.template → .gitignore, CLAUDE.md.template → CLAUDE.md
33
+ function destPath(templateRelPath) {
34
+ if (templateRelPath.endsWith('.template')) {
35
+ return templateRelPath.slice(0, -'.template'.length);
36
+ }
37
+ return templateRelPath;
38
+ }
39
+
40
+ function templatePath(userPath, templatesDir) {
41
+ const withSuffix = userPath + '.template';
42
+ if (fs.existsSync(path.join(templatesDir, withSuffix))) {
43
+ return withSuffix;
44
+ }
45
+ return userPath;
46
+ }
47
+
28
48
  function printUsage() {
29
49
  console.log(`
30
50
  Usage: thepopebot <command>
@@ -49,7 +69,7 @@ function getTemplateFiles(templatesDir) {
49
69
  const fullPath = path.join(dir, entry.name);
50
70
  if (entry.isDirectory()) {
51
71
  walk(fullPath);
52
- } else {
72
+ } else if (!EXCLUDED_FILENAMES.includes(entry.name)) {
53
73
  files.push(path.relative(templatesDir, fullPath));
54
74
  }
55
75
  }
@@ -58,12 +78,47 @@ function getTemplateFiles(templatesDir) {
58
78
  return files;
59
79
  }
60
80
 
61
- function init() {
62
- const cwd = process.cwd();
81
+ async function init() {
82
+ let cwd = process.cwd();
63
83
  const packageDir = path.join(__dirname, '..');
64
84
  const templatesDir = path.join(packageDir, 'templates');
65
85
  const noManaged = args.includes('--no-managed');
66
86
 
87
+ // Guard: warn if the directory is not empty (unless it's an existing thepopebot project)
88
+ const entries = fs.readdirSync(cwd);
89
+ if (entries.length > 0) {
90
+ const pkgPath = path.join(cwd, 'package.json');
91
+ let isExistingProject = false;
92
+ if (fs.existsSync(pkgPath)) {
93
+ try {
94
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
95
+ const deps = pkg.dependencies || {};
96
+ const devDeps = pkg.devDependencies || {};
97
+ if (deps.thepopebot || devDeps.thepopebot) {
98
+ isExistingProject = true;
99
+ }
100
+ } catch {}
101
+ }
102
+
103
+ if (!isExistingProject) {
104
+ console.log('\nThis directory is not empty.');
105
+ const { default: inquirer } = await import('inquirer');
106
+ const { dirName } = await inquirer.prompt([
107
+ {
108
+ type: 'input',
109
+ name: 'dirName',
110
+ message: 'Project directory name:',
111
+ default: 'my-popebot',
112
+ },
113
+ ]);
114
+ const newDir = path.resolve(cwd, dirName);
115
+ fs.mkdirSync(newDir, { recursive: true });
116
+ process.chdir(newDir);
117
+ cwd = newDir;
118
+ console.log(`\nCreated ${dirName}/`);
119
+ }
120
+ }
121
+
67
122
  console.log('\nScaffolding thepopebot project...\n');
68
123
 
69
124
  const templateFiles = getTemplateFiles(templatesDir);
@@ -74,29 +129,30 @@ function init() {
74
129
 
75
130
  for (const relPath of templateFiles) {
76
131
  const src = path.join(templatesDir, relPath);
77
- const dest = path.join(cwd, relPath);
132
+ const outPath = destPath(relPath);
133
+ const dest = path.join(cwd, outPath);
78
134
 
79
135
  if (!fs.existsSync(dest)) {
80
136
  // File doesn't exist — create it
81
137
  fs.mkdirSync(path.dirname(dest), { recursive: true });
82
138
  fs.copyFileSync(src, dest);
83
- created.push(relPath);
84
- console.log(` Created ${relPath}`);
139
+ created.push(outPath);
140
+ console.log(` Created ${outPath}`);
85
141
  } else {
86
142
  // File exists — check if template has changed
87
143
  const srcContent = fs.readFileSync(src);
88
144
  const destContent = fs.readFileSync(dest);
89
145
  if (srcContent.equals(destContent)) {
90
- skipped.push(relPath);
91
- } else if (!noManaged && isManaged(relPath)) {
146
+ skipped.push(outPath);
147
+ } else if (!noManaged && isManaged(outPath)) {
92
148
  // Managed file differs — auto-update to match package
93
149
  fs.mkdirSync(path.dirname(dest), { recursive: true });
94
150
  fs.copyFileSync(src, dest);
95
- updated.push(relPath);
96
- console.log(` Updated ${relPath}`);
151
+ updated.push(outPath);
152
+ console.log(` Updated ${outPath}`);
97
153
  } else {
98
- changed.push(relPath);
99
- console.log(` Skipped ${relPath} (already exists)`);
154
+ changed.push(outPath);
155
+ console.log(` Skipped ${outPath} (already exists)`);
100
156
  }
101
157
  }
102
158
  }
@@ -163,14 +219,6 @@ function init() {
163
219
  console.log(' To reset to default: npx thepopebot reset <file>');
164
220
  }
165
221
 
166
- // Handle gitignore rename (npm strips .gitignore from packages)
167
- const gitignoreSrc = path.join(templatesDir, 'gitignore');
168
- const gitignoreDest = path.join(cwd, '.gitignore');
169
- if (fs.existsSync(gitignoreSrc) && !fs.existsSync(gitignoreDest)) {
170
- fs.copyFileSync(gitignoreSrc, gitignoreDest);
171
- console.log(' Created .gitignore');
172
- }
173
-
174
222
  // Run npm install
175
223
  console.log('\nInstalling dependencies...\n');
176
224
  execSync('npm install', { stdio: 'inherit', cwd });
@@ -207,14 +255,15 @@ function reset(filePath) {
207
255
  console.log('\nAvailable template files:\n');
208
256
  const files = getTemplateFiles(templatesDir);
209
257
  for (const file of files) {
210
- console.log(` ${file}`);
258
+ console.log(` ${destPath(file)}`);
211
259
  }
212
260
  console.log('\nUsage: thepopebot reset <file>');
213
261
  console.log('Example: thepopebot reset config/SOUL.md\n');
214
262
  return;
215
263
  }
216
264
 
217
- const src = path.join(templatesDir, filePath);
265
+ const tmplPath = templatePath(filePath, templatesDir);
266
+ const src = path.join(templatesDir, tmplPath);
218
267
  const dest = path.join(cwd, filePath);
219
268
 
220
269
  if (!fs.existsSync(src)) {
@@ -225,7 +274,7 @@ function reset(filePath) {
225
274
 
226
275
  if (fs.statSync(src).isDirectory()) {
227
276
  console.log(`\nRestoring ${filePath}/...\n`);
228
- copyDirSyncForce(src, dest);
277
+ copyDirSyncForce(src, dest, tmplPath);
229
278
  } else {
230
279
  fs.mkdirSync(path.dirname(dest), { recursive: true });
231
280
  fs.copyFileSync(src, dest);
@@ -248,16 +297,17 @@ function diff(filePath) {
248
297
  let anyDiff = false;
249
298
  for (const file of files) {
250
299
  const src = path.join(templatesDir, file);
251
- const dest = path.join(cwd, file);
300
+ const outPath = destPath(file);
301
+ const dest = path.join(cwd, outPath);
252
302
  if (fs.existsSync(dest)) {
253
303
  const srcContent = fs.readFileSync(src);
254
304
  const destContent = fs.readFileSync(dest);
255
305
  if (!srcContent.equals(destContent)) {
256
- console.log(` ${file}`);
306
+ console.log(` ${outPath}`);
257
307
  anyDiff = true;
258
308
  }
259
309
  } else {
260
- console.log(` ${file} (missing)`);
310
+ console.log(` ${outPath} (missing)`);
261
311
  anyDiff = true;
262
312
  }
263
313
  }
@@ -269,7 +319,8 @@ function diff(filePath) {
269
319
  return;
270
320
  }
271
321
 
272
- const src = path.join(templatesDir, filePath);
322
+ const tmplPath = templatePath(filePath, templatesDir);
323
+ const src = path.join(templatesDir, tmplPath);
273
324
  const dest = path.join(cwd, filePath);
274
325
 
275
326
  if (!fs.existsSync(src)) {
@@ -293,17 +344,22 @@ function diff(filePath) {
293
344
  }
294
345
  }
295
346
 
296
- function copyDirSyncForce(src, dest) {
347
+ function copyDirSyncForce(src, dest, templateRelBase = '') {
297
348
  fs.mkdirSync(dest, { recursive: true });
298
349
  const entries = fs.readdirSync(src, { withFileTypes: true });
299
350
  for (const entry of entries) {
351
+ if (EXCLUDED_FILENAMES.includes(entry.name)) continue;
300
352
  const srcPath = path.join(src, entry.name);
301
- const destPath = path.join(dest, entry.name);
353
+ const templateRel = templateRelBase
354
+ ? path.join(templateRelBase, entry.name)
355
+ : entry.name;
356
+ const outName = path.basename(destPath(templateRel));
357
+ const destFile = path.join(dest, outName);
302
358
  if (entry.isDirectory()) {
303
- copyDirSyncForce(srcPath, destPath);
359
+ copyDirSyncForce(srcPath, destFile, templateRel);
304
360
  } else {
305
- fs.copyFileSync(srcPath, destPath);
306
- console.log(` Restored ${path.relative(process.cwd(), destPath)}`);
361
+ fs.copyFileSync(srcPath, destFile);
362
+ console.log(` Restored ${path.relative(process.cwd(), destFile)}`);
307
363
  }
308
364
  }
309
365
  }
@@ -345,7 +401,7 @@ async function resetAuth() {
345
401
 
346
402
  switch (command) {
347
403
  case 'init':
348
- init();
404
+ await init();
349
405
  break;
350
406
  case 'setup':
351
407
  setup();
package/config/index.js CHANGED
@@ -16,6 +16,8 @@ export function withThepopebot(nextConfig = {}) {
16
16
  serverExternalPackages: [
17
17
  ...(nextConfig.serverExternalPackages || []),
18
18
  'better-sqlite3',
19
+ 'drizzle-orm',
20
+ 'bcrypt-ts',
19
21
  ],
20
22
  };
21
23
  }
@@ -1,10 +1,9 @@
1
1
  import NextAuth from 'next-auth';
2
2
  import Credentials from 'next-auth/providers/credentials';
3
-
4
- // DB imports are dynamic inside authorize() so they don't get pulled in
5
- // at module level — middleware runs on Edge which has no fs/better-sqlite3.
3
+ import { authConfig } from './edge-config.js';
6
4
 
7
5
  export const { handlers, signIn, signOut, auth } = NextAuth({
6
+ ...authConfig,
8
7
  providers: [
9
8
  Credentials({
10
9
  credentials: {
@@ -25,21 +24,4 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
25
24
  },
26
25
  }),
27
26
  ],
28
- session: { strategy: 'jwt' },
29
- pages: { signIn: '/login' },
30
- callbacks: {
31
- jwt({ token, user }) {
32
- if (user) {
33
- token.role = user.role;
34
- }
35
- return token;
36
- },
37
- session({ session, token }) {
38
- if (session.user) {
39
- session.user.id = token.sub;
40
- session.user.role = token.role;
41
- }
42
- return session;
43
- },
44
- },
45
27
  });
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Edge-safe auth configuration — shared between middleware and server.
3
+ * Contains only JWT/session/callbacks/pages config. No providers, no DB imports.
4
+ * Both instances use the same AUTH_SECRET for JWT signing/verification.
5
+ *
6
+ * Official pattern: https://authjs.dev/guides/edge-compatibility
7
+ */
8
+ export const authConfig = {
9
+ session: { strategy: 'jwt' },
10
+ pages: { signIn: '/login' },
11
+ callbacks: {
12
+ jwt({ token, user }) {
13
+ if (user) {
14
+ token.role = user.role;
15
+ }
16
+ return token;
17
+ },
18
+ session({ session, token }) {
19
+ if (session.user) {
20
+ session.user.id = token.sub;
21
+ session.user.role = token.role;
22
+ }
23
+ return session;
24
+ },
25
+ },
26
+ };
@@ -1,6 +1,9 @@
1
- import { auth } from './config.js';
1
+ import NextAuth from 'next-auth';
2
+ import { authConfig } from './edge-config.js';
2
3
  import { NextResponse } from 'next/server';
3
4
 
5
+ const { auth } = NextAuth(authConfig);
6
+
4
7
  export const middleware = auth((req) => {
5
8
  const { pathname } = req.nextUrl;
6
9
 
@@ -21,12 +21,14 @@ class TelegramAdapter extends ChannelAdapter {
21
21
  async receive(request) {
22
22
  const { TELEGRAM_WEBHOOK_SECRET, TELEGRAM_CHAT_ID, TELEGRAM_VERIFICATION } = process.env;
23
23
 
24
- // Validate secret token if configured
25
- if (TELEGRAM_WEBHOOK_SECRET) {
26
- const headerSecret = request.headers.get('x-telegram-bot-api-secret-token');
27
- if (headerSecret !== TELEGRAM_WEBHOOK_SECRET) {
28
- return null;
29
- }
24
+ // Validate secret token (required)
25
+ if (!TELEGRAM_WEBHOOK_SECRET) {
26
+ console.error('[telegram] TELEGRAM_WEBHOOK_SECRET not configured — rejecting webhook');
27
+ return null;
28
+ }
29
+ const headerSecret = request.headers.get('x-telegram-bot-api-secret-token');
30
+ if (headerSecret !== TELEGRAM_WEBHOOK_SECRET) {
31
+ return null;
30
32
  }
31
33
 
32
34
  const update = await request.json();
@@ -1,4 +1,4 @@
1
- import { randomUUID, randomBytes, createHash } from 'crypto';
1
+ import { randomUUID, randomBytes, createHash, timingSafeEqual } from 'crypto';
2
2
  import { eq } from 'drizzle-orm';
3
3
  import { getDb } from './index.js';
4
4
  import { settings } from './schema.js';
@@ -137,7 +137,10 @@ export function verifyApiKey(rawKey) {
137
137
  const keyHash = hashApiKey(rawKey);
138
138
  const cached = _ensureCache();
139
139
 
140
- if (!cached || cached.keyHash !== keyHash) return null;
140
+ if (!cached) return null;
141
+ const a = Buffer.from(cached.keyHash, 'hex');
142
+ const b = Buffer.from(keyHash, 'hex');
143
+ if (a.length !== b.length || !timingSafeEqual(a, b)) return null;
141
144
 
142
145
  // Update last_used_at in background (non-blocking)
143
146
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thepopebot",
3
- "version": "1.2.57",
3
+ "version": "1.2.59",
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": {
File without changes
File without changes
File without changes