charon-hooks 0.2.3 → 0.2.4
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/dist/server/index.js +480 -42
- package/package.json +2 -1
package/dist/server/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { Hono as Hono8 } from "hono";
|
|
3
3
|
import { dirname as dirname3, resolve as resolve4 } from "path";
|
|
4
4
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
5
|
-
import { existsSync as
|
|
5
|
+
import { existsSync as existsSync7 } from "fs";
|
|
6
6
|
|
|
7
7
|
// src/server/middleware/logger.ts
|
|
8
8
|
import { createMiddleware } from "hono/factory";
|
|
@@ -106,7 +106,7 @@ 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 as
|
|
109
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3, watch, existsSync as existsSync5 } from "fs";
|
|
110
110
|
import { dirname as dirname2 } from "path";
|
|
111
111
|
|
|
112
112
|
// src/lib/config/schema.ts
|
|
@@ -158,9 +158,9 @@ function parseConfig(yamlContent) {
|
|
|
158
158
|
var createDatabase;
|
|
159
159
|
var isBun = typeof globalThis.Bun !== "undefined";
|
|
160
160
|
if (isBun) {
|
|
161
|
-
const { Database } = await import("bun:sqlite");
|
|
161
|
+
const { Database: Database2 } = await import("bun:sqlite");
|
|
162
162
|
createDatabase = (path) => {
|
|
163
|
-
const db2 = new
|
|
163
|
+
const db2 = new Database2(path);
|
|
164
164
|
return {
|
|
165
165
|
exec: (sql) => db2.run(sql),
|
|
166
166
|
prepare: (sql) => {
|
|
@@ -424,6 +424,7 @@ function listEvents(db2, filter) {
|
|
|
424
424
|
// src/lib/pipeline/sanitizer.ts
|
|
425
425
|
import { resolve as resolve3 } from "path";
|
|
426
426
|
import { pathToFileURL } from "url";
|
|
427
|
+
import { createJiti } from "jiti";
|
|
427
428
|
|
|
428
429
|
// src/lib/data-dir.ts
|
|
429
430
|
import { existsSync as existsSync2, mkdirSync, copyFileSync, readdirSync } from "fs";
|
|
@@ -498,20 +499,28 @@ function copyDefaultFile(src, dest) {
|
|
|
498
499
|
|
|
499
500
|
// src/lib/pipeline/sanitizer.ts
|
|
500
501
|
var sanitizerCache = /* @__PURE__ */ new Map();
|
|
502
|
+
var jiti = createJiti(import.meta.url);
|
|
501
503
|
async function loadSanitizer(name) {
|
|
502
504
|
if (sanitizerCache.has(name)) {
|
|
503
505
|
return sanitizerCache.get(name);
|
|
504
506
|
}
|
|
507
|
+
const sanitizerPath = resolve3(sanitizersDir, `${name}.ts`);
|
|
505
508
|
try {
|
|
506
|
-
const sanitizerPath = resolve3(sanitizersDir, `${name}.ts`);
|
|
507
509
|
const sanitizerUrl = pathToFileURL(sanitizerPath).href;
|
|
508
510
|
const mod = await import(sanitizerUrl);
|
|
509
511
|
const fn = mod.default;
|
|
510
512
|
sanitizerCache.set(name, fn);
|
|
511
513
|
return fn;
|
|
512
514
|
} catch {
|
|
513
|
-
|
|
514
|
-
|
|
515
|
+
try {
|
|
516
|
+
const mod = await jiti.import(sanitizerPath);
|
|
517
|
+
const fn = mod.default;
|
|
518
|
+
sanitizerCache.set(name, fn);
|
|
519
|
+
return fn;
|
|
520
|
+
} catch {
|
|
521
|
+
sanitizerCache.set(name, null);
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
515
524
|
}
|
|
516
525
|
}
|
|
517
526
|
async function executeSanitizer(sanitizer, payload, headers, trigger) {
|
|
@@ -522,25 +531,108 @@ async function executeSanitizer(sanitizer, payload, headers, trigger) {
|
|
|
522
531
|
return result;
|
|
523
532
|
}
|
|
524
533
|
|
|
525
|
-
// src/
|
|
526
|
-
var
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
return egressCache.get(name);
|
|
534
|
+
// src/egress/console.ts
|
|
535
|
+
var consoleEgress = (task) => {
|
|
536
|
+
if (isDevMode) {
|
|
537
|
+
console.log("[egress:console]", JSON.stringify(task, null, 2));
|
|
530
538
|
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
+
};
|
|
540
|
+
var console_default = consoleEgress;
|
|
541
|
+
|
|
542
|
+
// src/egress/cli.ts
|
|
543
|
+
import { spawn } from "child_process";
|
|
544
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync } from "fs";
|
|
545
|
+
import { join as join2 } from "path";
|
|
546
|
+
function ensureMcpConfig() {
|
|
547
|
+
const mcpDir = join2(process.cwd(), "mcp");
|
|
548
|
+
const mcpConfigPath = join2(mcpDir, "charon-mcp.json");
|
|
549
|
+
const finishTaskPath = join2(mcpDir, "finish-task.ts");
|
|
550
|
+
if (!existsSync3(mcpConfigPath)) {
|
|
551
|
+
if (!existsSync3(mcpDir)) {
|
|
552
|
+
mkdirSync2(mcpDir, { recursive: true });
|
|
553
|
+
}
|
|
554
|
+
const config = {
|
|
555
|
+
mcpServers: {
|
|
556
|
+
"finish-task": {
|
|
557
|
+
type: "stdio",
|
|
558
|
+
command: "bun",
|
|
559
|
+
args: [finishTaskPath],
|
|
560
|
+
env: {
|
|
561
|
+
CHARON_API_URL: process.env.CHARON_API_URL || "http://localhost:3000"
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
writeFileSync(mcpConfigPath, JSON.stringify(config, null, 2) + "\n");
|
|
567
|
+
console.log("[egress:cli] Created MCP config:", mcpConfigPath);
|
|
539
568
|
}
|
|
569
|
+
return mcpConfigPath;
|
|
540
570
|
}
|
|
541
|
-
|
|
542
|
-
|
|
571
|
+
function shellEscape(str) {
|
|
572
|
+
return str.replace(/'/g, "'\\''").replace(/\n/g, "\\n");
|
|
573
|
+
}
|
|
574
|
+
function composeCommand(template, context) {
|
|
575
|
+
return template.replace(/\{(\w+)\}/g, (match, key) => {
|
|
576
|
+
if (key in context) {
|
|
577
|
+
const value = context[key];
|
|
578
|
+
const str = typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
579
|
+
return shellEscape(str);
|
|
580
|
+
}
|
|
581
|
+
return match;
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
function composeWorkingDir(workingDir, context) {
|
|
585
|
+
if (!workingDir) return void 0;
|
|
586
|
+
return composeCommand(workingDir, context);
|
|
543
587
|
}
|
|
588
|
+
var cliEgress = (task, trigger) => {
|
|
589
|
+
const cliTemplate = trigger.context?.cli_template;
|
|
590
|
+
if (!cliTemplate || typeof cliTemplate !== "string") {
|
|
591
|
+
console.error("[egress:cli] Missing cli_template in trigger context");
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
const mcpConfig = cliTemplate.includes("{mcp_config}") ? ensureMcpConfig() : void 0;
|
|
595
|
+
const context = {
|
|
596
|
+
...trigger.context,
|
|
597
|
+
...task.task.context,
|
|
598
|
+
description: task.task.description,
|
|
599
|
+
trigger_id: task.trigger_id,
|
|
600
|
+
run_id: task.run_id,
|
|
601
|
+
...mcpConfig && { mcp_config: mcpConfig }
|
|
602
|
+
};
|
|
603
|
+
const command = composeCommand(cliTemplate, context);
|
|
604
|
+
const workingDir = composeWorkingDir(
|
|
605
|
+
trigger.context?.working_dir,
|
|
606
|
+
context
|
|
607
|
+
);
|
|
608
|
+
console.log("[egress:cli] Executing:", command);
|
|
609
|
+
if (workingDir) {
|
|
610
|
+
console.log("[egress:cli] Working directory:", workingDir);
|
|
611
|
+
}
|
|
612
|
+
const child = spawn(command, [], {
|
|
613
|
+
shell: true,
|
|
614
|
+
cwd: workingDir,
|
|
615
|
+
detached: true,
|
|
616
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
617
|
+
});
|
|
618
|
+
child.stdout?.on("data", (data) => {
|
|
619
|
+
console.log("[egress:cli] stdout:", data.toString().trim());
|
|
620
|
+
});
|
|
621
|
+
child.stderr?.on("data", (data) => {
|
|
622
|
+
console.error("[egress:cli] stderr:", data.toString().trim());
|
|
623
|
+
});
|
|
624
|
+
child.on("error", (err) => {
|
|
625
|
+
console.error("[egress:cli] spawn error:", err.message);
|
|
626
|
+
});
|
|
627
|
+
child.on("exit", (code, signal) => {
|
|
628
|
+
if (code !== 0) {
|
|
629
|
+
console.error("[egress:cli] exited with code:", code, "signal:", signal);
|
|
630
|
+
}
|
|
631
|
+
});
|
|
632
|
+
child.unref();
|
|
633
|
+
console.log("[egress:cli] Spawned process PID:", child.pid);
|
|
634
|
+
};
|
|
635
|
+
var cli_default = cliEgress;
|
|
544
636
|
|
|
545
637
|
// src/lib/pipeline/composer.ts
|
|
546
638
|
function compose(template, context) {
|
|
@@ -553,6 +645,352 @@ function compose(template, context) {
|
|
|
553
645
|
});
|
|
554
646
|
}
|
|
555
647
|
|
|
648
|
+
// src/egress/kanban.ts
|
|
649
|
+
import Database from "better-sqlite3";
|
|
650
|
+
import { homedir as homedir2 } from "os";
|
|
651
|
+
import { join as join3 } from "path";
|
|
652
|
+
function uuidToBlob(uuid) {
|
|
653
|
+
const hex = uuid.replace(/-/g, "");
|
|
654
|
+
const bytes = new Uint8Array(16);
|
|
655
|
+
for (let i = 0; i < 16; i++) {
|
|
656
|
+
bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
|
|
657
|
+
}
|
|
658
|
+
return bytes;
|
|
659
|
+
}
|
|
660
|
+
function blobToUUID(blob) {
|
|
661
|
+
const hex = Array.from(blob).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
662
|
+
return hex.slice(0, 8) + "-" + hex.slice(8, 12) + "-" + hex.slice(12, 16) + "-" + hex.slice(16, 20) + "-" + hex.slice(20);
|
|
663
|
+
}
|
|
664
|
+
function lookupRepoForProject(projectId) {
|
|
665
|
+
let db2 = null;
|
|
666
|
+
try {
|
|
667
|
+
const dbPath2 = join3(homedir2(), ".local/share/vibe-kanban/db.sqlite");
|
|
668
|
+
db2 = new Database(dbPath2, { readonly: true, timeout: 5e3 });
|
|
669
|
+
const projectBlob = uuidToBlob(projectId);
|
|
670
|
+
const result = db2.prepare("SELECT repo_id FROM project_repos WHERE project_id = ? LIMIT 1").get(projectBlob);
|
|
671
|
+
if (result?.repo_id) {
|
|
672
|
+
return blobToUUID(new Uint8Array(result.repo_id));
|
|
673
|
+
}
|
|
674
|
+
return null;
|
|
675
|
+
} catch (error) {
|
|
676
|
+
if (error instanceof Error && error.message.includes("locked")) {
|
|
677
|
+
console.error(
|
|
678
|
+
"[egress:kanban] Vibe Kanban database is locked. Please provide repo_id in config or try again later."
|
|
679
|
+
);
|
|
680
|
+
} else {
|
|
681
|
+
console.error("[egress:kanban] Failed to lookup repo:", error);
|
|
682
|
+
}
|
|
683
|
+
return null;
|
|
684
|
+
} finally {
|
|
685
|
+
db2?.close();
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
function extractTitle(description) {
|
|
689
|
+
const firstLine = description.split("\n")[0].trim();
|
|
690
|
+
return firstLine.length > 200 ? firstLine.slice(0, 197) + "..." : firstLine;
|
|
691
|
+
}
|
|
692
|
+
function composeTitle(template, context, fallbackDescription) {
|
|
693
|
+
const composed = compose(template, context);
|
|
694
|
+
const title = composed.length > 200 ? composed.slice(0, 197) + "..." : composed;
|
|
695
|
+
if (!title.trim() || title.includes("{")) {
|
|
696
|
+
return extractTitle(fallbackDescription);
|
|
697
|
+
}
|
|
698
|
+
return title;
|
|
699
|
+
}
|
|
700
|
+
async function startTaskAttempt(apiUrl, taskId, repoId, executor, variant, baseBranch) {
|
|
701
|
+
try {
|
|
702
|
+
const response = await fetch(`${apiUrl}/api/task-attempts`, {
|
|
703
|
+
method: "POST",
|
|
704
|
+
headers: {
|
|
705
|
+
"Content-Type": "application/json"
|
|
706
|
+
},
|
|
707
|
+
body: JSON.stringify({
|
|
708
|
+
task_id: taskId,
|
|
709
|
+
executor_profile_id: {
|
|
710
|
+
executor,
|
|
711
|
+
variant
|
|
712
|
+
},
|
|
713
|
+
repos: [
|
|
714
|
+
{
|
|
715
|
+
repo_id: repoId,
|
|
716
|
+
base_branch: baseBranch,
|
|
717
|
+
target_branch: baseBranch
|
|
718
|
+
}
|
|
719
|
+
]
|
|
720
|
+
})
|
|
721
|
+
});
|
|
722
|
+
if (!response.ok) {
|
|
723
|
+
const errorText = await response.text();
|
|
724
|
+
console.error(
|
|
725
|
+
"[egress:kanban] Failed to start task:",
|
|
726
|
+
response.status,
|
|
727
|
+
response.statusText,
|
|
728
|
+
errorText
|
|
729
|
+
);
|
|
730
|
+
return false;
|
|
731
|
+
}
|
|
732
|
+
const result = await response.json();
|
|
733
|
+
if (!result.success) {
|
|
734
|
+
console.error("[egress:kanban] Start task error:", result.message);
|
|
735
|
+
return false;
|
|
736
|
+
}
|
|
737
|
+
console.log("[egress:kanban] Task started successfully");
|
|
738
|
+
console.log("[egress:kanban] Attempt ID:", result.data?.id);
|
|
739
|
+
console.log("[egress:kanban] Branch:", result.data?.branch);
|
|
740
|
+
return true;
|
|
741
|
+
} catch (error) {
|
|
742
|
+
if (error instanceof Error) {
|
|
743
|
+
console.error("[egress:kanban] Failed to start task:", error.message);
|
|
744
|
+
} else {
|
|
745
|
+
console.error("[egress:kanban] Failed to start task:", error);
|
|
746
|
+
}
|
|
747
|
+
return false;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
var kanbanEgress = async (task, trigger) => {
|
|
751
|
+
const apiUrl = trigger.context?.api_url;
|
|
752
|
+
const projectId = trigger.context?.project_id;
|
|
753
|
+
const titleTemplate = trigger.context?.title_template;
|
|
754
|
+
const autoStart = trigger.context?.auto_start === true;
|
|
755
|
+
const repoId = trigger.context?.repo_id;
|
|
756
|
+
const executor = trigger.context?.executor || "CLAUDE_CODE";
|
|
757
|
+
const variant = trigger.context?.variant || "DEFAULT";
|
|
758
|
+
const baseBranch = trigger.context?.base_branch || "main";
|
|
759
|
+
if (!apiUrl || typeof apiUrl !== "string") {
|
|
760
|
+
console.error("[egress:kanban] Missing required config: api_url");
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
if (!projectId || typeof projectId !== "string") {
|
|
764
|
+
console.error("[egress:kanban] Missing required config: project_id");
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
let resolvedRepoId = repoId;
|
|
768
|
+
if (autoStart && !resolvedRepoId) {
|
|
769
|
+
console.log("[egress:kanban] No repo_id provided, looking up from Vibe Kanban database...");
|
|
770
|
+
resolvedRepoId = lookupRepoForProject(projectId) ?? void 0;
|
|
771
|
+
if (resolvedRepoId) {
|
|
772
|
+
console.log("[egress:kanban] Found repo_id:", resolvedRepoId);
|
|
773
|
+
} else {
|
|
774
|
+
console.error(
|
|
775
|
+
"[egress:kanban] Could not find repo for project. Either provide repo_id in config or ensure the project has a linked repository in Vibe Kanban."
|
|
776
|
+
);
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
const taskContext = task.task.context || {};
|
|
781
|
+
const title = titleTemplate ? composeTitle(titleTemplate, taskContext, task.task.description) : extractTitle(task.task.description);
|
|
782
|
+
const description = task.task.description;
|
|
783
|
+
console.log("[egress:kanban] Creating task:", title);
|
|
784
|
+
console.log("[egress:kanban] Project ID:", projectId);
|
|
785
|
+
console.log("[egress:kanban] API URL:", apiUrl);
|
|
786
|
+
if (autoStart) {
|
|
787
|
+
console.log("[egress:kanban] Auto-start enabled with executor:", executor);
|
|
788
|
+
}
|
|
789
|
+
try {
|
|
790
|
+
const response = await fetch(`${apiUrl}/api/tasks`, {
|
|
791
|
+
method: "POST",
|
|
792
|
+
headers: {
|
|
793
|
+
"Content-Type": "application/json"
|
|
794
|
+
},
|
|
795
|
+
body: JSON.stringify({
|
|
796
|
+
project_id: projectId,
|
|
797
|
+
title,
|
|
798
|
+
description
|
|
799
|
+
})
|
|
800
|
+
});
|
|
801
|
+
if (!response.ok) {
|
|
802
|
+
const errorText = await response.text();
|
|
803
|
+
console.error(
|
|
804
|
+
"[egress:kanban] API error:",
|
|
805
|
+
response.status,
|
|
806
|
+
response.statusText,
|
|
807
|
+
errorText
|
|
808
|
+
);
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
const result = await response.json();
|
|
812
|
+
if (!result.success || !result.data) {
|
|
813
|
+
console.error("[egress:kanban] API returned error:", result.message);
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
const taskId = result.data.id;
|
|
817
|
+
console.log("[egress:kanban] Task created successfully:", taskId);
|
|
818
|
+
console.log("[egress:kanban] Task status:", result.data.status);
|
|
819
|
+
if (autoStart && resolvedRepoId) {
|
|
820
|
+
console.log("[egress:kanban] Starting task with", executor, variant);
|
|
821
|
+
await startTaskAttempt(apiUrl, taskId, resolvedRepoId, executor, variant, baseBranch);
|
|
822
|
+
}
|
|
823
|
+
} catch (error) {
|
|
824
|
+
if (error instanceof Error) {
|
|
825
|
+
console.error("[egress:kanban] Connection failed:", error.message);
|
|
826
|
+
} else {
|
|
827
|
+
console.error("[egress:kanban] Connection failed:", error);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
};
|
|
831
|
+
var kanban_default = kanbanEgress;
|
|
832
|
+
|
|
833
|
+
// src/egress/auto-claude.ts
|
|
834
|
+
import { spawn as spawn2, spawnSync } from "child_process";
|
|
835
|
+
import { existsSync as existsSync4 } from "fs";
|
|
836
|
+
import { join as join4 } from "path";
|
|
837
|
+
import { homedir as homedir3 } from "os";
|
|
838
|
+
var DEB_BACKEND_PATH = "/opt/Auto-Claude/resources/backend";
|
|
839
|
+
var DEB_PYTHON_PATH = "/opt/Auto-Claude/resources/python/bin/python3";
|
|
840
|
+
var DEB_SITE_PACKAGES = "/opt/Auto-Claude/resources/python-site-packages";
|
|
841
|
+
function parseSpecId(output) {
|
|
842
|
+
const patterns = [
|
|
843
|
+
/Created spec:\s*(\d{3}[\w-]*)/i,
|
|
844
|
+
/Spec\s+(\d{3}[\w-]*)/i,
|
|
845
|
+
/specs\/(\d{3}[\w-]*)/,
|
|
846
|
+
/(\d{3}-[\w-]+)/
|
|
847
|
+
];
|
|
848
|
+
for (const pattern of patterns) {
|
|
849
|
+
const match = output.match(pattern);
|
|
850
|
+
if (match) {
|
|
851
|
+
return match[1];
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
return null;
|
|
855
|
+
}
|
|
856
|
+
function resolvePath(path) {
|
|
857
|
+
return path.replace(/^~/, homedir3());
|
|
858
|
+
}
|
|
859
|
+
var autoClaudeEgress = async (task, trigger) => {
|
|
860
|
+
const ctx = trigger.context || {};
|
|
861
|
+
const projectDir = ctx.project_dir;
|
|
862
|
+
if (!projectDir) {
|
|
863
|
+
console.error("[egress:auto-claude] Missing project_dir in trigger context");
|
|
864
|
+
console.error("[egress:auto-claude] This is the directory where Auto-Claude will create specs");
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
const resolvedProjectDir = resolvePath(projectDir);
|
|
868
|
+
if (!existsSync4(resolvedProjectDir)) {
|
|
869
|
+
console.error("[egress:auto-claude] Project directory does not exist:", resolvedProjectDir);
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
const backendPath = resolvePath(ctx.backend_path || DEB_BACKEND_PATH);
|
|
873
|
+
const specRunnerPath = join4(backendPath, "runners", "spec_runner.py");
|
|
874
|
+
const runPath = join4(backendPath, "run.py");
|
|
875
|
+
if (!existsSync4(specRunnerPath)) {
|
|
876
|
+
console.error("[egress:auto-claude] spec_runner.py not found at:", specRunnerPath);
|
|
877
|
+
console.error("[egress:auto-claude] Backend path:", backendPath);
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
const pythonPath = resolvePath(ctx.python_path || DEB_PYTHON_PATH);
|
|
881
|
+
if (!existsSync4(pythonPath)) {
|
|
882
|
+
console.error("[egress:auto-claude] Python not found at:", pythonPath);
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
const complexity = ctx.complexity;
|
|
886
|
+
const autoRun = ctx.auto_run !== false;
|
|
887
|
+
const maxIterations = ctx.max_iterations;
|
|
888
|
+
const skipQa = ctx.skip_qa === true;
|
|
889
|
+
const autoApprove = ctx.auto_approve !== false;
|
|
890
|
+
const description = task.task.description;
|
|
891
|
+
console.log("[egress:auto-claude] Creating spec for task:", description.substring(0, 100) + "...");
|
|
892
|
+
console.log("[egress:auto-claude] Project directory:", resolvedProjectDir);
|
|
893
|
+
console.log("[egress:auto-claude] Backend path:", backendPath);
|
|
894
|
+
const specArgs = [
|
|
895
|
+
specRunnerPath,
|
|
896
|
+
"--task",
|
|
897
|
+
description,
|
|
898
|
+
"--project-dir",
|
|
899
|
+
resolvedProjectDir
|
|
900
|
+
];
|
|
901
|
+
if (complexity) {
|
|
902
|
+
specArgs.push("--complexity", complexity);
|
|
903
|
+
}
|
|
904
|
+
if (autoApprove) {
|
|
905
|
+
specArgs.push("--auto-approve");
|
|
906
|
+
}
|
|
907
|
+
if (!autoRun) {
|
|
908
|
+
specArgs.push("--no-build");
|
|
909
|
+
}
|
|
910
|
+
console.log("[egress:auto-claude] Running:", pythonPath, specArgs.join(" "));
|
|
911
|
+
const env = {
|
|
912
|
+
...process.env,
|
|
913
|
+
PYTHONPATH: DEB_SITE_PACKAGES
|
|
914
|
+
};
|
|
915
|
+
const specResult = spawnSync(pythonPath, specArgs, {
|
|
916
|
+
cwd: backendPath,
|
|
917
|
+
encoding: "utf-8",
|
|
918
|
+
timeout: 12e4,
|
|
919
|
+
// 2 minutes for spec creation
|
|
920
|
+
env
|
|
921
|
+
});
|
|
922
|
+
if (specResult.error) {
|
|
923
|
+
console.error("[egress:auto-claude] Failed to create spec:", specResult.error.message);
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
const specOutput = (specResult.stdout || "") + (specResult.stderr || "");
|
|
927
|
+
console.log("[egress:auto-claude] spec_runner.py output:", specOutput);
|
|
928
|
+
if (specResult.status !== 0) {
|
|
929
|
+
console.error("[egress:auto-claude] spec_runner.py failed with status:", specResult.status);
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
const specId = parseSpecId(specOutput);
|
|
933
|
+
if (specId) {
|
|
934
|
+
console.log("[egress:auto-claude] Created spec:", specId);
|
|
935
|
+
} else {
|
|
936
|
+
console.log("[egress:auto-claude] Spec created (could not parse ID from output)");
|
|
937
|
+
}
|
|
938
|
+
if (!autoRun) {
|
|
939
|
+
console.log("[egress:auto-claude] auto_run is disabled, spec created but not started");
|
|
940
|
+
} else if (specId && existsSync4(runPath)) {
|
|
941
|
+
if (maxIterations || skipQa) {
|
|
942
|
+
const runArgs = [
|
|
943
|
+
runPath,
|
|
944
|
+
"--spec",
|
|
945
|
+
specId,
|
|
946
|
+
"--project-dir",
|
|
947
|
+
resolvedProjectDir,
|
|
948
|
+
"--auto-continue"
|
|
949
|
+
];
|
|
950
|
+
if (maxIterations) {
|
|
951
|
+
runArgs.push("--max-iterations", String(maxIterations));
|
|
952
|
+
}
|
|
953
|
+
if (skipQa) {
|
|
954
|
+
runArgs.push("--skip-qa");
|
|
955
|
+
}
|
|
956
|
+
console.log("[egress:auto-claude] Running spec with options:", pythonPath, runArgs.join(" "));
|
|
957
|
+
const runChild = spawn2(pythonPath, runArgs, {
|
|
958
|
+
cwd: backendPath,
|
|
959
|
+
detached: true,
|
|
960
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
961
|
+
env
|
|
962
|
+
});
|
|
963
|
+
runChild.stdout?.on("data", (data) => {
|
|
964
|
+
console.log("[egress:auto-claude] run.py stdout:", data.toString().trim());
|
|
965
|
+
});
|
|
966
|
+
runChild.stderr?.on("data", (data) => {
|
|
967
|
+
console.error("[egress:auto-claude] run.py stderr:", data.toString().trim());
|
|
968
|
+
});
|
|
969
|
+
runChild.on("error", (err) => {
|
|
970
|
+
console.error("[egress:auto-claude] run.py spawn error:", err.message);
|
|
971
|
+
});
|
|
972
|
+
runChild.unref();
|
|
973
|
+
console.log("[egress:auto-claude] Spawned run.py PID:", runChild.pid);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
console.log("[egress:auto-claude] Task queued successfully");
|
|
977
|
+
};
|
|
978
|
+
var auto_claude_default = autoClaudeEgress;
|
|
979
|
+
|
|
980
|
+
// src/lib/egress/loader.ts
|
|
981
|
+
var egressRegistry = {
|
|
982
|
+
console: console_default,
|
|
983
|
+
cli: cli_default,
|
|
984
|
+
kanban: kanban_default,
|
|
985
|
+
"auto-claude": auto_claude_default
|
|
986
|
+
};
|
|
987
|
+
async function loadEgress(name) {
|
|
988
|
+
return egressRegistry[name] || null;
|
|
989
|
+
}
|
|
990
|
+
async function executeEgress(egress, task, trigger) {
|
|
991
|
+
await egress(task, trigger);
|
|
992
|
+
}
|
|
993
|
+
|
|
556
994
|
// src/lib/pipeline/processor.ts
|
|
557
995
|
function generateTaskId() {
|
|
558
996
|
return "task_" + Math.random().toString(36).slice(2, 10);
|
|
@@ -910,9 +1348,9 @@ var DEFAULT_CONFIG = `# Charon trigger configuration
|
|
|
910
1348
|
triggers: []
|
|
911
1349
|
`;
|
|
912
1350
|
async function loadConfig(path = triggersPath) {
|
|
913
|
-
if (!
|
|
914
|
-
|
|
915
|
-
|
|
1351
|
+
if (!existsSync5(path)) {
|
|
1352
|
+
mkdirSync3(dirname2(path), { recursive: true });
|
|
1353
|
+
writeFileSync2(path, DEFAULT_CONFIG, "utf-8");
|
|
916
1354
|
console.log(`[config] Created default config at ${path}`);
|
|
917
1355
|
}
|
|
918
1356
|
const content = readFileSync2(path, "utf-8");
|
|
@@ -949,7 +1387,7 @@ function getTrigger(id) {
|
|
|
949
1387
|
return config.triggers.find((t) => t.id === id);
|
|
950
1388
|
}
|
|
951
1389
|
async function deleteTrigger(id, configPath2 = triggersPath) {
|
|
952
|
-
if (!
|
|
1390
|
+
if (!existsSync5(configPath2)) {
|
|
953
1391
|
return false;
|
|
954
1392
|
}
|
|
955
1393
|
const content = readFileSync2(configPath2, "utf-8");
|
|
@@ -967,7 +1405,7 @@ async function deleteTrigger(id, configPath2 = triggersPath) {
|
|
|
967
1405
|
config.triggers.splice(triggerIndex, 1);
|
|
968
1406
|
const yaml = await import("yaml");
|
|
969
1407
|
const newContent = yaml.stringify(config);
|
|
970
|
-
|
|
1408
|
+
writeFileSync2(configPath2, newContent, "utf-8");
|
|
971
1409
|
console.log(`[config] Deleted trigger '${id}'`);
|
|
972
1410
|
cachedConfig = config;
|
|
973
1411
|
return true;
|
|
@@ -977,7 +1415,7 @@ function startConfigWatcher(configPath2 = triggersPath) {
|
|
|
977
1415
|
return;
|
|
978
1416
|
}
|
|
979
1417
|
stopConfigWatcher();
|
|
980
|
-
if (!
|
|
1418
|
+
if (!existsSync5(configPath2)) {
|
|
981
1419
|
console.warn(`[config] Config file not found: ${configPath2}, skipping watcher`);
|
|
982
1420
|
return;
|
|
983
1421
|
}
|
|
@@ -1024,7 +1462,7 @@ async function reloadConfig(configPath2) {
|
|
|
1024
1462
|
}
|
|
1025
1463
|
|
|
1026
1464
|
// src/lib/config/writer.ts
|
|
1027
|
-
import { readFileSync as readFileSync3, writeFileSync as
|
|
1465
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
1028
1466
|
import { parse as parseYaml2, stringify as stringifyYaml } from "yaml";
|
|
1029
1467
|
var DEFAULT_CONFIG_PATH = "config/triggers.yaml";
|
|
1030
1468
|
async function writeTrigger(trigger, configPath2 = DEFAULT_CONFIG_PATH) {
|
|
@@ -1068,7 +1506,7 @@ async function writeTrigger(trigger, configPath2 = DEFAULT_CONFIG_PATH) {
|
|
|
1068
1506
|
defaultStringType: "PLAIN",
|
|
1069
1507
|
defaultKeyType: "PLAIN"
|
|
1070
1508
|
});
|
|
1071
|
-
|
|
1509
|
+
writeFileSync3(configPath2, yamlOutput);
|
|
1072
1510
|
return { success: true };
|
|
1073
1511
|
} catch (err) {
|
|
1074
1512
|
return { success: false, error: `Write failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
@@ -1091,7 +1529,7 @@ async function deleteTrigger2(id, configPath2 = DEFAULT_CONFIG_PATH) {
|
|
|
1091
1529
|
defaultStringType: "PLAIN",
|
|
1092
1530
|
defaultKeyType: "PLAIN"
|
|
1093
1531
|
});
|
|
1094
|
-
|
|
1532
|
+
writeFileSync3(configPath2, yamlOutput);
|
|
1095
1533
|
return { success: true };
|
|
1096
1534
|
} catch (err) {
|
|
1097
1535
|
return { success: false, error: `Delete failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
@@ -1130,7 +1568,7 @@ async function writeTunnelConfig(tunnel, configPath2 = DEFAULT_CONFIG_PATH) {
|
|
|
1130
1568
|
defaultStringType: "PLAIN",
|
|
1131
1569
|
defaultKeyType: "PLAIN"
|
|
1132
1570
|
});
|
|
1133
|
-
|
|
1571
|
+
writeFileSync3(configPath2, yamlOutput);
|
|
1134
1572
|
return { success: true };
|
|
1135
1573
|
} catch (err) {
|
|
1136
1574
|
return { success: false, error: `Write failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
@@ -1432,8 +1870,8 @@ runsRoutes.get("/:id", async (c) => {
|
|
|
1432
1870
|
|
|
1433
1871
|
// src/server/routes/sanitizers.ts
|
|
1434
1872
|
import { Hono as Hono3 } from "hono";
|
|
1435
|
-
import { readdirSync as readdirSync2, existsSync as
|
|
1436
|
-
import { join as
|
|
1873
|
+
import { readdirSync as readdirSync2, existsSync as existsSync6, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4 } from "fs";
|
|
1874
|
+
import { join as join5 } from "path";
|
|
1437
1875
|
var sanitizersRoutes = new Hono3();
|
|
1438
1876
|
var BOILERPLATE = `/**
|
|
1439
1877
|
* Sanitizer function for processing webhook payloads.
|
|
@@ -1455,13 +1893,13 @@ function sanitizeName(name) {
|
|
|
1455
1893
|
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
|
|
1456
1894
|
}
|
|
1457
1895
|
function listSanitizersInternal(dir = sanitizersDir) {
|
|
1458
|
-
if (!
|
|
1896
|
+
if (!existsSync6(dir)) {
|
|
1459
1897
|
return [];
|
|
1460
1898
|
}
|
|
1461
1899
|
const files = readdirSync2(dir);
|
|
1462
1900
|
return files.filter((f) => f.endsWith(".ts")).map((f) => ({
|
|
1463
1901
|
name: f.replace(".ts", ""),
|
|
1464
|
-
path:
|
|
1902
|
+
path: join5(dir, f)
|
|
1465
1903
|
}));
|
|
1466
1904
|
}
|
|
1467
1905
|
function createSanitizerInternal(rawName, dir = sanitizersDir) {
|
|
@@ -1472,14 +1910,14 @@ function createSanitizerInternal(rawName, dir = sanitizersDir) {
|
|
|
1472
1910
|
if (!name) {
|
|
1473
1911
|
return { success: false, error: "Invalid name", status: 400 };
|
|
1474
1912
|
}
|
|
1475
|
-
const filePath =
|
|
1476
|
-
if (
|
|
1913
|
+
const filePath = join5(dir, `${name}.ts`);
|
|
1914
|
+
if (existsSync6(filePath)) {
|
|
1477
1915
|
return { success: false, error: `Sanitizer '${name}' already exists`, status: 409 };
|
|
1478
1916
|
}
|
|
1479
|
-
if (!
|
|
1480
|
-
|
|
1917
|
+
if (!existsSync6(dir)) {
|
|
1918
|
+
mkdirSync4(dir, { recursive: true });
|
|
1481
1919
|
}
|
|
1482
|
-
|
|
1920
|
+
writeFileSync4(filePath, BOILERPLATE);
|
|
1483
1921
|
return { success: true, name, path: filePath, status: 201 };
|
|
1484
1922
|
}
|
|
1485
1923
|
sanitizersRoutes.get("/", async (c) => {
|
|
@@ -1785,7 +2223,7 @@ async function tunnelProxyMiddleware(c, next) {
|
|
|
1785
2223
|
var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
|
|
1786
2224
|
var prodClientDir = resolve4(__dirname2, "../client");
|
|
1787
2225
|
var devClientDir = resolve4(process.cwd(), "dist/client");
|
|
1788
|
-
var clientDir =
|
|
2226
|
+
var clientDir = existsSync7(devClientDir) ? devClientDir : prodClientDir;
|
|
1789
2227
|
var app = new Hono8();
|
|
1790
2228
|
app.use("*", quietLogger);
|
|
1791
2229
|
app.use("*", tunnelProxyMiddleware);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "charon-hooks",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "Autonomous task triggering service - webhooks and cron to AI agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"clsx": "^2.1.1",
|
|
37
37
|
"date-fns": "^4.1.0",
|
|
38
38
|
"hono": "^4.7.0",
|
|
39
|
+
"jiti": "^2.6.1",
|
|
39
40
|
"lucide-react": "^0.562.0",
|
|
40
41
|
"node-cron": "^4.2.1",
|
|
41
42
|
"radix-ui": "^1.4.3",
|