@harbinger-ai/harbinger 0.1.0 → 0.1.1
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/bin/cli.js +40 -38
- package/bin/local.sh +2 -2
- package/bin/postinstall.js +8 -8
- package/config/index.js +6 -3
- package/config/instrumentation.js +11 -11
- package/lib/chat/components/sidebar-user-nav.js +1 -1
- package/lib/chat/components/sidebar-user-nav.jsx +1 -1
- package/lib/chat/components/upgrade-dialog.js +2 -2
- package/lib/chat/components/upgrade-dialog.jsx +2 -2
- package/lib/cron.js +11 -7
- package/lib/db/index.js +6 -1
- package/lib/mcp/actions.js +1 -1
- package/lib/mcp/handler.js +2 -2
- package/lib/mcp/server.js +1 -1
- package/lib/paths.js +1 -1
- package/package.json +1 -1
- package/templates/.env.example +4 -4
- package/templates/.github/workflows/rebuild-event-handler.yml +20 -20
- package/templates/.github/workflows/run-job.yml +6 -6
- package/templates/.github/workflows/upgrade-event-handler.yml +12 -12
- package/templates/CLAUDE.md +3 -3
- package/templates/CLAUDE.md.template +9 -9
- package/templates/app/api/[...thepopebot]/route.js +1 -1
- package/templates/app/api/auth/[...nextauth]/route.js +1 -1
- package/templates/app/chat/[chatId]/page.js +2 -2
- package/templates/app/chats/page.js +2 -2
- package/templates/app/components/setup-form.jsx +1 -1
- package/templates/app/findings/page.js +2 -2
- package/templates/app/globals.css +1 -1
- package/templates/app/layout.js +1 -1
- package/templates/app/login/page.js +1 -1
- package/templates/app/notifications/page.js +2 -2
- package/templates/app/page.js +2 -2
- package/templates/app/settings/crons/page.js +1 -1
- package/templates/app/settings/layout.js +2 -2
- package/templates/app/settings/mcp/page.js +1 -1
- package/templates/app/settings/secrets/page.js +1 -1
- package/templates/app/settings/triggers/page.js +1 -1
- package/templates/app/stream/chat/route.js +1 -1
- package/templates/app/swarm/page.js +2 -2
- package/templates/app/targets/page.js +2 -2
- package/templates/app/toolbox/page.js +2 -2
- package/templates/config/AGENT.md +2 -2
- package/templates/config/EVENT_HANDLER.md +3 -3
- package/templates/config/SKILL_BUILDING_GUIDE.md +1 -1
- package/templates/config/SOUL.md +1 -1
- package/templates/docker/event-handler/Dockerfile +1 -1
- package/templates/docker-compose.yml +2 -2
- package/templates/instrumentation.js +1 -1
- package/templates/middleware.js +1 -1
- package/templates/next.config.mjs +2 -2
package/bin/cli.js
CHANGED
|
@@ -49,10 +49,10 @@ function templatePath(userPath, templatesDir) {
|
|
|
49
49
|
|
|
50
50
|
function printUsage() {
|
|
51
51
|
console.log(`
|
|
52
|
-
Usage:
|
|
52
|
+
Usage: harbinger <command>
|
|
53
53
|
|
|
54
54
|
Commands:
|
|
55
|
-
init Scaffold a new
|
|
55
|
+
init Scaffold a new Harbinger project
|
|
56
56
|
setup Run interactive setup wizard
|
|
57
57
|
setup-telegram Reconfigure Telegram webhook
|
|
58
58
|
reset-auth Regenerate AUTH_SECRET (invalidates all sessions)
|
|
@@ -90,7 +90,7 @@ async function init() {
|
|
|
90
90
|
const templatesDir = path.join(packageDir, 'templates');
|
|
91
91
|
const noManaged = args.includes('--no-managed');
|
|
92
92
|
|
|
93
|
-
// Guard: warn if the directory is not empty (unless it's an existing
|
|
93
|
+
// Guard: warn if the directory is not empty (unless it's an existing Harbinger project)
|
|
94
94
|
const entries = fs.readdirSync(cwd);
|
|
95
95
|
if (entries.length > 0) {
|
|
96
96
|
const pkgPath = path.join(cwd, 'package.json');
|
|
@@ -100,7 +100,7 @@ async function init() {
|
|
|
100
100
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
101
101
|
const deps = pkg.dependencies || {};
|
|
102
102
|
const devDeps = pkg.devDependencies || {};
|
|
103
|
-
if (deps.thepopebot || devDeps.thepopebot) {
|
|
103
|
+
if (deps['@harbinger-ai/harbinger'] || devDeps['@harbinger-ai/harbinger'] || deps.thepopebot || devDeps.thepopebot) {
|
|
104
104
|
isExistingProject = true;
|
|
105
105
|
}
|
|
106
106
|
} catch {}
|
|
@@ -111,7 +111,7 @@ async function init() {
|
|
|
111
111
|
const { text, isCancel } = await import('@clack/prompts');
|
|
112
112
|
const dirName = await text({
|
|
113
113
|
message: 'Project directory name:',
|
|
114
|
-
defaultValue: 'my-
|
|
114
|
+
defaultValue: 'my-agent',
|
|
115
115
|
});
|
|
116
116
|
if (isCancel(dirName)) {
|
|
117
117
|
console.log('\nCancelled.\n');
|
|
@@ -125,7 +125,7 @@ async function init() {
|
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
console.log('\nScaffolding
|
|
128
|
+
console.log('\nScaffolding Harbinger project...\n');
|
|
129
129
|
|
|
130
130
|
const templateFiles = getTemplateFiles(templatesDir);
|
|
131
131
|
const created = [];
|
|
@@ -168,7 +168,7 @@ async function init() {
|
|
|
168
168
|
if (!fs.existsSync(pkgPath)) {
|
|
169
169
|
const dirName = path.basename(cwd);
|
|
170
170
|
const { version } = JSON.parse(fs.readFileSync(path.join(packageDir, 'package.json'), 'utf8'));
|
|
171
|
-
const
|
|
171
|
+
const harbingerDep = version.includes('-') ? version : '^1.0.0';
|
|
172
172
|
const pkg = {
|
|
173
173
|
name: dirName,
|
|
174
174
|
private: true,
|
|
@@ -176,12 +176,12 @@ async function init() {
|
|
|
176
176
|
dev: 'next dev --turbopack',
|
|
177
177
|
build: 'next build',
|
|
178
178
|
start: 'next start',
|
|
179
|
-
setup: '
|
|
180
|
-
'setup-telegram': '
|
|
181
|
-
'reset-auth': '
|
|
179
|
+
setup: 'harbinger setup',
|
|
180
|
+
'setup-telegram': 'harbinger setup-telegram',
|
|
181
|
+
'reset-auth': 'harbinger reset-auth',
|
|
182
182
|
},
|
|
183
183
|
dependencies: {
|
|
184
|
-
|
|
184
|
+
'@harbinger-ai/harbinger': harbingerDep,
|
|
185
185
|
next: '^15.5.12',
|
|
186
186
|
'next-auth': '5.0.0-beta.30',
|
|
187
187
|
'next-themes': '^0.4.0',
|
|
@@ -247,12 +247,12 @@ async function init() {
|
|
|
247
247
|
if (changed.length > 0) {
|
|
248
248
|
console.log('\n Updated templates available:');
|
|
249
249
|
console.log(' These files differ from the current package templates.');
|
|
250
|
-
console.log(' This may be from your edits, or from a
|
|
250
|
+
console.log(' This may be from your edits, or from a Harbinger update.\n');
|
|
251
251
|
for (const file of changed) {
|
|
252
252
|
console.log(` ${file}`);
|
|
253
253
|
}
|
|
254
|
-
console.log('\n To view differences: npx
|
|
255
|
-
console.log(' To reset to default: npx
|
|
254
|
+
console.log('\n To view differences: npx harbinger diff <file>');
|
|
255
|
+
console.log(' To reset to default: npx harbinger reset <file>');
|
|
256
256
|
}
|
|
257
257
|
|
|
258
258
|
// Run npm install
|
|
@@ -262,32 +262,34 @@ async function init() {
|
|
|
262
262
|
// Create or update .env with auto-generated infrastructure values
|
|
263
263
|
const envPath = path.join(cwd, '.env');
|
|
264
264
|
const { randomBytes } = await import('crypto');
|
|
265
|
-
const
|
|
266
|
-
const version =
|
|
265
|
+
const harbingerPkg = JSON.parse(fs.readFileSync(path.join(packageDir, 'package.json'), 'utf8'));
|
|
266
|
+
const version = harbingerPkg.version;
|
|
267
267
|
|
|
268
268
|
if (!fs.existsSync(envPath)) {
|
|
269
269
|
// Seed .env for new projects
|
|
270
270
|
const authSecret = randomBytes(32).toString('base64');
|
|
271
|
-
const seedEnv = `#
|
|
271
|
+
const seedEnv = `# Harbinger Configuration
|
|
272
272
|
# Run "npm run setup" to complete configuration
|
|
273
273
|
|
|
274
274
|
AUTH_SECRET=${authSecret}
|
|
275
275
|
AUTH_TRUST_HOST=true
|
|
276
|
-
|
|
276
|
+
HARBINGER_VERSION=${version}
|
|
277
277
|
`;
|
|
278
278
|
fs.writeFileSync(envPath, seedEnv);
|
|
279
|
-
console.log(` Created .env (AUTH_SECRET,
|
|
279
|
+
console.log(` Created .env (AUTH_SECRET, HARBINGER_VERSION=${version})`);
|
|
280
280
|
} else {
|
|
281
|
-
// Update
|
|
281
|
+
// Update HARBINGER_VERSION in existing .env (also migrate legacy THEPOPEBOT_VERSION key)
|
|
282
282
|
try {
|
|
283
283
|
let envContent = fs.readFileSync(envPath, 'utf8');
|
|
284
|
-
if (envContent.match(/^
|
|
285
|
-
envContent = envContent.replace(/^
|
|
284
|
+
if (envContent.match(/^HARBINGER_VERSION=.*/m)) {
|
|
285
|
+
envContent = envContent.replace(/^HARBINGER_VERSION=.*/m, `HARBINGER_VERSION=${version}`);
|
|
286
|
+
} else if (envContent.match(/^THEPOPEBOT_VERSION=.*/m)) {
|
|
287
|
+
envContent = envContent.replace(/^THEPOPEBOT_VERSION=.*/m, `HARBINGER_VERSION=${version}`);
|
|
286
288
|
} else {
|
|
287
|
-
envContent = envContent.trimEnd() + `\
|
|
289
|
+
envContent = envContent.trimEnd() + `\nHARBINGER_VERSION=${version}\n`;
|
|
288
290
|
}
|
|
289
291
|
fs.writeFileSync(envPath, envContent);
|
|
290
|
-
console.log(` Updated
|
|
292
|
+
console.log(` Updated HARBINGER_VERSION to ${version}`);
|
|
291
293
|
} catch {}
|
|
292
294
|
}
|
|
293
295
|
|
|
@@ -308,8 +310,8 @@ function reset(filePath) {
|
|
|
308
310
|
for (const file of files) {
|
|
309
311
|
console.log(` ${destPath(file)}`);
|
|
310
312
|
}
|
|
311
|
-
console.log('\nUsage:
|
|
312
|
-
console.log('Example:
|
|
313
|
+
console.log('\nUsage: harbinger reset <file>');
|
|
314
|
+
console.log('Example: harbinger reset config/SOUL.md\n');
|
|
313
315
|
return;
|
|
314
316
|
}
|
|
315
317
|
|
|
@@ -319,7 +321,7 @@ function reset(filePath) {
|
|
|
319
321
|
|
|
320
322
|
if (!fs.existsSync(src)) {
|
|
321
323
|
console.error(`\nTemplate not found: ${filePath}`);
|
|
322
|
-
console.log('Run "
|
|
324
|
+
console.log('Run "harbinger reset" to see available templates.\n');
|
|
323
325
|
process.exit(1);
|
|
324
326
|
}
|
|
325
327
|
|
|
@@ -365,8 +367,8 @@ function diff(filePath) {
|
|
|
365
367
|
if (!anyDiff) {
|
|
366
368
|
console.log(' All files match package templates.');
|
|
367
369
|
}
|
|
368
|
-
console.log('\nUsage:
|
|
369
|
-
console.log('Example:
|
|
370
|
+
console.log('\nUsage: harbinger diff <file>');
|
|
371
|
+
console.log('Example: harbinger diff config/SOUL.md\n');
|
|
370
372
|
return;
|
|
371
373
|
}
|
|
372
374
|
|
|
@@ -381,7 +383,7 @@ function diff(filePath) {
|
|
|
381
383
|
|
|
382
384
|
if (!fs.existsSync(dest)) {
|
|
383
385
|
console.log(`\n${filePath} does not exist in your project.`);
|
|
384
|
-
console.log(`Run "
|
|
386
|
+
console.log(`Run "harbinger reset ${filePath}" to create it.\n`);
|
|
385
387
|
return;
|
|
386
388
|
}
|
|
387
389
|
|
|
@@ -391,7 +393,7 @@ function diff(filePath) {
|
|
|
391
393
|
console.log('\nFiles are identical.\n');
|
|
392
394
|
} catch (e) {
|
|
393
395
|
// git diff exits with 1 when files differ (output already printed)
|
|
394
|
-
console.log(`\n To reset:
|
|
396
|
+
console.log(`\n To reset: harbinger reset ${filePath}\n`);
|
|
395
397
|
}
|
|
396
398
|
}
|
|
397
399
|
|
|
@@ -489,7 +491,7 @@ function readStdin() {
|
|
|
489
491
|
|
|
490
492
|
/**
|
|
491
493
|
* Prompt for a secret value interactively if not provided as an argument.
|
|
492
|
-
* Supports piped stdin (e.g. echo "val" |
|
|
494
|
+
* Supports piped stdin (e.g. echo "val" | harbinger set-var KEY).
|
|
493
495
|
*/
|
|
494
496
|
async function promptForValue(key) {
|
|
495
497
|
const stdin = await readStdin();
|
|
@@ -516,8 +518,8 @@ async function promptForValue(key) {
|
|
|
516
518
|
|
|
517
519
|
async function setAgentSecret(key, value) {
|
|
518
520
|
if (!key) {
|
|
519
|
-
console.error('\n Usage:
|
|
520
|
-
console.error(' Example:
|
|
521
|
+
console.error('\n Usage: harbinger set-agent-secret <KEY> [VALUE]\n');
|
|
522
|
+
console.error(' Example: harbinger set-agent-secret ANTHROPIC_API_KEY\n');
|
|
521
523
|
process.exit(1);
|
|
522
524
|
}
|
|
523
525
|
|
|
@@ -543,8 +545,8 @@ async function setAgentSecret(key, value) {
|
|
|
543
545
|
|
|
544
546
|
async function setAgentLlmSecret(key, value) {
|
|
545
547
|
if (!key) {
|
|
546
|
-
console.error('\n Usage:
|
|
547
|
-
console.error(' Example:
|
|
548
|
+
console.error('\n Usage: harbinger set-agent-llm-secret <KEY> [VALUE]\n');
|
|
549
|
+
console.error(' Example: harbinger set-agent-llm-secret BRAVE_API_KEY\n');
|
|
548
550
|
process.exit(1);
|
|
549
551
|
}
|
|
550
552
|
|
|
@@ -566,8 +568,8 @@ async function setAgentLlmSecret(key, value) {
|
|
|
566
568
|
|
|
567
569
|
async function setVar(key, value) {
|
|
568
570
|
if (!key) {
|
|
569
|
-
console.error('\n Usage:
|
|
570
|
-
console.error(' Example:
|
|
571
|
+
console.error('\n Usage: harbinger set-var <KEY> [VALUE]\n');
|
|
572
|
+
console.error(' Example: harbinger set-var LLM_MODEL claude-sonnet-4-5-20250929\n');
|
|
571
573
|
process.exit(1);
|
|
572
574
|
}
|
|
573
575
|
|
package/bin/local.sh
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
set -e
|
|
3
3
|
|
|
4
4
|
PACKAGE_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
5
|
-
DEV_DIR="${1:-/tmp/
|
|
5
|
+
DEV_DIR="${1:-/tmp/harbinger.local}"
|
|
6
6
|
ENV_BACKUP="/tmp/env.$(uuidgen)"
|
|
7
7
|
|
|
8
8
|
HAS_ENV=false
|
|
@@ -17,7 +17,7 @@ cd "$DEV_DIR"
|
|
|
17
17
|
|
|
18
18
|
node "$PACKAGE_DIR/bin/cli.js" init
|
|
19
19
|
|
|
20
|
-
sed -i '' "s|\"
|
|
20
|
+
sed -i '' "s|\"@harbinger-ai/harbinger\": \".*\"|\"@harbinger-ai/harbinger\": \"file:$PACKAGE_DIR\"|" package.json
|
|
21
21
|
|
|
22
22
|
rm -rf node_modules package-lock.json
|
|
23
23
|
npm install --install-links
|
package/bin/postinstall.js
CHANGED
|
@@ -8,20 +8,20 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
8
8
|
const __dirname = path.dirname(__filename);
|
|
9
9
|
|
|
10
10
|
// postinstall runs from the package dir inside node_modules.
|
|
11
|
-
// The user's project root is two levels up: node_modules/
|
|
12
|
-
const projectRoot = path.resolve(__dirname, '..', '..', '..');
|
|
11
|
+
// The user's project root is two levels up: node_modules/@harbinger-ai/harbinger/ -> project root
|
|
12
|
+
const projectRoot = path.resolve(__dirname, '..', '..', '..', '..');
|
|
13
13
|
const templatesDir = path.join(__dirname, '..', 'templates');
|
|
14
14
|
|
|
15
15
|
// Skip if templates dir doesn't exist (shouldn't happen, but be safe)
|
|
16
16
|
if (!fs.existsSync(templatesDir)) process.exit(0);
|
|
17
17
|
|
|
18
|
-
// Skip if this doesn't look like a user project (no package.json with
|
|
18
|
+
// Skip if this doesn't look like a user project (no package.json with harbinger dep)
|
|
19
19
|
const pkgPath = path.join(projectRoot, 'package.json');
|
|
20
20
|
if (!fs.existsSync(pkgPath)) process.exit(0);
|
|
21
21
|
try {
|
|
22
22
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
23
23
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
24
|
-
if (!deps || !deps.thepopebot) process.exit(0);
|
|
24
|
+
if (!deps || (!deps['@harbinger-ai/harbinger'] && !deps.thepopebot)) process.exit(0);
|
|
25
25
|
} catch { process.exit(0); }
|
|
26
26
|
|
|
27
27
|
function walk(dir) {
|
|
@@ -52,12 +52,12 @@ for (const relPath of walk(templatesDir)) {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
if (changed.length > 0) {
|
|
55
|
-
console.log('\n
|
|
56
|
-
console.log(' This is normal if you\'ve customized them. If
|
|
55
|
+
console.log('\n Harbinger: these project files differ from the latest package templates.');
|
|
56
|
+
console.log(' This is normal if you\'ve customized them. If Harbinger was just');
|
|
57
57
|
console.log(' updated, new defaults may be available.\n');
|
|
58
58
|
for (const file of changed) {
|
|
59
59
|
console.log(` ${file}`);
|
|
60
60
|
}
|
|
61
|
-
console.log('\n To compare: npx
|
|
62
|
-
console.log(' To restore: npx
|
|
61
|
+
console.log('\n To compare: npx harbinger diff <file>');
|
|
62
|
+
console.log(' To restore: npx harbinger reset <file>\n');
|
|
63
63
|
}
|
package/config/index.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Next.js config wrapper for
|
|
2
|
+
* Next.js config wrapper for Harbinger.
|
|
3
3
|
* Enables instrumentation hook for cron scheduling on server start.
|
|
4
4
|
*
|
|
5
5
|
* Usage in user's next.config.mjs:
|
|
6
|
-
* import {
|
|
7
|
-
* export default
|
|
6
|
+
* import { withHarbinger } from '@harbinger-ai/harbinger/config';
|
|
7
|
+
* export default withHarbinger({});
|
|
8
|
+
*
|
|
9
|
+
* Legacy alias also available:
|
|
10
|
+
* import { withThepopebot } from '@harbinger-ai/harbinger/config';
|
|
8
11
|
*
|
|
9
12
|
* @param {Object} nextConfig - User's Next.js config
|
|
10
13
|
* @returns {Object} Enhanced Next.js config
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Next.js instrumentation hook for
|
|
2
|
+
* Next.js instrumentation hook for Harbinger.
|
|
3
3
|
* This file is loaded by Next.js on server start when instrumentationHook is enabled.
|
|
4
4
|
*
|
|
5
5
|
* Users should create an instrumentation.js in their project root that imports this:
|
|
6
6
|
*
|
|
7
|
-
* export { register } from '
|
|
7
|
+
* export { register } from '@harbinger-ai/harbinger/instrumentation';
|
|
8
8
|
*
|
|
9
9
|
* Or they can re-export and add their own logic.
|
|
10
10
|
*/
|
|
@@ -48,19 +48,19 @@ export async function register() {
|
|
|
48
48
|
const agents = discoverAgents();
|
|
49
49
|
if (agents.length > 0) {
|
|
50
50
|
const names = agents.map(a => a.codename || a.id).join(', ');
|
|
51
|
-
console.log(`
|
|
52
|
-
console.log('
|
|
51
|
+
console.log(`harbinger: ${agents.length} agent profiles loaded (${names})`);
|
|
52
|
+
console.log('harbinger: use @AGENT_NAME in chat to route to a specific agent');
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
// Optionally start autonomous thinking engine
|
|
56
56
|
if (process.env.AUTONOMOUS_THINKING === 'true') {
|
|
57
57
|
const { startAutonomousEngine } = await import('../lib/ai/autonomous-engine.js');
|
|
58
58
|
startAutonomousEngine({
|
|
59
|
-
agentId: '
|
|
60
|
-
agentName: '
|
|
59
|
+
agentId: 'harbinger',
|
|
60
|
+
agentName: 'HARBINGER',
|
|
61
61
|
interval: Number(process.env.AUTONOMOUS_INTERVAL) || 60000,
|
|
62
62
|
});
|
|
63
|
-
console.log('
|
|
63
|
+
console.log('harbinger: autonomous thinking engine started');
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
// Start cron scheduler
|
|
@@ -77,17 +77,17 @@ export async function register() {
|
|
|
77
77
|
const stored = getAvailableVersion();
|
|
78
78
|
if (stored) setUpdateAvailable(stored);
|
|
79
79
|
} catch (err) {
|
|
80
|
-
console.warn('
|
|
80
|
+
console.warn('harbinger: update check warm-up failed:', err.message);
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
// Pre-warm MCP client (load external tools)
|
|
84
84
|
try {
|
|
85
85
|
const { loadMcpTools } = await import('../lib/mcp/client.js');
|
|
86
86
|
const mcpTools = await loadMcpTools();
|
|
87
|
-
if (mcpTools.length > 0) console.log(`
|
|
87
|
+
if (mcpTools.length > 0) console.log(`harbinger: ${mcpTools.length} MCP tools loaded`);
|
|
88
88
|
} catch (err) {
|
|
89
|
-
console.warn('
|
|
89
|
+
console.warn('harbinger: MCP tools pre-warm failed:', err.message);
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
console.log('
|
|
92
|
+
console.log('harbinger initialized');
|
|
93
93
|
}
|
|
@@ -39,7 +39,7 @@ function SidebarUserNav({ user, collapsed }) {
|
|
|
39
39
|
theme === "dark" ? /* @__PURE__ */ jsx(SunIcon, { size: 14 }) : /* @__PURE__ */ jsx(MoonIcon, { size: 14 }),
|
|
40
40
|
/* @__PURE__ */ jsx("span", { className: "ml-2", children: theme === "dark" ? "Light Mode" : "Dark Mode" })
|
|
41
41
|
] }),
|
|
42
|
-
/* @__PURE__ */ jsxs(DropdownMenuItem, { onClick: () => window.open("https://github.com/
|
|
42
|
+
/* @__PURE__ */ jsxs(DropdownMenuItem, { onClick: () => window.open("https://github.com/Haribinger/bot/issues", "_blank"), children: [
|
|
43
43
|
/* @__PURE__ */ jsx(BugIcon, { size: 14 }),
|
|
44
44
|
/* @__PURE__ */ jsx("span", { className: "ml-2", children: "Report Issues" })
|
|
45
45
|
] }),
|
|
@@ -53,7 +53,7 @@ export function SidebarUserNav({ user, collapsed }) {
|
|
|
53
53
|
<span className="ml-2">{theme === 'dark' ? 'Light Mode' : 'Dark Mode'}</span>
|
|
54
54
|
</DropdownMenuItem>
|
|
55
55
|
)}
|
|
56
|
-
<DropdownMenuItem onClick={() => window.open('https://github.com/
|
|
56
|
+
<DropdownMenuItem onClick={() => window.open('https://github.com/Haribinger/bot/issues', '_blank')}>
|
|
57
57
|
<BugIcon size={14} />
|
|
58
58
|
<span className="ml-2">Report Issues</span>
|
|
59
59
|
</DropdownMenuItem>
|
|
@@ -71,7 +71,7 @@ function UpgradeDialog({ open, onClose, version, updateAvailable, changelog }) {
|
|
|
71
71
|
/* @__PURE__ */ jsx(
|
|
72
72
|
"a",
|
|
73
73
|
{
|
|
74
|
-
href: "https://github.com/
|
|
74
|
+
href: "https://github.com/Haribinger/bot?tab=readme-ov-file#understanding-init",
|
|
75
75
|
target: "_blank",
|
|
76
76
|
rel: "noopener noreferrer",
|
|
77
77
|
className: "text-emerald-500 hover:underline",
|
|
@@ -87,7 +87,7 @@ function UpgradeDialog({ open, onClose, version, updateAvailable, changelog }) {
|
|
|
87
87
|
/* @__PURE__ */ jsx(
|
|
88
88
|
"a",
|
|
89
89
|
{
|
|
90
|
-
href: "https://github.com/
|
|
90
|
+
href: "https://github.com/Haribinger/bot?tab=readme-ov-file#manual-updating",
|
|
91
91
|
target: "_blank",
|
|
92
92
|
rel: "noopener noreferrer",
|
|
93
93
|
className: "text-emerald-500 hover:underline",
|
|
@@ -78,7 +78,7 @@ export function UpgradeDialog({ open, onClose, version, updateAvailable, changel
|
|
|
78
78
|
<p>
|
|
79
79
|
Some files (prompts, crons, triggers) won't be auto-updated to avoid breaking your bot.{' '}
|
|
80
80
|
<a
|
|
81
|
-
href="https://github.com/
|
|
81
|
+
href="https://github.com/Haribinger/bot?tab=readme-ov-file#understanding-init"
|
|
82
82
|
target="_blank"
|
|
83
83
|
rel="noopener noreferrer"
|
|
84
84
|
className="text-emerald-500 hover:underline"
|
|
@@ -92,7 +92,7 @@ export function UpgradeDialog({ open, onClose, version, updateAvailable, changel
|
|
|
92
92
|
<p className="text-xs text-muted-foreground">
|
|
93
93
|
If you hit unrecoverable errors, see the{' '}
|
|
94
94
|
<a
|
|
95
|
-
href="https://github.com/
|
|
95
|
+
href="https://github.com/Haribinger/bot?tab=readme-ov-file#manual-updating"
|
|
96
96
|
target="_blank"
|
|
97
97
|
rel="noopener noreferrer"
|
|
98
98
|
className="text-emerald-500 hover:underline"
|
package/lib/cron.js
CHANGED
|
@@ -5,8 +5,12 @@ import { cronsFile, cronDir } from './paths.js';
|
|
|
5
5
|
import { executeAction } from './actions.js';
|
|
6
6
|
|
|
7
7
|
function getInstalledVersion() {
|
|
8
|
-
const
|
|
9
|
-
|
|
8
|
+
const names = ['@harbinger-ai/harbinger', 'thepopebot'];
|
|
9
|
+
for (const name of names) {
|
|
10
|
+
const pkgPath = path.join(process.cwd(), 'node_modules', ...name.split('/'), 'package.json');
|
|
11
|
+
try { return JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version; } catch {}
|
|
12
|
+
}
|
|
13
|
+
return '0.0.0';
|
|
10
14
|
}
|
|
11
15
|
|
|
12
16
|
// In-memory flag for available update (read by sidebar, written by cron)
|
|
@@ -98,7 +102,7 @@ function compareVersions(a, b) {
|
|
|
98
102
|
async function fetchAndStoreReleaseNotes(target) {
|
|
99
103
|
try {
|
|
100
104
|
const ghRes = await fetch(
|
|
101
|
-
`https://api.github.com/repos/
|
|
105
|
+
`https://api.github.com/repos/Haribinger/bot/releases/tags/v${target}`
|
|
102
106
|
);
|
|
103
107
|
if (!ghRes.ok) return;
|
|
104
108
|
const release = await ghRes.json();
|
|
@@ -110,7 +114,7 @@ async function fetchAndStoreReleaseNotes(target) {
|
|
|
110
114
|
}
|
|
111
115
|
|
|
112
116
|
/**
|
|
113
|
-
* Check npm registry for a newer version of
|
|
117
|
+
* Check npm registry for a newer version of harbinger.
|
|
114
118
|
*/
|
|
115
119
|
async function runVersionCheck() {
|
|
116
120
|
try {
|
|
@@ -119,8 +123,8 @@ async function runVersionCheck() {
|
|
|
119
123
|
if (isPrerelease(installed)) {
|
|
120
124
|
// Beta path: check both stable and beta dist-tags
|
|
121
125
|
const results = await Promise.allSettled([
|
|
122
|
-
fetch('https://registry.npmjs.org/
|
|
123
|
-
fetch('https://registry.npmjs.org/
|
|
126
|
+
fetch('https://registry.npmjs.org/@harbinger-ai%2fharbinger/latest'),
|
|
127
|
+
fetch('https://registry.npmjs.org/@harbinger-ai%2fharbinger/beta'),
|
|
124
128
|
]);
|
|
125
129
|
|
|
126
130
|
const candidates = [];
|
|
@@ -151,7 +155,7 @@ async function runVersionCheck() {
|
|
|
151
155
|
}
|
|
152
156
|
} else {
|
|
153
157
|
// Stable path: existing logic, untouched
|
|
154
|
-
const res = await fetch('https://registry.npmjs.org/
|
|
158
|
+
const res = await fetch('https://registry.npmjs.org/@harbinger-ai%2fharbinger/latest');
|
|
155
159
|
if (!res.ok) {
|
|
156
160
|
console.warn(`[version check] npm registry returned ${res.status}`);
|
|
157
161
|
return;
|
package/lib/db/index.js
CHANGED
|
@@ -41,7 +41,12 @@ export function initDatabase() {
|
|
|
41
41
|
|
|
42
42
|
// Resolve migrations folder from the installed package.
|
|
43
43
|
// import.meta.url doesn't survive webpack bundling, so resolve from PROJECT_ROOT.
|
|
44
|
-
|
|
44
|
+
// Try the new scoped package name first, fall back to the legacy name.
|
|
45
|
+
const packageNames = ['@harbinger-ai/harbinger', 'thepopebot'];
|
|
46
|
+
const migrationsFolder = packageNames
|
|
47
|
+
.map(name => path.join(PROJECT_ROOT, 'node_modules', ...name.split('/'), 'drizzle'))
|
|
48
|
+
.find(p => { try { return fs.existsSync(p); } catch { return false; } })
|
|
49
|
+
|| path.join(PROJECT_ROOT, 'node_modules', '@harbinger-ai', 'harbinger', 'drizzle');
|
|
45
50
|
|
|
46
51
|
migrate(db, { migrationsFolder });
|
|
47
52
|
|
package/lib/mcp/actions.js
CHANGED
|
@@ -44,7 +44,7 @@ export async function getMcpStatus() {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
|
-
* Get metadata about
|
|
47
|
+
* Get metadata about Harbinger's own MCP server capabilities.
|
|
48
48
|
*/
|
|
49
49
|
export async function getOwnMcpServerInfo() {
|
|
50
50
|
await requireAuth();
|
package/lib/mcp/handler.js
CHANGED
|
@@ -40,9 +40,9 @@ export async function handleMcpRequest(request) {
|
|
|
40
40
|
|
|
41
41
|
if (method === 'GET') {
|
|
42
42
|
return Response.json({
|
|
43
|
-
name: '
|
|
43
|
+
name: 'harbinger',
|
|
44
44
|
version,
|
|
45
|
-
description: '
|
|
45
|
+
description: 'Harbinger MCP server — autonomous AI agent platform',
|
|
46
46
|
tools: ['create_job', 'get_job_status', 'chat', 'list_agents', 'get_agent_profile'],
|
|
47
47
|
resources: ['agent://agents', 'agent://{agentId}/soul', 'config://soul', 'config://crons', 'config://triggers'],
|
|
48
48
|
prompts: ['agent-prompt'],
|
package/lib/mcp/server.js
CHANGED
package/lib/paths.js
CHANGED
package/package.json
CHANGED
package/templates/.env.example
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Harbinger Configuration
|
|
2
2
|
# Copy this file to .env and fill in your values
|
|
3
3
|
# NEVER commit the actual .env file with real secrets!
|
|
4
4
|
|
|
@@ -55,9 +55,9 @@ TELEGRAM_CHAT_ID=
|
|
|
55
55
|
# If not set, Traefik uses a self-signed certificate
|
|
56
56
|
# LETSENCRYPT_EMAIL=
|
|
57
57
|
|
|
58
|
-
#
|
|
59
|
-
#
|
|
58
|
+
# Harbinger version — auto-set by postinstall, used by docker-compose for image tags
|
|
59
|
+
# HARBINGER_VERSION=
|
|
60
60
|
|
|
61
|
-
# Custom Docker images (optional, overrides the default
|
|
61
|
+
# Custom Docker images (optional, overrides the default harbinger-ai/harbinger images)
|
|
62
62
|
# EVENT_HANDLER_IMAGE_URL=
|
|
63
63
|
# JOB_IMAGE_URL=
|
|
@@ -16,7 +16,7 @@ jobs:
|
|
|
16
16
|
- name: Pull latest and detect version change
|
|
17
17
|
id: pull
|
|
18
18
|
run: |
|
|
19
|
-
docker exec
|
|
19
|
+
docker exec harbinger-event-handler bash -c '
|
|
20
20
|
export GH_TOKEN=$(grep "^GH_TOKEN=" /app/.env | cut -d= -f2-)
|
|
21
21
|
echo "${GH_TOKEN}" | gh auth login --with-token
|
|
22
22
|
gh auth setup-git
|
|
@@ -29,35 +29,35 @@ jobs:
|
|
|
29
29
|
exit 0
|
|
30
30
|
fi
|
|
31
31
|
|
|
32
|
-
# Detect
|
|
32
|
+
# Detect harbinger version change from package-lock.json in git
|
|
33
33
|
git show HEAD:package-lock.json > /tmp/old-lock.json 2>/dev/null || echo "{}" > /tmp/old-lock.json
|
|
34
34
|
git show origin/main:package-lock.json > /tmp/new-lock.json
|
|
35
|
-
OLD_TPB=$(node -p "try{require(\"/tmp/old-lock.json\").packages[\"node_modules/
|
|
36
|
-
NEW_TPB=$(node -p "try{require(\"/tmp/new-lock.json\").packages[\"node_modules/
|
|
35
|
+
OLD_TPB=$(node -p "try{require(\"/tmp/old-lock.json\").packages[\"node_modules/@harbinger-ai/harbinger\"]?.version||\"\"}catch(e){\"\"}")
|
|
36
|
+
NEW_TPB=$(node -p "try{require(\"/tmp/new-lock.json\").packages[\"node_modules/@harbinger-ai/harbinger\"]?.version||\"\"}catch(e){\"\"}")
|
|
37
37
|
rm -f /tmp/old-lock.json /tmp/new-lock.json
|
|
38
38
|
|
|
39
39
|
git reset --hard origin/main
|
|
40
40
|
|
|
41
41
|
if [ -n "$OLD_TPB" ] && [ "$OLD_TPB" != "$NEW_TPB" ]; then
|
|
42
|
-
# Version changed — run
|
|
42
|
+
# Version changed — run harbinger init to scaffold new templates
|
|
43
43
|
if echo "$NEW_TPB" | grep -q "-"; then
|
|
44
|
-
npx --yes "
|
|
44
|
+
npx --yes "@harbinger-ai/harbinger@${NEW_TPB}" init
|
|
45
45
|
else
|
|
46
|
-
npx --yes
|
|
46
|
+
npx --yes @harbinger-ai/harbinger@latest init
|
|
47
47
|
fi
|
|
48
48
|
|
|
49
49
|
# Commit any template changes from init
|
|
50
50
|
git add -A
|
|
51
51
|
if ! git diff --cached --quiet; then
|
|
52
|
-
git commit -m "chore: apply
|
|
52
|
+
git commit -m "chore: apply harbinger init after upgrade"
|
|
53
53
|
git push origin main
|
|
54
54
|
fi
|
|
55
55
|
|
|
56
|
-
# Update
|
|
57
|
-
if grep -q "^
|
|
58
|
-
sed -i "s/^
|
|
56
|
+
# Update HARBINGER_VERSION in .env so docker compose pulls the right image
|
|
57
|
+
if grep -q "^HARBINGER_VERSION=" /app/.env; then
|
|
58
|
+
sed -i "s/^HARBINGER_VERSION=.*/HARBINGER_VERSION=$NEW_TPB/" /app/.env
|
|
59
59
|
else
|
|
60
|
-
echo "
|
|
60
|
+
echo "HARBINGER_VERSION=$NEW_TPB" >> /app/.env
|
|
61
61
|
fi
|
|
62
62
|
echo "VERSION_CHANGED" > /app/.rebuild-status
|
|
63
63
|
else
|
|
@@ -66,14 +66,14 @@ jobs:
|
|
|
66
66
|
fi
|
|
67
67
|
'
|
|
68
68
|
|
|
69
|
-
STATUS=$(docker exec
|
|
70
|
-
docker exec
|
|
69
|
+
STATUS=$(docker exec harbinger-event-handler cat /app/.rebuild-status)
|
|
70
|
+
docker exec harbinger-event-handler rm -f /app/.rebuild-status
|
|
71
71
|
echo "status=$STATUS" >> $GITHUB_OUTPUT
|
|
72
72
|
|
|
73
73
|
- name: Rebuild (no version change)
|
|
74
74
|
if: steps.pull.outputs.status == 'REBUILD'
|
|
75
75
|
run: |
|
|
76
|
-
docker exec
|
|
76
|
+
docker exec harbinger-event-handler bash -c '
|
|
77
77
|
rm -rf .next-new .next-old
|
|
78
78
|
NEXT_BUILD_DIR=.next-new npm run build
|
|
79
79
|
|
|
@@ -89,10 +89,10 @@ jobs:
|
|
|
89
89
|
- name: Pull new image and restart container
|
|
90
90
|
if: steps.pull.outputs.status == 'VERSION_CHANGED'
|
|
91
91
|
run: |
|
|
92
|
-
HOST_DIR=$(docker inspect
|
|
92
|
+
HOST_DIR=$(docker inspect harbinger-event-handler --format '{{index .Config.Labels "com.docker.compose.project.working_dir"}}')
|
|
93
93
|
docker compose -f /project/docker-compose.yml --project-directory "$HOST_DIR" --env-file /project/.env pull event-handler
|
|
94
|
-
docker stop
|
|
95
|
-
docker rm
|
|
94
|
+
docker stop harbinger-event-handler || true
|
|
95
|
+
docker rm harbinger-event-handler || true
|
|
96
96
|
docker compose -f /project/docker-compose.yml --project-directory "$HOST_DIR" --env-file /project/.env up -d event-handler
|
|
97
97
|
|
|
98
98
|
- name: Rebuild in new container
|
|
@@ -100,13 +100,13 @@ jobs:
|
|
|
100
100
|
run: |
|
|
101
101
|
echo "Waiting for new container..."
|
|
102
102
|
for i in $(seq 1 30); do
|
|
103
|
-
if docker exec
|
|
103
|
+
if docker exec harbinger-event-handler echo "ready" 2>/dev/null; then
|
|
104
104
|
break
|
|
105
105
|
fi
|
|
106
106
|
sleep 2
|
|
107
107
|
done
|
|
108
108
|
|
|
109
|
-
docker exec
|
|
109
|
+
docker exec harbinger-event-handler bash -c '
|
|
110
110
|
npm install --omit=dev
|
|
111
111
|
|
|
112
112
|
rm -rf .next-new .next-old
|
|
@@ -21,10 +21,10 @@ jobs:
|
|
|
21
21
|
logs/*/job.config.json
|
|
22
22
|
sparse-checkout-cone-mode: false
|
|
23
23
|
|
|
24
|
-
- name: Get
|
|
24
|
+
- name: Get harbinger version
|
|
25
25
|
id: version
|
|
26
26
|
run: |
|
|
27
|
-
VERSION=$(jq -r '.packages["node_modules/
|
|
27
|
+
VERSION=$(jq -r '.packages["node_modules/@harbinger-ai/harbinger"].version // "latest"' package-lock.json)
|
|
28
28
|
echo "tag=$VERSION" >> $GITHUB_OUTPUT
|
|
29
29
|
|
|
30
30
|
- name: Read job config overrides
|
|
@@ -47,11 +47,11 @@ jobs:
|
|
|
47
47
|
username: ${{ github.actor }}
|
|
48
48
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
49
49
|
|
|
50
|
-
- name: Run
|
|
50
|
+
- name: Run Harbinger Agent
|
|
51
51
|
env:
|
|
52
52
|
ALL_SECRETS: ${{ toJson(secrets) }}
|
|
53
53
|
JOB_IMAGE_URL: ${{ vars.JOB_IMAGE_URL }}
|
|
54
|
-
|
|
54
|
+
HARBINGER_VERSION: ${{ steps.version.outputs.tag }}
|
|
55
55
|
LLM_MODEL: ${{ steps.job-config.outputs.llm_model || vars.LLM_MODEL }}
|
|
56
56
|
LLM_PROVIDER: ${{ steps.job-config.outputs.llm_provider || vars.LLM_PROVIDER }}
|
|
57
57
|
OPENAI_BASE_URL: ${{ vars.OPENAI_BASE_URL }}
|
|
@@ -61,9 +61,9 @@ jobs:
|
|
|
61
61
|
if [ -n "$JOB_IMAGE_URL" ]; then
|
|
62
62
|
IMAGE="${JOB_IMAGE_URL}:latest"
|
|
63
63
|
elif [ "$AGENT_BACKEND" = "claude-code" ]; then
|
|
64
|
-
IMAGE="
|
|
64
|
+
IMAGE="harbinger-ai/harbinger:job-claude-code-${HARBINGER_VERSION}"
|
|
65
65
|
else
|
|
66
|
-
IMAGE="
|
|
66
|
+
IMAGE="harbinger-ai/harbinger:job-${HARBINGER_VERSION}"
|
|
67
67
|
fi
|
|
68
68
|
echo "Using image: $IMAGE"
|
|
69
69
|
|
|
@@ -17,9 +17,9 @@ jobs:
|
|
|
17
17
|
runs-on: self-hosted
|
|
18
18
|
timeout-minutes: 20
|
|
19
19
|
steps:
|
|
20
|
-
- name: Upgrade
|
|
20
|
+
- name: Upgrade Harbinger
|
|
21
21
|
run: |
|
|
22
|
-
docker exec
|
|
22
|
+
docker exec harbinger-event-handler bash -c '
|
|
23
23
|
export GH_TOKEN=$(grep "^GH_TOKEN=" /app/.env | cut -d= -f2-)
|
|
24
24
|
echo "${GH_TOKEN}" | gh auth login --with-token
|
|
25
25
|
gh auth setup-git
|
|
@@ -29,33 +29,33 @@ jobs:
|
|
|
29
29
|
git clone --depth 1 "$REPO_URL" "$WORK_DIR"
|
|
30
30
|
cd "$WORK_DIR"
|
|
31
31
|
|
|
32
|
-
git config user.name "
|
|
33
|
-
git config user.email "
|
|
32
|
+
git config user.name "harbinger"
|
|
33
|
+
git config user.email "harbinger@users.noreply.github.com"
|
|
34
34
|
|
|
35
35
|
npm install --omit=dev
|
|
36
|
-
OLD_VERSION=$(node -p "require(\"./node_modules/
|
|
36
|
+
OLD_VERSION=$(node -p "require(\"./node_modules/@harbinger-ai/harbinger/package.json\").version")
|
|
37
37
|
|
|
38
38
|
TARGET="${{ github.event.inputs.target_version }}"
|
|
39
39
|
if [ -n "$TARGET" ] && echo "$TARGET" | grep -q "-"; then
|
|
40
40
|
# Beta: npm update cannot upgrade an exact-pinned dependency, must use install
|
|
41
|
-
npm install "
|
|
41
|
+
npm install "@harbinger-ai/harbinger@${TARGET}" --prefer-online
|
|
42
42
|
else
|
|
43
43
|
# Stable: npm update resolves to latest within semver range
|
|
44
|
-
npm update
|
|
44
|
+
npm update @harbinger-ai/harbinger --prefer-online
|
|
45
45
|
fi
|
|
46
46
|
|
|
47
|
-
NEW_VERSION=$(node -p "require(\"./node_modules/
|
|
47
|
+
NEW_VERSION=$(node -p "require(\"./node_modules/@harbinger-ai/harbinger/package.json\").version")
|
|
48
48
|
|
|
49
49
|
if [ "$OLD_VERSION" != "$NEW_VERSION" ]; then
|
|
50
|
-
BRANCH="upgrade/
|
|
50
|
+
BRANCH="upgrade/harbinger-${NEW_VERSION}-$(date +%s)"
|
|
51
51
|
git add -A
|
|
52
52
|
git checkout -b "$BRANCH"
|
|
53
|
-
git commit -m "chore: upgrade
|
|
53
|
+
git commit -m "chore: upgrade harbinger to $NEW_VERSION"
|
|
54
54
|
git push origin "$BRANCH"
|
|
55
|
-
gh pr create --title "chore: upgrade
|
|
55
|
+
gh pr create --title "chore: upgrade harbinger" --body "Automated upgrade via upgrade-event-handler workflow." --base main --head "$BRANCH"
|
|
56
56
|
gh pr merge "$BRANCH" --squash --auto --delete-branch
|
|
57
57
|
else
|
|
58
|
-
echo "No changes —
|
|
58
|
+
echo "No changes — harbinger is already up to date."
|
|
59
59
|
fi
|
|
60
60
|
|
|
61
61
|
rm -rf "$WORK_DIR"
|
package/templates/CLAUDE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Templates Directory — Scaffolding Only
|
|
2
2
|
|
|
3
|
-
This directory contains files that get copied into user projects when they run `npx
|
|
3
|
+
This directory contains files that get copied into user projects when they run `npx harbinger init`. It is **not** where event handler logic, API routes, or core features live.
|
|
4
4
|
|
|
5
5
|
## Rules
|
|
6
6
|
|
|
@@ -10,7 +10,7 @@ This directory contains files that get copied into user projects when they run `
|
|
|
10
10
|
|
|
11
11
|
## What belongs here
|
|
12
12
|
|
|
13
|
-
- **Next.js wiring**: `next.config.mjs`, `instrumentation.js`, catch-all route, middleware — thin re-exports from
|
|
13
|
+
- **Next.js wiring**: `next.config.mjs`, `instrumentation.js`, catch-all route, middleware — thin re-exports from `@harbinger-ai/harbinger/*`
|
|
14
14
|
- **User-editable config**: `config/SOUL.md`, `config/EVENT_HANDLER.md`, `config/CRONS.json`, `config/TRIGGERS.json`, etc.
|
|
15
15
|
- **GitHub Actions workflows**: `.github/workflows/`
|
|
16
16
|
- **Docker files**: `docker/`, `docker-compose.yml`
|
|
@@ -24,6 +24,6 @@ This directory contains files that get copied into user projects when they run `
|
|
|
24
24
|
- Database operations
|
|
25
25
|
- LLM/AI integrations
|
|
26
26
|
- Tool implementations
|
|
27
|
-
- Anything that should be shared across all users via `npm update
|
|
27
|
+
- Anything that should be shared across all users via `npm update @harbinger-ai/harbinger`
|
|
28
28
|
|
|
29
29
|
If you're adding a feature to the event handler, put it in the package. Templates just wire into it.
|
|
@@ -2,26 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
## Overview
|
|
4
4
|
|
|
5
|
-
This is an autonomous AI agent powered by [
|
|
5
|
+
This is an autonomous AI agent powered by [Harbinger](https://github.com/harbinger-ai/harbinger). It uses a **two-layer architecture**:
|
|
6
6
|
|
|
7
7
|
1. **Event Handler** — A Next.js server that orchestrates everything: web UI, Telegram chat, cron scheduling, webhook triggers, and job creation.
|
|
8
8
|
2. **Docker Agent** — A container that runs the Pi coding agent for autonomous task execution. Each job gets its own branch, container, and PR.
|
|
9
9
|
|
|
10
|
-
All core logic lives in the `
|
|
10
|
+
All core logic lives in the `@harbinger-ai/harbinger` npm package. This project is a scaffolded shell — thin Next.js wiring, user-editable configuration, GitHub Actions workflows, and Docker files.
|
|
11
11
|
|
|
12
12
|
## Directory Structure
|
|
13
13
|
|
|
14
14
|
```
|
|
15
15
|
project-root/
|
|
16
16
|
├── CLAUDE.md # This file (project documentation)
|
|
17
|
-
├── next.config.mjs # Next.js config (wraps
|
|
17
|
+
├── next.config.mjs # Next.js config (wraps withHarbinger())
|
|
18
18
|
├── instrumentation.js # Server startup hook (re-exports from package)
|
|
19
19
|
├── middleware.js # Auth middleware (re-exports from package)
|
|
20
20
|
├── .env # API keys and tokens (gitignored)
|
|
21
21
|
├── package.json
|
|
22
22
|
│
|
|
23
23
|
├── app/ # Next.js app directory (page shells + client components)
|
|
24
|
-
│ ├── api/[...thepopebot]/route.js # Catch-all API route (re-exports from
|
|
24
|
+
│ ├── api/[...thepopebot]/route.js # Catch-all API route (re-exports from @harbinger-ai/harbinger/api)
|
|
25
25
|
│ ├── stream/chat/route.js # Chat streaming endpoint (session auth)
|
|
26
26
|
│ └── components/ # Client-side components
|
|
27
27
|
│
|
|
@@ -44,7 +44,7 @@ project-root/
|
|
|
44
44
|
├── cron/ # Scripts for command-type cron actions
|
|
45
45
|
├── triggers/ # Scripts for command-type trigger actions
|
|
46
46
|
├── logs/ # Per-job output (logs/<JOB_ID>/job.md + session .jsonl)
|
|
47
|
-
└── data/ # SQLite database (data/
|
|
47
|
+
└── data/ # SQLite database (data/harbinger.sqlite)
|
|
48
48
|
```
|
|
49
49
|
|
|
50
50
|
## Two-Layer Architecture
|
|
@@ -228,7 +228,7 @@ NextAuth v5 with Credentials provider (email/password), JWT in httpOnly cookies.
|
|
|
228
228
|
|
|
229
229
|
## Database
|
|
230
230
|
|
|
231
|
-
SQLite via Drizzle ORM at `data/
|
|
231
|
+
SQLite via Drizzle ORM at `data/harbinger.sqlite`. Auto-initialized and auto-migrated on server startup. Tables: `users`, `chats`, `messages`, `notifications`, `subscriptions`, `settings` (key-value store, also stores API keys). Column naming: camelCase in JS → snake_case in SQL.
|
|
232
232
|
|
|
233
233
|
## GitHub Actions Workflows
|
|
234
234
|
|
|
@@ -236,7 +236,7 @@ SQLite via Drizzle ORM at `data/thepopebot.sqlite`. Auto-initialized and auto-mi
|
|
|
236
236
|
|----------|---------|---------|
|
|
237
237
|
| `run-job.yml` | `job/*` branch created | Runs the Docker agent container |
|
|
238
238
|
| `rebuild-event-handler.yml` | Push to `main` | Rebuilds server (fast path or Docker restart) |
|
|
239
|
-
| `upgrade-event-handler.yml` | Manual `workflow_dispatch` | Creates PR to upgrade
|
|
239
|
+
| `upgrade-event-handler.yml` | Manual `workflow_dispatch` | Creates PR to upgrade Harbinger package |
|
|
240
240
|
| `build-image.yml` | `docker/job-pi-coding-agent/**` changes | Builds Pi coding agent Docker image to GHCR |
|
|
241
241
|
| `auto-merge.yml` | Job PR opened | Squash-merges if changes are within `ALLOWED_PATHS` |
|
|
242
242
|
| `notify-pr-complete.yml` | After `auto-merge.yml` | Sends job completion notification |
|
|
@@ -261,8 +261,8 @@ SQLite via Drizzle ORM at `data/thepopebot.sqlite`. Auto-initialized and auto-mi
|
|
|
261
261
|
| `APP_URL` | Public URL for the event handler | Required |
|
|
262
262
|
| `AUTO_MERGE` | Set to `"false"` to disable auto-merge | Enabled |
|
|
263
263
|
| `ALLOWED_PATHS` | Comma-separated path prefixes for auto-merge | `/logs` |
|
|
264
|
-
| `JOB_IMAGE_URL` | Docker image for job agent (GHCR URLs trigger auto-builds) | Default
|
|
265
|
-
| `EVENT_HANDLER_IMAGE_URL` | Docker image for event handler | Default
|
|
264
|
+
| `JOB_IMAGE_URL` | Docker image for job agent (GHCR URLs trigger auto-builds) | Default Harbinger image |
|
|
265
|
+
| `EVENT_HANDLER_IMAGE_URL` | Docker image for event handler | Default Harbinger image |
|
|
266
266
|
| `RUNS_ON` | GitHub Actions runner label | `ubuntu-latest` |
|
|
267
267
|
| `LLM_PROVIDER` | LLM provider for Docker agent | `anthropic` |
|
|
268
268
|
| `LLM_MODEL` | LLM model name for Docker agent | Provider default |
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { GET, POST } from '
|
|
1
|
+
export { GET, POST } from '@harbinger-ai/harbinger/api';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { GET, POST } from '
|
|
1
|
+
export { GET, POST } from '@harbinger-ai/harbinger/auth';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { auth } from '
|
|
2
|
-
import { ChatPage } from '
|
|
1
|
+
import { auth } from '@harbinger-ai/harbinger/auth';
|
|
2
|
+
import { ChatPage } from '@harbinger-ai/harbinger/chat';
|
|
3
3
|
|
|
4
4
|
export default async function ChatRoute({ params }) {
|
|
5
5
|
const { chatId } = await params;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { auth } from '
|
|
2
|
-
import { ChatsPage } from '
|
|
1
|
+
import { auth } from '@harbinger-ai/harbinger/auth';
|
|
2
|
+
import { ChatsPage } from '@harbinger-ai/harbinger/chat';
|
|
3
3
|
|
|
4
4
|
export default async function ChatsRoute() {
|
|
5
5
|
const session = await auth();
|
|
@@ -6,7 +6,7 @@ import { Button } from './ui/button';
|
|
|
6
6
|
import { Input } from './ui/input';
|
|
7
7
|
import { Label } from './ui/label';
|
|
8
8
|
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from './ui/card';
|
|
9
|
-
import { setupAdmin } from '
|
|
9
|
+
import { setupAdmin } from '@harbinger-ai/harbinger/auth/actions';
|
|
10
10
|
|
|
11
11
|
export function SetupForm() {
|
|
12
12
|
const router = useRouter();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { auth } from '
|
|
2
|
-
import { FindingsPage } from '
|
|
1
|
+
import { auth } from '@harbinger-ai/harbinger/auth';
|
|
2
|
+
import { FindingsPage } from '@harbinger-ai/harbinger/chat';
|
|
3
3
|
|
|
4
4
|
export default async function FindingsRoute() {
|
|
5
5
|
const session = await auth();
|
package/templates/app/layout.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getPageAuthState } from '
|
|
1
|
+
import { getPageAuthState } from '@harbinger-ai/harbinger/auth';
|
|
2
2
|
import { AsciiLogo } from '../components/ascii-logo';
|
|
3
3
|
import { SetupForm } from '../components/setup-form';
|
|
4
4
|
import { LoginForm } from '../components/login-form';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { auth } from '
|
|
2
|
-
import { NotificationsPage } from '
|
|
1
|
+
import { auth } from '@harbinger-ai/harbinger/auth';
|
|
2
|
+
import { NotificationsPage } from '@harbinger-ai/harbinger/chat';
|
|
3
3
|
|
|
4
4
|
export default async function NotificationsRoute() {
|
|
5
5
|
const session = await auth();
|
package/templates/app/page.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { auth } from '
|
|
2
|
-
import { ChatPage } from '
|
|
1
|
+
import { auth } from '@harbinger-ai/harbinger/auth';
|
|
2
|
+
import { ChatPage } from '@harbinger-ai/harbinger/chat';
|
|
3
3
|
|
|
4
4
|
export default async function Home() {
|
|
5
5
|
const session = await auth();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { auth } from '
|
|
2
|
-
import { SettingsLayout } from '
|
|
1
|
+
import { auth } from '@harbinger-ai/harbinger/auth';
|
|
2
|
+
import { SettingsLayout } from '@harbinger-ai/harbinger/chat';
|
|
3
3
|
|
|
4
4
|
export default async function Layout({ children }) {
|
|
5
5
|
const session = await auth();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { POST } from '
|
|
1
|
+
export { POST } from '@harbinger-ai/harbinger/chat/api';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { auth } from '
|
|
2
|
-
import { SwarmPage } from '
|
|
1
|
+
import { auth } from '@harbinger-ai/harbinger/auth';
|
|
2
|
+
import { SwarmPage } from '@harbinger-ai/harbinger/chat';
|
|
3
3
|
|
|
4
4
|
export default async function SwarmRoute() {
|
|
5
5
|
const session = await auth();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { auth } from '
|
|
2
|
-
import { TargetsPage } from '
|
|
1
|
+
import { auth } from '@harbinger-ai/harbinger/auth';
|
|
2
|
+
import { TargetsPage } from '@harbinger-ai/harbinger/chat';
|
|
3
3
|
|
|
4
4
|
export default async function TargetsRoute() {
|
|
5
5
|
const session = await auth();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { auth } from '
|
|
2
|
-
import { RegistryPage } from '
|
|
1
|
+
import { auth } from '@harbinger-ai/harbinger/auth';
|
|
2
|
+
import { RegistryPage } from '@harbinger-ai/harbinger/chat';
|
|
3
3
|
|
|
4
4
|
export default async function ToolboxRoute() {
|
|
5
5
|
const session = await auth();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Harbinger Agent Environment
|
|
2
2
|
|
|
3
3
|
**This document describes what you are and your operating environment**
|
|
4
4
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
## 1. What You Are
|
|
8
8
|
|
|
9
|
-
You are **
|
|
9
|
+
You are **Harbinger**, an autonomous AI agent running inside a Docker container.
|
|
10
10
|
- You have full access to the machine and anything it can do to get the job done.
|
|
11
11
|
|
|
12
12
|
---
|
|
@@ -104,7 +104,7 @@ If a skill needs an API key:
|
|
|
104
104
|
|
|
105
105
|
1. **Tell the user** what credential is needed and where to get it
|
|
106
106
|
2. **Suggest setting it up now** so the skill can be tested in the same job:
|
|
107
|
-
- Run: `npx
|
|
107
|
+
- Run: `npx harbinger set-agent-llm-secret <KEY_NAME> <value>`
|
|
108
108
|
- The value is stored exactly as provided, no transformation needed
|
|
109
109
|
- This creates a GitHub secret with the `AGENT_LLM_` prefix — the Docker container exposes it as an environment variable (e.g., `AGENT_LLM_BRAVE_API_KEY` → `BRAVE_API_KEY`)
|
|
110
110
|
- They can rotate the key later with the same command
|
|
@@ -172,7 +172,7 @@ This applies to every job — including simple or obvious tasks. Even if the use
|
|
|
172
172
|
>
|
|
173
173
|
> If you set it up now, I can build AND test the skill in one job:
|
|
174
174
|
> ```
|
|
175
|
-
> npx
|
|
175
|
+
> npx harbinger set-agent-llm-secret SLACK_WEBHOOK_URL <your-url>
|
|
176
176
|
> ```
|
|
177
177
|
> (You can rotate this later with the same command.)
|
|
178
178
|
>
|
|
@@ -205,7 +205,7 @@ New skill creation:
|
|
|
205
205
|
> 1. Create `SKILL.md` with frontmatter (name: slack-post, description: "Post messages to Slack channels via incoming webhook.") and usage docs referencing `skills/slack-post/post.sh <message>`
|
|
206
206
|
> 2. Create `post.sh` — bash script that takes a message argument, sends it to the Slack webhook URL via curl using $SLACK_WEBHOOK_URL. Make it executable.
|
|
207
207
|
> 3. Activate: `ln -s ../slack-post skills/active/slack-post`
|
|
208
|
-
> 4. Test: run `skills/slack-post/post.sh "test message from
|
|
208
|
+
> 4. Test: run `skills/slack-post/post.sh "test message from harbinger"` and verify successful delivery. Fix any issues before committing.
|
|
209
209
|
|
|
210
210
|
---
|
|
211
211
|
|
|
@@ -84,7 +84,7 @@ Tell the agent to test the skill with real input after creating it and fix any i
|
|
|
84
84
|
## Credential setup
|
|
85
85
|
|
|
86
86
|
If a skill needs an API key, the user should set it up BEFORE the job runs:
|
|
87
|
-
- `npx
|
|
87
|
+
- `npx harbinger set-agent-llm-secret <KEY_NAME> <value>` — creates a GitHub secret with `AGENT_LLM_` prefix, exposed as an env var in the Docker container
|
|
88
88
|
- The value is stored exactly as provided, no transformation needed
|
|
89
89
|
- Also add to `.env` for local development
|
|
90
90
|
- Keys can be rotated later with the same command
|
package/templates/config/SOUL.md
CHANGED
|
@@ -12,7 +12,7 @@ RUN npm install -g pm2
|
|
|
12
12
|
WORKDIR /app
|
|
13
13
|
COPY package.json package-lock.json* ./
|
|
14
14
|
RUN npm install --omit=dev && \
|
|
15
|
-
npm install --no-save
|
|
15
|
+
npm install --no-save @harbinger-ai/harbinger@$(node -p "require('./package.json').version")
|
|
16
16
|
|
|
17
17
|
COPY /templates/docker/event-handler/ecosystem.config.cjs /opt/ecosystem.config.cjs
|
|
18
18
|
|
|
@@ -22,8 +22,8 @@ services:
|
|
|
22
22
|
restart: unless-stopped
|
|
23
23
|
|
|
24
24
|
event-handler:
|
|
25
|
-
container_name:
|
|
26
|
-
image: ${EVENT_HANDLER_IMAGE_URL:-
|
|
25
|
+
container_name: harbinger-event-handler
|
|
26
|
+
image: ${EVENT_HANDLER_IMAGE_URL:-harbinger-ai/harbinger:event-handler-${HARBINGER_VERSION:-latest}}
|
|
27
27
|
volumes:
|
|
28
28
|
- .:/app
|
|
29
29
|
- /app/node_modules
|
package/templates/middleware.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { middleware, config } from '
|
|
1
|
+
export { middleware, config } from '@harbinger-ai/harbinger/middleware';
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { withHarbinger } from '@harbinger-ai/harbinger/config';
|
|
2
2
|
|
|
3
|
-
export default
|
|
3
|
+
export default withHarbinger({});
|