charon-hooks 0.2.0 → 0.2.3
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 +20 -34
- package/bin/charon.js +49 -22
- package/dist/server/index.js +145 -73
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -38,6 +38,18 @@ Because Charon is just a trigger layer, it's completely agnostic:
|
|
|
38
38
|
- **Swap agents anytime** - Today it's Claude, tomorrow it's GPT or your own thing. Change one line, keep all your triggers.
|
|
39
39
|
- **Mix and match** - Route different events to different agents. Bug reports to Claude, billing issues to a custom script.
|
|
40
40
|
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
**Via npx:**
|
|
44
|
+
```bash
|
|
45
|
+
npx charon-hooks
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**Via Claude Code Plugin:**
|
|
49
|
+
See [charon-plugin](https://github.com/NaxYo/charon-plugin)
|
|
50
|
+
|
|
51
|
+
**Advanced options:** See [Installation Guide](docs/INSTALL.md)
|
|
52
|
+
|
|
41
53
|
---
|
|
42
54
|
|
|
43
55
|
## How It Works
|
|
@@ -76,33 +88,16 @@ export default function(payload: any) {
|
|
|
76
88
|
## Quick Start
|
|
77
89
|
|
|
78
90
|
```bash
|
|
79
|
-
|
|
80
|
-
cd charon
|
|
81
|
-
bun install
|
|
82
|
-
bun dev
|
|
91
|
+
npx charon-hooks
|
|
83
92
|
```
|
|
84
93
|
|
|
85
|
-
Open http://localhost:3000 - create your first trigger from the dashboard, or edit
|
|
86
|
-
|
|
87
|
-
See [Trigger Configuration](docs/TRIGGER-CONFIG-UI.md) and [CLI Egress](docs/EGRESS_CLI.md) for the full setup guide.
|
|
88
|
-
|
|
89
|
-
### Screenshots
|
|
94
|
+
Open http://localhost:3000 - create your first trigger from the dashboard, or edit `~/.charon/config/triggers.yaml` directly.
|
|
90
95
|
|
|
91
96
|
<p align="center">
|
|
92
97
|
<img src="docs/images/dashboard.png" width="600" alt="Dashboard" />
|
|
93
98
|
</p>
|
|
94
99
|
<p align="center"><em>Dashboard - view triggers and recent runs</em></p>
|
|
95
100
|
|
|
96
|
-
<p align="center">
|
|
97
|
-
<img src="docs/images/trigger.png" width="600" alt="Trigger Configuration" />
|
|
98
|
-
</p>
|
|
99
|
-
<p align="center"><em>Trigger configuration - set up webhooks, cron, and egress</em></p>
|
|
100
|
-
|
|
101
|
-
<p align="center">
|
|
102
|
-
<img src="docs/images/tunnel.png" width="400" alt="Tunnel Configuration" />
|
|
103
|
-
</p>
|
|
104
|
-
<p align="center"><em>Tunnel configuration - expose webhooks via ngrok</em></p>
|
|
105
|
-
|
|
106
101
|
---
|
|
107
102
|
|
|
108
103
|
## Features
|
|
@@ -133,21 +128,12 @@ No external services required. Single process, single database file, runs anywhe
|
|
|
133
128
|
|
|
134
129
|
| Document | Description |
|
|
135
130
|
|----------|-------------|
|
|
136
|
-
| [
|
|
137
|
-
| [
|
|
138
|
-
| [
|
|
139
|
-
| [
|
|
140
|
-
| [
|
|
141
|
-
| [
|
|
142
|
-
| [UI.md](docs/UI.md) | Dashboard documentation |
|
|
143
|
-
|
|
144
|
-
---
|
|
145
|
-
|
|
146
|
-
## Why "Charon"?
|
|
147
|
-
|
|
148
|
-
In Greek mythology, Charon is the ferryman who transports souls across the River Styx.
|
|
149
|
-
|
|
150
|
-
This Charon ferries events to your agents. It doesn't do the work - it makes sure the right work gets to the right worker.
|
|
131
|
+
| [Installation Guide](docs/INSTALL.md) | Setup and service installation |
|
|
132
|
+
| [Trigger Configuration](docs/TRIGGER-CONFIG-UI.md) | Configure triggers via UI |
|
|
133
|
+
| [CLI Egress](docs/EGRESS_CLI.md) | CLI egress handler details |
|
|
134
|
+
| [Tunnel Setup](docs/TUNNEL.md) | ngrok tunnel configuration |
|
|
135
|
+
| [Architecture](docs/ARCHITECTURE.md) | System design and data flow |
|
|
136
|
+
| [API Contracts](docs/CONTRACTS.md) | API contracts and data formats |
|
|
151
137
|
|
|
152
138
|
---
|
|
153
139
|
|
package/bin/charon.js
CHANGED
|
@@ -25,13 +25,13 @@ Options:
|
|
|
25
25
|
--service <command> Service management (status|start|stop|install)
|
|
26
26
|
|
|
27
27
|
Service Commands:
|
|
28
|
-
--service status Check
|
|
28
|
+
--service status Check status (JSON: running, port, url, webhook_base)
|
|
29
29
|
--service start Start Charon in background
|
|
30
30
|
--service stop Stop background Charon process
|
|
31
31
|
--service install Generate system service files
|
|
32
32
|
|
|
33
33
|
Promise Commands (for Claude Code integration):
|
|
34
|
-
--wait <uuid> --trigger <id> Wait for
|
|
34
|
+
--wait <uuid> --trigger <id> Wait for webhook (prints URL to stderr)
|
|
35
35
|
--resolve <uuid> --description <text> Resolve a pending promise
|
|
36
36
|
|
|
37
37
|
Configuration:
|
|
@@ -42,8 +42,8 @@ Environment:
|
|
|
42
42
|
CHARON_DATA_DIR Override data directory (default: ~/.charon)
|
|
43
43
|
|
|
44
44
|
Data Location:
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
Configuration and data are stored in ~/.charon/
|
|
46
|
+
Override with CHARON_DATA_DIR environment variable.
|
|
47
47
|
|
|
48
48
|
Documentation:
|
|
49
49
|
https://github.com/NaxYo/charon
|
|
@@ -58,17 +58,10 @@ if (args.includes('--version') || args.includes('-v')) {
|
|
|
58
58
|
process.exit(0);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
let dataDir;
|
|
66
|
-
if (existsSync(localConfig)) {
|
|
67
|
-
dataDir = process.cwd();
|
|
68
|
-
} else {
|
|
69
|
-
dataDir = userDataDir;
|
|
70
|
-
ensureDataDir(dataDir);
|
|
71
|
-
}
|
|
61
|
+
// Data directory is always ~/.charon/ (or CHARON_DATA_DIR if set)
|
|
62
|
+
// Dev mode uses `bun dev` directly, not this CLI
|
|
63
|
+
const dataDir = process.env.CHARON_DATA_DIR || resolve(homedir(), '.charon');
|
|
64
|
+
ensureDataDir(dataDir);
|
|
72
65
|
|
|
73
66
|
// Set environment for the server
|
|
74
67
|
process.env.CHARON_DATA_DIR = dataDir;
|
|
@@ -130,11 +123,7 @@ if (isRunning) {
|
|
|
130
123
|
}
|
|
131
124
|
|
|
132
125
|
// Show startup info
|
|
133
|
-
|
|
134
|
-
console.log('[charon] Dev mode: using local config');
|
|
135
|
-
} else {
|
|
136
|
-
console.log('[charon] Using user config:', resolve(dataDir, 'config'));
|
|
137
|
-
}
|
|
126
|
+
console.log('[charon] Data dir:', dataDir);
|
|
138
127
|
console.log('[charon] Server port:', port);
|
|
139
128
|
|
|
140
129
|
// Write PID file
|
|
@@ -149,6 +138,24 @@ process.on('SIGTERM', () => { cleanupPidFile(pidFile); process.exit(0); });
|
|
|
149
138
|
const serverPath = resolve(__dirname, '..', 'dist', 'server', 'index.js');
|
|
150
139
|
await import(serverPath);
|
|
151
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Get the base URL for webhooks (tunnel URL if active, otherwise localhost)
|
|
143
|
+
*/
|
|
144
|
+
async function getWebhookBaseUrl(port) {
|
|
145
|
+
try {
|
|
146
|
+
const response = await fetch(`http://localhost:${port}/api/tunnel`);
|
|
147
|
+
if (response.ok) {
|
|
148
|
+
const tunnel = await response.json();
|
|
149
|
+
if (tunnel.connected && tunnel.url) {
|
|
150
|
+
return tunnel.url;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
} catch {
|
|
154
|
+
// Tunnel API not available or error, fall back to localhost
|
|
155
|
+
}
|
|
156
|
+
return `http://localhost:${port}`;
|
|
157
|
+
}
|
|
158
|
+
|
|
152
159
|
/**
|
|
153
160
|
* Handle --wait command: blocks until the promise is resolved
|
|
154
161
|
*/
|
|
@@ -159,6 +166,12 @@ async function handleWaitCommand(uuid, triggerId, port) {
|
|
|
159
166
|
process.exit(1);
|
|
160
167
|
}
|
|
161
168
|
|
|
169
|
+
// Get webhook URL and output to stderr (so stdout stays clean for resolved description)
|
|
170
|
+
const baseUrl = await getWebhookBaseUrl(port);
|
|
171
|
+
const webhookUrl = `${baseUrl}/api/webhook/${triggerId}/${uuid}`;
|
|
172
|
+
console.error(`[charon] Webhook URL: ${webhookUrl}`);
|
|
173
|
+
console.error(`[charon] Waiting for webhook...`);
|
|
174
|
+
|
|
162
175
|
try {
|
|
163
176
|
const controller = new AbortController();
|
|
164
177
|
|
|
@@ -265,9 +278,23 @@ async function handleServiceCommand(command, port, pidFile) {
|
|
|
265
278
|
const running = await isCharonRunning(port);
|
|
266
279
|
if (running) {
|
|
267
280
|
const pid = readPidFile(pidFile);
|
|
268
|
-
|
|
281
|
+
const baseUrl = await getWebhookBaseUrl(port);
|
|
282
|
+
// Output JSON for machine-readable status
|
|
283
|
+
console.log(JSON.stringify({
|
|
284
|
+
running: true,
|
|
285
|
+
port,
|
|
286
|
+
pid: pid || null,
|
|
287
|
+
url: baseUrl,
|
|
288
|
+
webhook_base: `${baseUrl}/api/webhook`
|
|
289
|
+
}));
|
|
269
290
|
} else {
|
|
270
|
-
console.log(
|
|
291
|
+
console.log(JSON.stringify({
|
|
292
|
+
running: false,
|
|
293
|
+
port,
|
|
294
|
+
pid: null,
|
|
295
|
+
url: null,
|
|
296
|
+
webhook_base: null
|
|
297
|
+
}));
|
|
271
298
|
}
|
|
272
299
|
break;
|
|
273
300
|
}
|
package/dist/server/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// src/server/app.ts
|
|
2
2
|
import { Hono as Hono8 } from "hono";
|
|
3
|
-
import { dirname as
|
|
4
|
-
import { fileURLToPath } from "url";
|
|
5
|
-
import { existsSync as
|
|
3
|
+
import { dirname as dirname3, resolve as resolve4 } from "path";
|
|
4
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
5
|
+
import { existsSync as existsSync5 } from "fs";
|
|
6
6
|
|
|
7
7
|
// src/server/middleware/logger.ts
|
|
8
8
|
import { createMiddleware } from "hono/factory";
|
|
@@ -106,8 +106,8 @@ function serveStatic({ root, path: fallbackPath }) {
|
|
|
106
106
|
import { Hono } from "hono";
|
|
107
107
|
|
|
108
108
|
// src/lib/config/loader.ts
|
|
109
|
-
import { readFileSync as readFileSync2, writeFileSync, mkdirSync, watch, existsSync as
|
|
110
|
-
import { dirname } from "path";
|
|
109
|
+
import { readFileSync as readFileSync2, writeFileSync, mkdirSync as mkdirSync2, watch, existsSync as existsSync3 } from "fs";
|
|
110
|
+
import { dirname as dirname2 } from "path";
|
|
111
111
|
|
|
112
112
|
// src/lib/config/schema.ts
|
|
113
113
|
import { z } from "zod";
|
|
@@ -258,8 +258,8 @@ function initSchema(db2) {
|
|
|
258
258
|
var db = null;
|
|
259
259
|
function getDb() {
|
|
260
260
|
if (!db) {
|
|
261
|
-
const
|
|
262
|
-
db = createDatabase(
|
|
261
|
+
const dbPath2 = process.env.CHARON_DB || "charon.db";
|
|
262
|
+
db = createDatabase(dbPath2);
|
|
263
263
|
initSchema(db);
|
|
264
264
|
}
|
|
265
265
|
return db;
|
|
@@ -422,16 +422,88 @@ function listEvents(db2, filter) {
|
|
|
422
422
|
}
|
|
423
423
|
|
|
424
424
|
// src/lib/pipeline/sanitizer.ts
|
|
425
|
-
import { resolve as
|
|
425
|
+
import { resolve as resolve3 } from "path";
|
|
426
426
|
import { pathToFileURL } from "url";
|
|
427
|
+
|
|
428
|
+
// src/lib/data-dir.ts
|
|
429
|
+
import { existsSync as existsSync2, mkdirSync, copyFileSync, readdirSync } from "fs";
|
|
430
|
+
import { resolve as resolve2, dirname } from "path";
|
|
431
|
+
import { homedir } from "os";
|
|
432
|
+
import { fileURLToPath } from "url";
|
|
433
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
434
|
+
var isDevMode = process.env.CHARON_DEV === "1";
|
|
435
|
+
var dataDir = isDevMode ? process.cwd() : process.env.CHARON_DATA_DIR || resolve2(homedir(), ".charon");
|
|
436
|
+
var configDir = resolve2(dataDir, "config");
|
|
437
|
+
var triggersPath = resolve2(configDir, "triggers.yaml");
|
|
438
|
+
var configPath = resolve2(configDir, "config.yaml");
|
|
439
|
+
var sanitizersDir = resolve2(dataDir, "sanitizers");
|
|
440
|
+
var dbPath = process.env.CHARON_DB || resolve2(dataDir, "charon.db");
|
|
441
|
+
function getBundledDir() {
|
|
442
|
+
if (isDevMode) {
|
|
443
|
+
return process.cwd();
|
|
444
|
+
}
|
|
445
|
+
return resolve2(__dirname, "../..");
|
|
446
|
+
}
|
|
447
|
+
function initializeDataDir() {
|
|
448
|
+
if (isDevMode) {
|
|
449
|
+
console.log("[data-dir] Dev mode: using", dataDir);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
console.log("[data-dir] Prod mode: using", dataDir);
|
|
453
|
+
if (!existsSync2(dataDir)) {
|
|
454
|
+
mkdirSync(dataDir, { recursive: true });
|
|
455
|
+
}
|
|
456
|
+
if (!existsSync2(configDir)) {
|
|
457
|
+
mkdirSync(configDir, { recursive: true });
|
|
458
|
+
}
|
|
459
|
+
if (!existsSync2(sanitizersDir)) {
|
|
460
|
+
mkdirSync(sanitizersDir, { recursive: true });
|
|
461
|
+
}
|
|
462
|
+
const bundledDir = getBundledDir();
|
|
463
|
+
copyDefaultFile(
|
|
464
|
+
resolve2(bundledDir, "config/config.yaml.dist"),
|
|
465
|
+
resolve2(configDir, "config.yaml")
|
|
466
|
+
);
|
|
467
|
+
copyDefaultFile(
|
|
468
|
+
resolve2(bundledDir, "config/triggers.yaml.dist"),
|
|
469
|
+
resolve2(configDir, "triggers.yaml")
|
|
470
|
+
);
|
|
471
|
+
const bundledSanitizersDir = resolve2(bundledDir, "sanitizers");
|
|
472
|
+
if (existsSync2(bundledSanitizersDir)) {
|
|
473
|
+
const existingSanitizers = existsSync2(sanitizersDir) ? readdirSync(sanitizersDir).filter((f) => f.endsWith(".ts")) : [];
|
|
474
|
+
if (existingSanitizers.length === 0) {
|
|
475
|
+
const defaultSanitizers = readdirSync(bundledSanitizersDir).filter(
|
|
476
|
+
(f) => f.endsWith(".ts")
|
|
477
|
+
);
|
|
478
|
+
for (const file of defaultSanitizers) {
|
|
479
|
+
copyDefaultFile(
|
|
480
|
+
resolve2(bundledSanitizersDir, file),
|
|
481
|
+
resolve2(sanitizersDir, file)
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
if (defaultSanitizers.length > 0) {
|
|
485
|
+
console.log(
|
|
486
|
+
`[data-dir] Installed ${defaultSanitizers.length} default sanitizer(s)`
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
function copyDefaultFile(src, dest) {
|
|
493
|
+
if (!existsSync2(dest) && existsSync2(src)) {
|
|
494
|
+
copyFileSync(src, dest);
|
|
495
|
+
console.log(`[data-dir] Created ${dest}`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// src/lib/pipeline/sanitizer.ts
|
|
427
500
|
var sanitizerCache = /* @__PURE__ */ new Map();
|
|
428
|
-
var SANITIZERS_DIR = process.env.CHARON_SANITIZERS_DIR || "sanitizers";
|
|
429
501
|
async function loadSanitizer(name) {
|
|
430
502
|
if (sanitizerCache.has(name)) {
|
|
431
503
|
return sanitizerCache.get(name);
|
|
432
504
|
}
|
|
433
505
|
try {
|
|
434
|
-
const sanitizerPath =
|
|
506
|
+
const sanitizerPath = resolve3(sanitizersDir, `${name}.ts`);
|
|
435
507
|
const sanitizerUrl = pathToFileURL(sanitizerPath).href;
|
|
436
508
|
const mod = await import(sanitizerUrl);
|
|
437
509
|
const fn = mod.default;
|
|
@@ -754,7 +826,7 @@ async function startTunnel(config, port = 3e3) {
|
|
|
754
826
|
}
|
|
755
827
|
try {
|
|
756
828
|
await ngrok.disconnect();
|
|
757
|
-
await new Promise((
|
|
829
|
+
await new Promise((resolve5) => setTimeout(resolve5, 1e3));
|
|
758
830
|
} catch {
|
|
759
831
|
}
|
|
760
832
|
listener = null;
|
|
@@ -837,9 +909,9 @@ var DEFAULT_CONFIG = `# Charon trigger configuration
|
|
|
837
909
|
|
|
838
910
|
triggers: []
|
|
839
911
|
`;
|
|
840
|
-
async function loadConfig(path =
|
|
841
|
-
if (!
|
|
842
|
-
|
|
912
|
+
async function loadConfig(path = triggersPath) {
|
|
913
|
+
if (!existsSync3(path)) {
|
|
914
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
843
915
|
writeFileSync(path, DEFAULT_CONFIG, "utf-8");
|
|
844
916
|
console.log(`[config] Created default config at ${path}`);
|
|
845
917
|
}
|
|
@@ -851,8 +923,8 @@ async function loadConfig(path = "config/triggers.yaml") {
|
|
|
851
923
|
cachedConfig = result.data;
|
|
852
924
|
return result.data;
|
|
853
925
|
}
|
|
854
|
-
async function initializeApp(
|
|
855
|
-
const config = await loadConfig(
|
|
926
|
+
async function initializeApp(configPath2 = triggersPath) {
|
|
927
|
+
const config = await loadConfig(configPath2);
|
|
856
928
|
const db2 = getDb();
|
|
857
929
|
initScheduler(db2, config.triggers);
|
|
858
930
|
if (config.tunnel) {
|
|
@@ -860,7 +932,7 @@ async function initializeApp(configPath = "config/triggers.yaml") {
|
|
|
860
932
|
} else {
|
|
861
933
|
await stopTunnel();
|
|
862
934
|
}
|
|
863
|
-
startConfigWatcher(
|
|
935
|
+
startConfigWatcher(configPath2);
|
|
864
936
|
return {
|
|
865
937
|
config,
|
|
866
938
|
tunnel: getTunnelStatus()
|
|
@@ -876,11 +948,11 @@ function getTrigger(id) {
|
|
|
876
948
|
const config = getConfig();
|
|
877
949
|
return config.triggers.find((t) => t.id === id);
|
|
878
950
|
}
|
|
879
|
-
async function deleteTrigger(id,
|
|
880
|
-
if (!
|
|
951
|
+
async function deleteTrigger(id, configPath2 = triggersPath) {
|
|
952
|
+
if (!existsSync3(configPath2)) {
|
|
881
953
|
return false;
|
|
882
954
|
}
|
|
883
|
-
const content = readFileSync2(
|
|
955
|
+
const content = readFileSync2(configPath2, "utf-8");
|
|
884
956
|
const result = parseConfig(content);
|
|
885
957
|
if (!result.success) {
|
|
886
958
|
console.error(`[config] Cannot delete trigger: invalid config`);
|
|
@@ -895,22 +967,22 @@ async function deleteTrigger(id, configPath = "config/triggers.yaml") {
|
|
|
895
967
|
config.triggers.splice(triggerIndex, 1);
|
|
896
968
|
const yaml = await import("yaml");
|
|
897
969
|
const newContent = yaml.stringify(config);
|
|
898
|
-
writeFileSync(
|
|
970
|
+
writeFileSync(configPath2, newContent, "utf-8");
|
|
899
971
|
console.log(`[config] Deleted trigger '${id}'`);
|
|
900
972
|
cachedConfig = config;
|
|
901
973
|
return true;
|
|
902
974
|
}
|
|
903
|
-
function startConfigWatcher(
|
|
904
|
-
if (configWatcher && watchedConfigPath ===
|
|
975
|
+
function startConfigWatcher(configPath2 = triggersPath) {
|
|
976
|
+
if (configWatcher && watchedConfigPath === configPath2) {
|
|
905
977
|
return;
|
|
906
978
|
}
|
|
907
979
|
stopConfigWatcher();
|
|
908
|
-
if (!
|
|
909
|
-
console.warn(`[config] Config file not found: ${
|
|
980
|
+
if (!existsSync3(configPath2)) {
|
|
981
|
+
console.warn(`[config] Config file not found: ${configPath2}, skipping watcher`);
|
|
910
982
|
return;
|
|
911
983
|
}
|
|
912
|
-
watchedConfigPath =
|
|
913
|
-
configWatcher = watch(
|
|
984
|
+
watchedConfigPath = configPath2;
|
|
985
|
+
configWatcher = watch(configPath2, (eventType) => {
|
|
914
986
|
if (eventType === "change") {
|
|
915
987
|
if (reloadTimeout) {
|
|
916
988
|
clearTimeout(reloadTimeout);
|
|
@@ -918,7 +990,7 @@ function startConfigWatcher(configPath = "config/triggers.yaml") {
|
|
|
918
990
|
reloadTimeout = setTimeout(async () => {
|
|
919
991
|
console.log("[config] File changed, reloading...");
|
|
920
992
|
try {
|
|
921
|
-
await reloadConfig(
|
|
993
|
+
await reloadConfig(configPath2);
|
|
922
994
|
console.log("[config] Reload complete");
|
|
923
995
|
} catch (err) {
|
|
924
996
|
console.error("[config] Reload failed:", err instanceof Error ? err.message : err);
|
|
@@ -926,7 +998,7 @@ function startConfigWatcher(configPath = "config/triggers.yaml") {
|
|
|
926
998
|
}, 300);
|
|
927
999
|
}
|
|
928
1000
|
});
|
|
929
|
-
console.log(`[config] Watching ${
|
|
1001
|
+
console.log(`[config] Watching ${configPath2} for changes`);
|
|
930
1002
|
}
|
|
931
1003
|
function stopConfigWatcher() {
|
|
932
1004
|
if (reloadTimeout) {
|
|
@@ -940,8 +1012,8 @@ function stopConfigWatcher() {
|
|
|
940
1012
|
console.log("[config] Stopped watching config file");
|
|
941
1013
|
}
|
|
942
1014
|
}
|
|
943
|
-
async function reloadConfig(
|
|
944
|
-
const config = await loadConfig(
|
|
1015
|
+
async function reloadConfig(configPath2) {
|
|
1016
|
+
const config = await loadConfig(configPath2);
|
|
945
1017
|
const db2 = getDb();
|
|
946
1018
|
initScheduler(db2, config.triggers);
|
|
947
1019
|
if (config.tunnel) {
|
|
@@ -955,14 +1027,14 @@ async function reloadConfig(configPath) {
|
|
|
955
1027
|
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
956
1028
|
import { parse as parseYaml2, stringify as stringifyYaml } from "yaml";
|
|
957
1029
|
var DEFAULT_CONFIG_PATH = "config/triggers.yaml";
|
|
958
|
-
async function writeTrigger(trigger,
|
|
1030
|
+
async function writeTrigger(trigger, configPath2 = DEFAULT_CONFIG_PATH) {
|
|
959
1031
|
const validation = validateTrigger(trigger);
|
|
960
1032
|
if (!validation.success) {
|
|
961
1033
|
const messages = validation.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join(", ");
|
|
962
1034
|
return { success: false, error: `Validation failed: ${messages}` };
|
|
963
1035
|
}
|
|
964
1036
|
try {
|
|
965
|
-
const content = readFileSync3(
|
|
1037
|
+
const content = readFileSync3(configPath2, "utf-8");
|
|
966
1038
|
const config = parseYaml2(content) || {};
|
|
967
1039
|
if (!config.triggers) {
|
|
968
1040
|
config.triggers = [];
|
|
@@ -996,15 +1068,15 @@ async function writeTrigger(trigger, configPath = DEFAULT_CONFIG_PATH) {
|
|
|
996
1068
|
defaultStringType: "PLAIN",
|
|
997
1069
|
defaultKeyType: "PLAIN"
|
|
998
1070
|
});
|
|
999
|
-
writeFileSync2(
|
|
1071
|
+
writeFileSync2(configPath2, yamlOutput);
|
|
1000
1072
|
return { success: true };
|
|
1001
1073
|
} catch (err) {
|
|
1002
1074
|
return { success: false, error: `Write failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
1003
1075
|
}
|
|
1004
1076
|
}
|
|
1005
|
-
async function deleteTrigger2(id,
|
|
1077
|
+
async function deleteTrigger2(id, configPath2 = DEFAULT_CONFIG_PATH) {
|
|
1006
1078
|
try {
|
|
1007
|
-
const content = readFileSync3(
|
|
1079
|
+
const content = readFileSync3(configPath2, "utf-8");
|
|
1008
1080
|
const config = parseYaml2(content) || {};
|
|
1009
1081
|
if (!config.triggers || !Array.isArray(config.triggers)) {
|
|
1010
1082
|
return { success: false, error: `Trigger '${id}' not found` };
|
|
@@ -1019,15 +1091,15 @@ async function deleteTrigger2(id, configPath = DEFAULT_CONFIG_PATH) {
|
|
|
1019
1091
|
defaultStringType: "PLAIN",
|
|
1020
1092
|
defaultKeyType: "PLAIN"
|
|
1021
1093
|
});
|
|
1022
|
-
writeFileSync2(
|
|
1094
|
+
writeFileSync2(configPath2, yamlOutput);
|
|
1023
1095
|
return { success: true };
|
|
1024
1096
|
} catch (err) {
|
|
1025
1097
|
return { success: false, error: `Delete failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
1026
1098
|
}
|
|
1027
1099
|
}
|
|
1028
|
-
async function listTriggerIds(
|
|
1100
|
+
async function listTriggerIds(configPath2 = DEFAULT_CONFIG_PATH) {
|
|
1029
1101
|
try {
|
|
1030
|
-
const content = readFileSync3(
|
|
1102
|
+
const content = readFileSync3(configPath2, "utf-8");
|
|
1031
1103
|
const config = parseYaml2(content) || {};
|
|
1032
1104
|
if (!config.triggers || !Array.isArray(config.triggers)) {
|
|
1033
1105
|
return [];
|
|
@@ -1037,9 +1109,9 @@ async function listTriggerIds(configPath = DEFAULT_CONFIG_PATH) {
|
|
|
1037
1109
|
return [];
|
|
1038
1110
|
}
|
|
1039
1111
|
}
|
|
1040
|
-
async function writeTunnelConfig(tunnel,
|
|
1112
|
+
async function writeTunnelConfig(tunnel, configPath2 = DEFAULT_CONFIG_PATH) {
|
|
1041
1113
|
try {
|
|
1042
|
-
const content = readFileSync3(
|
|
1114
|
+
const content = readFileSync3(configPath2, "utf-8");
|
|
1043
1115
|
const config = parseYaml2(content) || {};
|
|
1044
1116
|
const tunnelObj = {
|
|
1045
1117
|
enabled: tunnel.enabled,
|
|
@@ -1058,15 +1130,15 @@ async function writeTunnelConfig(tunnel, configPath = DEFAULT_CONFIG_PATH) {
|
|
|
1058
1130
|
defaultStringType: "PLAIN",
|
|
1059
1131
|
defaultKeyType: "PLAIN"
|
|
1060
1132
|
});
|
|
1061
|
-
writeFileSync2(
|
|
1133
|
+
writeFileSync2(configPath2, yamlOutput);
|
|
1062
1134
|
return { success: true };
|
|
1063
1135
|
} catch (err) {
|
|
1064
1136
|
return { success: false, error: `Write failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
1065
1137
|
}
|
|
1066
1138
|
}
|
|
1067
|
-
async function getTunnelConfig(
|
|
1139
|
+
async function getTunnelConfig(configPath2 = DEFAULT_CONFIG_PATH) {
|
|
1068
1140
|
try {
|
|
1069
|
-
const content = readFileSync3(
|
|
1141
|
+
const content = readFileSync3(configPath2, "utf-8");
|
|
1070
1142
|
const config = parseYaml2(content) || {};
|
|
1071
1143
|
if (!config.tunnel) {
|
|
1072
1144
|
return null;
|
|
@@ -1085,7 +1157,7 @@ async function getTunnelConfig(configPath = DEFAULT_CONFIG_PATH) {
|
|
|
1085
1157
|
|
|
1086
1158
|
// src/server/routes/triggers.ts
|
|
1087
1159
|
var triggersRoutes = new Hono();
|
|
1088
|
-
async function createTriggerInternal(trigger,
|
|
1160
|
+
async function createTriggerInternal(trigger, configPath2) {
|
|
1089
1161
|
const validation = validateTrigger(trigger);
|
|
1090
1162
|
if (!validation.success) {
|
|
1091
1163
|
return {
|
|
@@ -1095,7 +1167,7 @@ async function createTriggerInternal(trigger, configPath) {
|
|
|
1095
1167
|
status: 400
|
|
1096
1168
|
};
|
|
1097
1169
|
}
|
|
1098
|
-
const existingIds = await listTriggerIds(
|
|
1170
|
+
const existingIds = await listTriggerIds(configPath2);
|
|
1099
1171
|
if (existingIds.includes(trigger.id)) {
|
|
1100
1172
|
return {
|
|
1101
1173
|
success: false,
|
|
@@ -1103,7 +1175,7 @@ async function createTriggerInternal(trigger, configPath) {
|
|
|
1103
1175
|
status: 409
|
|
1104
1176
|
};
|
|
1105
1177
|
}
|
|
1106
|
-
const result = await writeTrigger(trigger,
|
|
1178
|
+
const result = await writeTrigger(trigger, configPath2);
|
|
1107
1179
|
if (!result.success) {
|
|
1108
1180
|
return {
|
|
1109
1181
|
success: false,
|
|
@@ -1117,7 +1189,7 @@ async function createTriggerInternal(trigger, configPath) {
|
|
|
1117
1189
|
status: 201
|
|
1118
1190
|
};
|
|
1119
1191
|
}
|
|
1120
|
-
async function updateTriggerInternal(id, trigger,
|
|
1192
|
+
async function updateTriggerInternal(id, trigger, configPath2) {
|
|
1121
1193
|
const validation = validateTrigger(trigger);
|
|
1122
1194
|
if (!validation.success) {
|
|
1123
1195
|
return {
|
|
@@ -1127,7 +1199,7 @@ async function updateTriggerInternal(id, trigger, configPath) {
|
|
|
1127
1199
|
status: 400
|
|
1128
1200
|
};
|
|
1129
1201
|
}
|
|
1130
|
-
const existingIds = await listTriggerIds(
|
|
1202
|
+
const existingIds = await listTriggerIds(configPath2);
|
|
1131
1203
|
if (!existingIds.includes(id)) {
|
|
1132
1204
|
return {
|
|
1133
1205
|
success: false,
|
|
@@ -1144,7 +1216,7 @@ async function updateTriggerInternal(id, trigger, configPath) {
|
|
|
1144
1216
|
};
|
|
1145
1217
|
}
|
|
1146
1218
|
if (idChanged) {
|
|
1147
|
-
const deleteResult = await deleteTrigger2(id,
|
|
1219
|
+
const deleteResult = await deleteTrigger2(id, configPath2);
|
|
1148
1220
|
if (!deleteResult.success) {
|
|
1149
1221
|
return {
|
|
1150
1222
|
success: false,
|
|
@@ -1153,7 +1225,7 @@ async function updateTriggerInternal(id, trigger, configPath) {
|
|
|
1153
1225
|
};
|
|
1154
1226
|
}
|
|
1155
1227
|
}
|
|
1156
|
-
const result = await writeTrigger(trigger,
|
|
1228
|
+
const result = await writeTrigger(trigger, configPath2);
|
|
1157
1229
|
if (!result.success) {
|
|
1158
1230
|
return {
|
|
1159
1231
|
success: false,
|
|
@@ -1169,8 +1241,8 @@ async function updateTriggerInternal(id, trigger, configPath) {
|
|
|
1169
1241
|
status: 200
|
|
1170
1242
|
};
|
|
1171
1243
|
}
|
|
1172
|
-
async function deleteTriggerInternal(id,
|
|
1173
|
-
const existingIds = await listTriggerIds(
|
|
1244
|
+
async function deleteTriggerInternal(id, configPath2) {
|
|
1245
|
+
const existingIds = await listTriggerIds(configPath2);
|
|
1174
1246
|
if (!existingIds.includes(id)) {
|
|
1175
1247
|
return {
|
|
1176
1248
|
success: false,
|
|
@@ -1178,7 +1250,7 @@ async function deleteTriggerInternal(id, configPath) {
|
|
|
1178
1250
|
status: 404
|
|
1179
1251
|
};
|
|
1180
1252
|
}
|
|
1181
|
-
const result = await deleteTrigger2(id,
|
|
1253
|
+
const result = await deleteTrigger2(id, configPath2);
|
|
1182
1254
|
if (!result.success) {
|
|
1183
1255
|
return {
|
|
1184
1256
|
success: false,
|
|
@@ -1199,9 +1271,9 @@ async function ensureConfig() {
|
|
|
1199
1271
|
configLoaded = true;
|
|
1200
1272
|
}
|
|
1201
1273
|
}
|
|
1202
|
-
async function testTriggerInternal(id, payload,
|
|
1203
|
-
if (
|
|
1204
|
-
await loadConfig(
|
|
1274
|
+
async function testTriggerInternal(id, payload, configPath2) {
|
|
1275
|
+
if (configPath2) {
|
|
1276
|
+
await loadConfig(configPath2);
|
|
1205
1277
|
} else {
|
|
1206
1278
|
await ensureConfig();
|
|
1207
1279
|
}
|
|
@@ -1360,10 +1432,9 @@ runsRoutes.get("/:id", async (c) => {
|
|
|
1360
1432
|
|
|
1361
1433
|
// src/server/routes/sanitizers.ts
|
|
1362
1434
|
import { Hono as Hono3 } from "hono";
|
|
1363
|
-
import { readdirSync, existsSync as
|
|
1435
|
+
import { readdirSync as readdirSync2, existsSync as existsSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
|
|
1364
1436
|
import { join as join2 } from "path";
|
|
1365
1437
|
var sanitizersRoutes = new Hono3();
|
|
1366
|
-
var DEFAULT_SANITIZERS_DIR = process.env.CHARON_SANITIZERS_DIR || "sanitizers";
|
|
1367
1438
|
var BOILERPLATE = `/**
|
|
1368
1439
|
* Sanitizer function for processing webhook payloads.
|
|
1369
1440
|
*
|
|
@@ -1383,17 +1454,17 @@ export default sanitize;
|
|
|
1383
1454
|
function sanitizeName(name) {
|
|
1384
1455
|
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
|
|
1385
1456
|
}
|
|
1386
|
-
function listSanitizersInternal(
|
|
1387
|
-
if (!
|
|
1457
|
+
function listSanitizersInternal(dir = sanitizersDir) {
|
|
1458
|
+
if (!existsSync4(dir)) {
|
|
1388
1459
|
return [];
|
|
1389
1460
|
}
|
|
1390
|
-
const files =
|
|
1461
|
+
const files = readdirSync2(dir);
|
|
1391
1462
|
return files.filter((f) => f.endsWith(".ts")).map((f) => ({
|
|
1392
1463
|
name: f.replace(".ts", ""),
|
|
1393
|
-
path: join2(
|
|
1464
|
+
path: join2(dir, f)
|
|
1394
1465
|
}));
|
|
1395
1466
|
}
|
|
1396
|
-
function createSanitizerInternal(rawName,
|
|
1467
|
+
function createSanitizerInternal(rawName, dir = sanitizersDir) {
|
|
1397
1468
|
if (!rawName || typeof rawName !== "string") {
|
|
1398
1469
|
return { success: false, error: "Name is required", status: 400 };
|
|
1399
1470
|
}
|
|
@@ -1401,12 +1472,12 @@ function createSanitizerInternal(rawName, sanitizersDir = DEFAULT_SANITIZERS_DIR
|
|
|
1401
1472
|
if (!name) {
|
|
1402
1473
|
return { success: false, error: "Invalid name", status: 400 };
|
|
1403
1474
|
}
|
|
1404
|
-
const filePath = join2(
|
|
1405
|
-
if (
|
|
1475
|
+
const filePath = join2(dir, `${name}.ts`);
|
|
1476
|
+
if (existsSync4(filePath)) {
|
|
1406
1477
|
return { success: false, error: `Sanitizer '${name}' already exists`, status: 409 };
|
|
1407
1478
|
}
|
|
1408
|
-
if (!
|
|
1409
|
-
|
|
1479
|
+
if (!existsSync4(dir)) {
|
|
1480
|
+
mkdirSync3(dir, { recursive: true });
|
|
1410
1481
|
}
|
|
1411
1482
|
writeFileSync3(filePath, BOILERPLATE);
|
|
1412
1483
|
return { success: true, name, path: filePath, status: 201 };
|
|
@@ -1711,10 +1782,10 @@ async function tunnelProxyMiddleware(c, next) {
|
|
|
1711
1782
|
}
|
|
1712
1783
|
|
|
1713
1784
|
// src/server/app.ts
|
|
1714
|
-
var
|
|
1715
|
-
var prodClientDir =
|
|
1716
|
-
var devClientDir =
|
|
1717
|
-
var clientDir =
|
|
1785
|
+
var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
|
|
1786
|
+
var prodClientDir = resolve4(__dirname2, "../client");
|
|
1787
|
+
var devClientDir = resolve4(process.cwd(), "dist/client");
|
|
1788
|
+
var clientDir = existsSync5(devClientDir) ? devClientDir : prodClientDir;
|
|
1718
1789
|
var app = new Hono8();
|
|
1719
1790
|
app.use("*", quietLogger);
|
|
1720
1791
|
app.use("*", tunnelProxyMiddleware);
|
|
@@ -1726,11 +1797,12 @@ app.route("/api/webhook", webhookRoutes);
|
|
|
1726
1797
|
app.route("/api/task", taskRoutes);
|
|
1727
1798
|
app.route("/api/promise", promiseRoutes);
|
|
1728
1799
|
app.use("/*", serveStatic({ root: clientDir }));
|
|
1729
|
-
app.get("*", serveStatic({ path:
|
|
1800
|
+
app.get("*", serveStatic({ path: resolve4(clientDir, "index.html") }));
|
|
1730
1801
|
|
|
1731
1802
|
// src/server/init.ts
|
|
1732
1803
|
async function initializeServices() {
|
|
1733
1804
|
try {
|
|
1805
|
+
initializeDataDir();
|
|
1734
1806
|
const { config, tunnel } = await initializeApp();
|
|
1735
1807
|
console.log(`[init] Loaded ${config.triggers.length} triggers`);
|
|
1736
1808
|
if (tunnel.connected) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "charon-hooks",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Autonomous task triggering service - webhooks and cron to AI agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,17 +12,17 @@
|
|
|
12
12
|
"dist/"
|
|
13
13
|
],
|
|
14
14
|
"scripts": {
|
|
15
|
-
"dev": "bun run dev:server",
|
|
16
|
-
"dev:server": "bun --watch src/server/index.ts",
|
|
15
|
+
"dev": "CHARON_DEV=1 bun run dev:server",
|
|
16
|
+
"dev:server": "CHARON_DEV=1 bun --watch src/server/index.ts",
|
|
17
17
|
"dev:client": "vite",
|
|
18
|
-
"dev:all": "bun run build:client && bun run dev:server",
|
|
18
|
+
"dev:all": "bun run build:client && CHARON_DEV=1 bun run dev:server",
|
|
19
19
|
"build": "bun run build:client && bun run build:server",
|
|
20
20
|
"build:client": "vite build",
|
|
21
21
|
"build:server": "tsup",
|
|
22
22
|
"start": "node dist/server/index.js",
|
|
23
23
|
"lint": "eslint",
|
|
24
|
-
"test": "CHARON_DB=:memory: bun test",
|
|
25
|
-
"test:watch": "CHARON_DB=:memory: bun test --watch"
|
|
24
|
+
"test": "CHARON_DEV=1 CHARON_DB=:memory: bun test --max-concurrency=1",
|
|
25
|
+
"test:watch": "CHARON_DEV=1 CHARON_DB=:memory: bun test --watch --max-concurrency=1"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@hono/node-server": "^1.19.8",
|