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 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
- git clone https://github.com/NaxYo/charon
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 `config/triggers.yaml` directly. Both work, pick your style.
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
- | [SPEC.md](docs/SPEC.md) | Design principles and specification |
137
- | [ARCHITECTURE.md](docs/ARCHITECTURE.md) | System components and data flow |
138
- | [CONTRACTS.md](docs/CONTRACTS.md) | API contracts and data formats |
139
- | [EGRESS_CLI.md](docs/EGRESS_CLI.md) | CLI egress handler details |
140
- | [EGRESS_AUTO_CLAUDE.md](docs/EGRESS_AUTO_CLAUDE.md) | Auto-Claude egress handler |
141
- | [TUNNEL.md](docs/TUNNEL.md) | ngrok tunnel setup |
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 if Charon is running
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 a promise to be resolved
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
- If ./config/config.yaml exists, uses current directory (dev mode).
46
- Otherwise, uses ~/.charon/ for configuration and data.
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
- // Determine data directory based on config file presence
62
- const localConfig = resolve(process.cwd(), 'config/config.yaml');
63
- const userDataDir = process.env.CHARON_DATA_DIR || resolve(homedir(), '.charon');
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
- if (existsSync(localConfig)) {
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
- console.log(`[charon] Running on port ${port}${pid ? ` (PID: ${pid})` : ''}`);
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(`[charon] Not running`);
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
  }
@@ -1,8 +1,8 @@
1
1
  // src/server/app.ts
2
2
  import { Hono as Hono8 } from "hono";
3
- import { dirname as dirname2, resolve as resolve3 } from "path";
4
- import { fileURLToPath } from "url";
5
- import { existsSync as existsSync4 } from "fs";
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 existsSync2 } from "fs";
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 dbPath = process.env.CHARON_DB || "charon.db";
262
- db = createDatabase(dbPath);
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 resolve2 } from "path";
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 = resolve2(SANITIZERS_DIR, `${name}.ts`);
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((resolve4) => setTimeout(resolve4, 1e3));
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 = "config/triggers.yaml") {
841
- if (!existsSync2(path)) {
842
- mkdirSync(dirname(path), { recursive: true });
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(configPath = "config/triggers.yaml") {
855
- const config = await loadConfig(configPath);
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(configPath);
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, configPath = "config/triggers.yaml") {
880
- if (!existsSync2(configPath)) {
951
+ async function deleteTrigger(id, configPath2 = triggersPath) {
952
+ if (!existsSync3(configPath2)) {
881
953
  return false;
882
954
  }
883
- const content = readFileSync2(configPath, "utf-8");
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(configPath, newContent, "utf-8");
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(configPath = "config/triggers.yaml") {
904
- if (configWatcher && watchedConfigPath === configPath) {
975
+ function startConfigWatcher(configPath2 = triggersPath) {
976
+ if (configWatcher && watchedConfigPath === configPath2) {
905
977
  return;
906
978
  }
907
979
  stopConfigWatcher();
908
- if (!existsSync2(configPath)) {
909
- console.warn(`[config] Config file not found: ${configPath}, skipping watcher`);
980
+ if (!existsSync3(configPath2)) {
981
+ console.warn(`[config] Config file not found: ${configPath2}, skipping watcher`);
910
982
  return;
911
983
  }
912
- watchedConfigPath = configPath;
913
- configWatcher = watch(configPath, (eventType) => {
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(configPath);
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 ${configPath} for changes`);
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(configPath) {
944
- const config = await loadConfig(configPath);
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, configPath = DEFAULT_CONFIG_PATH) {
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(configPath, "utf-8");
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(configPath, yamlOutput);
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, configPath = DEFAULT_CONFIG_PATH) {
1077
+ async function deleteTrigger2(id, configPath2 = DEFAULT_CONFIG_PATH) {
1006
1078
  try {
1007
- const content = readFileSync3(configPath, "utf-8");
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(configPath, yamlOutput);
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(configPath = DEFAULT_CONFIG_PATH) {
1100
+ async function listTriggerIds(configPath2 = DEFAULT_CONFIG_PATH) {
1029
1101
  try {
1030
- const content = readFileSync3(configPath, "utf-8");
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, configPath = DEFAULT_CONFIG_PATH) {
1112
+ async function writeTunnelConfig(tunnel, configPath2 = DEFAULT_CONFIG_PATH) {
1041
1113
  try {
1042
- const content = readFileSync3(configPath, "utf-8");
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(configPath, yamlOutput);
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(configPath = DEFAULT_CONFIG_PATH) {
1139
+ async function getTunnelConfig(configPath2 = DEFAULT_CONFIG_PATH) {
1068
1140
  try {
1069
- const content = readFileSync3(configPath, "utf-8");
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, configPath) {
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(configPath);
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, configPath);
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, configPath) {
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(configPath);
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, configPath);
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, configPath);
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, configPath) {
1173
- const existingIds = await listTriggerIds(configPath);
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, configPath);
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, configPath) {
1203
- if (configPath) {
1204
- await loadConfig(configPath);
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 existsSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2 } from "fs";
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(sanitizersDir = DEFAULT_SANITIZERS_DIR) {
1387
- if (!existsSync3(sanitizersDir)) {
1457
+ function listSanitizersInternal(dir = sanitizersDir) {
1458
+ if (!existsSync4(dir)) {
1388
1459
  return [];
1389
1460
  }
1390
- const files = readdirSync(sanitizersDir);
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(sanitizersDir, f)
1464
+ path: join2(dir, f)
1394
1465
  }));
1395
1466
  }
1396
- function createSanitizerInternal(rawName, sanitizersDir = DEFAULT_SANITIZERS_DIR) {
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(sanitizersDir, `${name}.ts`);
1405
- if (existsSync3(filePath)) {
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 (!existsSync3(sanitizersDir)) {
1409
- mkdirSync2(sanitizersDir, { recursive: true });
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 __dirname = dirname2(fileURLToPath(import.meta.url));
1715
- var prodClientDir = resolve3(__dirname, "../client");
1716
- var devClientDir = resolve3(process.cwd(), "dist/client");
1717
- var clientDir = existsSync4(devClientDir) ? devClientDir : prodClientDir;
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: resolve3(clientDir, "index.html") }));
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.0",
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",