mergen-server 1.4.0
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/LICENSE +57 -0
- package/README.md +96 -0
- package/dist/__stubs__/calibration.js +471 -0
- package/dist/__stubs__/causal.js +601 -0
- package/dist/__stubs__/closed-source.js +150 -0
- package/dist/__stubs__/license.js +35 -0
- package/dist/__stubs__/plans.js +197 -0
- package/dist/app.js +467 -0
- package/dist/cli.js +250 -0
- package/dist/commands/agent-identity.js +79 -0
- package/dist/commands/gate.js +107 -0
- package/dist/commands/github.js +632 -0
- package/dist/commands/incident.js +644 -0
- package/dist/commands/policy.js +710 -0
- package/dist/commands/scan.js +243 -0
- package/dist/commands/setup.js +1218 -0
- package/dist/commands/shared.js +109 -0
- package/dist/commands/team.js +809 -0
- package/dist/datadog/blame-attribution.js +228 -0
- package/dist/datadog/client.js +159 -0
- package/dist/datadog/compactor.js +169 -0
- package/dist/datadog/fingerprinter.js +34 -0
- package/dist/datadog/incident-state.js +21 -0
- package/dist/datadog/line-matcher.js +56 -0
- package/dist/datadog/memory-store.js +399 -0
- package/dist/datadog/otel-trace.js +67 -0
- package/dist/index.js +505 -0
- package/dist/intelligence/action-risk.js +132 -0
- package/dist/intelligence/activity-feed.js +40 -0
- package/dist/intelligence/agent-identity.js +115 -0
- package/dist/intelligence/agent-pipeline.js +329 -0
- package/dist/intelligence/agent-profiles.js +81 -0
- package/dist/intelligence/ai-commit.js +16 -0
- package/dist/intelligence/approval-analytics.js +42 -0
- package/dist/intelligence/approval-events.js +5 -0
- package/dist/intelligence/arch-boundaries.js +94 -0
- package/dist/intelligence/arch-graph.js +117 -0
- package/dist/intelligence/autonomy.js +397 -0
- package/dist/intelligence/azure-broker.js +57 -0
- package/dist/intelligence/azure-proxy-sessions.js +49 -0
- package/dist/intelligence/baseline.js +1 -0
- package/dist/intelligence/behavior-baseline.js +134 -0
- package/dist/intelligence/billing.js +1 -0
- package/dist/intelligence/blast-radius.js +224 -0
- package/dist/intelligence/bypass.js +157 -0
- package/dist/intelligence/calibration-classifier.js +77 -0
- package/dist/intelligence/calibration-git-sync.js +74 -0
- package/dist/intelligence/calibration.js +1 -0
- package/dist/intelligence/cascade-detector.js +58 -0
- package/dist/intelligence/case-study-generator.js +126 -0
- package/dist/intelligence/causal-graph.js +126 -0
- package/dist/intelligence/causal.js +1 -0
- package/dist/intelligence/change-risk.js +102 -0
- package/dist/intelligence/compliance-report.js +134 -0
- package/dist/intelligence/confidence-report.js +85 -0
- package/dist/intelligence/corpus-to-policy.js +140 -0
- package/dist/intelligence/credential-broker.js +112 -0
- package/dist/intelligence/credential-misuse.js +39 -0
- package/dist/intelligence/debug-sessions.js +108 -0
- package/dist/intelligence/decision-escalation.js +36 -0
- package/dist/intelligence/decision-memory.js +47 -0
- package/dist/intelligence/decision-similarity.js +85 -0
- package/dist/intelligence/default-safety-tests.js +53 -0
- package/dist/intelligence/degradation-watcher.js +108 -0
- package/dist/intelligence/detector-plugins.js +70 -0
- package/dist/intelligence/detectors.js +1 -0
- package/dist/intelligence/device-auth.js +138 -0
- package/dist/intelligence/diff-size.js +42 -0
- package/dist/intelligence/ebpf-verify.js +45 -0
- package/dist/intelligence/enterprise-policy-engine.js +1122 -0
- package/dist/intelligence/error-fingerprint.js +1 -0
- package/dist/intelligence/execution-gate.js +100 -0
- package/dist/intelligence/execution-mode.js +22 -0
- package/dist/intelligence/gate-analytics.js +256 -0
- package/dist/intelligence/gate-decision.js +64 -0
- package/dist/intelligence/gcp-broker.js +50 -0
- package/dist/intelligence/git-adr-sync.js +207 -0
- package/dist/intelligence/hitl-hold.js +460 -0
- package/dist/intelligence/hypothesis-history.js +93 -0
- package/dist/intelligence/idp-identity.js +83 -0
- package/dist/intelligence/impl-critic.js +216 -0
- package/dist/intelligence/incident-autopilot.js +622 -0
- package/dist/intelligence/incident-replay.js +162 -0
- package/dist/intelligence/incident-result-cache.js +33 -0
- package/dist/intelligence/incident-similarity.js +58 -0
- package/dist/intelligence/infra-detectors.js +312 -0
- package/dist/intelligence/license.js +185 -0
- package/dist/intelligence/llm-spokesperson.js +62 -0
- package/dist/intelligence/mcp-prompts.js +320 -0
- package/dist/intelligence/mcp-resources.js +171 -0
- package/dist/intelligence/normalize.js +108 -0
- package/dist/intelligence/notifications.js +83 -0
- package/dist/intelligence/oidc-issuer.js +87 -0
- package/dist/intelligence/override-corpus.js +597 -0
- package/dist/intelligence/planning-gate.js +84 -0
- package/dist/intelligence/plans.js +223 -0
- package/dist/intelligence/platt-scaling.js +164 -0
- package/dist/intelligence/policy-proposals.js +84 -0
- package/dist/intelligence/policy-suggester.js +161 -0
- package/dist/intelligence/policy-sync.js +103 -0
- package/dist/intelligence/policy-test-runner.js +35 -0
- package/dist/intelligence/postmortem-parser.js +181 -0
- package/dist/intelligence/postmortem-retrieval.js +224 -0
- package/dist/intelligence/postmortem-store.js +596 -0
- package/dist/intelligence/pr-commenter.js +81 -0
- package/dist/intelligence/pr-policy-gate.js +38 -0
- package/dist/intelligence/pr-shadow-analyzer.js +229 -0
- package/dist/intelligence/process-tree.js +108 -0
- package/dist/intelligence/prompts.js +1 -0
- package/dist/intelligence/replay.js +38 -0
- package/dist/intelligence/repro-steps.js +1 -0
- package/dist/intelligence/resource-extractors.js +0 -0
- package/dist/intelligence/resource-file-context.js +138 -0
- package/dist/intelligence/risk-score.js +22 -0
- package/dist/intelligence/rollback.js +137 -0
- package/dist/intelligence/runbook-updater.js +143 -0
- package/dist/intelligence/sandbox.js +194 -0
- package/dist/intelligence/script-trust.js +104 -0
- package/dist/intelligence/session-metrics.js +67 -0
- package/dist/intelligence/session-threat-tracker.js +231 -0
- package/dist/intelligence/shadow-digest-cron.js +81 -0
- package/dist/intelligence/shadow-log.js +284 -0
- package/dist/intelligence/shell-ast.js +145 -0
- package/dist/intelligence/siem-forward.js +104 -0
- package/dist/intelligence/slack-digest.js +151 -0
- package/dist/intelligence/slack-override-loop.js +217 -0
- package/dist/intelligence/slack-routing.js +67 -0
- package/dist/intelligence/slack.js +900 -0
- package/dist/intelligence/sql-ast.js +31 -0
- package/dist/intelligence/team.js +1 -0
- package/dist/intelligence/telemetry.js +76 -0
- package/dist/intelligence/threshold-optimizer.js +75 -0
- package/dist/intelligence/token-budget.js +66 -0
- package/dist/intelligence/tool-guard.js +708 -0
- package/dist/intelligence/tool-manifest 2.js +74 -0
- package/dist/intelligence/tool-manifest.js +125 -0
- package/dist/intelligence/tools-analysis.js +857 -0
- package/dist/intelligence/tools-arch.js +152 -0
- package/dist/intelligence/tools-autonomy.js +495 -0
- package/dist/intelligence/tools-blast-radius.js +140 -0
- package/dist/intelligence/tools-browser.js +431 -0
- package/dist/intelligence/tools-change-timeline.js +154 -0
- package/dist/intelligence/tools-credentials.js +117 -0
- package/dist/intelligence/tools-datadog.js +233 -0
- package/dist/intelligence/tools-debug-sessions.js +209 -0
- package/dist/intelligence/tools-discovery.js +284 -0
- package/dist/intelligence/tools-infra.js +612 -0
- package/dist/intelligence/tools-intent.js +128 -0
- package/dist/intelligence/tools-memory.js +514 -0
- package/dist/intelligence/tools-runbook.js +951 -0
- package/dist/intelligence/tools-sessions.js +107 -0
- package/dist/intelligence/tools-state.js +232 -0
- package/dist/intelligence/tools-utility.js +430 -0
- package/dist/intelligence/tools-validate.js +215 -0
- package/dist/intelligence/tools.js +1 -0
- package/dist/intelligence/trace-context.js +11 -0
- package/dist/intelligence/unclassified-clusters.js +87 -0
- package/dist/intelligence/usage.js +195 -0
- package/dist/routes/active-authors.js +63 -0
- package/dist/routes/activity-feed.js +21 -0
- package/dist/routes/adr.js +117 -0
- package/dist/routes/agent-activity.js +184 -0
- package/dist/routes/agent-blunders.js +163 -0
- package/dist/routes/agents.js +175 -0
- package/dist/routes/api-keys.js +65 -0
- package/dist/routes/arch.js +75 -0
- package/dist/routes/audit-export.js +93 -0
- package/dist/routes/billing-dashboard.js +158 -0
- package/dist/routes/billing-outcome.js +77 -0
- package/dist/routes/calibration.js +305 -0
- package/dist/routes/ci-gate-history.js +20 -0
- package/dist/routes/ci-gate.js +232 -0
- package/dist/routes/ci.js +293 -0
- package/dist/routes/compliance-report.js +24 -0
- package/dist/routes/confidence.js +30 -0
- package/dist/routes/credentials.js +119 -0
- package/dist/routes/dashboard.js +1407 -0
- package/dist/routes/decision-memory.js +42 -0
- package/dist/routes/demo.js +844 -0
- package/dist/routes/device-auth-stub.js +173 -0
- package/dist/routes/explain-why.js +39 -0
- package/dist/routes/gate-analytics.js +213 -0
- package/dist/routes/gate.js +28 -0
- package/dist/routes/github-webhook.js +364 -0
- package/dist/routes/habituation.js +30 -0
- package/dist/routes/health-integrations.js +243 -0
- package/dist/routes/heartbeats.js +45 -0
- package/dist/routes/hitl.js +364 -0
- package/dist/routes/impact-report.js +645 -0
- package/dist/routes/incident-webhook.js +63 -0
- package/dist/routes/incidents.js +366 -0
- package/dist/routes/layers.js +62 -0
- package/dist/routes/license.js +187 -0
- package/dist/routes/oidc-issuer.js +30 -0
- package/dist/routes/onboarding.js +170 -0
- package/dist/routes/otel.js +67 -0
- package/dist/routes/otlp-receiver.js +179 -0
- package/dist/routes/overrides.js +241 -0
- package/dist/routes/pagerduty.js +258 -0
- package/dist/routes/policies.js +809 -0
- package/dist/routes/policy-nl.js +169 -0
- package/dist/routes/postmortem.js +102 -0
- package/dist/routes/pr-shadow.js +47 -0
- package/dist/routes/rbac.js +61 -0
- package/dist/routes/replay.js +17 -0
- package/dist/routes/risk-report.js +164 -0
- package/dist/routes/runbooks.js +125 -0
- package/dist/routes/safety-test.js +174 -0
- package/dist/routes/sdk.js +222 -0
- package/dist/routes/sensor.js +819 -0
- package/dist/routes/sentry.js +140 -0
- package/dist/routes/sessions.js +43 -0
- package/dist/routes/setup-ui.js +594 -0
- package/dist/routes/shadow-report.js +107 -0
- package/dist/routes/slack-routing.js +171 -0
- package/dist/routes/team-usage.js +71 -0
- package/dist/routes/telemetry.js +28 -0
- package/dist/routes/tenants.js +199 -0
- package/dist/routes/tickets.js +167 -0
- package/dist/routes/validate.js +13 -0
- package/dist/routes/war-room.js +113 -0
- package/dist/scripts/check-arch.js +22 -0
- package/dist/seeds/community-corpus.js +176 -0
- package/dist/seeds/corpus.js +1224 -0
- package/dist/sensor/action-ledger.js +327 -0
- package/dist/sensor/adr-store.js +140 -0
- package/dist/sensor/agent-blunder-store.js +370 -0
- package/dist/sensor/agent-context-store.js +225 -0
- package/dist/sensor/agent-memory-store.js +231 -0
- package/dist/sensor/audit-fetch.js +23 -0
- package/dist/sensor/audit-log.js +273 -0
- package/dist/sensor/buffer-schemas.js +225 -0
- package/dist/sensor/buffer.js +599 -0
- package/dist/sensor/bypass-tracker.js +93 -0
- package/dist/sensor/ci-gate-history.js +70 -0
- package/dist/sensor/cloud-auth.js +183 -0
- package/dist/sensor/commit-context-store.js +235 -0
- package/dist/sensor/docker-log-stream.js +164 -0
- package/dist/sensor/docker-monitor.js +122 -0
- package/dist/sensor/extended-buffer.js +46 -0
- package/dist/sensor/feedback-token.js +41 -0
- package/dist/sensor/file-lock.js +6 -0
- package/dist/sensor/fs-watcher.js +106 -0
- package/dist/sensor/gate-heartbeat.js +122 -0
- package/dist/sensor/git-suspect.js +136 -0
- package/dist/sensor/habituation-store.js +77 -0
- package/dist/sensor/heartbeat-monitor.js +133 -0
- package/dist/sensor/incident-store.js +340 -0
- package/dist/sensor/infra-normalizer.js +513 -0
- package/dist/sensor/ingest.js +339 -0
- package/dist/sensor/jest-reporter.js +52 -0
- package/dist/sensor/k8s-events.js +132 -0
- package/dist/sensor/layer2-store.js +111 -0
- package/dist/sensor/layer3-store.js +230 -0
- package/dist/sensor/layer4-store.js +147 -0
- package/dist/sensor/logger.js +13 -0
- package/dist/sensor/otel-exporter.js +161 -0
- package/dist/sensor/paths.js +71 -0
- package/dist/sensor/policy-history.js +147 -0
- package/dist/sensor/pr-shadow-store.js +83 -0
- package/dist/sensor/process-watcher.js +162 -0
- package/dist/sensor/rbac.js +92 -0
- package/dist/sensor/redact.js +114 -0
- package/dist/sensor/redis-store.js +191 -0
- package/dist/sensor/route-reachability.js +64 -0
- package/dist/sensor/security-utils.js +18 -0
- package/dist/sensor/service-graph.js +170 -0
- package/dist/sensor/service-topology.js +227 -0
- package/dist/sensor/session-history.js +74 -0
- package/dist/sensor/session-persist.js +38 -0
- package/dist/sensor/shadow-promote.js +122 -0
- package/dist/sensor/sourcemap.js +281 -0
- package/dist/sensor/sqlite-store.js +205 -0
- package/dist/sensor/sso.js +164 -0
- package/dist/sensor/vitest-reporter.js +65 -0
- package/dist/sensor/watcher.js +48 -0
- package/dist/storage/interfaces.js +0 -0
- package/dist/storage/pg/pg-action-ledger.js +138 -0
- package/dist/storage/pg/pg-approval-store.js +107 -0
- package/dist/storage/pg/pg-blunder-store.js +193 -0
- package/dist/storage/pg/pg-ci-gate-history.js +83 -0
- package/dist/storage/pg/pg-client.js +24 -0
- package/dist/storage/pg/pg-event-store.js +63 -0
- package/dist/storage/pg/pg-incident-store.js +190 -0
- package/dist/storage/pg/pg-migrations.js +26 -0
- package/dist/storage/pg/pg-override-corpus.js +447 -0
- package/dist/storage/pg/pg-shadow-log.js +129 -0
- package/dist/storage/sqlite/sqlite-action-ledger.js +18 -0
- package/dist/storage/sqlite/sqlite-approval-store.js +37 -0
- package/dist/storage/sqlite/sqlite-blunder-store.js +27 -0
- package/dist/storage/sqlite/sqlite-ci-gate-history.js +19 -0
- package/dist/storage/sqlite/sqlite-event-store.js +29 -0
- package/dist/storage/sqlite/sqlite-incident-store.js +34 -0
- package/dist/storage/sqlite/sqlite-override-corpus.js +75 -0
- package/dist/storage/sqlite/sqlite-shadow-log.js +36 -0
- package/dist/storage/store-factory.js +55 -0
- package/dist/storage/store-registry.js +31 -0
- package/dist/update-checker.js +107 -0
- package/dist/workers/autopilot-worker.js +28 -0
- package/dist/workers/notification-worker.js +28 -0
- package/dist/workers/queues.js +58 -0
- package/dist/workers/validation-worker.js +24 -0
- package/dist/workers/worker-registry.js +18 -0
- package/package.json +123 -0
- package/sdk/mergen-inject.js +260 -0
- package/sdk/node.js +313 -0
- package/sdk/vite-plugin.ts +57 -0
- package/sdk/webpack-plugin.js +106 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { createHash } from "crypto";
|
|
4
|
+
import { DATA_DIR } from "../sensor/paths.js";
|
|
5
|
+
const CLUSTERS_FILE = path.join(DATA_DIR, "unclassified-clusters.json");
|
|
6
|
+
const MAX_CLUSTERS = 200;
|
|
7
|
+
let _clusters = /* @__PURE__ */ new Map();
|
|
8
|
+
let _loaded = false;
|
|
9
|
+
function load() {
|
|
10
|
+
if (_loaded) return;
|
|
11
|
+
_loaded = true;
|
|
12
|
+
try {
|
|
13
|
+
if (!fs.existsSync(CLUSTERS_FILE)) return;
|
|
14
|
+
const parsed = JSON.parse(fs.readFileSync(CLUSTERS_FILE, "utf8"));
|
|
15
|
+
if (parsed?.version === 1 && Array.isArray(parsed.clusters)) {
|
|
16
|
+
for (const c of parsed.clusters) {
|
|
17
|
+
_clusters.set(c.fingerprint, c);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
} catch {
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function persist() {
|
|
24
|
+
try {
|
|
25
|
+
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
26
|
+
const clusters = Array.from(_clusters.values()).sort((a, b) => b.count - a.count).slice(0, MAX_CLUSTERS);
|
|
27
|
+
fs.writeFileSync(CLUSTERS_FILE, JSON.stringify({ version: 1, clusters }), "utf8");
|
|
28
|
+
} catch {
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function computeStructuralFingerprint(chain, networkStatuses) {
|
|
32
|
+
const kinds = chain.map((e) => e.kind);
|
|
33
|
+
const statusStr = networkStatuses.slice(0, 6).join(",");
|
|
34
|
+
const kindStr = kinds.join(",");
|
|
35
|
+
const input = `${kindStr}|${statusStr}`;
|
|
36
|
+
const fingerprint = createHash("sha256").update(input).digest("hex").slice(0, 16);
|
|
37
|
+
const kindCounts = kinds.reduce((acc, k) => {
|
|
38
|
+
acc[k] = (acc[k] ?? 0) + 1;
|
|
39
|
+
return acc;
|
|
40
|
+
}, {});
|
|
41
|
+
const patternParts = Object.entries(kindCounts).map(([k, n]) => n > 1 ? `${k}\xD7${n}` : k);
|
|
42
|
+
const pattern = patternParts.join(" \u2192 ") + (statusStr ? ` [${statusStr}]` : "");
|
|
43
|
+
return { fingerprint, pattern };
|
|
44
|
+
}
|
|
45
|
+
function recordUnclassifiedChain(chain, networkStatuses) {
|
|
46
|
+
load();
|
|
47
|
+
const { fingerprint, pattern } = computeStructuralFingerprint(chain, networkStatuses);
|
|
48
|
+
const now = Date.now();
|
|
49
|
+
const existing = _clusters.get(fingerprint);
|
|
50
|
+
if (existing) {
|
|
51
|
+
existing.count++;
|
|
52
|
+
existing.lastSeen = now;
|
|
53
|
+
} else {
|
|
54
|
+
_clusters.set(fingerprint, {
|
|
55
|
+
fingerprint,
|
|
56
|
+
pattern,
|
|
57
|
+
count: 1,
|
|
58
|
+
firstSeen: now,
|
|
59
|
+
lastSeen: now,
|
|
60
|
+
sample: {
|
|
61
|
+
chainKinds: chain.map((e) => e.kind),
|
|
62
|
+
networkStatuses: networkStatuses.slice(0, 6),
|
|
63
|
+
hasErrors: chain.some((e) => e.kind === "error"),
|
|
64
|
+
hasNetworkFails: chain.some((e) => e.kind === "network_fail")
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
persist();
|
|
69
|
+
}
|
|
70
|
+
function getClusters(minCount = 3) {
|
|
71
|
+
load();
|
|
72
|
+
return Array.from(_clusters.values()).filter((c) => c.count >= minCount).sort((a, b) => b.count - a.count);
|
|
73
|
+
}
|
|
74
|
+
function getAllClusters() {
|
|
75
|
+
load();
|
|
76
|
+
return Array.from(_clusters.values()).sort((a, b) => b.count - a.count);
|
|
77
|
+
}
|
|
78
|
+
function _resetClustersForTesting() {
|
|
79
|
+
_clusters = /* @__PURE__ */ new Map();
|
|
80
|
+
_loaded = true;
|
|
81
|
+
}
|
|
82
|
+
export {
|
|
83
|
+
_resetClustersForTesting,
|
|
84
|
+
getAllClusters,
|
|
85
|
+
getClusters,
|
|
86
|
+
recordUnclassifiedChain
|
|
87
|
+
};
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import { DATA_DIR, USAGE_FILE } from "../sensor/paths.js";
|
|
3
|
+
import { getActivePlanId, getLicenseState } from "./license.js";
|
|
4
|
+
import { getPlan } from "./plans.js";
|
|
5
|
+
import logger from "../sensor/logger.js";
|
|
6
|
+
import { incidentStore } from "../sensor/incident-store.js";
|
|
7
|
+
const currentMonth = () => (/* @__PURE__ */ new Date()).toISOString().slice(0, 7);
|
|
8
|
+
let _state = {
|
|
9
|
+
month: currentMonth(),
|
|
10
|
+
used: 0,
|
|
11
|
+
overageReported: 0,
|
|
12
|
+
overagePending: 0
|
|
13
|
+
};
|
|
14
|
+
let _mutex = Promise.resolve();
|
|
15
|
+
let _sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
16
|
+
const MAX_RETRIES = 3;
|
|
17
|
+
const RETRY_BASE_MS = 1e3;
|
|
18
|
+
function _resetForTesting(partial) {
|
|
19
|
+
_state = {
|
|
20
|
+
month: currentMonth(),
|
|
21
|
+
used: 0,
|
|
22
|
+
overageReported: 0,
|
|
23
|
+
overagePending: 0,
|
|
24
|
+
...partial
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function _setSleepForTesting(fn) {
|
|
28
|
+
_sleep = fn;
|
|
29
|
+
}
|
|
30
|
+
async function persist() {
|
|
31
|
+
try {
|
|
32
|
+
await fs.mkdir(DATA_DIR, { recursive: true });
|
|
33
|
+
await fs.writeFile(USAGE_FILE, JSON.stringify(_state, null, 2));
|
|
34
|
+
} catch (err) {
|
|
35
|
+
logger.warn({ err }, "usage: persist failed");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function load() {
|
|
39
|
+
try {
|
|
40
|
+
const raw = await fs.readFile(USAGE_FILE, "utf8");
|
|
41
|
+
const parsed = JSON.parse(raw);
|
|
42
|
+
_state = {
|
|
43
|
+
month: parsed.month ?? currentMonth(),
|
|
44
|
+
used: parsed.used ?? 0,
|
|
45
|
+
overageReported: parsed.overageReported ?? 0,
|
|
46
|
+
overagePending: parsed.overagePending ?? 0
|
|
47
|
+
};
|
|
48
|
+
} catch (err) {
|
|
49
|
+
if (err.code !== "ENOENT") {
|
|
50
|
+
logger.warn({ err }, "usage: load failed");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async function maybeRollover() {
|
|
55
|
+
const now = currentMonth();
|
|
56
|
+
if (_state.month === now) return;
|
|
57
|
+
if (_state.overagePending > 0) {
|
|
58
|
+
await _flushOverage();
|
|
59
|
+
}
|
|
60
|
+
const unflushed = _state.overagePending;
|
|
61
|
+
_state = { month: now, used: 0, overageReported: 0, overagePending: unflushed };
|
|
62
|
+
}
|
|
63
|
+
async function _flushOverage() {
|
|
64
|
+
const license = getLicenseState();
|
|
65
|
+
const apiKey = process.env.LS_API_KEY;
|
|
66
|
+
if (!apiKey || !license?.lsSubscriptionItemId || _state.overagePending <= 0) return;
|
|
67
|
+
const pending = _state.overagePending;
|
|
68
|
+
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
69
|
+
if (attempt > 0) await _sleep(RETRY_BASE_MS * attempt);
|
|
70
|
+
try {
|
|
71
|
+
const res = await fetch(
|
|
72
|
+
`https://api.lemonsqueezy.com/v1/subscription-items/${license.lsSubscriptionItemId}`,
|
|
73
|
+
{
|
|
74
|
+
method: "PATCH",
|
|
75
|
+
headers: {
|
|
76
|
+
"content-type": "application/json",
|
|
77
|
+
"authorization": `Bearer ${apiKey}`
|
|
78
|
+
},
|
|
79
|
+
body: JSON.stringify({
|
|
80
|
+
data: {
|
|
81
|
+
type: "subscription-items",
|
|
82
|
+
id: license.lsSubscriptionItemId,
|
|
83
|
+
attributes: { quantity: _state.overageReported + pending }
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
);
|
|
88
|
+
if (res.ok) {
|
|
89
|
+
_state.overageReported += pending;
|
|
90
|
+
_state.overagePending = 0;
|
|
91
|
+
await persist();
|
|
92
|
+
logger.info({ credits: pending }, "usage: overage flushed");
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
logger.warn({ status: res.status, attempt }, "usage: overage flush failed, retrying");
|
|
96
|
+
} catch (err) {
|
|
97
|
+
logger.warn({ err, attempt }, "usage: overage flush error");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
logger.warn({ pending }, "usage: overage flush gave up after max retries");
|
|
101
|
+
}
|
|
102
|
+
async function flushOverageOnShutdown() {
|
|
103
|
+
await _flushOverage();
|
|
104
|
+
}
|
|
105
|
+
async function consumeCredit() {
|
|
106
|
+
return new Promise((resolve, reject) => {
|
|
107
|
+
_mutex = _mutex.then(() => _consume().then(resolve, reject));
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
async function _consume() {
|
|
111
|
+
await maybeRollover();
|
|
112
|
+
const planId = getActivePlanId();
|
|
113
|
+
const plan = getPlan(planId);
|
|
114
|
+
const included = plan.analyzeCreditsPerMonth;
|
|
115
|
+
const overage = plan.overageCentsPerCredit;
|
|
116
|
+
if (included === 0) {
|
|
117
|
+
_state.used++;
|
|
118
|
+
await persist();
|
|
119
|
+
return { allowed: true };
|
|
120
|
+
}
|
|
121
|
+
if (_state.used < included) {
|
|
122
|
+
_state.used++;
|
|
123
|
+
await persist();
|
|
124
|
+
const isLast = overage === 0 && _state.used === included;
|
|
125
|
+
const notice = isLast ? `You've used your last free credit. Upgrade at https://mergen.app/pricing` : void 0;
|
|
126
|
+
return { allowed: true, ...notice ? { notice } : {} };
|
|
127
|
+
}
|
|
128
|
+
if (overage > 0) {
|
|
129
|
+
_state.used++;
|
|
130
|
+
_state.overagePending++;
|
|
131
|
+
await persist();
|
|
132
|
+
_flushOverage().catch((err) => logger.warn({ err }, "usage: background flush failed"));
|
|
133
|
+
return { allowed: true };
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
allowed: false,
|
|
137
|
+
reason: `Monthly limit of ${included} analyze credits reached. Upgrade at https://mergen.app/pricing`
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function getUsageSnapshot() {
|
|
141
|
+
const planId = getActivePlanId();
|
|
142
|
+
const plan = getPlan(planId);
|
|
143
|
+
const license = getLicenseState();
|
|
144
|
+
const included = plan.analyzeCreditsPerMonth;
|
|
145
|
+
const isUnlimited = included === 0;
|
|
146
|
+
const isPayAsYouGo = isUnlimited && plan.overageCentsPerCredit > 0;
|
|
147
|
+
const overage = isPayAsYouGo ? _state.used : isUnlimited ? 0 : Math.max(0, _state.used - included);
|
|
148
|
+
const remaining = isUnlimited ? null : Math.max(0, included - _state.used);
|
|
149
|
+
const monthStart = (/* @__PURE__ */ new Date(_state.month + "-01T00:00:00Z")).getTime();
|
|
150
|
+
const resetsAt = license?.resetsAt ?? monthStart + 31 * 24 * 60 * 60 * 1e3;
|
|
151
|
+
const lowCredits = remaining !== null && remaining <= 2;
|
|
152
|
+
const billingStatus = license?.status ?? "active";
|
|
153
|
+
return {
|
|
154
|
+
planName: plan.name,
|
|
155
|
+
planId,
|
|
156
|
+
month: _state.month,
|
|
157
|
+
resetsAt,
|
|
158
|
+
used: _state.used,
|
|
159
|
+
included: isUnlimited ? null : included,
|
|
160
|
+
remaining,
|
|
161
|
+
lowCredits,
|
|
162
|
+
overage,
|
|
163
|
+
overagePendingCredits: _state.overagePending,
|
|
164
|
+
overageConfirmedCredits: _state.overageReported,
|
|
165
|
+
overageCentsPerCredit: plan.overageCentsPerCredit,
|
|
166
|
+
estimatedOverageCents: overage * plan.overageCentsPerCredit,
|
|
167
|
+
billingStatus,
|
|
168
|
+
helpfulRate7d: null
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function getIncidentCount() {
|
|
172
|
+
return incidentStore.list(void 0, 200).length;
|
|
173
|
+
}
|
|
174
|
+
async function recordExplainWhyFeedback(..._args) {
|
|
175
|
+
}
|
|
176
|
+
async function consumeIncident() {
|
|
177
|
+
return consumeCredit();
|
|
178
|
+
}
|
|
179
|
+
function recordExplainWhy(..._args) {
|
|
180
|
+
}
|
|
181
|
+
async function initUsage() {
|
|
182
|
+
await load();
|
|
183
|
+
}
|
|
184
|
+
export {
|
|
185
|
+
_resetForTesting,
|
|
186
|
+
_setSleepForTesting,
|
|
187
|
+
consumeCredit,
|
|
188
|
+
consumeIncident,
|
|
189
|
+
flushOverageOnShutdown,
|
|
190
|
+
getIncidentCount,
|
|
191
|
+
getUsageSnapshot,
|
|
192
|
+
initUsage,
|
|
193
|
+
recordExplainWhy,
|
|
194
|
+
recordExplainWhyFeedback
|
|
195
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import { commitContextStore } from "../sensor/commit-context-store.js";
|
|
3
|
+
import { getHabituationEvents } from "../sensor/habituation-store.js";
|
|
4
|
+
function createActiveAuthorsRouter() {
|
|
5
|
+
const router = Router();
|
|
6
|
+
router.get("/billing/active-authors", (req, res) => {
|
|
7
|
+
const repoFilter = typeof req.query.repo === "string" ? req.query.repo : null;
|
|
8
|
+
const monthsBack = Math.min(24, Math.max(1, parseInt(String(req.query.months ?? "12"), 10) || 12));
|
|
9
|
+
const excludeBots = req.query.exclude_bots === "true";
|
|
10
|
+
const now = /* @__PURE__ */ new Date();
|
|
11
|
+
const buckets = [];
|
|
12
|
+
for (let i = 0; i < monthsBack; i++) {
|
|
13
|
+
const d = new Date(now.getFullYear(), now.getMonth() - i, 1);
|
|
14
|
+
buckets.push(`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`);
|
|
15
|
+
}
|
|
16
|
+
function toMonthKey(ts) {
|
|
17
|
+
const d = new Date(ts);
|
|
18
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`;
|
|
19
|
+
}
|
|
20
|
+
const authorsByMonth = /* @__PURE__ */ new Map();
|
|
21
|
+
for (const b of buckets) authorsByMonth.set(b, /* @__PURE__ */ new Set());
|
|
22
|
+
const windowStart = new Date(now.getFullYear(), now.getMonth() - monthsBack + 1, 1).getTime();
|
|
23
|
+
const windowEnd = Date.now();
|
|
24
|
+
const commitCtxs = repoFilter ? commitContextStore.listByWindow(windowStart, windowEnd, repoFilter, 5e3) : commitContextStore.listByWindow(windowStart, windowEnd, void 0, 5e3);
|
|
25
|
+
for (const ctx of commitCtxs) {
|
|
26
|
+
const key = toMonthKey(ctx.mergedAt ?? ctx.capturedAt);
|
|
27
|
+
const bucket = authorsByMonth.get(key);
|
|
28
|
+
if (!bucket) continue;
|
|
29
|
+
if (ctx.author) {
|
|
30
|
+
if (!excludeBots || !ctx.author.endsWith("[bot]")) bucket.add(ctx.author);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const habEvents = getHabituationEvents().filter(
|
|
34
|
+
(e) => e.recordedAt >= windowStart && e.eventType === "comment_posted" && (!repoFilter || e.repo === repoFilter)
|
|
35
|
+
);
|
|
36
|
+
for (const e of habEvents) {
|
|
37
|
+
const key = toMonthKey(e.recordedAt);
|
|
38
|
+
const bucket = authorsByMonth.get(key);
|
|
39
|
+
if (!bucket) continue;
|
|
40
|
+
if (!excludeBots || !e.actor.endsWith("[bot]")) bucket.add(e.actor);
|
|
41
|
+
}
|
|
42
|
+
const monthly = buckets.map((month) => ({
|
|
43
|
+
month,
|
|
44
|
+
activeAuthors: authorsByMonth.get(month)?.size ?? 0,
|
|
45
|
+
authors: [...authorsByMonth.get(month) ?? []].sort()
|
|
46
|
+
}));
|
|
47
|
+
const currentMonth = monthly[0];
|
|
48
|
+
const prevMonth = monthly[1] ?? null;
|
|
49
|
+
const mom = prevMonth && prevMonth.activeAuthors > 0 ? Math.round((currentMonth.activeAuthors - prevMonth.activeAuthors) / prevMonth.activeAuthors * 100) : null;
|
|
50
|
+
res.json({
|
|
51
|
+
billingUnit: "active_pr_authors_per_month",
|
|
52
|
+
currentMonth: currentMonth.month,
|
|
53
|
+
currentCount: currentMonth.activeAuthors,
|
|
54
|
+
momChangePercent: mom,
|
|
55
|
+
excludeBots,
|
|
56
|
+
monthly
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
return router;
|
|
60
|
+
}
|
|
61
|
+
export {
|
|
62
|
+
createActiveAuthorsRouter
|
|
63
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import { getRecentActivity, subscribeToActivity } from "../intelligence/activity-feed.js";
|
|
3
|
+
function createActivityFeedRouter() {
|
|
4
|
+
const router = Router();
|
|
5
|
+
router.get("/activity-feed", (req, res) => {
|
|
6
|
+
const limit = Math.min(Number(req.query.limit) || 50, 200);
|
|
7
|
+
res.json({ ok: true, events: getRecentActivity(limit) });
|
|
8
|
+
});
|
|
9
|
+
router.get("/activity-feed/stream", (req, res) => {
|
|
10
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
11
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
12
|
+
res.setHeader("Connection", "keep-alive");
|
|
13
|
+
res.flushHeaders();
|
|
14
|
+
const unsubscribe = subscribeToActivity(res);
|
|
15
|
+
req.on("close", unsubscribe);
|
|
16
|
+
});
|
|
17
|
+
return router;
|
|
18
|
+
}
|
|
19
|
+
export {
|
|
20
|
+
createActivityFeedRouter
|
|
21
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { adrStore } from "../sensor/adr-store.js";
|
|
4
|
+
import { getStores } from "../storage/store-registry.js";
|
|
5
|
+
import logger from "../sensor/logger.js";
|
|
6
|
+
const NewAdrSchema = z.object({
|
|
7
|
+
title: z.string().min(1).max(200),
|
|
8
|
+
status: z.enum(["proposed", "accepted", "deprecated", "superseded"]).default("proposed"),
|
|
9
|
+
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).default(() => (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)),
|
|
10
|
+
decision: z.string().min(1).max(2e3),
|
|
11
|
+
alternatives: z.array(z.string().max(500)).default([]),
|
|
12
|
+
rationale: z.string().min(1).max(2e3),
|
|
13
|
+
consequences: z.string().max(2e3).default("")
|
|
14
|
+
});
|
|
15
|
+
async function compileOverrideFromAdr(adr, tenantId) {
|
|
16
|
+
const text = `${adr.title} ${adr.decision} ${adr.rationale}`.toLowerCase();
|
|
17
|
+
let incidentTag = "";
|
|
18
|
+
let proposedCommand = "remediation_command";
|
|
19
|
+
if (text.includes("oom") || text.includes("memory") || text.includes("limit")) {
|
|
20
|
+
incidentTag = "infra_oom_kill";
|
|
21
|
+
proposedCommand = "docker restart web";
|
|
22
|
+
} else if (text.includes("rate") || text.includes("limit") || text.includes("throttl")) {
|
|
23
|
+
incidentTag = "infra_rate_limit_cascade";
|
|
24
|
+
proposedCommand = "iptables -A INPUT -p tcp --dport 80 -m limit";
|
|
25
|
+
} else if (text.includes("cert") || text.includes("tls") || text.includes("expiry") || text.includes("ssl")) {
|
|
26
|
+
incidentTag = "infra_certificate_expiry";
|
|
27
|
+
proposedCommand = "certbot renew";
|
|
28
|
+
} else if (text.includes("pool") || text.includes("connection") || text.includes("db")) {
|
|
29
|
+
incidentTag = "infra_db_connection_pool";
|
|
30
|
+
proposedCommand = "systemctl restart postgresql";
|
|
31
|
+
} else if (text.includes("disk") || text.includes("space") || text.includes("full")) {
|
|
32
|
+
incidentTag = "infra_disk_pressure";
|
|
33
|
+
proposedCommand = "rm -rf /tmp/*";
|
|
34
|
+
} else if (text.includes("slow") || text.includes("query") || text.includes("latency")) {
|
|
35
|
+
incidentTag = "infra_slow_query";
|
|
36
|
+
proposedCommand = "systemctl reload nginx";
|
|
37
|
+
}
|
|
38
|
+
if (!incidentTag) return null;
|
|
39
|
+
let reason = "on-call-discretion";
|
|
40
|
+
let note = `Auto-compiled from ADR: ${adr.id} (${adr.title})`;
|
|
41
|
+
if (text.includes("window") || text.includes("settlement") || text.includes("friday") || text.includes("batch")) {
|
|
42
|
+
reason = "batch-window";
|
|
43
|
+
} else if (text.includes("cost") || text.includes("budget") || text.includes("scale") || text.includes("expensive")) {
|
|
44
|
+
reason = "cost-constraint";
|
|
45
|
+
} else if (text.includes("cab") || text.includes("freeze") || text.includes("compliance") || text.includes("security")) {
|
|
46
|
+
reason = "compliance-hold";
|
|
47
|
+
} else if (text.includes("replica") || text.includes("read") || text.includes("primary")) {
|
|
48
|
+
reason = "prefer-read-replica";
|
|
49
|
+
} else if (text.includes("maintenance") || text.includes("scheduled")) {
|
|
50
|
+
reason = "maintenance-window";
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
const event = await getStores().overrides.recordOverride({
|
|
54
|
+
incidentTag,
|
|
55
|
+
proposedCommand,
|
|
56
|
+
overrideReason: reason,
|
|
57
|
+
note,
|
|
58
|
+
service: "all",
|
|
59
|
+
environment: "production",
|
|
60
|
+
actor: `ADR Compiler (${adr.id})`
|
|
61
|
+
}, tenantId);
|
|
62
|
+
return event;
|
|
63
|
+
} catch (err) {
|
|
64
|
+
logger.warn({ err, adrId: adr.id }, "adr-compiler: failed to record override from ADR");
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function createAdrRouter() {
|
|
69
|
+
const router = Router();
|
|
70
|
+
router.get("/adrs", (req, res) => {
|
|
71
|
+
const q = typeof req.query.q === "string" ? req.query.q : void 0;
|
|
72
|
+
res.json({ ok: true, adrs: adrStore.list(q) });
|
|
73
|
+
});
|
|
74
|
+
router.get("/adrs/:id", (req, res) => {
|
|
75
|
+
const adr = adrStore.get(req.params.id);
|
|
76
|
+
if (!adr) {
|
|
77
|
+
res.status(404).json({ error: "ADR not found" });
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
res.json({ ok: true, adr });
|
|
81
|
+
});
|
|
82
|
+
router.post("/adrs", async (req, res) => {
|
|
83
|
+
const parsed = NewAdrSchema.safeParse(req.body);
|
|
84
|
+
if (!parsed.success) {
|
|
85
|
+
res.status(400).json({ error: "validation failed", details: parsed.error.issues });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const adr = adrStore.add(parsed.data);
|
|
89
|
+
const override = await compileOverrideFromAdr(adr, req.tenantId);
|
|
90
|
+
res.status(201).json({
|
|
91
|
+
ok: true,
|
|
92
|
+
adr,
|
|
93
|
+
overrideCompiled: override ? true : false,
|
|
94
|
+
overrideId: override?.id
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
router.post("/ci/adr", async (req, res) => {
|
|
98
|
+
const parsed = NewAdrSchema.safeParse(req.body);
|
|
99
|
+
if (!parsed.success) {
|
|
100
|
+
res.status(400).json({ error: "validation failed", details: parsed.error.issues });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const adr = adrStore.add(parsed.data);
|
|
104
|
+
const override = await compileOverrideFromAdr(adr, req.tenantId);
|
|
105
|
+
res.status(201).json({
|
|
106
|
+
ok: true,
|
|
107
|
+
adr,
|
|
108
|
+
overrideCompiled: override ? true : false,
|
|
109
|
+
overrideId: override?.id
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
return router;
|
|
113
|
+
}
|
|
114
|
+
export {
|
|
115
|
+
compileOverrideFromAdr,
|
|
116
|
+
createAdrRouter
|
|
117
|
+
};
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
import { Router } from "express";
|
|
3
|
+
function createAgentActivityRouter() {
|
|
4
|
+
const router = Router();
|
|
5
|
+
router.get("/agent-activity", (_req, res) => {
|
|
6
|
+
const nonce = randomUUID().replace(/-/g, "");
|
|
7
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
8
|
+
res.setHeader(
|
|
9
|
+
"Content-Security-Policy",
|
|
10
|
+
`default-src 'self'; style-src 'unsafe-inline'; script-src 'nonce-${nonce}'; connect-src 'self'`
|
|
11
|
+
);
|
|
12
|
+
res.send(buildActivityHtml(nonce));
|
|
13
|
+
});
|
|
14
|
+
return router;
|
|
15
|
+
}
|
|
16
|
+
function buildActivityHtml(nonce) {
|
|
17
|
+
return `<!DOCTYPE html>
|
|
18
|
+
<html lang="en">
|
|
19
|
+
<head>
|
|
20
|
+
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
21
|
+
<title>Agent Activity \xB7 Mergen</title>
|
|
22
|
+
<style>
|
|
23
|
+
:root{--bg:#0f1117;--surface:#1a1d26;--border:#2a2d3a;--text:#e2e8f0;--muted:#64748b;
|
|
24
|
+
--green:#22c55e;--yellow:#f59e0b;--red:#ef4444;--blue:#3b82f6;--purple:#a78bfa;}
|
|
25
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
26
|
+
body{background:var(--bg);color:var(--text);font-family:'SF Mono',ui-monospace,monospace;font-size:12px;line-height:1.5;height:100vh;display:flex;flex-direction:column;overflow:hidden}
|
|
27
|
+
header{padding:12px 20px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:16px;flex-shrink:0;background:var(--surface)}
|
|
28
|
+
header h1{font-size:14px;font-weight:700;letter-spacing:.02em}
|
|
29
|
+
.dot{width:8px;height:8px;border-radius:50%;background:var(--green);animation:pulse 2s infinite}
|
|
30
|
+
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
|
|
31
|
+
.stats{display:flex;gap:16px;margin-left:auto;font-size:11px}
|
|
32
|
+
.stat{display:flex;align-items:center;gap:6px;color:var(--muted)}
|
|
33
|
+
.stat b{color:var(--text)}
|
|
34
|
+
.nav{display:flex;gap:12px;font-size:11px;padding:0 20px;border-bottom:1px solid var(--border);background:var(--surface)}
|
|
35
|
+
.nav a{color:var(--muted);text-decoration:none;padding:8px 0;display:block}
|
|
36
|
+
.nav a:hover{color:var(--blue)}
|
|
37
|
+
.filters{padding:10px 20px;border-bottom:1px solid var(--border);display:flex;gap:12px;flex-shrink:0;background:#13151e}
|
|
38
|
+
.filter-btn{padding:3px 10px;border-radius:10px;border:1px solid var(--border);background:transparent;color:var(--muted);font-size:10px;font-weight:700;cursor:pointer;letter-spacing:.04em}
|
|
39
|
+
.filter-btn.active{border-color:currentColor}
|
|
40
|
+
.filter-btn.all.active{color:var(--text);border-color:var(--text)}
|
|
41
|
+
.filter-btn.pass.active{color:var(--green);border-color:var(--green)}
|
|
42
|
+
.filter-btn.block.active{color:var(--red);border-color:var(--red)}
|
|
43
|
+
.filter-btn.hold.active{color:var(--yellow);border-color:var(--yellow)}
|
|
44
|
+
#feed{flex:1;overflow-y:auto;padding:0}
|
|
45
|
+
.event{display:grid;grid-template-columns:120px 60px 160px 1fr 80px;align-items:center;gap:12px;padding:8px 20px;border-bottom:1px solid rgba(42,45,58,.4);cursor:default;transition:.1s}
|
|
46
|
+
.event:hover{background:rgba(255,255,255,.03)}
|
|
47
|
+
.event.new{animation:slideIn .25s ease-out}
|
|
48
|
+
@keyframes slideIn{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:none}}
|
|
49
|
+
.ts{color:var(--muted);font-size:10px}
|
|
50
|
+
.verdict{font-size:10px;font-weight:700;letter-spacing:.06em;padding:2px 8px;border-radius:8px;text-align:center}
|
|
51
|
+
.verdict.PASS{background:rgba(34,197,94,.12);color:var(--green)}
|
|
52
|
+
.verdict.BLOCK{background:rgba(239,68,68,.12);color:var(--red)}
|
|
53
|
+
.verdict.HOLD{background:rgba(245,158,11,.12);color:var(--yellow)}
|
|
54
|
+
.tool{color:var(--purple);font-size:11px}
|
|
55
|
+
.cmd{color:var(--text);font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
56
|
+
.rules{color:var(--muted);font-size:10px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
57
|
+
.empty{text-align:center;color:var(--muted);padding:60px 20px;font-size:13px}
|
|
58
|
+
.conn-badge{font-size:10px;font-weight:700;padding:2px 8px;border-radius:8px;background:rgba(34,197,94,.1);color:var(--green)}
|
|
59
|
+
.conn-badge.disconnected{background:rgba(239,68,68,.1);color:var(--red)}
|
|
60
|
+
.header-row{display:grid;grid-template-columns:120px 60px 160px 1fr 80px;gap:12px;padding:6px 20px;font-size:10px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--muted);border-bottom:1px solid var(--border);flex-shrink:0;background:#13151e}
|
|
61
|
+
</style>
|
|
62
|
+
</head>
|
|
63
|
+
<body>
|
|
64
|
+
<header>
|
|
65
|
+
<div class="dot" id="status-dot"></div>
|
|
66
|
+
<h1>Agent Activity \u2014 Live</h1>
|
|
67
|
+
<div class="stats">
|
|
68
|
+
<div class="stat">PASS <b id="cnt-pass">0</b></div>
|
|
69
|
+
<div class="stat">BLOCK <b id="cnt-block">0</b></div>
|
|
70
|
+
<div class="stat">HOLD <b id="cnt-hold">0</b></div>
|
|
71
|
+
</div>
|
|
72
|
+
<span class="conn-badge" id="conn-badge">CONNECTING</span>
|
|
73
|
+
</header>
|
|
74
|
+
<div class="nav">
|
|
75
|
+
<a href="/dashboard">\u2190 Dashboard</a>
|
|
76
|
+
<a href="/policies">Policy Editor</a>
|
|
77
|
+
<a href="/agent-blunders">Blunder Log</a>
|
|
78
|
+
<a href="/agent-activity">Live Activity</a>
|
|
79
|
+
</div>
|
|
80
|
+
<div class="filters">
|
|
81
|
+
<button class="filter-btn all active" onclick="setFilter('all',this)">ALL</button>
|
|
82
|
+
<button class="filter-btn pass" onclick="setFilter('PASS',this)">PASS</button>
|
|
83
|
+
<button class="filter-btn block" onclick="setFilter('BLOCK',this)">BLOCK</button>
|
|
84
|
+
<button class="filter-btn hold" onclick="setFilter('HOLD',this)">HOLD</button>
|
|
85
|
+
</div>
|
|
86
|
+
<div class="header-row">
|
|
87
|
+
<span>TIME</span><span>VERDICT</span><span>TOOL</span><span>COMMAND / ARGS</span><span>RULES</span>
|
|
88
|
+
</div>
|
|
89
|
+
<div id="feed"><div class="empty" id="empty-msg">Waiting for agent activity\u2026<br><span style="font-size:11px;margin-top:8px;display:block">Tool calls will appear here in real time as agents make them.</span></div></div>
|
|
90
|
+
|
|
91
|
+
<script nonce="${nonce}">
|
|
92
|
+
let filter = 'all';
|
|
93
|
+
let counts = {PASS:0, BLOCK:0, HOLD:0};
|
|
94
|
+
const MAX_ROWS = 200;
|
|
95
|
+
|
|
96
|
+
function setFilter(f, btn) {
|
|
97
|
+
filter = f;
|
|
98
|
+
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
|
|
99
|
+
btn.classList.add('active');
|
|
100
|
+
document.querySelectorAll('.event').forEach(row => {
|
|
101
|
+
row.style.display = (filter === 'all' || row.dataset.verdict === filter) ? '' : 'none';
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function fmtTime(ts) {
|
|
106
|
+
const d = new Date(ts);
|
|
107
|
+
return d.toLocaleTimeString('en-US', {hour12:false,hour:'2-digit',minute:'2-digit',second:'2-digit'}) +
|
|
108
|
+
'.' + String(d.getMilliseconds()).padStart(3,'0');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function esc(s) {
|
|
112
|
+
return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function addEvent(ev) {
|
|
116
|
+
const empty = document.getElementById('empty-msg');
|
|
117
|
+
if (empty) empty.remove();
|
|
118
|
+
|
|
119
|
+
counts[ev.verdict] = (counts[ev.verdict]||0) + 1;
|
|
120
|
+
document.getElementById('cnt-pass').textContent = counts['PASS'] || 0;
|
|
121
|
+
document.getElementById('cnt-block').textContent = counts['BLOCK'] || 0;
|
|
122
|
+
document.getElementById('cnt-hold').textContent = counts['HOLD'] || 0;
|
|
123
|
+
|
|
124
|
+
const feed = document.getElementById('feed');
|
|
125
|
+
const row = document.createElement('div');
|
|
126
|
+
row.className = 'event new';
|
|
127
|
+
row.dataset.verdict = ev.verdict;
|
|
128
|
+
if (filter !== 'all' && ev.verdict !== filter) row.style.display = 'none';
|
|
129
|
+
|
|
130
|
+
const rulesText = (ev.ruleNames||[]).join(', ') || (ev.triggeredRules||[]).join(', ') || '\u2014';
|
|
131
|
+
row.innerHTML = \`
|
|
132
|
+
<span class="ts">\${fmtTime(ev.timestamp)}</span>
|
|
133
|
+
<span class="verdict \${esc(ev.verdict)}">\${esc(ev.verdict)}</span>
|
|
134
|
+
<span class="tool">\${esc(ev.toolName)}</span>
|
|
135
|
+
<span class="cmd" title="\${esc(ev.commandArg)}">\${esc(ev.commandArg||'\u2014')}</span>
|
|
136
|
+
<span class="rules" title="\${esc(rulesText)}">\${esc(rulesText)}</span>
|
|
137
|
+
\`;
|
|
138
|
+
feed.insertBefore(row, feed.firstChild);
|
|
139
|
+
|
|
140
|
+
// Prune old rows
|
|
141
|
+
const rows = feed.querySelectorAll('.event');
|
|
142
|
+
if (rows.length > ${200}) rows[rows.length-1].remove();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function connect() {
|
|
146
|
+
const badge = document.getElementById('conn-badge');
|
|
147
|
+
const dot = document.getElementById('status-dot');
|
|
148
|
+
const es = new EventSource('/activity-feed/stream');
|
|
149
|
+
|
|
150
|
+
es.onopen = () => {
|
|
151
|
+
badge.textContent = 'LIVE';
|
|
152
|
+
badge.className = 'conn-badge';
|
|
153
|
+
dot.style.background = 'var(--green)';
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
es.onmessage = (e) => {
|
|
157
|
+
try { addEvent(JSON.parse(e.data)); } catch {}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
es.onerror = () => {
|
|
161
|
+
badge.textContent = 'RECONNECTING';
|
|
162
|
+
badge.className = 'conn-badge disconnected';
|
|
163
|
+
dot.style.background = 'var(--red)';
|
|
164
|
+
es.close();
|
|
165
|
+
setTimeout(connect, 3000);
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Also hydrate with recent events
|
|
170
|
+
fetch('/activity-feed?limit=50').then(r=>r.json()).then(d => {
|
|
171
|
+
if (d.events && d.events.length > 0) {
|
|
172
|
+
// events are newest-first; add oldest first so they stack correctly
|
|
173
|
+
[...d.events].reverse().forEach(addEvent);
|
|
174
|
+
}
|
|
175
|
+
}).catch(()=>{});
|
|
176
|
+
|
|
177
|
+
connect();
|
|
178
|
+
</script>
|
|
179
|
+
</body>
|
|
180
|
+
</html>`;
|
|
181
|
+
}
|
|
182
|
+
export {
|
|
183
|
+
createAgentActivityRouter
|
|
184
|
+
};
|