@ra3orblade/swarm 0.11.3 → 0.12.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/swarm-hook.js +2 -0
- package/dist/swarm.js +2 -0
- package/dist/swarmd.js +604 -2
- package/package.json +1 -1
- package/web/app.js +406 -10
- package/web/index.html +62 -7
- package/web/release-notes.js +1 -1
- package/web/viz.js +42 -5
package/dist/swarmd.js
CHANGED
|
@@ -830,6 +830,8 @@ var AUDIT_TYPES = new Set([
|
|
|
830
830
|
"claim.released",
|
|
831
831
|
"claim.expired",
|
|
832
832
|
"claim.orphaned",
|
|
833
|
+
"claim.denied",
|
|
834
|
+
"rules.changed",
|
|
833
835
|
"worktree.created",
|
|
834
836
|
"worktree.removed",
|
|
835
837
|
"worktree.bootstrapped",
|
|
@@ -2202,6 +2204,82 @@ function collisionGraph(rows, writeTools = WRITE_TOOLS) {
|
|
|
2202
2204
|
contested: out.filter((f) => f.contested).length
|
|
2203
2205
|
};
|
|
2204
2206
|
}
|
|
2207
|
+
// packages/core/src/heat.ts
|
|
2208
|
+
var HEAT_DEFAULTS = {
|
|
2209
|
+
floor: 2,
|
|
2210
|
+
candidateSessions: 2,
|
|
2211
|
+
candidateRereads: 3,
|
|
2212
|
+
candidateWriteShare: 0.2,
|
|
2213
|
+
top: 40
|
|
2214
|
+
};
|
|
2215
|
+
function dirOf(path) {
|
|
2216
|
+
const i = path.lastIndexOf("/");
|
|
2217
|
+
return i <= 0 ? i === 0 ? "/" : "." : path.slice(0, i);
|
|
2218
|
+
}
|
|
2219
|
+
function fileHeat(rows, opts = {}, writeTools = WRITE_TOOLS) {
|
|
2220
|
+
const o = { ...HEAT_DEFAULTS, ...opts };
|
|
2221
|
+
const files = new Map;
|
|
2222
|
+
for (const r of rows) {
|
|
2223
|
+
if (!r.path || !r.sessionId)
|
|
2224
|
+
continue;
|
|
2225
|
+
const f = files.get(r.path) ?? { touches: 0, reads: 0, writes: 0, perSession: new Map };
|
|
2226
|
+
f.touches++;
|
|
2227
|
+
if (writeTools.has(r.tool))
|
|
2228
|
+
f.writes++;
|
|
2229
|
+
else
|
|
2230
|
+
f.reads++;
|
|
2231
|
+
f.perSession.set(r.sessionId, (f.perSession.get(r.sessionId) ?? 0) + 1);
|
|
2232
|
+
files.set(r.path, f);
|
|
2233
|
+
}
|
|
2234
|
+
const all = [...files.entries()].map(([path, f]) => {
|
|
2235
|
+
const rereads = [...f.perSession.values()].reduce((n, c) => n + (c - 1), 0);
|
|
2236
|
+
const writeShare = f.touches ? f.writes / f.touches : 0;
|
|
2237
|
+
return {
|
|
2238
|
+
path,
|
|
2239
|
+
touches: f.touches,
|
|
2240
|
+
sessions: f.perSession.size,
|
|
2241
|
+
reads: f.reads,
|
|
2242
|
+
writes: f.writes,
|
|
2243
|
+
rereads,
|
|
2244
|
+
candidate: f.perSession.size >= o.candidateSessions && rereads >= o.candidateRereads && writeShare <= o.candidateWriteShare
|
|
2245
|
+
};
|
|
2246
|
+
});
|
|
2247
|
+
const byTouches = (a, b) => b.touches - a.touches || b.sessions - a.sessions || a.path.localeCompare(b.path);
|
|
2248
|
+
const dirs = new Map;
|
|
2249
|
+
for (const [path, f] of files) {
|
|
2250
|
+
const d = dirOf(path);
|
|
2251
|
+
const cur = dirs.get(d) ?? {
|
|
2252
|
+
touches: 0,
|
|
2253
|
+
writes: 0,
|
|
2254
|
+
sessions: new Set,
|
|
2255
|
+
files: new Set
|
|
2256
|
+
};
|
|
2257
|
+
cur.touches += f.touches;
|
|
2258
|
+
cur.writes += f.writes;
|
|
2259
|
+
cur.files.add(path);
|
|
2260
|
+
for (const s of f.perSession.keys())
|
|
2261
|
+
cur.sessions.add(s);
|
|
2262
|
+
dirs.set(d, cur);
|
|
2263
|
+
}
|
|
2264
|
+
return {
|
|
2265
|
+
files: all.filter((f) => f.touches >= o.floor).sort(byTouches).slice(0, o.top),
|
|
2266
|
+
dirs: [...dirs.entries()].map(([dir, d]) => ({
|
|
2267
|
+
dir,
|
|
2268
|
+
touches: d.touches,
|
|
2269
|
+
sessions: d.sessions.size,
|
|
2270
|
+
files: d.files.size,
|
|
2271
|
+
writes: d.writes
|
|
2272
|
+
})).sort((a, b) => b.touches - a.touches || a.dir.localeCompare(b.dir)).slice(0, o.top),
|
|
2273
|
+
candidates: all.filter((f) => f.candidate).sort((a, b) => b.rereads - a.rereads || byTouches(a, b)).slice(0, o.top),
|
|
2274
|
+
totals: {
|
|
2275
|
+
files: all.length,
|
|
2276
|
+
touches: all.reduce((n, f) => n + f.touches, 0),
|
|
2277
|
+
sessions: new Set(rows.filter((r) => r.path && r.sessionId).map((r) => r.sessionId)).size,
|
|
2278
|
+
rereads: all.reduce((n, f) => n + f.rereads, 0),
|
|
2279
|
+
cold: all.filter((f) => f.touches === 1).length
|
|
2280
|
+
}
|
|
2281
|
+
};
|
|
2282
|
+
}
|
|
2205
2283
|
// packages/core/src/worktree.ts
|
|
2206
2284
|
import { join as join3 } from "path";
|
|
2207
2285
|
function planBootstrap(cfg, repoRoot, worktree) {
|
|
@@ -3366,6 +3444,107 @@ function formatOpenQuestions(qs) {
|
|
|
3366
3444
|
return null;
|
|
3367
3445
|
return `[swarm] waiting on a human for: ${open.map((q) => `#${q.id} "${q.text.slice(0, 120)}"`).join("; ")} \u2014 the answer arrives as context on a later tool call, or via swarm_inbox`;
|
|
3368
3446
|
}
|
|
3447
|
+
// packages/core/src/resourcegraph.ts
|
|
3448
|
+
var nodeId = (kind, name, projectId) => `${kind}:${projectId ?? ""}:${name}`;
|
|
3449
|
+
function resourceGraph(held, wanted = [], now = Date.now()) {
|
|
3450
|
+
const resources = new Map;
|
|
3451
|
+
const holders = new Map;
|
|
3452
|
+
for (const h of held) {
|
|
3453
|
+
if (!h.name || !h.owner)
|
|
3454
|
+
continue;
|
|
3455
|
+
const id = nodeId(h.kind, h.name, h.projectId);
|
|
3456
|
+
const expired = h.expiresAt ? Date.parse(h.expiresAt) < now : false;
|
|
3457
|
+
const orphaned = Boolean(h.sessionEndedAt) || expired;
|
|
3458
|
+
if (!resources.has(id))
|
|
3459
|
+
resources.set(id, {
|
|
3460
|
+
id,
|
|
3461
|
+
kind: h.kind,
|
|
3462
|
+
name: h.name,
|
|
3463
|
+
holder: h.owner,
|
|
3464
|
+
orphaned,
|
|
3465
|
+
wanted: [],
|
|
3466
|
+
projectId: h.projectId ?? null
|
|
3467
|
+
});
|
|
3468
|
+
const o = holders.get(h.owner) ?? { holds: 0, wants: 0, live: 0 };
|
|
3469
|
+
o.holds++;
|
|
3470
|
+
if (!orphaned)
|
|
3471
|
+
o.live++;
|
|
3472
|
+
holders.set(h.owner, o);
|
|
3473
|
+
}
|
|
3474
|
+
for (const w of wanted) {
|
|
3475
|
+
if (!w.name || !w.owner)
|
|
3476
|
+
continue;
|
|
3477
|
+
const r = resources.get(nodeId(w.kind, w.name, w.projectId));
|
|
3478
|
+
if (!r || r.holder === w.owner)
|
|
3479
|
+
continue;
|
|
3480
|
+
if (!r.wanted.includes(w.owner))
|
|
3481
|
+
r.wanted.push(w.owner);
|
|
3482
|
+
const o = holders.get(w.owner) ?? { holds: 0, wants: 0, live: 0 };
|
|
3483
|
+
o.wants++;
|
|
3484
|
+
holders.set(w.owner, o);
|
|
3485
|
+
}
|
|
3486
|
+
for (const r of resources.values())
|
|
3487
|
+
r.wanted.sort();
|
|
3488
|
+
const edges = [];
|
|
3489
|
+
for (const r of [...resources.values()].sort((a, b) => a.id.localeCompare(b.id))) {
|
|
3490
|
+
if (r.holder)
|
|
3491
|
+
edges.push({ from: r.holder, to: r.id, kind: "holds" });
|
|
3492
|
+
for (const w of r.wanted)
|
|
3493
|
+
edges.push({ from: w, to: r.id, kind: "wants" });
|
|
3494
|
+
}
|
|
3495
|
+
const resourceList = [...resources.values()].sort((a, b) => Number(b.wanted.length > 0) - Number(a.wanted.length > 0) || Number(b.orphaned) - Number(a.orphaned) || a.id.localeCompare(b.id));
|
|
3496
|
+
const holderList = [...holders.entries()].map(([id, o]) => ({ id, holds: o.holds, wants: o.wants, gone: o.holds > 0 && o.live === 0 })).sort((a, b) => b.holds + b.wants - (a.holds + a.wants) || a.id.localeCompare(b.id));
|
|
3497
|
+
return {
|
|
3498
|
+
holders: holderList,
|
|
3499
|
+
resources: resourceList,
|
|
3500
|
+
edges,
|
|
3501
|
+
contention: findContention(resourceList),
|
|
3502
|
+
totals: {
|
|
3503
|
+
held: resourceList.length,
|
|
3504
|
+
orphaned: resourceList.filter((r) => r.orphaned).length,
|
|
3505
|
+
contested: resourceList.filter((r) => r.wanted.length > 0).length
|
|
3506
|
+
}
|
|
3507
|
+
};
|
|
3508
|
+
}
|
|
3509
|
+
function findContention(resources) {
|
|
3510
|
+
const next = new Map;
|
|
3511
|
+
for (const r of resources) {
|
|
3512
|
+
if (!r.holder)
|
|
3513
|
+
continue;
|
|
3514
|
+
for (const w of r.wanted) {
|
|
3515
|
+
const list = next.get(w) ?? [];
|
|
3516
|
+
list.push({ to: r.holder, via: r.name });
|
|
3517
|
+
next.set(w, list);
|
|
3518
|
+
}
|
|
3519
|
+
}
|
|
3520
|
+
for (const list of next.values())
|
|
3521
|
+
list.sort((a, b) => a.to.localeCompare(b.to) || a.via.localeCompare(b.via));
|
|
3522
|
+
const found = new Map;
|
|
3523
|
+
const walk = (start, at, owners, vias, depth) => {
|
|
3524
|
+
if (depth > 6)
|
|
3525
|
+
return;
|
|
3526
|
+
for (const step of next.get(at) ?? []) {
|
|
3527
|
+
if (step.to === start) {
|
|
3528
|
+
const ring = [...owners];
|
|
3529
|
+
const res = [...vias, step.via];
|
|
3530
|
+
const pivot = ring.indexOf([...ring].sort()[0]);
|
|
3531
|
+
const key = [...ring.slice(pivot), ...ring.slice(0, pivot)].join(">");
|
|
3532
|
+
if (!found.has(key))
|
|
3533
|
+
found.set(key, {
|
|
3534
|
+
owners: [...ring.slice(pivot), ...ring.slice(0, pivot)],
|
|
3535
|
+
resources: [...res.slice(pivot), ...res.slice(0, pivot)]
|
|
3536
|
+
});
|
|
3537
|
+
continue;
|
|
3538
|
+
}
|
|
3539
|
+
if (owners.includes(step.to))
|
|
3540
|
+
continue;
|
|
3541
|
+
walk(start, step.to, [...owners, step.to], [...vias, step.via], depth + 1);
|
|
3542
|
+
}
|
|
3543
|
+
};
|
|
3544
|
+
for (const owner of [...next.keys()].sort())
|
|
3545
|
+
walk(owner, owner, [owner], [], 0);
|
|
3546
|
+
return [...found.values()].sort((a, b) => a.owners.length - b.owners.length || a.owners.join().localeCompare(b.owners.join()));
|
|
3547
|
+
}
|
|
3369
3548
|
// packages/core/src/resources.ts
|
|
3370
3549
|
var DEFAULT_RESOURCE_LEASE_MINUTES = 60;
|
|
3371
3550
|
function isTrackedPid(pid) {
|
|
@@ -3499,6 +3678,202 @@ ${lines.join(`
|
|
|
3499
3678
|
`)}` : ""}`
|
|
3500
3679
|
};
|
|
3501
3680
|
}
|
|
3681
|
+
// packages/core/src/ruleeffect.ts
|
|
3682
|
+
var DAY = 86400000;
|
|
3683
|
+
var dayOf = (iso) => iso.slice(0, 10);
|
|
3684
|
+
function commandSignature(command) {
|
|
3685
|
+
const segments = (command ?? "").split(/&&|\|\||[;\n|]/).map((x) => x.trim()).filter(Boolean);
|
|
3686
|
+
const first = segments.find((seg) => !/^(cd|pushd|export|source|\.)\b/.test(seg)) ?? segments[0] ?? "";
|
|
3687
|
+
if (!first)
|
|
3688
|
+
return "(none)";
|
|
3689
|
+
const tokens = first.split(/\s+/).filter(Boolean);
|
|
3690
|
+
const head = tokens[0] ?? "";
|
|
3691
|
+
const name = head.includes("/") ? head.split("/").pop() ?? head : head;
|
|
3692
|
+
const second = tokens[1];
|
|
3693
|
+
const more = tokens.length > 2;
|
|
3694
|
+
return `${name}${second ? ` ${second}` : ""}${more ? " \u2026" : ""}`;
|
|
3695
|
+
}
|
|
3696
|
+
function ruleEffect(incidents, changes = [], now = Date.now(), days3 = 30) {
|
|
3697
|
+
const since = now - days3 * DAY;
|
|
3698
|
+
const rows = incidents.filter((i) => i.rule && Date.parse(i.at) >= since);
|
|
3699
|
+
const byRule = new Map;
|
|
3700
|
+
for (const i of rows)
|
|
3701
|
+
byRule.set(i.rule, [...byRule.get(i.rule) ?? [], i]);
|
|
3702
|
+
const dayList = [];
|
|
3703
|
+
for (let t = since;t <= now; t += DAY)
|
|
3704
|
+
dayList.push(dayOf(new Date(t).toISOString()));
|
|
3705
|
+
const rules = [...byRule.entries()].map(([rule, list]) => {
|
|
3706
|
+
const sorted = [...list].sort((a, b) => a.at.localeCompare(b.at));
|
|
3707
|
+
const counts = new Map;
|
|
3708
|
+
for (const i of sorted)
|
|
3709
|
+
counts.set(dayOf(i.at), (counts.get(dayOf(i.at)) ?? 0) + 1);
|
|
3710
|
+
const perDay = dayList.map((day) => ({ day, n: counts.get(day) ?? 0 }));
|
|
3711
|
+
const half = Math.floor(perDay.length / 2);
|
|
3712
|
+
const early = perDay.slice(0, half).reduce((n, d) => n + d.n, 0);
|
|
3713
|
+
const late = perDay.slice(half).reduce((n, d) => n + d.n, 0);
|
|
3714
|
+
const trend = Math.abs(late - early) <= 1 ? "steady" : late > early ? "rising" : "falling";
|
|
3715
|
+
const sig = new Map;
|
|
3716
|
+
for (const i of sorted) {
|
|
3717
|
+
const key = commandSignature(i.command);
|
|
3718
|
+
const cur = sig.get(key) ?? { hits: 0, example: i.command };
|
|
3719
|
+
cur.hits++;
|
|
3720
|
+
sig.set(key, cur);
|
|
3721
|
+
}
|
|
3722
|
+
const clusters = [...sig.entries()].map(([signature, c]) => ({ signature, hits: c.hits, example: c.example })).sort((a, b) => b.hits - a.hits || a.signature.localeCompare(b.signature));
|
|
3723
|
+
const added = changes.filter((c) => c.added.includes(rule) && Date.parse(c.at) >= since).sort((a, b) => b.at.localeCompare(a.at))[0];
|
|
3724
|
+
let landed = null;
|
|
3725
|
+
if (added) {
|
|
3726
|
+
const at = Date.parse(added.at);
|
|
3727
|
+
const beforeDays = Math.max(1, (at - since) / DAY);
|
|
3728
|
+
const afterDays = Math.max(1, (now - at) / DAY);
|
|
3729
|
+
landed = {
|
|
3730
|
+
at: added.at,
|
|
3731
|
+
beforePerDay: sorted.filter((i) => Date.parse(i.at) < at).length / beforeDays,
|
|
3732
|
+
afterPerDay: sorted.filter((i) => Date.parse(i.at) >= at).length / afterDays
|
|
3733
|
+
};
|
|
3734
|
+
}
|
|
3735
|
+
return {
|
|
3736
|
+
rule,
|
|
3737
|
+
total: sorted.length,
|
|
3738
|
+
acked: sorted.filter((i) => i.acked).length,
|
|
3739
|
+
firstAt: sorted[0].at,
|
|
3740
|
+
lastAt: sorted.at(-1).at,
|
|
3741
|
+
perDay,
|
|
3742
|
+
trend,
|
|
3743
|
+
concentration: sorted.length ? (clusters[0]?.hits ?? 0) / sorted.length : 0,
|
|
3744
|
+
clusters: clusters.slice(0, 5),
|
|
3745
|
+
landed
|
|
3746
|
+
};
|
|
3747
|
+
});
|
|
3748
|
+
rules.sort((a, b) => b.total - a.total || a.rule.localeCompare(b.rule));
|
|
3749
|
+
return {
|
|
3750
|
+
rules,
|
|
3751
|
+
totals: {
|
|
3752
|
+
incidents: rows.length,
|
|
3753
|
+
rules: rules.length,
|
|
3754
|
+
acked: rows.filter((i) => i.acked).length,
|
|
3755
|
+
unchanged: rules.filter((r) => r.trend !== "falling" && r.total > 1).length
|
|
3756
|
+
},
|
|
3757
|
+
noChangeHistory: !changes.some((c) => Date.parse(c.at) >= since)
|
|
3758
|
+
};
|
|
3759
|
+
}
|
|
3760
|
+
// packages/core/src/security.ts
|
|
3761
|
+
var LOCAL = /^(localhost|127\.|0\.0\.0\.0|::1|\[::1\]|192\.168\.|10\.|172\.(1[6-9]|2\d|3[01])\.)/i;
|
|
3762
|
+
function hostsIn(text) {
|
|
3763
|
+
const out = new Set;
|
|
3764
|
+
for (const m of text.matchAll(/\bhttps?:\/\/(\[[^\]]+\][^/\s"'`)}>,;]*|[^/\s"'`)\]}>,;]+)/gi)) {
|
|
3765
|
+
const hostPort = m[1].replace(/^[^@]*@/, "");
|
|
3766
|
+
const bracket = /^\[([^\]]+)\]/.exec(hostPort);
|
|
3767
|
+
const host = (bracket?.[1] ?? hostPort.split(":")[0]).toLowerCase();
|
|
3768
|
+
if (host)
|
|
3769
|
+
out.add(host);
|
|
3770
|
+
}
|
|
3771
|
+
return [...out].sort();
|
|
3772
|
+
}
|
|
3773
|
+
var INSTALLERS = [
|
|
3774
|
+
{ ecosystem: "npm", re: /^npm\s+(?:i|install|add)\b(.*)$/i },
|
|
3775
|
+
{ ecosystem: "pnpm", re: /^pnpm\s+(?:i|install|add)\b(.*)$/i },
|
|
3776
|
+
{ ecosystem: "yarn", re: /^yarn\s+add\b(.*)$/i },
|
|
3777
|
+
{ ecosystem: "bun", re: /^bun\s+(?:i|install|add)\b(.*)$/i },
|
|
3778
|
+
{ ecosystem: "pip", re: /^pip3?\s+install\b(.*)$/i },
|
|
3779
|
+
{ ecosystem: "cargo", re: /^cargo\s+(?:add|install)\b(.*)$/i },
|
|
3780
|
+
{ ecosystem: "go", re: /^go\s+(?:get|install)\b(.*)$/i },
|
|
3781
|
+
{ ecosystem: "gem", re: /^gem\s+install\b(.*)$/i },
|
|
3782
|
+
{ ecosystem: "brew", re: /^brew\s+install\b(.*)$/i },
|
|
3783
|
+
{ ecosystem: "apt", re: /^(?:sudo\s+)?apt(?:-get)?\s+install\b(.*)$/i }
|
|
3784
|
+
];
|
|
3785
|
+
var PKG_NAME = /^(@[\w.-]+\/)?[a-z0-9][\w.\-/]*([@=<>~^]=?[\w.\-^~*]+)?$/i;
|
|
3786
|
+
function installsIn(text) {
|
|
3787
|
+
const out = [];
|
|
3788
|
+
for (const raw of text.split(/&&|\|\||[;\n|]/)) {
|
|
3789
|
+
const segment = raw.trim();
|
|
3790
|
+
if (!segment)
|
|
3791
|
+
continue;
|
|
3792
|
+
for (const { ecosystem, re } of INSTALLERS) {
|
|
3793
|
+
const m = re.exec(segment);
|
|
3794
|
+
if (!m)
|
|
3795
|
+
continue;
|
|
3796
|
+
const args = [];
|
|
3797
|
+
for (const tok of (m[1] ?? "").split(/\s+/)) {
|
|
3798
|
+
const a = tok.trim();
|
|
3799
|
+
if (!a)
|
|
3800
|
+
continue;
|
|
3801
|
+
if (/^\d*[<>&]/.test(a))
|
|
3802
|
+
break;
|
|
3803
|
+
if (a.startsWith("-"))
|
|
3804
|
+
continue;
|
|
3805
|
+
if (PKG_NAME.test(a))
|
|
3806
|
+
args.push(a);
|
|
3807
|
+
}
|
|
3808
|
+
out.push(...args.length ? args.map((pkg) => ({ ecosystem, pkg })) : [{ ecosystem, pkg: "(from manifest)" }]);
|
|
3809
|
+
}
|
|
3810
|
+
}
|
|
3811
|
+
return out;
|
|
3812
|
+
}
|
|
3813
|
+
var SECRETS = [
|
|
3814
|
+
{ what: ".env file", re: /(^|[/\s"'`])\.env(\.[\w-]+)?\b/i },
|
|
3815
|
+
{ what: "SSH private key", re: /(^|[/\s"'`])(id_rsa|id_ed25519|id_ecdsa|id_dsa)\b/i },
|
|
3816
|
+
{ what: "AWS credentials", re: /\.aws\/(credentials|config)\b/i },
|
|
3817
|
+
{ what: "npm token", re: /(^|[/\s"'`])\.npmrc\b/i },
|
|
3818
|
+
{ what: "kubeconfig", re: /\.kube\/config\b/i },
|
|
3819
|
+
{ what: "Google cloud credentials", re: /gcloud\/[\w-]*credential/i },
|
|
3820
|
+
{ what: "PEM or key file", re: /\.(pem|p12|pfx|key)\b/i },
|
|
3821
|
+
{ what: "macOS keychain", re: /\bsecurity\s+find-(generic|internet)-password\b/i },
|
|
3822
|
+
{ what: "netrc", re: /(^|[/\s"'`])\.netrc\b/i }
|
|
3823
|
+
];
|
|
3824
|
+
function secretsIn(text) {
|
|
3825
|
+
return SECRETS.filter((s) => s.re.test(text)).map((s) => s.what);
|
|
3826
|
+
}
|
|
3827
|
+
function securityScan(rows) {
|
|
3828
|
+
const egress = new Map;
|
|
3829
|
+
const installs = new Map;
|
|
3830
|
+
const secrets = new Map;
|
|
3831
|
+
let scanned = 0;
|
|
3832
|
+
for (const r of rows) {
|
|
3833
|
+
if (!r.sessionId)
|
|
3834
|
+
continue;
|
|
3835
|
+
const text = `${r.command ?? ""} ${r.path ?? ""}`.trim();
|
|
3836
|
+
if (!text)
|
|
3837
|
+
continue;
|
|
3838
|
+
scanned++;
|
|
3839
|
+
for (const host of hostsIn(text)) {
|
|
3840
|
+
const e = egress.get(host) ?? { hits: 0, sessions: new Set };
|
|
3841
|
+
e.hits++;
|
|
3842
|
+
e.sessions.add(r.sessionId);
|
|
3843
|
+
egress.set(host, e);
|
|
3844
|
+
}
|
|
3845
|
+
for (const i of installsIn(r.command ?? "")) {
|
|
3846
|
+
const key = `${i.ecosystem} ${i.pkg}`;
|
|
3847
|
+
const cur = installs.get(key) ?? { ...i, hits: 0, sessions: new Set };
|
|
3848
|
+
cur.hits++;
|
|
3849
|
+
cur.sessions.add(r.sessionId);
|
|
3850
|
+
installs.set(key, cur);
|
|
3851
|
+
}
|
|
3852
|
+
for (const what of secretsIn(text)) {
|
|
3853
|
+
const cur = secrets.get(what) ?? { hits: 0, sessions: new Set };
|
|
3854
|
+
cur.hits++;
|
|
3855
|
+
cur.sessions.add(r.sessionId);
|
|
3856
|
+
secrets.set(what, cur);
|
|
3857
|
+
}
|
|
3858
|
+
}
|
|
3859
|
+
const egressList = [...egress.entries()].map(([host, e]) => ({
|
|
3860
|
+
host,
|
|
3861
|
+
hits: e.hits,
|
|
3862
|
+
sessions: e.sessions.size,
|
|
3863
|
+
local: LOCAL.test(host)
|
|
3864
|
+
})).sort((a, b) => Number(a.local) - Number(b.local) || b.hits - a.hits || a.host.localeCompare(b.host));
|
|
3865
|
+
return {
|
|
3866
|
+
egress: egressList,
|
|
3867
|
+
installs: [...installs.values()].map((i) => ({ ecosystem: i.ecosystem, pkg: i.pkg, hits: i.hits, sessions: i.sessions.size })).sort((a, b) => b.hits - a.hits || a.ecosystem.localeCompare(b.ecosystem) || a.pkg.localeCompare(b.pkg)),
|
|
3868
|
+
secrets: [...secrets.entries()].map(([what, s]) => ({ what, hits: s.hits, sessions: s.sessions.size })).sort((a, b) => b.hits - a.hits || a.what.localeCompare(b.what)),
|
|
3869
|
+
totals: {
|
|
3870
|
+
scanned,
|
|
3871
|
+
remoteHosts: egressList.filter((h) => !h.local).length,
|
|
3872
|
+
installs: [...installs.values()].reduce((n, i) => n + i.hits, 0),
|
|
3873
|
+
secrets: [...secrets.values()].reduce((n, s) => n + s.hits, 0)
|
|
3874
|
+
}
|
|
3875
|
+
};
|
|
3876
|
+
}
|
|
3502
3877
|
// packages/core/src/stall.ts
|
|
3503
3878
|
var STALL_DEFAULTS = { window: 12, repeat: 3, repeatErrors: 2, errors: 4 };
|
|
3504
3879
|
function toolResponseErrored(resp) {
|
|
@@ -3730,6 +4105,70 @@ function clusterProjectKey(remoteUrl) {
|
|
|
3730
4105
|
return null;
|
|
3731
4106
|
return `${host}/${m[2]}`;
|
|
3732
4107
|
}
|
|
4108
|
+
// packages/core/src/transitions.ts
|
|
4109
|
+
function transitionGraph(steps, { minWeight = 1 } = {}) {
|
|
4110
|
+
const calls = new Map;
|
|
4111
|
+
const selfLoops = new Map;
|
|
4112
|
+
const edges = new Map;
|
|
4113
|
+
const bySession = new Map;
|
|
4114
|
+
let transitions = 0;
|
|
4115
|
+
for (const s of steps) {
|
|
4116
|
+
if (!s.sessionId || !s.tool)
|
|
4117
|
+
continue;
|
|
4118
|
+
calls.set(s.tool, (calls.get(s.tool) ?? 0) + 1);
|
|
4119
|
+
const prev = bySession.get(s.sessionId);
|
|
4120
|
+
if (prev !== undefined) {
|
|
4121
|
+
const key = `${prev}\x00${s.tool}`;
|
|
4122
|
+
const e = edges.get(key) ?? {
|
|
4123
|
+
from: prev,
|
|
4124
|
+
to: s.tool,
|
|
4125
|
+
weight: 0,
|
|
4126
|
+
sessions: new Set
|
|
4127
|
+
};
|
|
4128
|
+
e.weight++;
|
|
4129
|
+
e.sessions.add(s.sessionId);
|
|
4130
|
+
edges.set(key, e);
|
|
4131
|
+
if (prev === s.tool)
|
|
4132
|
+
selfLoops.set(s.tool, (selfLoops.get(s.tool) ?? 0) + 1);
|
|
4133
|
+
transitions++;
|
|
4134
|
+
}
|
|
4135
|
+
bySession.set(s.sessionId, s.tool);
|
|
4136
|
+
}
|
|
4137
|
+
const edgeList = [...edges.values()].filter((e) => e.weight >= minWeight).map((e) => ({ from: e.from, to: e.to, weight: e.weight, sessions: e.sessions.size })).sort((a, b) => b.weight - a.weight || a.from.localeCompare(b.from) || a.to.localeCompare(b.to));
|
|
4138
|
+
const nodeList = [...calls.entries()].map(([tool, n]) => ({ tool, calls: n, selfLoops: selfLoops.get(tool) ?? 0 })).sort((a, b) => b.calls - a.calls || a.tool.localeCompare(b.tool));
|
|
4139
|
+
return {
|
|
4140
|
+
nodes: nodeList,
|
|
4141
|
+
edges: edgeList,
|
|
4142
|
+
loops: findLoops(edgeList),
|
|
4143
|
+
sessions: bySession.size,
|
|
4144
|
+
steps: steps.filter((s) => s.sessionId && s.tool).length,
|
|
4145
|
+
transitions
|
|
4146
|
+
};
|
|
4147
|
+
}
|
|
4148
|
+
function findLoops(edges) {
|
|
4149
|
+
const at = new Map(edges.map((e) => [`${e.from}\x00${e.to}`, e]));
|
|
4150
|
+
const out = [];
|
|
4151
|
+
const seen = new Set;
|
|
4152
|
+
for (const e of edges) {
|
|
4153
|
+
if (e.from === e.to) {
|
|
4154
|
+
out.push({ tools: [e.from], weight: e.weight, sessions: e.sessions });
|
|
4155
|
+
continue;
|
|
4156
|
+
}
|
|
4157
|
+
const back = at.get(`${e.to}\x00${e.from}`);
|
|
4158
|
+
if (!back)
|
|
4159
|
+
continue;
|
|
4160
|
+
const pair = [e.from, e.to].sort().join("\x00");
|
|
4161
|
+
if (seen.has(pair))
|
|
4162
|
+
continue;
|
|
4163
|
+
seen.add(pair);
|
|
4164
|
+
out.push({
|
|
4165
|
+
tools: [e.from, e.to],
|
|
4166
|
+
weight: Math.min(e.weight, back.weight),
|
|
4167
|
+
sessions: Math.max(e.sessions, back.sessions)
|
|
4168
|
+
});
|
|
4169
|
+
}
|
|
4170
|
+
return out.sort((a, b) => b.weight - a.weight || a.tools.join().localeCompare(b.tools.join()));
|
|
4171
|
+
}
|
|
3733
4172
|
// packages/core/src/waiting.ts
|
|
3734
4173
|
var emptyByKind = () => ({
|
|
3735
4174
|
permission: { episodes: 0, blockedMs: 0 },
|
|
@@ -7891,8 +8330,46 @@ ${err}
|
|
|
7891
8330
|
const loaded = loadConfigDetailed({ repoRoot, home: this.home });
|
|
7892
8331
|
this.policyCache.set(key, { at: Date.now(), loaded });
|
|
7893
8332
|
this.writePolicyCache(loaded);
|
|
8333
|
+
this.noteRuleChange(key, loaded.config.rules);
|
|
7894
8334
|
return loaded;
|
|
7895
8335
|
}
|
|
8336
|
+
projectIdForRoot(root) {
|
|
8337
|
+
if (!root)
|
|
8338
|
+
return null;
|
|
8339
|
+
const row = this.db.query("SELECT id FROM projects WHERE root = ?").get(root);
|
|
8340
|
+
return row?.id ?? null;
|
|
8341
|
+
}
|
|
8342
|
+
noteRuleChange(key, rules2) {
|
|
8343
|
+
const sig = JSON.stringify(Object.entries(rules2).sort());
|
|
8344
|
+
const metaKey = `rules.sig:${key}`;
|
|
8345
|
+
const prev = this.db.query("SELECT value FROM meta WHERE key = ?").get(metaKey)?.value;
|
|
8346
|
+
if (prev === sig)
|
|
8347
|
+
return;
|
|
8348
|
+
this.db.query("INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)").run(metaKey, sig);
|
|
8349
|
+
if (prev === undefined)
|
|
8350
|
+
return;
|
|
8351
|
+
const before = new Map(JSON.parse(prev));
|
|
8352
|
+
const after = new Map(Object.entries(rules2));
|
|
8353
|
+
const added = [...after.keys()].filter((r) => !before.has(r)).sort();
|
|
8354
|
+
const removed = [...before.keys()].filter((r) => !after.has(r)).sort();
|
|
8355
|
+
const retuned = [...after.entries()].filter(([r, mode]) => before.has(r) && before.get(r) !== mode).map(([r, mode]) => `${r}=${mode}`).sort();
|
|
8356
|
+
if (!added.length && !removed.length && !retuned.length)
|
|
8357
|
+
return;
|
|
8358
|
+
this.append({
|
|
8359
|
+
ts: new Date().toISOString(),
|
|
8360
|
+
type: "rules.changed",
|
|
8361
|
+
projectId: this.projectIdForRoot(key) ?? "",
|
|
8362
|
+
sessionId: null,
|
|
8363
|
+
payload: {
|
|
8364
|
+
repo: key || null,
|
|
8365
|
+
added,
|
|
8366
|
+
removed,
|
|
8367
|
+
retuned,
|
|
8368
|
+
rules: [...after.keys()].sort(),
|
|
8369
|
+
summary: `rules changed${added.length ? ` +${added.join(",")}` : ""}${removed.length ? ` -${removed.join(",")}` : ""}${retuned.length ? ` ~${retuned.join(",")}` : ""}`
|
|
8370
|
+
}
|
|
8371
|
+
});
|
|
8372
|
+
}
|
|
7896
8373
|
writePolicyCache(loaded) {
|
|
7897
8374
|
const file = join8(this.home, POLICY_CACHE_FILE);
|
|
7898
8375
|
try {
|
|
@@ -8928,8 +9405,24 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
8928
9405
|
return { ok: false, error: "unknown project" };
|
|
8929
9406
|
const now = Date.now();
|
|
8930
9407
|
const decision = canClaim(this.claimRows(projectId), task, owner, now);
|
|
8931
|
-
if (!decision.ok)
|
|
9408
|
+
if (!decision.ok) {
|
|
9409
|
+
if (decision.heldBy !== owner)
|
|
9410
|
+
this.append({
|
|
9411
|
+
ts: new Date(now).toISOString(),
|
|
9412
|
+
type: "claim.denied",
|
|
9413
|
+
projectId,
|
|
9414
|
+
sessionId,
|
|
9415
|
+
actor: this.actorFor(owner, sessionId),
|
|
9416
|
+
payload: {
|
|
9417
|
+
task,
|
|
9418
|
+
owner,
|
|
9419
|
+
heldBy: decision.heldBy,
|
|
9420
|
+
until: decision.until,
|
|
9421
|
+
summary: `${owner} was refused ${task} \u2014 held by ${decision.heldBy}`
|
|
9422
|
+
}
|
|
9423
|
+
});
|
|
8932
9424
|
return { ok: false, error: claimRefusalMessage(decision, task) };
|
|
9425
|
+
}
|
|
8933
9426
|
const branch = `task/${task}`;
|
|
8934
9427
|
const worktree2 = this.worktreePath(projectId, task);
|
|
8935
9428
|
if (existsSync6(worktree2))
|
|
@@ -9491,6 +9984,110 @@ ${p2.reason ?? ""}`.trim(),
|
|
|
9491
9984
|
})
|
|
9492
9985
|
};
|
|
9493
9986
|
}
|
|
9987
|
+
transitions(projectId, days3 = 7, minWeight = 1) {
|
|
9988
|
+
const since = new Date(Date.now() - days3 * 86400000).toISOString();
|
|
9989
|
+
const rows = this.db.query(`SELECT session_id, json_extract(payload,'$.tool') AS tool
|
|
9990
|
+
FROM events
|
|
9991
|
+
WHERE type = 'tool.requested' AND ts >= ?
|
|
9992
|
+
AND json_extract(payload,'$.tool') IS NOT NULL${projectId ? " AND project_id = ?" : ""}
|
|
9993
|
+
ORDER BY session_id, seq`).all(...projectId ? [since, projectId] : [since]);
|
|
9994
|
+
return transitionGraph(rows.map((r) => ({ sessionId: r.session_id ?? "", tool: r.tool ?? "" })), { minWeight });
|
|
9995
|
+
}
|
|
9996
|
+
resourceHolding(projectId, days3 = 3) {
|
|
9997
|
+
const since = new Date(Date.now() - days3 * 86400000).toISOString();
|
|
9998
|
+
const p = projectId ? " AND c.project_id = ?" : "";
|
|
9999
|
+
const args = projectId ? [projectId] : [];
|
|
10000
|
+
const ended = new Map(this.db.query("SELECT id, ended_at FROM sessions WHERE ended_at IS NOT NULL").all().map((r) => [r.id, r.ended_at]));
|
|
10001
|
+
const claims = this.db.query(`SELECT c.task AS name, c.owner, c.project_id, c.expires_at, c.actor_id AS session_id
|
|
10002
|
+
FROM claims c WHERE c.state = 'held' AND c.released_at IS NULL${p}`).all(...args);
|
|
10003
|
+
const resources2 = this.db.query(`SELECT c.name, c.owner, c.project_id, c.expires_at, c.session_id, c.port
|
|
10004
|
+
FROM resources c WHERE c.released = 0${p}`).all(...args);
|
|
10005
|
+
const procs = this.db.query(`SELECT c.name, c.owner, c.project_id, c.session_id, c.port
|
|
10006
|
+
FROM processes c WHERE c.ended_at IS NULL${p}`).all(...args);
|
|
10007
|
+
const denials = this.db.query(`SELECT json_extract(payload,'$.task') AS name, json_extract(payload,'$.owner') AS owner,
|
|
10008
|
+
json_extract(payload,'$.heldBy') AS held_by, ts, project_id
|
|
10009
|
+
FROM events WHERE type = 'claim.denied' AND ts >= ?${projectId ? " AND project_id = ?" : ""}`).all(since, ...args);
|
|
10010
|
+
const held = [
|
|
10011
|
+
...claims.map((r) => ({
|
|
10012
|
+
kind: "claim",
|
|
10013
|
+
name: r.name,
|
|
10014
|
+
owner: r.owner,
|
|
10015
|
+
sessionId: r.session_id,
|
|
10016
|
+
sessionEndedAt: r.session_id ? ended.get(r.session_id) ?? null : null,
|
|
10017
|
+
expiresAt: r.expires_at,
|
|
10018
|
+
projectId: r.project_id
|
|
10019
|
+
})),
|
|
10020
|
+
...resources2.map((r) => ({
|
|
10021
|
+
kind: r.port ? "port" : "lease",
|
|
10022
|
+
name: r.port ? String(r.port) : r.name,
|
|
10023
|
+
owner: r.owner ?? "unknown",
|
|
10024
|
+
sessionId: r.session_id,
|
|
10025
|
+
sessionEndedAt: r.session_id ? ended.get(r.session_id) ?? null : null,
|
|
10026
|
+
expiresAt: r.expires_at,
|
|
10027
|
+
projectId: r.project_id
|
|
10028
|
+
})),
|
|
10029
|
+
...procs.map((r) => ({
|
|
10030
|
+
kind: "process",
|
|
10031
|
+
name: r.name ?? (r.port ? `:${r.port}` : "process"),
|
|
10032
|
+
owner: r.owner ?? "unknown",
|
|
10033
|
+
sessionId: r.session_id,
|
|
10034
|
+
sessionEndedAt: r.session_id ? ended.get(r.session_id) ?? null : null,
|
|
10035
|
+
expiresAt: null,
|
|
10036
|
+
projectId: r.project_id
|
|
10037
|
+
}))
|
|
10038
|
+
];
|
|
10039
|
+
const wanted = denials.filter((d) => d.name && d.owner && d.held_by).map((d) => ({
|
|
10040
|
+
kind: "claim",
|
|
10041
|
+
name: d.name,
|
|
10042
|
+
owner: d.owner,
|
|
10043
|
+
heldBy: d.held_by,
|
|
10044
|
+
at: d.ts,
|
|
10045
|
+
projectId: d.project_id
|
|
10046
|
+
}));
|
|
10047
|
+
return resourceGraph(held, wanted);
|
|
10048
|
+
}
|
|
10049
|
+
fileHeat(projectId, days3 = 14) {
|
|
10050
|
+
const since = new Date(Date.now() - days3 * 86400000).toISOString();
|
|
10051
|
+
const rows = this.db.query(`SELECT session_id, json_extract(payload,'$.tool') AS tool,
|
|
10052
|
+
json_extract(payload,'$.toolInput.file_path') AS path
|
|
10053
|
+
FROM events
|
|
10054
|
+
WHERE type = 'tool.requested' AND ts >= ?
|
|
10055
|
+
AND json_extract(payload,'$.toolInput.file_path') IS NOT NULL${projectId ? " AND project_id = ?" : ""}`).all(...projectId ? [since, projectId] : [since]);
|
|
10056
|
+
return fileHeat(rows.map((r) => ({ sessionId: r.session_id ?? "", tool: r.tool ?? "", path: r.path ?? "" })));
|
|
10057
|
+
}
|
|
10058
|
+
security(projectId, days3 = 14) {
|
|
10059
|
+
const since = new Date(Date.now() - days3 * 86400000).toISOString();
|
|
10060
|
+
const rows = this.db.query(`SELECT session_id, ts, json_extract(payload,'$.tool') AS tool,
|
|
10061
|
+
COALESCE(json_extract(payload,'$.toolInput.command'),
|
|
10062
|
+
json_extract(payload,'$.toolInput.url'), '') AS command,
|
|
10063
|
+
json_extract(payload,'$.toolInput.file_path') AS path
|
|
10064
|
+
FROM events
|
|
10065
|
+
WHERE type = 'tool.requested' AND ts >= ?${projectId ? " AND project_id = ?" : ""}`).all(...projectId ? [since, projectId] : [since]);
|
|
10066
|
+
return securityScan(rows.map((r) => ({
|
|
10067
|
+
sessionId: r.session_id ?? "",
|
|
10068
|
+
tool: r.tool ?? "",
|
|
10069
|
+
command: r.command ?? "",
|
|
10070
|
+
path: r.path,
|
|
10071
|
+
at: r.ts
|
|
10072
|
+
})));
|
|
10073
|
+
}
|
|
10074
|
+
ruleEffect(projectId, days3 = 30) {
|
|
10075
|
+
const since = new Date(Date.now() - days3 * 86400000).toISOString();
|
|
10076
|
+
const rows = this.db.query(`SELECT e.seq, e.ts,
|
|
10077
|
+
json_extract(e.payload,'$.rule') AS rule,
|
|
10078
|
+
COALESCE(json_extract(e.payload,'$.command'), '') AS command,
|
|
10079
|
+
(a.seq IS NOT NULL) AS acked
|
|
10080
|
+
FROM events e LEFT JOIN incident_acks a ON a.seq = e.seq
|
|
10081
|
+
WHERE e.type = 'incident.opened' AND e.ts >= ?${projectId ? " AND e.project_id = ?" : ""}`).all(...projectId ? [since, projectId] : [since]);
|
|
10082
|
+
const changes = this.db.query(`SELECT ts, COALESCE(json_extract(payload,'$.added'), '[]') AS added
|
|
10083
|
+
FROM events WHERE type = 'rules.changed' AND ts >= ?`).all(since);
|
|
10084
|
+
return ruleEffect(rows.map((r) => ({
|
|
10085
|
+
rule: r.rule ?? "",
|
|
10086
|
+
command: r.command ?? "",
|
|
10087
|
+
at: r.ts,
|
|
10088
|
+
acked: Boolean(r.acked)
|
|
10089
|
+
})), changes.map((c) => ({ at: c.ts, added: JSON.parse(c.added) })), Date.now(), days3);
|
|
10090
|
+
}
|
|
9494
10091
|
stalls = new Map;
|
|
9495
10092
|
checkStalls() {
|
|
9496
10093
|
const live = this.db.query("SELECT id, project_id FROM sessions WHERE state IN ('active','waiting') AND ended_at IS NULL AND last_seen_at >= ?").all(new Date(Date.now() - IDLE_MS).toISOString());
|
|
@@ -11120,7 +11717,7 @@ class WorkflowEngine {
|
|
|
11120
11717
|
}
|
|
11121
11718
|
|
|
11122
11719
|
// packages/daemon/src/app.ts
|
|
11123
|
-
var VERSION = "0.
|
|
11720
|
+
var VERSION = "0.12.0";
|
|
11124
11721
|
var WEB_DIR = (() => {
|
|
11125
11722
|
if (process.env.SWARM_WEB_DIR)
|
|
11126
11723
|
return process.env.SWARM_WEB_DIR;
|
|
@@ -11247,6 +11844,11 @@ function createApp(store = new Store, hooks2 = {}) {
|
|
|
11247
11844
|
app.get("/v1/stats", (c) => c.json(store.stats(c.req.query("project") || undefined)));
|
|
11248
11845
|
app.get("/v1/graphs/collisions", (c) => c.json(store.collisions(c.req.query("project") || undefined)));
|
|
11249
11846
|
app.get("/v1/graphs/lineage", (c) => c.json(store.lineage(c.req.query("project") || undefined, Math.max(1, Math.min(90, Number(c.req.query("days") ?? 14) || 14)), c.req.queries("expand") ?? [])));
|
|
11847
|
+
app.get("/v1/rules/effect", (c) => c.json(store.ruleEffect(c.req.query("project") || undefined, Math.max(1, Math.min(365, Number(c.req.query("days") ?? 30) || 30)))));
|
|
11848
|
+
app.get("/v1/security", (c) => c.json(store.security(c.req.query("project") || undefined, Math.max(1, Math.min(90, Number(c.req.query("days") ?? 14) || 14)))));
|
|
11849
|
+
app.get("/v1/heat", (c) => c.json(store.fileHeat(c.req.query("project") || undefined, Math.max(1, Math.min(90, Number(c.req.query("days") ?? 14) || 14)))));
|
|
11850
|
+
app.get("/v1/graphs/resources", (c) => c.json(store.resourceHolding(c.req.query("project") || undefined, Math.max(1, Math.min(30, Number(c.req.query("days") ?? 3) || 3)))));
|
|
11851
|
+
app.get("/v1/graphs/transitions", (c) => c.json(store.transitions(c.req.query("project") || undefined, Math.max(1, Math.min(90, Number(c.req.query("days") ?? 7) || 7)), Math.max(1, Math.min(50, Number(c.req.query("min") ?? 2) || 2)))));
|
|
11250
11852
|
app.get("/v1/context", (c) => c.json(store.context(c.req.query("project") || undefined, Math.max(1, Math.min(90, Number(c.req.query("days") ?? 7) || 7)))));
|
|
11251
11853
|
app.get("/v1/mcp/health", (c) => c.json(store.mcpHealth(c.req.query("project") || undefined, Math.max(1, Math.min(90, Number(c.req.query("days") ?? 7) || 7)))));
|
|
11252
11854
|
app.get("/v1/hygiene", (c) => c.json(store.hygiene(c.req.query("project") || undefined)));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ra3orblade/swarm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Local-first control plane for AI-agent development: watch every Claude Code / Codex / Grok session on your machine, ledger tasks and worktrees, enforce rules as hook denials.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|