@datasynx/agentic-ai-cartography 2.7.0 → 2.9.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/dist/api-bin.js +1 -1
- package/dist/{chunk-HLWNO3RF.js → chunk-LRUWWHMQ.js} +174 -15
- package/dist/chunk-LRUWWHMQ.js.map +1 -0
- package/dist/{chunk-TBPGFEMQ.js → chunk-W4Q3TXHR.js} +162 -2
- package/dist/chunk-W4Q3TXHR.js.map +1 -0
- package/dist/cli.js +18 -5
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +322 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +124 -10
- package/dist/index.d.ts +124 -10
- package/dist/index.js +319 -13
- package/dist/index.js.map +1 -1
- package/dist/mcp-bin.js +1 -1
- package/package.json +3 -2
- package/server.json +2 -2
- package/dist/chunk-HLWNO3RF.js.map +0 -1
- package/dist/chunk-TBPGFEMQ.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -49,6 +49,7 @@ __export(src_exports, {
|
|
|
49
49
|
DEFAULT_SERVER_NAME: () => DEFAULT_SERVER_NAME,
|
|
50
50
|
DEFAULT_TENANT: () => DEFAULT_TENANT,
|
|
51
51
|
DriftConfigSchema: () => DriftConfigSchema,
|
|
52
|
+
GraphStoreBackend: () => GraphStoreBackend,
|
|
52
53
|
INGEST_SCHEMA_VERSION: () => INGEST_SCHEMA_VERSION,
|
|
53
54
|
IngestEnvelopeSchema: () => IngestEnvelopeSchema,
|
|
54
55
|
InvalidTenantError: () => InvalidTenantError,
|
|
@@ -136,6 +137,7 @@ __export(src_exports, {
|
|
|
136
137
|
createSqliteQueryBackend: () => createSqliteQueryBackend,
|
|
137
138
|
currentOs: () => currentOs,
|
|
138
139
|
cursorDeeplink: () => cursorDeeplink,
|
|
140
|
+
dashboardHtml: () => dashboardHtml,
|
|
139
141
|
databasesScanner: () => databasesScanner,
|
|
140
142
|
deepMerge: () => deepMerge,
|
|
141
143
|
defaultAllowedHosts: () => defaultAllowedHosts,
|
|
@@ -220,6 +222,7 @@ __export(src_exports, {
|
|
|
220
222
|
nodesToAssets: () => nodesToAssets,
|
|
221
223
|
normalizeId: () => normalizeId,
|
|
222
224
|
normalizeTenant: () => normalizeTenant,
|
|
225
|
+
openStoreBackend: () => openStoreBackend,
|
|
223
226
|
orgKeyPath: () => orgKeyPath,
|
|
224
227
|
osUser: () => osUser,
|
|
225
228
|
parseApiArgs: () => parseApiArgs,
|
|
@@ -4653,6 +4656,148 @@ var SqliteStoreBackend = class {
|
|
|
4653
4656
|
}
|
|
4654
4657
|
};
|
|
4655
4658
|
|
|
4659
|
+
// src/store/graph.ts
|
|
4660
|
+
function toNum(v) {
|
|
4661
|
+
if (typeof v === "number") return v;
|
|
4662
|
+
if (v && typeof v === "object" && "toNumber" in v && typeof v.toNumber === "function") {
|
|
4663
|
+
return v.toNumber();
|
|
4664
|
+
}
|
|
4665
|
+
return Number(v ?? 0);
|
|
4666
|
+
}
|
|
4667
|
+
var GraphStoreBackend = class {
|
|
4668
|
+
constructor(driver) {
|
|
4669
|
+
this.driver = driver;
|
|
4670
|
+
}
|
|
4671
|
+
async run(cypher, params) {
|
|
4672
|
+
const session = this.driver.session();
|
|
4673
|
+
try {
|
|
4674
|
+
return await session.run(cypher, params);
|
|
4675
|
+
} finally {
|
|
4676
|
+
await session.close();
|
|
4677
|
+
}
|
|
4678
|
+
}
|
|
4679
|
+
async upsertNode(org, node, identity, contributor) {
|
|
4680
|
+
const res = await this.run(
|
|
4681
|
+
`MERGE (n:Node {org: $org, globalId: $globalId})
|
|
4682
|
+
ON CREATE SET n._created = true
|
|
4683
|
+
ON MATCH SET n._created = false
|
|
4684
|
+
SET n.id = $id, n.contentHash = $contentHash, n.type = $type, n.name = $name,
|
|
4685
|
+
n.domain = $domain, n.owner = $owner,
|
|
4686
|
+
n.confidence = CASE WHEN n.confidence IS NULL OR $confidence > n.confidence THEN $confidence ELSE n.confidence END
|
|
4687
|
+
MERGE (c:Contributor {org: $org, globalId: $globalId, machineId: $machineId})
|
|
4688
|
+
SET c.hostname = $hostname, c.user = $user, c.at = $at,
|
|
4689
|
+
c.confidence = CASE WHEN c.confidence IS NULL OR $contribConfidence > c.confidence THEN $contribConfidence ELSE c.confidence END
|
|
4690
|
+
RETURN n._created AS created`,
|
|
4691
|
+
{
|
|
4692
|
+
org,
|
|
4693
|
+
globalId: identity.globalId,
|
|
4694
|
+
contentHash: identity.contentHash,
|
|
4695
|
+
id: node.id,
|
|
4696
|
+
type: node.type,
|
|
4697
|
+
name: node.name,
|
|
4698
|
+
domain: node.domain ?? null,
|
|
4699
|
+
owner: node.owner ?? null,
|
|
4700
|
+
confidence: node.confidence,
|
|
4701
|
+
machineId: contributor.machineId,
|
|
4702
|
+
hostname: contributor.hostname,
|
|
4703
|
+
user: contributor.user,
|
|
4704
|
+
at: contributor.at,
|
|
4705
|
+
contribConfidence: contributor.confidence
|
|
4706
|
+
}
|
|
4707
|
+
);
|
|
4708
|
+
return res.records[0]?.get("created") === true ? "created" : "merged";
|
|
4709
|
+
}
|
|
4710
|
+
async insertEdge(org, edge) {
|
|
4711
|
+
await this.run(
|
|
4712
|
+
`MATCH (s:Node {org: $org, id: $source})
|
|
4713
|
+
MATCH (t:Node {org: $org, id: $target})
|
|
4714
|
+
MERGE (s)-[r:DEPENDS {relationship: $rel}]->(t)
|
|
4715
|
+
SET r.evidence = $evidence, r.confidence = $confidence`,
|
|
4716
|
+
{ org, source: edge.sourceId, target: edge.targetId, rel: edge.relationship, evidence: edge.evidence, confidence: edge.confidence }
|
|
4717
|
+
);
|
|
4718
|
+
}
|
|
4719
|
+
async getSummary(org) {
|
|
4720
|
+
const totals = await this.run(
|
|
4721
|
+
`MATCH (n:Node {org: $org})
|
|
4722
|
+
OPTIONAL MATCH (n)-[r:DEPENDS]->(:Node {org: $org})
|
|
4723
|
+
RETURN count(DISTINCT n) AS nodes, count(r) AS edges`,
|
|
4724
|
+
{ org }
|
|
4725
|
+
);
|
|
4726
|
+
const byType = await this.run(`MATCH (n:Node {org: $org}) RETURN n.type AS k, count(*) AS c`, { org });
|
|
4727
|
+
const byDomain = await this.run(`MATCH (n:Node {org: $org}) RETURN coalesce(n.domain, '(none)') AS k, count(*) AS c`, { org });
|
|
4728
|
+
const byRel = await this.run(`MATCH (:Node {org: $org})-[r:DEPENDS]->(:Node {org: $org}) RETURN r.relationship AS k, count(*) AS c`, { org });
|
|
4729
|
+
const top = await this.run(
|
|
4730
|
+
`MATCH (n:Node {org: $org})
|
|
4731
|
+
OPTIONAL MATCH (n)-[r:DEPENDS]-(:Node {org: $org})
|
|
4732
|
+
RETURN n.id AS id, n.name AS name, n.type AS type, count(r) AS degree
|
|
4733
|
+
ORDER BY degree DESC, id ASC LIMIT 10`,
|
|
4734
|
+
{ org }
|
|
4735
|
+
);
|
|
4736
|
+
const contrib = await this.run(`MATCH (c:Contributor {org: $org}) RETURN count(DISTINCT c.machineId) AS contributors`, { org });
|
|
4737
|
+
const counts = (r) => {
|
|
4738
|
+
const out = {};
|
|
4739
|
+
for (const rec of r.records) out[String(rec.get("k"))] = toNum(rec.get("c"));
|
|
4740
|
+
return out;
|
|
4741
|
+
};
|
|
4742
|
+
return {
|
|
4743
|
+
org,
|
|
4744
|
+
totals: { nodes: toNum(totals.records[0]?.get("nodes")), edges: toNum(totals.records[0]?.get("edges")) },
|
|
4745
|
+
nodesByType: counts(byType),
|
|
4746
|
+
nodesByDomain: counts(byDomain),
|
|
4747
|
+
edgesByRelationship: counts(byRel),
|
|
4748
|
+
topConnected: top.records.map((rec) => ({
|
|
4749
|
+
id: String(rec.get("id")),
|
|
4750
|
+
name: String(rec.get("name")),
|
|
4751
|
+
type: String(rec.get("type")),
|
|
4752
|
+
degree: toNum(rec.get("degree"))
|
|
4753
|
+
})),
|
|
4754
|
+
contributors: toNum(contrib.records[0]?.get("contributors"))
|
|
4755
|
+
};
|
|
4756
|
+
}
|
|
4757
|
+
async getContributors(globalId2) {
|
|
4758
|
+
const res = await this.run(
|
|
4759
|
+
`MATCH (c:Contributor {globalId: $globalId})
|
|
4760
|
+
RETURN c.machineId AS machineId, c.hostname AS hostname, c.user AS user, c.org AS org, c.at AS at, c.confidence AS confidence`,
|
|
4761
|
+
{ globalId: globalId2 }
|
|
4762
|
+
);
|
|
4763
|
+
return res.records.map((rec) => ({
|
|
4764
|
+
machineId: String(rec.get("machineId")),
|
|
4765
|
+
hostname: String(rec.get("hostname")),
|
|
4766
|
+
user: String(rec.get("user")),
|
|
4767
|
+
organization: rec.get("org") != null ? String(rec.get("org")) : void 0,
|
|
4768
|
+
at: String(rec.get("at")),
|
|
4769
|
+
confidence: toNum(rec.get("confidence"))
|
|
4770
|
+
}));
|
|
4771
|
+
}
|
|
4772
|
+
async close() {
|
|
4773
|
+
await this.driver.close();
|
|
4774
|
+
}
|
|
4775
|
+
};
|
|
4776
|
+
|
|
4777
|
+
// src/store/index.ts
|
|
4778
|
+
async function defaultNeo4jDriver(url, user, password) {
|
|
4779
|
+
const mod = await import("neo4j-driver");
|
|
4780
|
+
return mod.default.driver(url, mod.default.auth.basic(user, password));
|
|
4781
|
+
}
|
|
4782
|
+
async function openStoreBackend(db, opts = {}) {
|
|
4783
|
+
if (opts.backend === "graph" && opts.graphUrl) {
|
|
4784
|
+
try {
|
|
4785
|
+
const make = opts.driverFactory ?? defaultNeo4jDriver;
|
|
4786
|
+
const driver = await make(opts.graphUrl, opts.graphUser ?? "neo4j", opts.graphPassword ?? "");
|
|
4787
|
+
if (driver.verifyConnectivity) await driver.verifyConnectivity();
|
|
4788
|
+
logInfo("central store: graph backend active", { host: stripSensitive(opts.graphUrl) });
|
|
4789
|
+
return new GraphStoreBackend(driver);
|
|
4790
|
+
} catch (err) {
|
|
4791
|
+
logWarn("central store: graph backend unavailable \u2014 falling back to SQLite", {
|
|
4792
|
+
host: stripSensitive(opts.graphUrl),
|
|
4793
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
4794
|
+
});
|
|
4795
|
+
return new SqliteStoreBackend(db);
|
|
4796
|
+
}
|
|
4797
|
+
}
|
|
4798
|
+
return new SqliteStoreBackend(db);
|
|
4799
|
+
}
|
|
4800
|
+
|
|
4656
4801
|
// src/store/query.ts
|
|
4657
4802
|
var NotFoundError = class extends Error {
|
|
4658
4803
|
constructor(message) {
|
|
@@ -4967,9 +5112,9 @@ var IngestEnvelopeSchema = import_zod5.z.object({
|
|
|
4967
5112
|
contributor: ContributorSchema.optional(),
|
|
4968
5113
|
anonymizationLevel: import_zod5.z.enum(["none", "anonymized", "full"]).optional()
|
|
4969
5114
|
});
|
|
4970
|
-
function ingestEnvelope(store, envelope, opts = {}) {
|
|
5115
|
+
async function ingestEnvelope(store, envelope, opts = {}) {
|
|
4971
5116
|
const anonMode = opts.anonMode ?? "reject";
|
|
4972
|
-
const org = envelope.org ?? opts.defaultOrg
|
|
5117
|
+
const org = normalizeTenant(envelope.org ?? opts.defaultOrg);
|
|
4973
5118
|
const level = envelope.anonymizationLevel ?? "anonymized";
|
|
4974
5119
|
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
4975
5120
|
const contributor = {
|
|
@@ -5020,7 +5165,7 @@ function ingestEnvelope(store, envelope, opts = {}) {
|
|
|
5020
5165
|
}
|
|
5021
5166
|
const safe = check.node;
|
|
5022
5167
|
const identity = computeIdentity(org, safe);
|
|
5023
|
-
const outcome = store.upsertNode(org, safe, identity, { ...contributor, confidence: safe.confidence });
|
|
5168
|
+
const outcome = await store.upsertNode(org, safe, identity, { ...contributor, confidence: safe.confidence });
|
|
5024
5169
|
accepted += 1;
|
|
5025
5170
|
if (outcome === "merged") merged += 1;
|
|
5026
5171
|
acceptedNodeIds.add(safe.id);
|
|
@@ -5036,7 +5181,7 @@ function ingestEnvelope(store, envelope, opts = {}) {
|
|
|
5036
5181
|
if (acceptedNodeIds.size > 0 && (!acceptedNodeIds.has(edge.sourceId) || !acceptedNodeIds.has(edge.targetId))) {
|
|
5037
5182
|
continue;
|
|
5038
5183
|
}
|
|
5039
|
-
store.insertEdge(org, edge);
|
|
5184
|
+
await store.insertEdge(org, edge);
|
|
5040
5185
|
edges += 1;
|
|
5041
5186
|
}
|
|
5042
5187
|
logInfo("ingest", { org, accepted, merged, rejected, edges, violations, level, anonMode });
|
|
@@ -5046,7 +5191,7 @@ function ingestEnvelope(store, envelope, opts = {}) {
|
|
|
5046
5191
|
// src/central/server.ts
|
|
5047
5192
|
function createIngestHandler(store, opts = {}) {
|
|
5048
5193
|
const quota = opts.quota;
|
|
5049
|
-
return (body) => {
|
|
5194
|
+
return async (body) => {
|
|
5050
5195
|
const parsed = IngestEnvelopeSchema.safeParse(body);
|
|
5051
5196
|
if (!parsed.success) {
|
|
5052
5197
|
const issues = parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`);
|
|
@@ -5062,7 +5207,7 @@ function createIngestHandler(store, opts = {}) {
|
|
|
5062
5207
|
}
|
|
5063
5208
|
}
|
|
5064
5209
|
try {
|
|
5065
|
-
const result = ingestEnvelope(store, parsed.data, opts);
|
|
5210
|
+
const result = await ingestEnvelope(store, parsed.data, opts);
|
|
5066
5211
|
return { status: 200, body: result };
|
|
5067
5212
|
} catch (err) {
|
|
5068
5213
|
logWarn("ingest: failed", { error: err instanceof Error ? err.message : String(err) });
|
|
@@ -5991,7 +6136,7 @@ async function resolveNlQuery(db, sessionId, search, raw, opts) {
|
|
|
5991
6136
|
|
|
5992
6137
|
// src/mcp/server.ts
|
|
5993
6138
|
var SERVER_NAME = "cartography";
|
|
5994
|
-
var SERVER_VERSION = "2.
|
|
6139
|
+
var SERVER_VERSION = "2.9.0";
|
|
5995
6140
|
var SERVICE_TYPES = NODE_TYPE_GROUPS.web;
|
|
5996
6141
|
var DATA_TYPES = NODE_TYPE_GROUPS.data;
|
|
5997
6142
|
var lexicalSearch = async (db, sessionId, query, opts) => db.searchNodes(sessionId, query, { types: opts.types, limit: opts.limit }).map((node) => ({ node }));
|
|
@@ -6066,9 +6211,10 @@ function createMcpServer(opts = {}) {
|
|
|
6066
6211
|
"graph-summary",
|
|
6067
6212
|
"cartography://graph/summary",
|
|
6068
6213
|
{ title: "Topology summary", description: "Low-token aggregate index of the whole landscape \u2014 read this first.", mimeType: "text/markdown" },
|
|
6069
|
-
(uri) => {
|
|
6214
|
+
async (uri) => {
|
|
6070
6215
|
if (org !== void 0) {
|
|
6071
|
-
|
|
6216
|
+
const s = opts.orgSummary ? await opts.orgSummary(org) : db.getOrgSummary(org);
|
|
6217
|
+
return { contents: [{ uri: uri.href, mimeType: "text/markdown", text: summaryText(s) }] };
|
|
6072
6218
|
}
|
|
6073
6219
|
const sid = resolveSession();
|
|
6074
6220
|
if (!sid) return { contents: [{ uri: uri.href, mimeType: "text/markdown", text: "No discovery session found. Run discovery first." }] };
|
|
@@ -6143,8 +6289,8 @@ function createMcpServer(opts = {}) {
|
|
|
6143
6289
|
server.registerTool(
|
|
6144
6290
|
"get_summary",
|
|
6145
6291
|
{ title: "Get topology summary", description: "Low-token overview of the whole landscape (counts, types, domains, most-connected, anomalies).", inputSchema: {}, annotations: readOnly },
|
|
6146
|
-
() => {
|
|
6147
|
-
if (org !== void 0) return json(db.getOrgSummary(org));
|
|
6292
|
+
async () => {
|
|
6293
|
+
if (org !== void 0) return json(opts.orgSummary ? await opts.orgSummary(org) : db.getOrgSummary(org));
|
|
6148
6294
|
const sid = resolveSession();
|
|
6149
6295
|
if (!sid) return json({ error: "No discovery session found." });
|
|
6150
6296
|
return json(db.getGraphSummary(sid));
|
|
@@ -6670,7 +6816,7 @@ async function runHttp(factory, opts = {}) {
|
|
|
6670
6816
|
res.writeHead(413, { "content-type": "application/json" }).end('{"error":"payload too large"}');
|
|
6671
6817
|
return;
|
|
6672
6818
|
}
|
|
6673
|
-
const out = onIngest(value);
|
|
6819
|
+
const out = await onIngest(value);
|
|
6674
6820
|
res.writeHead(out.status, { "content-type": "application/json", ...out.headers ?? {} }).end(JSON.stringify(out.body));
|
|
6675
6821
|
return;
|
|
6676
6822
|
}
|
|
@@ -8375,6 +8521,156 @@ function handleGraphqlGet() {
|
|
|
8375
8521
|
return { status: 200, body: SDL };
|
|
8376
8522
|
}
|
|
8377
8523
|
|
|
8524
|
+
// src/web/dashboard.ts
|
|
8525
|
+
var STYLE = `
|
|
8526
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
8527
|
+
:root{--bg:#0f1419;--panel:#161b22;--line:#2d333b;--fg:#e6edf3;--dim:#8b949e;--accent:#3b82f6;--ok:#3fb950;--warn:#d29922;--crit:#f85149}
|
|
8528
|
+
body{font:13px/1.5 ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,sans-serif;background:var(--bg);color:var(--fg);height:100vh;display:flex;flex-direction:column;overflow:hidden}
|
|
8529
|
+
header{display:flex;align-items:center;gap:12px;padding:8px 14px;border-bottom:1px solid var(--line);background:var(--panel)}
|
|
8530
|
+
header h1{font-size:15px;font-weight:600;letter-spacing:.3px}
|
|
8531
|
+
header .ver{color:var(--dim);font-size:11px}
|
|
8532
|
+
header .spacer{flex:1}
|
|
8533
|
+
header input{background:var(--bg);border:1px solid var(--line);color:var(--fg);border-radius:6px;padding:5px 8px;font-size:12px;width:200px}
|
|
8534
|
+
header input:focus{outline:none;border-color:var(--accent)}
|
|
8535
|
+
header button{background:var(--accent);border:none;color:#fff;border-radius:6px;padding:6px 12px;font-size:12px;cursor:pointer}
|
|
8536
|
+
main{flex:1;display:grid;grid-template-columns:300px 1fr 320px;overflow:hidden}
|
|
8537
|
+
.col{overflow:auto;padding:12px;border-right:1px solid var(--line)}
|
|
8538
|
+
.col:last-child{border-right:none;border-left:1px solid var(--line)}
|
|
8539
|
+
.card{background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:10px 12px;margin-bottom:10px}
|
|
8540
|
+
.card h2{font-size:11px;text-transform:uppercase;letter-spacing:.6px;color:var(--dim);margin-bottom:8px}
|
|
8541
|
+
.stat{display:flex;justify-content:space-between;padding:2px 0}
|
|
8542
|
+
.stat b{font-variant-numeric:tabular-nums}
|
|
8543
|
+
.bar{height:4px;border-radius:2px;background:var(--accent);margin-top:2px}
|
|
8544
|
+
#search{width:100%;background:var(--bg);border:1px solid var(--line);color:var(--fg);border-radius:6px;padding:6px 8px;margin-bottom:8px}
|
|
8545
|
+
.node-item{padding:6px 8px;border-radius:6px;cursor:pointer;border:1px solid transparent}
|
|
8546
|
+
.node-item:hover{background:var(--panel)}
|
|
8547
|
+
.node-item.sel{background:var(--panel);border-color:var(--accent)}
|
|
8548
|
+
.node-item .t{color:var(--dim);font-size:11px}
|
|
8549
|
+
#center{position:relative;padding:0}
|
|
8550
|
+
#graph{display:block;width:100%;height:100%;background:radial-gradient(circle at 50% 40%,#11161d,#0d1117)}
|
|
8551
|
+
#empty{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:var(--dim);text-align:center;padding:20px}
|
|
8552
|
+
.kv{display:flex;justify-content:space-between;gap:8px;padding:3px 0;border-bottom:1px solid var(--line)}
|
|
8553
|
+
.kv span:first-child{color:var(--dim)}
|
|
8554
|
+
.kv span:last-child{text-align:right;word-break:break-all}
|
|
8555
|
+
.chip{display:inline-block;background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:1px 8px;font-size:11px;margin:2px 2px 0 0}
|
|
8556
|
+
.sev-high,.sev-critical{color:var(--crit)} .sev-medium,.sev-warning{color:var(--warn)} .sev-low,.sev-info{color:var(--dim)}
|
|
8557
|
+
#toast{position:fixed;bottom:14px;left:50%;transform:translateX(-50%);background:var(--crit);color:#fff;padding:8px 14px;border-radius:6px;font-size:12px;opacity:0;transition:opacity .2s;pointer-events:none}
|
|
8558
|
+
#toast.show{opacity:1}
|
|
8559
|
+
`;
|
|
8560
|
+
var SCRIPT = String.raw`
|
|
8561
|
+
const $=(s)=>document.querySelector(s), api=(p)=>{
|
|
8562
|
+
const h={accept:'application/json'};
|
|
8563
|
+
const t=sessionStorage.getItem('cartograph_token'); if(t) h.authorization='Bearer '+t;
|
|
8564
|
+
const tn=sessionStorage.getItem('cartograph_tenant'); if(tn) h['x-cartograph-tenant']=tn;
|
|
8565
|
+
return fetch(p,{headers:h}).then(async r=>{ if(!r.ok){ const e=new Error('http '+r.status); e.status=r.status; throw e; } return r.json(); });
|
|
8566
|
+
};
|
|
8567
|
+
function toast(m){ const t=$('#toast'); t.textContent=m; t.classList.add('show'); setTimeout(()=>t.classList.remove('show'),2600); }
|
|
8568
|
+
let NODES=[], SELECTED=null;
|
|
8569
|
+
|
|
8570
|
+
async function boot(){
|
|
8571
|
+
try{
|
|
8572
|
+
const s=await api('/v1/summary'); renderSummary(s);
|
|
8573
|
+
const n=await api('/v1/nodes?limit=1000'); NODES=n.nodes; renderList(NODES);
|
|
8574
|
+
}catch(e){
|
|
8575
|
+
if(e.status===401){ toast('Unauthorized — enter a bearer token and Reload.'); }
|
|
8576
|
+
else if(e.status===404){ const em=$('#empty'); em.textContent='No discovery session yet. Run a scan, then Reload.'; em.style.display='flex'; }
|
|
8577
|
+
else toast('Failed to load: '+e.message);
|
|
8578
|
+
}
|
|
8579
|
+
}
|
|
8580
|
+
function renderSummary(s){
|
|
8581
|
+
const max=Math.max(1,...Object.values(s.nodesByType));
|
|
8582
|
+
const types=Object.entries(s.nodesByType).sort((a,b)=>b[1]-a[1]).slice(0,12)
|
|
8583
|
+
.map(([k,v])=>'<div class="stat"><span>'+esc(k)+'</span><b>'+v+'</b></div><div class="bar" style="width:'+(v/max*100)+'%"></div>').join('');
|
|
8584
|
+
const anom=(s.anomalies||[]).slice(0,12).map(a=>'<div class="stat"><span class="sev-'+a.severity+'">'+esc(a.kind)+'</span><span class="t">'+esc(a.nodeId)+'</span></div>').join('') || '<div class="t">none</div>';
|
|
8585
|
+
$('#summary').innerHTML=
|
|
8586
|
+
'<div class="card"><h2>Totals</h2><div class="stat"><span>Nodes</span><b>'+s.totals.nodes+'</b></div><div class="stat"><span>Edges</span><b>'+s.totals.edges+'</b></div>'+(s.contributors!=null?'<div class="stat"><span>Contributors</span><b>'+s.contributors+'</b></div>':'')+'</div>'+
|
|
8587
|
+
'<div class="card"><h2>Nodes by type</h2>'+types+'</div>'+
|
|
8588
|
+
'<div class="card"><h2>Anomalies</h2>'+anom+'</div>';
|
|
8589
|
+
}
|
|
8590
|
+
function renderList(nodes){
|
|
8591
|
+
$('#list').innerHTML=nodes.map(n=>'<div class="node-item" data-id="'+esc(n.id)+'"><div>'+esc(n.name)+'</div><div class="t">'+esc(n.type)+'</div></div>').join('')||'<div class="t">no nodes</div>';
|
|
8592
|
+
$('#list').querySelectorAll('.node-item').forEach(el=>el.onclick=()=>select(el.dataset.id));
|
|
8593
|
+
}
|
|
8594
|
+
function esc(s){ return String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c])); }
|
|
8595
|
+
|
|
8596
|
+
async function select(id){
|
|
8597
|
+
SELECTED=id;
|
|
8598
|
+
$('#list').querySelectorAll('.node-item').forEach(el=>el.classList.toggle('sel',el.dataset.id===id));
|
|
8599
|
+
const node=NODES.find(n=>n.id===id);
|
|
8600
|
+
try{
|
|
8601
|
+
const dep=await api('/v1/nodes/'+encodeURIComponent(id)+'/dependencies?direction=both&maxDepth=2');
|
|
8602
|
+
renderDetail(node,dep); drawGraph(id,dep);
|
|
8603
|
+
}catch(e){ toast('drill-down failed: '+e.message); }
|
|
8604
|
+
}
|
|
8605
|
+
function renderDetail(node,dep){
|
|
8606
|
+
if(!node){ $('#detail').innerHTML='<div class="t">node not in current page</div>'; return; }
|
|
8607
|
+
const fields=[['id',node.id],['type',node.type],['name',node.name],['confidence',node.confidence],['domain',node.domain],['owner',node.owner]]
|
|
8608
|
+
.filter(([,v])=>v!=null).map(([k,v])=>'<div class="kv"><span>'+k+'</span><span>'+esc(v)+'</span></div>').join('');
|
|
8609
|
+
const tags=(node.tags||[]).map(t=>'<span class="chip">'+esc(t)+'</span>').join('');
|
|
8610
|
+
const edges=(dep.edges||[]).map(e=>'<div class="kv"><span>'+esc(e.relationship)+'</span><span>'+esc(e.sourceId===node.id?('→ '+e.targetId):('← '+e.sourceId))+'</span></div>').join('')||'<div class="t">no dependencies</div>';
|
|
8611
|
+
$('#detail').innerHTML='<div class="card"><h2>Node</h2>'+fields+(tags?'<div style="margin-top:6px">'+tags+'</div>':'')+'</div><div class="card"><h2>Dependencies ('+(dep.nodes?dep.nodes.length:0)+')</h2>'+edges+'</div>';
|
|
8612
|
+
}
|
|
8613
|
+
|
|
8614
|
+
const cv=()=>$('#graph'), ctx=()=>cv().getContext('2d');
|
|
8615
|
+
function drawGraph(rootId,dep){
|
|
8616
|
+
$('#empty').style.display='none';
|
|
8617
|
+
const c=cv(); const dpr=window.devicePixelRatio||1; const w=c.clientWidth,h=c.clientHeight;
|
|
8618
|
+
c.width=w*dpr; c.height=h*dpr; const g=ctx(); g.setTransform(dpr,0,0,dpr,0,0); g.clearRect(0,0,w,h);
|
|
8619
|
+
const nodes=dep.nodes||[]; const cx=w/2,cy=h/2;
|
|
8620
|
+
// root at center; others on a circle, radius by depth.
|
|
8621
|
+
const pos={}; pos[rootId]={x:cx,y:cy};
|
|
8622
|
+
const others=nodes.filter(n=>n.id!==rootId);
|
|
8623
|
+
others.forEach((n,i)=>{ const a=(i/Math.max(1,others.length))*Math.PI*2; const r=90+(n.depth||1)*70; pos[n.id]={x:cx+Math.cos(a)*r,y:cy+Math.sin(a)*r}; });
|
|
8624
|
+
// edges
|
|
8625
|
+
g.strokeStyle='#30363d'; g.lineWidth=1.2;
|
|
8626
|
+
(dep.edges||[]).forEach(e=>{ const a=pos[e.sourceId],b=pos[e.targetId]; if(a&&b){ g.beginPath(); g.moveTo(a.x,a.y); g.lineTo(b.x,b.y); g.stroke(); } });
|
|
8627
|
+
// nodes
|
|
8628
|
+
const byId={}; nodes.forEach(n=>byId[n.id]=n);
|
|
8629
|
+
Object.entries(pos).forEach(([id,p])=>{ const n=byId[id]; const root=id===rootId;
|
|
8630
|
+
g.beginPath(); g.arc(p.x,p.y,root?13:8,0,Math.PI*2);
|
|
8631
|
+
g.fillStyle=root?'#3b82f6':'#21262d'; g.fill(); g.lineWidth=root?2:1; g.strokeStyle=root?'#60a5fa':'#484f58'; g.stroke();
|
|
8632
|
+
g.fillStyle='#c9d1d9'; g.font=(root?'600 12px':'11px')+' ui-sans-serif'; g.textAlign='center';
|
|
8633
|
+
g.fillText((n&&n.name?n.name:id).slice(0,22),p.x,p.y-(root?20:14));
|
|
8634
|
+
});
|
|
8635
|
+
}
|
|
8636
|
+
|
|
8637
|
+
document.addEventListener('DOMContentLoaded',()=>{
|
|
8638
|
+
const t=sessionStorage.getItem('cartograph_token'); if(t)$('#token').value=t;
|
|
8639
|
+
const tn=sessionStorage.getItem('cartograph_tenant'); if(tn)$('#tenant').value=tn;
|
|
8640
|
+
$('#reload').onclick=()=>{ sessionStorage.setItem('cartograph_token',$('#token').value.trim()); sessionStorage.setItem('cartograph_tenant',$('#tenant').value.trim()); boot(); };
|
|
8641
|
+
$('#search').oninput=(e)=>{ const q=e.target.value.toLowerCase(); renderList(NODES.filter(n=>n.name.toLowerCase().includes(q)||n.id.toLowerCase().includes(q)||n.type.toLowerCase().includes(q))); };
|
|
8642
|
+
boot();
|
|
8643
|
+
});
|
|
8644
|
+
`;
|
|
8645
|
+
function dashboardHtml(opts = {}) {
|
|
8646
|
+
const version = opts.version ?? "";
|
|
8647
|
+
return `<!DOCTYPE html>
|
|
8648
|
+
<html lang="en"><head>
|
|
8649
|
+
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
8650
|
+
<title>Cartograph dashboard</title>
|
|
8651
|
+
<style>${STYLE}</style>
|
|
8652
|
+
</head><body>
|
|
8653
|
+
<header>
|
|
8654
|
+
<h1>Cartograph</h1><span class="ver">${version ? `v${version}` : ""}</span>
|
|
8655
|
+
<span class="spacer"></span>
|
|
8656
|
+
<input id="tenant" placeholder="tenant (optional)" autocomplete="off">
|
|
8657
|
+
<input id="token" type="password" placeholder="bearer token" autocomplete="off">
|
|
8658
|
+
<button id="reload">Reload</button>
|
|
8659
|
+
</header>
|
|
8660
|
+
<main>
|
|
8661
|
+
<div class="col"><div id="summary"></div></div>
|
|
8662
|
+
<div class="col" id="center"><canvas id="graph"></canvas><div id="empty">Select a node to explore its dependencies.</div></div>
|
|
8663
|
+
<div class="col">
|
|
8664
|
+
<input id="search" placeholder="Search nodes\u2026" autocomplete="off">
|
|
8665
|
+
<div id="list"></div>
|
|
8666
|
+
<div id="detail"></div>
|
|
8667
|
+
</div>
|
|
8668
|
+
</main>
|
|
8669
|
+
<div id="toast"></div>
|
|
8670
|
+
<script>${SCRIPT}</script>
|
|
8671
|
+
</body></html>`;
|
|
8672
|
+
}
|
|
8673
|
+
|
|
8378
8674
|
// src/api/server.ts
|
|
8379
8675
|
var DEPENDENCIES_RE = /^\/v1\/nodes\/(.+)\/dependencies$/;
|
|
8380
8676
|
var MAX_GRAPHQL_BYTES = 1024 * 1024;
|
|
@@ -8416,6 +8712,8 @@ async function runApi(opts) {
|
|
|
8416
8712
|
});
|
|
8417
8713
|
const restDeps = { backend: opts.backend, version: opts.version };
|
|
8418
8714
|
const openApiDoc = buildOpenApiDocument({ version: opts.version });
|
|
8715
|
+
const dashboardEnabled = opts.dashboard !== false;
|
|
8716
|
+
const dashboardPage = dashboardEnabled ? dashboardHtml({ version: opts.version }) : "";
|
|
8419
8717
|
const allowedOrigins = opts.allowedOrigins ?? [];
|
|
8420
8718
|
assertSafeBind({ host: host2, port: requestedPort, ...opts.allowedHosts ? { allowedHosts: opts.allowedHosts } : {}, ...token ? { token } : {} });
|
|
8421
8719
|
let allowedHosts = opts.allowedHosts ?? [];
|
|
@@ -8458,6 +8756,11 @@ async function runApi(opts) {
|
|
|
8458
8756
|
finish(200);
|
|
8459
8757
|
return;
|
|
8460
8758
|
}
|
|
8759
|
+
if (dashboardEnabled && (path === "/" || path === "/app") && req.method === "GET") {
|
|
8760
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8", ...cors }).end(dashboardPage);
|
|
8761
|
+
finish(200);
|
|
8762
|
+
return;
|
|
8763
|
+
}
|
|
8461
8764
|
if (path === "/v1/health") {
|
|
8462
8765
|
if (req.method !== "GET") {
|
|
8463
8766
|
send(res, 405, { error: "method not allowed" }, { allow: "GET", ...cors });
|
|
@@ -8622,6 +8925,7 @@ function parseApiArgs(argv) {
|
|
|
8622
8925
|
const a = argv[i];
|
|
8623
8926
|
if (a === "--http") continue;
|
|
8624
8927
|
else if (a === "--no-graphql") opts.graphql = false;
|
|
8928
|
+
else if (a === "--no-dashboard") opts.dashboard = false;
|
|
8625
8929
|
else if (a === "--port") opts.port = Number(argv[++i]);
|
|
8626
8930
|
else if (a === "--host") opts.host = argv[++i];
|
|
8627
8931
|
else if (a === "--allowed-hosts") opts.allowedHosts = splitList(argv[++i]);
|
|
@@ -8657,12 +8961,14 @@ async function startApi(opts = {}) {
|
|
|
8657
8961
|
...opts.allowedOrigins ? { allowedOrigins: opts.allowedOrigins } : {},
|
|
8658
8962
|
...token ? { token } : {},
|
|
8659
8963
|
...opts.graphql === false ? { graphql: false } : {},
|
|
8964
|
+
...opts.dashboard === false ? { dashboard: false } : {},
|
|
8660
8965
|
...opts.tenant ? { tenant: { defaultTenant: normalizeTenant(opts.tenant) } } : {},
|
|
8661
8966
|
log: log2
|
|
8662
8967
|
});
|
|
8663
8968
|
const graphqlNote = opts.graphql === false ? " [REST only]" : " + /graphql";
|
|
8969
|
+
const dashNote = opts.dashboard === false ? "" : ` \xB7 dashboard http://${host2}:${port}/`;
|
|
8664
8970
|
log2(
|
|
8665
|
-
`Cartograph API (REST${graphqlNote}) on http://${host2}:${port}/v1${token ? " (auth: bearer token required)" : ""} (tenant: ${normalizeTenant(opts.tenant)})`
|
|
8971
|
+
`Cartograph API (REST${graphqlNote}) on http://${host2}:${port}/v1${token ? " (auth: bearer token required)" : ""} (tenant: ${normalizeTenant(opts.tenant)})${dashNote}`
|
|
8666
8972
|
);
|
|
8667
8973
|
return server;
|
|
8668
8974
|
}
|
|
@@ -11927,6 +12233,7 @@ function checkClaudePrerequisites() {
|
|
|
11927
12233
|
DEFAULT_SERVER_NAME,
|
|
11928
12234
|
DEFAULT_TENANT,
|
|
11929
12235
|
DriftConfigSchema,
|
|
12236
|
+
GraphStoreBackend,
|
|
11930
12237
|
INGEST_SCHEMA_VERSION,
|
|
11931
12238
|
IngestEnvelopeSchema,
|
|
11932
12239
|
InvalidTenantError,
|
|
@@ -12014,6 +12321,7 @@ function checkClaudePrerequisites() {
|
|
|
12014
12321
|
createSqliteQueryBackend,
|
|
12015
12322
|
currentOs,
|
|
12016
12323
|
cursorDeeplink,
|
|
12324
|
+
dashboardHtml,
|
|
12017
12325
|
databasesScanner,
|
|
12018
12326
|
deepMerge,
|
|
12019
12327
|
defaultAllowedHosts,
|
|
@@ -12098,6 +12406,7 @@ function checkClaudePrerequisites() {
|
|
|
12098
12406
|
nodesToAssets,
|
|
12099
12407
|
normalizeId,
|
|
12100
12408
|
normalizeTenant,
|
|
12409
|
+
openStoreBackend,
|
|
12101
12410
|
orgKeyPath,
|
|
12102
12411
|
osUser,
|
|
12103
12412
|
parseApiArgs,
|