jfl 0.2.5 → 0.4.2
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 +308 -28
- package/dist/commands/context-hub.d.ts.map +1 -1
- package/dist/commands/context-hub.js +428 -27
- package/dist/commands/context-hub.js.map +1 -1
- package/dist/commands/eval.d.ts +6 -0
- package/dist/commands/eval.d.ts.map +1 -0
- package/dist/commands/eval.js +236 -0
- package/dist/commands/eval.js.map +1 -0
- package/dist/commands/flows.d.ts +4 -1
- package/dist/commands/flows.d.ts.map +1 -1
- package/dist/commands/flows.js +160 -1
- package/dist/commands/flows.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +272 -145
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/peter.d.ts.map +1 -1
- package/dist/commands/peter.js +220 -1
- package/dist/commands/peter.js.map +1 -1
- package/dist/commands/pi.d.ts +21 -0
- package/dist/commands/pi.d.ts.map +1 -0
- package/dist/commands/pi.js +154 -0
- package/dist/commands/pi.js.map +1 -0
- package/dist/commands/portfolio.d.ts +6 -0
- package/dist/commands/portfolio.d.ts.map +1 -0
- package/dist/commands/portfolio.js +249 -0
- package/dist/commands/portfolio.js.map +1 -0
- package/dist/commands/predict.d.ts +6 -0
- package/dist/commands/predict.d.ts.map +1 -0
- package/dist/commands/predict.js +234 -0
- package/dist/commands/predict.js.map +1 -0
- package/dist/commands/scope.d.ts +1 -0
- package/dist/commands/scope.d.ts.map +1 -1
- package/dist/commands/scope.js +189 -2
- package/dist/commands/scope.js.map +1 -1
- package/dist/commands/synopsis.d.ts +44 -0
- package/dist/commands/synopsis.d.ts.map +1 -1
- package/dist/commands/synopsis.js +1 -1
- package/dist/commands/synopsis.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +49 -1
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/viz.d.ts +7 -0
- package/dist/commands/viz.d.ts.map +1 -0
- package/dist/commands/viz.js +460 -0
- package/dist/commands/viz.js.map +1 -0
- package/dist/commands/voice.js.map +1 -1
- package/dist/dashboard/index.d.ts +4 -5
- package/dist/dashboard/index.d.ts.map +1 -1
- package/dist/dashboard/index.js +57 -119
- package/dist/dashboard/index.js.map +1 -1
- package/dist/dashboard-static/assets/index-B6kRK9Rq.js +116 -0
- package/dist/dashboard-static/assets/index-BpdKJPLu.css +1 -0
- package/dist/dashboard-static/index.html +16 -0
- package/dist/index.js +120 -20
- package/dist/index.js.map +1 -1
- package/dist/lib/eval-store.d.ts +15 -0
- package/dist/lib/eval-store.d.ts.map +1 -0
- package/dist/lib/eval-store.js +179 -0
- package/dist/lib/eval-store.js.map +1 -0
- package/dist/lib/flow-engine.d.ts +13 -0
- package/dist/lib/flow-engine.d.ts.map +1 -1
- package/dist/lib/flow-engine.js +164 -3
- package/dist/lib/flow-engine.js.map +1 -1
- package/dist/lib/hub-client.d.ts +80 -0
- package/dist/lib/hub-client.d.ts.map +1 -0
- package/dist/lib/hub-client.js +46 -0
- package/dist/lib/hub-client.js.map +1 -0
- package/dist/lib/predictor.d.ts +99 -0
- package/dist/lib/predictor.d.ts.map +1 -0
- package/dist/lib/predictor.js +394 -0
- package/dist/lib/predictor.js.map +1 -0
- package/dist/lib/service-gtm.d.ts +88 -44
- package/dist/lib/service-gtm.d.ts.map +1 -1
- package/dist/lib/service-gtm.js +451 -243
- package/dist/lib/service-gtm.js.map +1 -1
- package/dist/lib/telemetry-agent.d.ts +57 -0
- package/dist/lib/telemetry-agent.d.ts.map +1 -0
- package/dist/lib/telemetry-agent.js +268 -0
- package/dist/lib/telemetry-agent.js.map +1 -0
- package/dist/lib/telemetry-digest.d.ts.map +1 -1
- package/dist/lib/telemetry-digest.js +17 -17
- package/dist/lib/telemetry-digest.js.map +1 -1
- package/dist/lib/telemetry.d.ts +1 -0
- package/dist/lib/telemetry.d.ts.map +1 -1
- package/dist/lib/telemetry.js +14 -6
- package/dist/lib/telemetry.js.map +1 -1
- package/dist/lib/trajectory-loader.d.ts +82 -0
- package/dist/lib/trajectory-loader.d.ts.map +1 -0
- package/dist/lib/trajectory-loader.js +406 -0
- package/dist/lib/trajectory-loader.js.map +1 -0
- package/dist/mcp/context-hub-mcp.js +60 -0
- package/dist/mcp/context-hub-mcp.js.map +1 -1
- package/dist/mcp/service-registry-mcp.js +0 -0
- package/dist/types/eval.d.ts +18 -0
- package/dist/types/eval.d.ts.map +1 -0
- package/dist/types/eval.js +5 -0
- package/dist/types/eval.js.map +1 -0
- package/dist/types/journal.d.ts +133 -0
- package/dist/types/journal.d.ts.map +1 -0
- package/dist/types/journal.js +59 -0
- package/dist/types/journal.js.map +1 -0
- package/dist/types/map.d.ts +1 -1
- package/dist/types/map.d.ts.map +1 -1
- package/dist/types/map.js.map +1 -1
- package/dist/ui/service-dashboard.js.map +1 -1
- package/dist/utils/jfl-paths.d.ts +1 -0
- package/dist/utils/jfl-paths.d.ts.map +1 -1
- package/dist/utils/jfl-paths.js +1 -0
- package/dist/utils/jfl-paths.js.map +1 -1
- package/dist/utils/wallet.js.map +1 -1
- package/package.json +7 -2
- package/scripts/generate-changesets.sh +113 -0
- package/scripts/migrate-to-branch-sessions.sh +201 -0
- package/scripts/pp-branch-pr.sh +115 -0
- package/scripts/session/session-cleanup.sh +29 -14
- package/scripts/session/session-end.sh +0 -10
- package/scripts/session/session-init.sh +0 -16
- package/scripts/session/session-sync.sh +0 -10
- package/template/.jfl/flows-self-driving.yaml +170 -0
- package/template/THEORY.md +26 -0
- package/template/scripts/session/session-cleanup.sh +28 -10
- package/dist/dashboard/components.d.ts +0 -7
- package/dist/dashboard/components.d.ts.map +0 -1
- package/dist/dashboard/components.js +0 -163
- package/dist/dashboard/components.js.map +0 -1
- package/dist/dashboard/pages.d.ts +0 -7
- package/dist/dashboard/pages.d.ts.map +0 -1
- package/dist/dashboard/pages.js +0 -742
- package/dist/dashboard/pages.js.map +0 -1
- package/dist/dashboard/styles.d.ts +0 -7
- package/dist/dashboard/styles.d.ts.map +0 -1
- package/dist/dashboard/styles.js +0 -497
- package/dist/dashboard/styles.js.map +0 -1
|
@@ -415,11 +415,89 @@ function getUnifiedContext(projectRoot, query, taskType) {
|
|
|
415
415
|
taskType
|
|
416
416
|
};
|
|
417
417
|
}
|
|
418
|
+
function getChildHubs(projectRoot) {
|
|
419
|
+
const configPath = path.join(projectRoot, ".jfl", "config.json");
|
|
420
|
+
if (!fs.existsSync(configPath))
|
|
421
|
+
return [];
|
|
422
|
+
try {
|
|
423
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
424
|
+
if (config.type !== "portfolio")
|
|
425
|
+
return [];
|
|
426
|
+
const children = [];
|
|
427
|
+
for (const service of config.registered_services || []) {
|
|
428
|
+
if (service.type === "gtm" && service.path) {
|
|
429
|
+
const childPort = getProjectPort(service.path);
|
|
430
|
+
const tokenPath = path.join(service.path, ".jfl", "context-hub.token");
|
|
431
|
+
const token = fs.existsSync(tokenPath)
|
|
432
|
+
? fs.readFileSync(tokenPath, "utf-8").trim()
|
|
433
|
+
: undefined;
|
|
434
|
+
children.push({ name: service.name, path: service.path, port: childPort, token });
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return children;
|
|
438
|
+
}
|
|
439
|
+
catch {
|
|
440
|
+
return [];
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
async function fetchChildContext(child, endpoint, body) {
|
|
444
|
+
try {
|
|
445
|
+
const headers = { "Content-Type": "application/json" };
|
|
446
|
+
if (child.token)
|
|
447
|
+
headers["Authorization"] = `Bearer ${child.token}`;
|
|
448
|
+
const response = await fetch(`http://localhost:${child.port}${endpoint}`, {
|
|
449
|
+
method: "POST",
|
|
450
|
+
headers,
|
|
451
|
+
body: JSON.stringify(body),
|
|
452
|
+
signal: AbortSignal.timeout(5000),
|
|
453
|
+
});
|
|
454
|
+
if (!response.ok)
|
|
455
|
+
return [];
|
|
456
|
+
const data = (await response.json());
|
|
457
|
+
return (data.items || []).map((item) => ({
|
|
458
|
+
...item,
|
|
459
|
+
title: `[${child.name}] ${item.title}`,
|
|
460
|
+
}));
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
return readJournalEntries(child.path, 10).map((item) => ({
|
|
464
|
+
...item,
|
|
465
|
+
title: `[${child.name}] ${item.title}`,
|
|
466
|
+
}));
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
async function getPortfolioContext(projectRoot, query, taskType, maxItems) {
|
|
470
|
+
const local = getUnifiedContext(projectRoot, query, taskType);
|
|
471
|
+
const children = getChildHubs(projectRoot);
|
|
472
|
+
if (children.length === 0)
|
|
473
|
+
return local;
|
|
474
|
+
const endpoint = query ? "/api/context/search" : "/api/context";
|
|
475
|
+
const body = { maxItems: maxItems || 20 };
|
|
476
|
+
if (query)
|
|
477
|
+
body.query = query;
|
|
478
|
+
if (taskType)
|
|
479
|
+
body.taskType = taskType;
|
|
480
|
+
const childResults = await Promise.all(children.map((child) => fetchChildContext(child, endpoint, body)));
|
|
481
|
+
let merged = [...local.items, ...childResults.flat()];
|
|
482
|
+
if (query) {
|
|
483
|
+
merged = semanticSearch(merged, query);
|
|
484
|
+
}
|
|
485
|
+
return {
|
|
486
|
+
items: maxItems ? merged.slice(0, maxItems) : merged,
|
|
487
|
+
sources: {
|
|
488
|
+
...local.sources,
|
|
489
|
+
journal: true,
|
|
490
|
+
knowledge: true,
|
|
491
|
+
},
|
|
492
|
+
query,
|
|
493
|
+
taskType,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
418
496
|
// ============================================================================
|
|
419
497
|
// HTTP Server
|
|
420
498
|
// ============================================================================
|
|
421
|
-
function createServer(projectRoot, port, eventBus) {
|
|
422
|
-
const server = http.createServer((req, res) => {
|
|
499
|
+
function createServer(projectRoot, port, eventBus, flowEngine) {
|
|
500
|
+
const server = http.createServer(async (req, res) => {
|
|
423
501
|
const requestStart = Date.now();
|
|
424
502
|
const pathname = new URL(req.url || "/", `http://localhost:${port}`).pathname;
|
|
425
503
|
// Intercept writeHead to capture status code for telemetry
|
|
@@ -533,29 +611,74 @@ function createServer(projectRoot, port, eventBus) {
|
|
|
533
611
|
}));
|
|
534
612
|
return;
|
|
535
613
|
}
|
|
536
|
-
// Status
|
|
614
|
+
// Status (includes child hub health for portfolio)
|
|
537
615
|
if (url.pathname === "/api/context/status" && req.method === "GET") {
|
|
538
616
|
const context = getUnifiedContext(projectRoot);
|
|
617
|
+
const children = getChildHubs(projectRoot);
|
|
618
|
+
// Read actual workspace type from config
|
|
619
|
+
let workspaceType = "standalone";
|
|
620
|
+
let workspaceConfig = {};
|
|
621
|
+
const configPath = path.join(projectRoot, ".jfl", "config.json");
|
|
622
|
+
if (fs.existsSync(configPath)) {
|
|
623
|
+
try {
|
|
624
|
+
const cfg = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
625
|
+
if (cfg.type === "portfolio" || cfg.type === "gtm" || cfg.type === "service") {
|
|
626
|
+
workspaceType = cfg.type;
|
|
627
|
+
}
|
|
628
|
+
workspaceConfig = {
|
|
629
|
+
name: cfg.name,
|
|
630
|
+
type: cfg.type,
|
|
631
|
+
description: cfg.description,
|
|
632
|
+
scope: cfg.context_scope || null,
|
|
633
|
+
registered_services: (cfg.registered_services || []).map((s) => ({
|
|
634
|
+
name: s.name,
|
|
635
|
+
path: s.path,
|
|
636
|
+
type: s.type,
|
|
637
|
+
status: s.status,
|
|
638
|
+
context_scope: s.context_scope || null,
|
|
639
|
+
})),
|
|
640
|
+
openclaw_agents: (cfg.openclaw_agents || []).map((a) => ({
|
|
641
|
+
id: a.id,
|
|
642
|
+
runtime: a.runtime,
|
|
643
|
+
registered_at: a.registered_at,
|
|
644
|
+
})),
|
|
645
|
+
gtm_parent: cfg.gtm_parent || null,
|
|
646
|
+
portfolio_parent: cfg.portfolio_parent || null,
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
catch { }
|
|
650
|
+
}
|
|
651
|
+
const childStatus = await Promise.all(children.map(async (child) => {
|
|
652
|
+
try {
|
|
653
|
+
const resp = await fetch(`http://localhost:${child.port}/health`, {
|
|
654
|
+
signal: AbortSignal.timeout(2000),
|
|
655
|
+
});
|
|
656
|
+
return { name: child.name, port: child.port, status: resp.ok ? "ok" : "error" };
|
|
657
|
+
}
|
|
658
|
+
catch {
|
|
659
|
+
return { name: child.name, port: child.port, status: "down" };
|
|
660
|
+
}
|
|
661
|
+
}));
|
|
539
662
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
540
663
|
res.end(JSON.stringify({
|
|
541
664
|
status: "running",
|
|
542
665
|
port,
|
|
666
|
+
type: workspaceType,
|
|
667
|
+
config: workspaceConfig,
|
|
543
668
|
sources: context.sources,
|
|
544
|
-
itemCount: context.items.length
|
|
669
|
+
itemCount: context.items.length,
|
|
670
|
+
...(children.length > 0 ? { children: childStatus } : {}),
|
|
545
671
|
}));
|
|
546
672
|
return;
|
|
547
673
|
}
|
|
548
|
-
// Get context
|
|
674
|
+
// Get context (portfolio-aware: fans out to child hubs)
|
|
549
675
|
if (url.pathname === "/api/context" && req.method === "POST") {
|
|
550
676
|
let body = "";
|
|
551
677
|
req.on("data", chunk => body += chunk);
|
|
552
|
-
req.on("end", () => {
|
|
678
|
+
req.on("end", async () => {
|
|
553
679
|
try {
|
|
554
680
|
const { query, taskType, maxItems } = JSON.parse(body || "{}");
|
|
555
|
-
const context =
|
|
556
|
-
if (maxItems && context.items.length > maxItems) {
|
|
557
|
-
context.items = context.items.slice(0, maxItems);
|
|
558
|
-
}
|
|
681
|
+
const context = await getPortfolioContext(projectRoot, query, taskType, maxItems);
|
|
559
682
|
telemetry.track({
|
|
560
683
|
category: 'context_hub',
|
|
561
684
|
event: 'context_hub:context_loaded',
|
|
@@ -575,11 +698,11 @@ function createServer(projectRoot, port, eventBus) {
|
|
|
575
698
|
});
|
|
576
699
|
return;
|
|
577
700
|
}
|
|
578
|
-
// Search
|
|
701
|
+
// Search (portfolio-aware: fans out to child hubs)
|
|
579
702
|
if (url.pathname === "/api/context/search" && req.method === "POST") {
|
|
580
703
|
let body = "";
|
|
581
704
|
req.on("data", chunk => body += chunk);
|
|
582
|
-
req.on("end", () => {
|
|
705
|
+
req.on("end", async () => {
|
|
583
706
|
try {
|
|
584
707
|
const { query, maxItems = 20 } = JSON.parse(body || "{}");
|
|
585
708
|
if (!query) {
|
|
@@ -588,7 +711,7 @@ function createServer(projectRoot, port, eventBus) {
|
|
|
588
711
|
return;
|
|
589
712
|
}
|
|
590
713
|
const searchStart = Date.now();
|
|
591
|
-
const context =
|
|
714
|
+
const context = await getPortfolioContext(projectRoot, query, undefined, maxItems);
|
|
592
715
|
context.items = context.items
|
|
593
716
|
.filter(item => item.relevance && item.relevance > 0)
|
|
594
717
|
.slice(0, maxItems);
|
|
@@ -707,6 +830,90 @@ function createServer(projectRoot, port, eventBus) {
|
|
|
707
830
|
});
|
|
708
831
|
return;
|
|
709
832
|
}
|
|
833
|
+
// Eval trajectory
|
|
834
|
+
if (url.pathname === "/api/eval/trajectory" && req.method === "GET") {
|
|
835
|
+
try {
|
|
836
|
+
const { getTrajectory } = await import("../lib/eval-store.js");
|
|
837
|
+
const agent = url.searchParams.get("agent") || "";
|
|
838
|
+
const metric = url.searchParams.get("metric") || "composite";
|
|
839
|
+
if (!agent) {
|
|
840
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
841
|
+
res.end(JSON.stringify({ error: "agent query param required" }));
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
const points = getTrajectory(agent, metric, projectRoot);
|
|
845
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
846
|
+
res.end(JSON.stringify({ agent, metric, points }));
|
|
847
|
+
}
|
|
848
|
+
catch (err) {
|
|
849
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
850
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
851
|
+
}
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
// Eval leaderboard
|
|
855
|
+
if (url.pathname === "/api/eval/leaderboard" && req.method === "GET") {
|
|
856
|
+
try {
|
|
857
|
+
const { readEvals, listAgents, getLatestEval, getTrajectory } = await import("../lib/eval-store.js");
|
|
858
|
+
const agents = listAgents(projectRoot);
|
|
859
|
+
const leaderboard = agents.map(agent => {
|
|
860
|
+
const latest = getLatestEval(agent, projectRoot);
|
|
861
|
+
const trajectory = getTrajectory(agent, "composite", projectRoot);
|
|
862
|
+
const prevPoint = trajectory.length >= 2 ? trajectory[trajectory.length - 2] : null;
|
|
863
|
+
const delta = latest?.composite != null && prevPoint
|
|
864
|
+
? latest.composite - prevPoint.value
|
|
865
|
+
: null;
|
|
866
|
+
return {
|
|
867
|
+
agent,
|
|
868
|
+
composite: latest?.composite ?? null,
|
|
869
|
+
metrics: latest?.metrics ?? {},
|
|
870
|
+
delta,
|
|
871
|
+
model_version: latest?.model_version ?? null,
|
|
872
|
+
lastTs: latest?.ts ?? null,
|
|
873
|
+
trajectory: trajectory.slice(-20).map(p => p.value),
|
|
874
|
+
};
|
|
875
|
+
}).sort((a, b) => (b.composite ?? 0) - (a.composite ?? 0));
|
|
876
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
877
|
+
res.end(JSON.stringify(leaderboard));
|
|
878
|
+
}
|
|
879
|
+
catch (err) {
|
|
880
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
881
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
882
|
+
}
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
// Synopsis (work summary)
|
|
886
|
+
if (url.pathname === "/api/synopsis" && req.method === "GET") {
|
|
887
|
+
try {
|
|
888
|
+
const hours = parseInt(url.searchParams.get("hours") || "24", 10);
|
|
889
|
+
const author = url.searchParams.get("author") || undefined;
|
|
890
|
+
const { generateSynopsis } = await import("./synopsis.js");
|
|
891
|
+
const synopsis = generateSynopsis(projectRoot, hours, author);
|
|
892
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
893
|
+
res.end(JSON.stringify(synopsis));
|
|
894
|
+
}
|
|
895
|
+
catch (err) {
|
|
896
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
897
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
898
|
+
}
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
// Prediction accuracy (Stratus)
|
|
902
|
+
if (url.pathname === "/api/eval/predictions" && req.method === "GET") {
|
|
903
|
+
try {
|
|
904
|
+
const { Predictor } = await import("../lib/predictor.js");
|
|
905
|
+
const predictor = new Predictor({ projectRoot });
|
|
906
|
+
const accuracy = predictor.getAccuracy();
|
|
907
|
+
const recent = predictor.getHistory(20).reverse();
|
|
908
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
909
|
+
res.end(JSON.stringify({ accuracy, recent }));
|
|
910
|
+
}
|
|
911
|
+
catch (err) {
|
|
912
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
913
|
+
res.end(JSON.stringify({ accuracy: { total: 0, resolved: 0, direction_accuracy: 0, mean_delta_error: 0, calibration: 0 }, recent: [] }));
|
|
914
|
+
}
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
710
917
|
// Cross-project health
|
|
711
918
|
if (url.pathname === "/api/projects" && req.method === "GET") {
|
|
712
919
|
const tracked = getTrackedProjects();
|
|
@@ -820,6 +1027,177 @@ function createServer(projectRoot, port, eventBus) {
|
|
|
820
1027
|
});
|
|
821
1028
|
return;
|
|
822
1029
|
}
|
|
1030
|
+
// Telemetry digest
|
|
1031
|
+
if (url.pathname === "/api/telemetry/digest" && req.method === "GET") {
|
|
1032
|
+
try {
|
|
1033
|
+
const { loadLocalEvents, analyzeEvents } = await import("../lib/telemetry-digest.js");
|
|
1034
|
+
const hours = parseInt(url.searchParams.get("hours") || "168", 10);
|
|
1035
|
+
const events = loadLocalEvents();
|
|
1036
|
+
const digest = analyzeEvents(events, hours);
|
|
1037
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1038
|
+
res.end(JSON.stringify(digest));
|
|
1039
|
+
}
|
|
1040
|
+
catch (err) {
|
|
1041
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1042
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1043
|
+
}
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
// Telemetry agent status
|
|
1047
|
+
if (url.pathname === "/api/telemetry/agent" && req.method === "GET") {
|
|
1048
|
+
const agent = server.__telemetryAgent;
|
|
1049
|
+
if (agent) {
|
|
1050
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1051
|
+
res.end(JSON.stringify(agent.getStatus()));
|
|
1052
|
+
}
|
|
1053
|
+
else {
|
|
1054
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1055
|
+
res.end(JSON.stringify({ running: false, lastRun: '', runCount: 0, lastInsights: [] }));
|
|
1056
|
+
}
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
// Telemetry agent: trigger manual run
|
|
1060
|
+
if (url.pathname === "/api/telemetry/agent/run" && req.method === "POST") {
|
|
1061
|
+
const agent = server.__telemetryAgent;
|
|
1062
|
+
if (agent) {
|
|
1063
|
+
try {
|
|
1064
|
+
const insights = await agent.run();
|
|
1065
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1066
|
+
res.end(JSON.stringify({ ok: true, insights }));
|
|
1067
|
+
}
|
|
1068
|
+
catch (err) {
|
|
1069
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1070
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
else {
|
|
1074
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
1075
|
+
res.end(JSON.stringify({ error: "Telemetry agent not running" }));
|
|
1076
|
+
}
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
// Flow definitions
|
|
1080
|
+
if (url.pathname === "/api/flows" && req.method === "GET") {
|
|
1081
|
+
if (!flowEngine) {
|
|
1082
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1083
|
+
res.end(JSON.stringify([]));
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1087
|
+
res.end(JSON.stringify(flowEngine.getFlows()));
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
// Flow executions
|
|
1091
|
+
if (url.pathname === "/api/flows/executions" && req.method === "GET") {
|
|
1092
|
+
if (!flowEngine) {
|
|
1093
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1094
|
+
res.end(JSON.stringify({ executions: [] }));
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1098
|
+
res.end(JSON.stringify({ executions: flowEngine.getExecutions() }));
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
// Flow approval
|
|
1102
|
+
if (url.pathname.match(/^\/api\/flows\/[^/]+\/approve$/) && req.method === "POST") {
|
|
1103
|
+
if (!flowEngine) {
|
|
1104
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
1105
|
+
res.end(JSON.stringify({ error: "Flow engine not initialized" }));
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
const flowName = decodeURIComponent(url.pathname.split("/")[3]);
|
|
1109
|
+
let body = "";
|
|
1110
|
+
req.on("data", chunk => body += chunk);
|
|
1111
|
+
req.on("end", async () => {
|
|
1112
|
+
try {
|
|
1113
|
+
const { trigger_event_id } = JSON.parse(body || "{}");
|
|
1114
|
+
if (!trigger_event_id) {
|
|
1115
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1116
|
+
res.end(JSON.stringify({ error: "trigger_event_id required" }));
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
const result = await flowEngine.approveGated(flowName, trigger_event_id);
|
|
1120
|
+
if (!result) {
|
|
1121
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1122
|
+
res.end(JSON.stringify({ error: "Gated execution not found" }));
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1126
|
+
res.end(JSON.stringify(result));
|
|
1127
|
+
}
|
|
1128
|
+
catch (err) {
|
|
1129
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1130
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1131
|
+
}
|
|
1132
|
+
});
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
if (url.pathname.match(/^\/api\/flows\/[^/]+\/toggle$/) && req.method === "POST") {
|
|
1136
|
+
if (!flowEngine) {
|
|
1137
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
1138
|
+
res.end(JSON.stringify({ error: "Flow engine not initialized" }));
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
const flowName = decodeURIComponent(url.pathname.split("/")[3]);
|
|
1142
|
+
let body = "";
|
|
1143
|
+
req.on("data", chunk => body += chunk);
|
|
1144
|
+
req.on("end", () => {
|
|
1145
|
+
try {
|
|
1146
|
+
const { enabled } = JSON.parse(body || "{}");
|
|
1147
|
+
const result = flowEngine.toggleFlow(flowName, enabled);
|
|
1148
|
+
if (!result) {
|
|
1149
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1150
|
+
res.end(JSON.stringify({ error: "Flow not found" }));
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1154
|
+
res.end(JSON.stringify({ ok: true, flow: flowName, enabled: result.enabled }));
|
|
1155
|
+
}
|
|
1156
|
+
catch (err) {
|
|
1157
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1158
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1159
|
+
}
|
|
1160
|
+
});
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
if (url.pathname === "/api/actions/spawn" && req.method === "POST") {
|
|
1164
|
+
let body = "";
|
|
1165
|
+
req.on("data", chunk => body += chunk);
|
|
1166
|
+
req.on("end", () => {
|
|
1167
|
+
try {
|
|
1168
|
+
const { command, args, cwd, event_type, event_data } = JSON.parse(body || "{}");
|
|
1169
|
+
if (!command) {
|
|
1170
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1171
|
+
res.end(JSON.stringify({ error: "command required" }));
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
const env = { ...process.env };
|
|
1175
|
+
delete env.ANTHROPIC_API_KEY;
|
|
1176
|
+
delete env.CLAUDE_CODE_ENTRYPOINT;
|
|
1177
|
+
const child = spawn(command, args || [], {
|
|
1178
|
+
cwd: cwd || projectRoot,
|
|
1179
|
+
detached: true,
|
|
1180
|
+
stdio: "ignore",
|
|
1181
|
+
env,
|
|
1182
|
+
});
|
|
1183
|
+
child.unref();
|
|
1184
|
+
if (event_type && eventBus) {
|
|
1185
|
+
eventBus.emit({
|
|
1186
|
+
type: event_type,
|
|
1187
|
+
source: "dashboard:action",
|
|
1188
|
+
data: event_data || { command, args, pid: child.pid },
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1192
|
+
res.end(JSON.stringify({ ok: true, pid: child.pid }));
|
|
1193
|
+
}
|
|
1194
|
+
catch (err) {
|
|
1195
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1196
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1197
|
+
}
|
|
1198
|
+
});
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
823
1201
|
// 404
|
|
824
1202
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
825
1203
|
res.end(JSON.stringify({ error: "Not found" }));
|
|
@@ -1187,17 +1565,18 @@ export async function contextHubCommand(action, options = {}) {
|
|
|
1187
1565
|
const isGlobal = options.global || false;
|
|
1188
1566
|
const projectRoot = isGlobal ? homedir() : process.cwd();
|
|
1189
1567
|
const port = options.port || getProjectPort(projectRoot);
|
|
1190
|
-
// Ensure directories exist
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1568
|
+
// Ensure directories exist (skip for actions that don't need local project root)
|
|
1569
|
+
const globalActions = ["ensure-all", "doctor", "install-daemon", "uninstall-daemon"];
|
|
1570
|
+
if (!globalActions.includes(action || "")) {
|
|
1571
|
+
if (isGlobal) {
|
|
1572
|
+
const { JFL_PATHS, ensureJflDirs } = await import("../utils/jfl-paths.js");
|
|
1573
|
+
ensureJflDirs();
|
|
1574
|
+
}
|
|
1575
|
+
else {
|
|
1576
|
+
const jflDir = path.join(projectRoot, ".jfl");
|
|
1577
|
+
if (!fs.existsSync(jflDir)) {
|
|
1578
|
+
fs.mkdirSync(jflDir, { recursive: true });
|
|
1579
|
+
}
|
|
1201
1580
|
}
|
|
1202
1581
|
}
|
|
1203
1582
|
switch (action) {
|
|
@@ -1397,7 +1776,8 @@ export async function contextHubCommand(action, options = {}) {
|
|
|
1397
1776
|
serviceEventsPath,
|
|
1398
1777
|
journalDir: fs.existsSync(journalDir) ? journalDir : null,
|
|
1399
1778
|
});
|
|
1400
|
-
const
|
|
1779
|
+
const flowEngine = new FlowEngine(eventBus, projectRoot);
|
|
1780
|
+
const server = createServer(projectRoot, port, eventBus, flowEngine);
|
|
1401
1781
|
let isListening = false;
|
|
1402
1782
|
// When spawned as daemon, ignore SIGTERM during startup grace period.
|
|
1403
1783
|
// The parent process (hook runner) may exit and send SIGTERM to the
|
|
@@ -1470,9 +1850,13 @@ export async function contextHubCommand(action, options = {}) {
|
|
|
1470
1850
|
console.error(`[${timestamp}] Failed to initialize memory system:`, err.message);
|
|
1471
1851
|
// Don't exit - memory is optional
|
|
1472
1852
|
}
|
|
1473
|
-
// Start flow engine
|
|
1853
|
+
// Start flow engine (with child hub connections for portfolio mode)
|
|
1474
1854
|
try {
|
|
1475
|
-
const
|
|
1855
|
+
const children = getChildHubs(projectRoot);
|
|
1856
|
+
if (children.length > 0) {
|
|
1857
|
+
flowEngine.setChildren(children);
|
|
1858
|
+
console.log(`[${timestamp}] Portfolio mode: connecting to ${children.length} child hub(s)`);
|
|
1859
|
+
}
|
|
1476
1860
|
const flowCount = await flowEngine.start();
|
|
1477
1861
|
if (flowCount > 0) {
|
|
1478
1862
|
console.log(`[${timestamp}] Flow engine started with ${flowCount} active flow(s)`);
|
|
@@ -1481,6 +1865,23 @@ export async function contextHubCommand(action, options = {}) {
|
|
|
1481
1865
|
catch (err) {
|
|
1482
1866
|
console.error(`[${timestamp}] Failed to start flow engine:`, err.message);
|
|
1483
1867
|
}
|
|
1868
|
+
// Start telemetry agent (periodic pattern detection)
|
|
1869
|
+
try {
|
|
1870
|
+
const { TelemetryAgent } = await import("../lib/telemetry-agent.js");
|
|
1871
|
+
const telemetryAgent = new TelemetryAgent({
|
|
1872
|
+
projectRoot,
|
|
1873
|
+
intervalMs: 30 * 60 * 1000,
|
|
1874
|
+
emitEvent: (type, data, source) => {
|
|
1875
|
+
eventBus.emit({ type: type, data, source: source || 'telemetry-agent' });
|
|
1876
|
+
},
|
|
1877
|
+
});
|
|
1878
|
+
telemetryAgent.start();
|
|
1879
|
+
server.__telemetryAgent = telemetryAgent;
|
|
1880
|
+
console.log(`[${timestamp}] Telemetry agent started (interval: 30m)`);
|
|
1881
|
+
}
|
|
1882
|
+
catch (err) {
|
|
1883
|
+
console.error(`[${timestamp}] Failed to start telemetry agent:`, err.message);
|
|
1884
|
+
}
|
|
1484
1885
|
console.log(`[${timestamp}] MAP event bus initialized (buffer: 1000, subscribers: ${eventBus.getSubscriberCount()})`);
|
|
1485
1886
|
console.log(`[${timestamp}] Ready to serve requests`);
|
|
1486
1887
|
});
|