@loupekit/mcp 0.10.26-next.73 → 0.10.27-next.74
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/index.js +348 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -528,6 +528,27 @@ function sessionSummaryText(s) {
|
|
|
528
528
|
return `${parts.join(" \xB7 ")}${top ? `
|
|
529
529
|
most used: ${top}` : ""}`;
|
|
530
530
|
}
|
|
531
|
+
function activitySummary(events2, opts = {}) {
|
|
532
|
+
const sessions = sessionsFromEvents(events2, opts.now);
|
|
533
|
+
const counts = /* @__PURE__ */ new Map();
|
|
534
|
+
const files = [];
|
|
535
|
+
let tools = 0;
|
|
536
|
+
let failures = 0;
|
|
537
|
+
for (const s of [...sessions].reverse()) {
|
|
538
|
+
for (const t of s.toolCounts) counts.set(t.tool, (counts.get(t.tool) ?? 0) + t.count);
|
|
539
|
+
tools += s.tools;
|
|
540
|
+
for (const f of s.files) if (!files.includes(f)) files.push(f);
|
|
541
|
+
}
|
|
542
|
+
for (const e of events2) {
|
|
543
|
+
if (e.type === "tool_result" && e.payload?.ok === false) failures++;
|
|
544
|
+
}
|
|
545
|
+
return {
|
|
546
|
+
totals: { events: events2.length, tools, files: files.length, failures, sessions: sessions.length },
|
|
547
|
+
sessions: opts.sessionLimit ? sessions.slice(0, opts.sessionLimit) : sessions,
|
|
548
|
+
toolCounts: [...counts.entries()].map(([tool, count]) => ({ tool, count })).sort((a, b) => b.count - a.count || a.tool.localeCompare(b.tool)),
|
|
549
|
+
files
|
|
550
|
+
};
|
|
551
|
+
}
|
|
531
552
|
|
|
532
553
|
// src/hooks/hook-installer.ts
|
|
533
554
|
import { copyFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
@@ -727,6 +748,258 @@ function normalizeHookEvent(hookName, payload, at) {
|
|
|
727
748
|
};
|
|
728
749
|
}
|
|
729
750
|
|
|
751
|
+
// src/bridge/dashboard.ts
|
|
752
|
+
var DASHBOARD_HTML = `<!doctype html>
|
|
753
|
+
<html lang="en">
|
|
754
|
+
<head>
|
|
755
|
+
<meta charset="utf-8">
|
|
756
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
757
|
+
<title>Loupe \u2014 agent activity</title>
|
|
758
|
+
<style>
|
|
759
|
+
:root {
|
|
760
|
+
--bg: #0c0d10; --panel: #14161b; --line: #23262e; --ink: #e8eaef; --muted: #8b91a0;
|
|
761
|
+
--accent: #7c6cf5; --ok: #3ecf8e; --warn: #e0a92c; --err: #f2555a;
|
|
762
|
+
--mono: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
763
|
+
}
|
|
764
|
+
@media (prefers-color-scheme: light) {
|
|
765
|
+
:root { --bg: #f7f8fa; --panel: #fff; --line: #e3e5ea; --ink: #14161b; --muted: #6b7280; }
|
|
766
|
+
}
|
|
767
|
+
* { box-sizing: border-box; }
|
|
768
|
+
body {
|
|
769
|
+
margin: 0; background: var(--bg); color: var(--ink);
|
|
770
|
+
font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
771
|
+
}
|
|
772
|
+
header {
|
|
773
|
+
position: sticky; top: 0; z-index: 2; display: flex; align-items: center; gap: 12px;
|
|
774
|
+
padding: 14px 20px; border-bottom: 1px solid var(--line); background: var(--bg);
|
|
775
|
+
}
|
|
776
|
+
h1 { font-size: 15px; margin: 0; font-weight: 650; letter-spacing: -0.01em; }
|
|
777
|
+
.live { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--muted); }
|
|
778
|
+
.dot { width: 7px; height: 7px; border-radius: 50%; background: var(--muted); }
|
|
779
|
+
.dot.on { background: var(--ok); box-shadow: 0 0 0 3px color-mix(in srgb, var(--ok) 22%, transparent); }
|
|
780
|
+
.spacer { flex: 1; }
|
|
781
|
+
.stat { text-align: right; }
|
|
782
|
+
.stat b { display: block; font-size: 18px; font-variant-numeric: tabular-nums; }
|
|
783
|
+
.stat span { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: .06em; }
|
|
784
|
+
main { display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); gap: 18px; padding: 18px 20px 40px; }
|
|
785
|
+
@media (max-width: 860px) { main { grid-template-columns: 1fr; } }
|
|
786
|
+
.card { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 14px 16px; }
|
|
787
|
+
.card h2 { font-size: 12px; text-transform: uppercase; letter-spacing: .07em; color: var(--muted); margin: 0 0 12px; font-weight: 600; }
|
|
788
|
+
.empty { color: var(--muted); font-size: 13px; padding: 8px 0; }
|
|
789
|
+
.row { display: flex; align-items: baseline; gap: 10px; padding: 5px 0; border-bottom: 1px solid var(--line); }
|
|
790
|
+
.row:last-child { border-bottom: 0; }
|
|
791
|
+
.when { font-family: var(--mono); font-size: 11px; color: var(--muted); flex: none; width: 62px; }
|
|
792
|
+
.type { font-size: 11px; padding: 1px 7px; border-radius: 999px; border: 1px solid var(--line); flex: none; }
|
|
793
|
+
.type.tool_use { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 45%, transparent); }
|
|
794
|
+
.type.tool_result { color: var(--ok); border-color: color-mix(in srgb, var(--ok) 40%, transparent); }
|
|
795
|
+
.type.notification { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 45%, transparent); }
|
|
796
|
+
.type.fail { color: var(--err); border-color: color-mix(in srgb, var(--err) 45%, transparent); }
|
|
797
|
+
.what { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
798
|
+
.bar { display: flex; align-items: center; gap: 8px; padding: 3px 0; }
|
|
799
|
+
.bar b { font-family: var(--mono); font-size: 12px; font-weight: 500; width: 120px; flex: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
800
|
+
.bar .track { flex: 1; height: 7px; border-radius: 4px; background: var(--line); overflow: hidden; }
|
|
801
|
+
.bar .fill { height: 100%; background: var(--accent); border-radius: 4px; }
|
|
802
|
+
.bar .n { font-family: var(--mono); font-size: 11px; color: var(--muted); width: 26px; text-align: right; flex: none; }
|
|
803
|
+
.file { font-family: var(--mono); font-size: 12px; padding: 3px 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
804
|
+
.sess { padding: 9px 0; border-bottom: 1px solid var(--line); }
|
|
805
|
+
.sess:last-child { border-bottom: 0; }
|
|
806
|
+
.sess .head { display: flex; align-items: center; gap: 8px; }
|
|
807
|
+
.sess .id { font-family: var(--mono); font-size: 12px; }
|
|
808
|
+
.sess .meta { font-size: 12px; color: var(--muted); margin-top: 3px; }
|
|
809
|
+
.pill { font-size: 11px; padding: 1px 7px; border-radius: 999px; border: 1px solid var(--line); }
|
|
810
|
+
.pill.live { color: var(--ok); border-color: color-mix(in srgb, var(--ok) 45%, transparent); }
|
|
811
|
+
footer { padding: 0 20px 24px; color: var(--muted); font-size: 12px; }
|
|
812
|
+
code { font-family: var(--mono); }
|
|
813
|
+
</style>
|
|
814
|
+
</head>
|
|
815
|
+
<body>
|
|
816
|
+
<header>
|
|
817
|
+
<h1>Loupe \xB7 agent activity</h1>
|
|
818
|
+
<span class="live"><span class="dot" id="dot"></span><span id="link">connecting\u2026</span></span>
|
|
819
|
+
<span class="spacer"></span>
|
|
820
|
+
<span class="stat"><b id="s-tools">0</b><span>tool calls</span></span>
|
|
821
|
+
<span class="stat"><b id="s-files">0</b><span>files</span></span>
|
|
822
|
+
<span class="stat"><b id="s-err" style="color:var(--err)">0</b><span>failures</span></span>
|
|
823
|
+
</header>
|
|
824
|
+
<main>
|
|
825
|
+
<section class="card">
|
|
826
|
+
<h2>Live timeline</h2>
|
|
827
|
+
<div id="timeline"><div class="empty">Nothing yet. Events appear here as the agent works.</div></div>
|
|
828
|
+
</section>
|
|
829
|
+
<div style="display:grid;gap:18px;align-content:start">
|
|
830
|
+
<section class="card"><h2>Sessions</h2><div id="sessions"><div class="empty">No sessions yet.</div></div></section>
|
|
831
|
+
<section class="card"><h2>Tool usage</h2><div id="tools"><div class="empty">No tool calls yet.</div></div></section>
|
|
832
|
+
<section class="card"><h2>Files touched</h2><div id="files"><div class="empty">No files yet.</div></div></section>
|
|
833
|
+
</div>
|
|
834
|
+
</main>
|
|
835
|
+
<footer>
|
|
836
|
+
Read-only. Events come from the Claude Code hooks; nothing here changes agent state.
|
|
837
|
+
</footer>
|
|
838
|
+
<script>
|
|
839
|
+
(function () {
|
|
840
|
+
var timeline = document.getElementById("timeline");
|
|
841
|
+
var events = [];
|
|
842
|
+
|
|
843
|
+
function el(tag, cls, text) {
|
|
844
|
+
var n = document.createElement(tag);
|
|
845
|
+
if (cls) n.className = cls;
|
|
846
|
+
if (text !== undefined) n.textContent = text;
|
|
847
|
+
return n;
|
|
848
|
+
}
|
|
849
|
+
function clock(iso) {
|
|
850
|
+
var d = new Date(iso);
|
|
851
|
+
return isNaN(d) ? "--:--:--" : d.toTimeString().slice(0, 8);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function renderTimeline() {
|
|
855
|
+
timeline.textContent = "";
|
|
856
|
+
if (!events.length) {
|
|
857
|
+
timeline.appendChild(el("div", "empty", "Nothing yet. Events appear here as the agent works."));
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
events.slice(0, 120).forEach(function (e) {
|
|
861
|
+
var row = el("div", "row");
|
|
862
|
+
row.appendChild(el("span", "when", clock(e.at)));
|
|
863
|
+
var failed = e.payload && e.payload.ok === false;
|
|
864
|
+
row.appendChild(el("span", "type " + (failed ? "fail" : e.type), failed ? "failed" : e.type.replace(/_/g, " ")));
|
|
865
|
+
var what = e.summary || e.tool || "";
|
|
866
|
+
if (e.files && e.files.length) what += (what ? " \xB7 " : "") + e.files.join(", ");
|
|
867
|
+
row.appendChild(el("span", "what", what));
|
|
868
|
+
row.title = what;
|
|
869
|
+
timeline.appendChild(row);
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function renderSummary(data) {
|
|
874
|
+
document.getElementById("s-tools").textContent = data.totals.tools;
|
|
875
|
+
document.getElementById("s-files").textContent = data.totals.files;
|
|
876
|
+
document.getElementById("s-err").textContent = data.totals.failures;
|
|
877
|
+
|
|
878
|
+
var sessions = document.getElementById("sessions");
|
|
879
|
+
sessions.textContent = "";
|
|
880
|
+
if (!data.sessions.length) sessions.appendChild(el("div", "empty", "No sessions yet."));
|
|
881
|
+
data.sessions.slice(0, 5).forEach(function (s) {
|
|
882
|
+
var box = el("div", "sess");
|
|
883
|
+
var head = el("div", "head");
|
|
884
|
+
head.appendChild(el("span", "id", s.id));
|
|
885
|
+
head.appendChild(el("span", "pill" + (s.active ? " live" : ""), s.active ? "live" : "ended"));
|
|
886
|
+
box.appendChild(head);
|
|
887
|
+
box.appendChild(el("div", "meta", s.prompts + " prompts \xB7 " + s.tools + " tool calls \xB7 " + s.files.length + " files"));
|
|
888
|
+
sessions.appendChild(box);
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
var tools = document.getElementById("tools");
|
|
892
|
+
tools.textContent = "";
|
|
893
|
+
if (!data.toolCounts.length) tools.appendChild(el("div", "empty", "No tool calls yet."));
|
|
894
|
+
var max = data.toolCounts.length ? data.toolCounts[0].count : 1;
|
|
895
|
+
data.toolCounts.slice(0, 10).forEach(function (t) {
|
|
896
|
+
var bar = el("div", "bar");
|
|
897
|
+
bar.appendChild(el("b", null, t.tool));
|
|
898
|
+
var track = el("div", "track");
|
|
899
|
+
var fill = el("div", "fill");
|
|
900
|
+
fill.style.width = Math.max(3, Math.round((t.count / max) * 100)) + "%";
|
|
901
|
+
track.appendChild(fill);
|
|
902
|
+
bar.appendChild(track);
|
|
903
|
+
bar.appendChild(el("span", "n", String(t.count)));
|
|
904
|
+
tools.appendChild(bar);
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
var files = document.getElementById("files");
|
|
908
|
+
files.textContent = "";
|
|
909
|
+
if (!data.files.length) files.appendChild(el("div", "empty", "No files yet."));
|
|
910
|
+
data.files.slice(0, 40).forEach(function (f) {
|
|
911
|
+
files.appendChild(el("div", "file", f));
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
function connected(on) {
|
|
916
|
+
document.getElementById("dot").className = "dot" + (on ? " on" : "");
|
|
917
|
+
document.getElementById("link").textContent = on ? "live" : "reconnecting\u2026";
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
function poll() {
|
|
921
|
+
fetch("/monitor/api/events?limit=200").then(function (r) { return r.json(); }).then(function (d) {
|
|
922
|
+
events = d.events || [];
|
|
923
|
+
renderTimeline();
|
|
924
|
+
connected(true);
|
|
925
|
+
}).catch(function () { connected(false); });
|
|
926
|
+
fetch("/monitor/api/summary").then(function (r) { return r.json(); }).then(renderSummary).catch(function () {});
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
poll();
|
|
930
|
+
|
|
931
|
+
// The stream is the point \u2014 the poll is only there so a dropped connection still
|
|
932
|
+
// shows something rather than an empty screen.
|
|
933
|
+
try {
|
|
934
|
+
var es = new EventSource("/events");
|
|
935
|
+
es.onopen = function () { connected(true); };
|
|
936
|
+
es.onerror = function () { connected(false); };
|
|
937
|
+
// The bridge names its events ("event: activity"), so they arrive as a named event
|
|
938
|
+
// and onmessage \u2014 which is only for unnamed ones \u2014 never fires.
|
|
939
|
+
es.addEventListener("activity", function (msg) {
|
|
940
|
+
var ev;
|
|
941
|
+
try { ev = JSON.parse(msg.data); } catch (e) { return; }
|
|
942
|
+
if (ev.data) { events.unshift(ev.data); renderTimeline(); }
|
|
943
|
+
// The headline numbers come from the same aggregation the tools use, so the
|
|
944
|
+
// page cannot disagree with what an agent would be told.
|
|
945
|
+
fetch("/monitor/api/summary").then(function (r) { return r.json(); }).then(renderSummary).catch(function () {});
|
|
946
|
+
});
|
|
947
|
+
setInterval(function () { if (!es || es.readyState !== 1) poll(); }, 5000);
|
|
948
|
+
} catch (e) {
|
|
949
|
+
setInterval(poll, 3000);
|
|
950
|
+
}
|
|
951
|
+
})();
|
|
952
|
+
</script>
|
|
953
|
+
</body>
|
|
954
|
+
</html>
|
|
955
|
+
`;
|
|
956
|
+
|
|
957
|
+
// src/bridge/notify.ts
|
|
958
|
+
import { spawn } from "child_process";
|
|
959
|
+
function escapeAppleScript(text) {
|
|
960
|
+
return text.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/[\r\n]+/g, " ");
|
|
961
|
+
}
|
|
962
|
+
function planNotification(title, body, platform = process.platform) {
|
|
963
|
+
if (platform === "linux") {
|
|
964
|
+
return { platform: "linux", command: "notify-send", args: ["--app-name=Loupe", title, body], usable: true };
|
|
965
|
+
}
|
|
966
|
+
if (platform === "darwin") {
|
|
967
|
+
const script = `display notification "${escapeAppleScript(body)}" with title "${escapeAppleScript(title)}"`;
|
|
968
|
+
return { platform: "darwin", command: "osascript", args: ["-e", script], usable: true };
|
|
969
|
+
}
|
|
970
|
+
if (platform === "win32") {
|
|
971
|
+
return { platform: "win32", command: "", args: [], usable: false, reason: "no portable notification tool on Windows" };
|
|
972
|
+
}
|
|
973
|
+
return { platform: "unsupported", command: "", args: [], usable: false, reason: `unknown platform ${platform}` };
|
|
974
|
+
}
|
|
975
|
+
function clip(text, cap = 200) {
|
|
976
|
+
const oneLine = text.replace(/\s+/g, " ").trim();
|
|
977
|
+
return oneLine.length <= cap ? oneLine : `${oneLine.slice(0, cap - 1)}\u2026`;
|
|
978
|
+
}
|
|
979
|
+
function notify(title, body, opts = {}) {
|
|
980
|
+
if (opts.enabled === false) return { sent: false, reason: "notifications are off" };
|
|
981
|
+
const plan = planNotification(clip(title, 80), clip(body), opts.platform);
|
|
982
|
+
if (!plan.usable) return { sent: false, reason: plan.reason };
|
|
983
|
+
const run = opts.run ?? defaultRun;
|
|
984
|
+
try {
|
|
985
|
+
run(plan.command, plan.args);
|
|
986
|
+
return { sent: true };
|
|
987
|
+
} catch (e) {
|
|
988
|
+
return { sent: false, reason: e.message };
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
function defaultRun(command, args) {
|
|
992
|
+
const child = spawn(command, args, {
|
|
993
|
+
// No shell: the body is arbitrary text from a person.
|
|
994
|
+
shell: false,
|
|
995
|
+
stdio: "ignore",
|
|
996
|
+
detached: true
|
|
997
|
+
});
|
|
998
|
+
child.on("error", () => {
|
|
999
|
+
});
|
|
1000
|
+
child.unref();
|
|
1001
|
+
}
|
|
1002
|
+
|
|
730
1003
|
// src/bridge/state-file.ts
|
|
731
1004
|
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, rmSync, writeFileSync as writeFileSync3 } from "fs";
|
|
732
1005
|
import { homedir } from "os";
|
|
@@ -1013,6 +1286,7 @@ async function handle(req, res, deps) {
|
|
|
1013
1286
|
if (!deps.events) return send(res, 503, { error: "event store is not enabled" }, origin);
|
|
1014
1287
|
const payload = parsed.payload && typeof parsed.payload === "object" ? parsed.payload : {};
|
|
1015
1288
|
const event = deps.events.add(normalizeHookEvent(parsed.event, payload, parsed.at));
|
|
1289
|
+
deps.bus.publish({ type: "activity", data: event, at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1016
1290
|
return send(res, 202, { ok: true, event }, origin);
|
|
1017
1291
|
}
|
|
1018
1292
|
if (method === "GET" && path === "/events/recent") {
|
|
@@ -1026,6 +1300,24 @@ async function handle(req, res, deps) {
|
|
|
1026
1300
|
if (!deps.events) return send(res, 503, { error: "event store is not enabled" }, origin);
|
|
1027
1301
|
return send(res, 200, { sessions: sessionsFromEvents(deps.events.recent(deps.events.size())) }, origin);
|
|
1028
1302
|
}
|
|
1303
|
+
if (method === "GET" && (path === "/monitor" || path === "/monitor/" || path === "/dashboard")) {
|
|
1304
|
+
cors(res, origin);
|
|
1305
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
1306
|
+
res.end(DASHBOARD_HTML);
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
if (method === "GET" && path === "/monitor/api/events") {
|
|
1310
|
+
if (!deps.events) return send(res, 503, { error: "event store is not enabled" }, origin);
|
|
1311
|
+
const limit = Number(url.searchParams.get("limit") ?? 200);
|
|
1312
|
+
return send(res, 200, {
|
|
1313
|
+
events: deps.events.latest(Number.isFinite(limit) ? limit : 200),
|
|
1314
|
+
total: deps.events.size()
|
|
1315
|
+
}, origin);
|
|
1316
|
+
}
|
|
1317
|
+
if (method === "GET" && path === "/monitor/api/summary") {
|
|
1318
|
+
if (!deps.events) return send(res, 503, { error: "event store is not enabled" }, origin);
|
|
1319
|
+
return send(res, 200, activitySummary(deps.events.recent(deps.events.size())), origin);
|
|
1320
|
+
}
|
|
1029
1321
|
if (method === "POST" && path === "/companion") {
|
|
1030
1322
|
const body = await readBody(req);
|
|
1031
1323
|
if (!body.ok) return send(res, 400, { error: body.error }, origin);
|
|
@@ -1047,6 +1339,7 @@ async function handle(req, res, deps) {
|
|
|
1047
1339
|
voice: parsed.voice === true
|
|
1048
1340
|
});
|
|
1049
1341
|
deps.bus.publishCompanion("message", message);
|
|
1342
|
+
if (process.env.LOUPE_NOTIFY !== "0") notify("Loupe", `${message.author?.name ?? "Someone"}: ${message.body}`);
|
|
1050
1343
|
return send(res, 201, { ok: true, message, queued: deps.companion.size() }, origin);
|
|
1051
1344
|
}
|
|
1052
1345
|
if (method === "GET" && path === "/companion") {
|
|
@@ -2167,6 +2460,7 @@ var registry = new AgentRegistry();
|
|
|
2167
2460
|
var presence = new PresenceRegistry();
|
|
2168
2461
|
var events = new EventStore({ file: process.env.LOUPE_EVENT_FILE || void 0 });
|
|
2169
2462
|
var companion = new CompanionQueue();
|
|
2463
|
+
var bridgeUrl = null;
|
|
2170
2464
|
var bus = new EventBus();
|
|
2171
2465
|
var ADMIN = process.env.LOUPE_ADMIN_KEY || "";
|
|
2172
2466
|
async function api(path, init) {
|
|
@@ -2323,7 +2617,7 @@ function hookScriptPath() {
|
|
|
2323
2617
|
];
|
|
2324
2618
|
return candidates.find((c) => existsSync4(c)) ?? candidates[0];
|
|
2325
2619
|
}
|
|
2326
|
-
var server = new McpServer({ name: "loupe", version: "0.10.
|
|
2620
|
+
var server = new McpServer({ name: "loupe", version: "0.10.27" });
|
|
2327
2621
|
function withCompanion(handler) {
|
|
2328
2622
|
return (async (...args) => {
|
|
2329
2623
|
const result = await handler(...args);
|
|
@@ -2468,6 +2762,58 @@ registerTool(
|
|
|
2468
2762
|
return wrap(picked.map(sessionSummaryText).join("\n\n"));
|
|
2469
2763
|
}
|
|
2470
2764
|
);
|
|
2765
|
+
registerTool(
|
|
2766
|
+
"get_recent_events",
|
|
2767
|
+
"The raw agent event stream \u2014 tool calls, prompts, session starts and stops \u2014 newest first. Use it when you need to know exactly what has run, rather than a summary.",
|
|
2768
|
+
{
|
|
2769
|
+
limit: z.number().int().positive().max(200).optional().describe("How many events. Default 30."),
|
|
2770
|
+
type: z.string().optional().describe('Filter to one type, e.g. "tool_use" or "prompt_submit".')
|
|
2771
|
+
},
|
|
2772
|
+
async ({ limit, type }) => {
|
|
2773
|
+
const events2 = events2.latest(limit ?? 30).filter((e) => !type || e.type === type);
|
|
2774
|
+
if (!events2.length) {
|
|
2775
|
+
return wrap(
|
|
2776
|
+
events2.size === 0 && !type ? "No agent events recorded yet. The hooks may not be installed \u2014 install_agent_hooks adds them." : `No events${type ? ` of type ${type}` : ""} recorded.`
|
|
2777
|
+
);
|
|
2778
|
+
}
|
|
2779
|
+
return wrap(
|
|
2780
|
+
events2.map((e) => {
|
|
2781
|
+
const ok = e.payload?.ok === false ? " [FAILED]" : "";
|
|
2782
|
+
const files = e.files?.length ? ` \u2014 ${e.files.join(", ")}` : "";
|
|
2783
|
+
return `${e.at} ${e.type}${e.tool ? ` (${e.tool})` : ""}${ok}: ${e.summary ?? ""}${files}`.trimEnd();
|
|
2784
|
+
}).join("\n")
|
|
2785
|
+
);
|
|
2786
|
+
}
|
|
2787
|
+
);
|
|
2788
|
+
registerTool(
|
|
2789
|
+
"get_files_touched",
|
|
2790
|
+
"Every file the agent has touched in the recorded sessions, in first-seen order. Use it to see the blast radius of the work so far before adding to it.",
|
|
2791
|
+
{},
|
|
2792
|
+
async () => {
|
|
2793
|
+
const summary = activitySummary(events.recent(events.size()));
|
|
2794
|
+
if (!summary.files.length) {
|
|
2795
|
+
return wrap("No files recorded yet. The hooks may not be installed \u2014 install_agent_hooks adds them.");
|
|
2796
|
+
}
|
|
2797
|
+
const bySession = summary.sessions.filter((s) => s.files.length).map((s) => `${s.active ? "\u25CF" : "\u25CB"} ${s.id}:
|
|
2798
|
+
${s.files.join("\n ")}`).join("\n\n");
|
|
2799
|
+
return wrap(`${summary.files.length} file${summary.files.length === 1 ? "" : "s"} touched:
|
|
2800
|
+
|
|
2801
|
+
${bySession}`);
|
|
2802
|
+
}
|
|
2803
|
+
);
|
|
2804
|
+
registerTool(
|
|
2805
|
+
"get_dashboard_url",
|
|
2806
|
+
"The local URL of the live activity dashboard \u2014 a page a person can open to watch what the agent is doing right now. Give them this when they ask to see progress.",
|
|
2807
|
+
{},
|
|
2808
|
+
async () => {
|
|
2809
|
+
if (!bridgeUrl) {
|
|
2810
|
+
return wrap("The activity dashboard is not running \u2014 the bridge did not start. Agents still work; there is just nothing to watch.");
|
|
2811
|
+
}
|
|
2812
|
+
return wrap(`Open ${bridgeUrl}/monitor to watch agent activity live.
|
|
2813
|
+
|
|
2814
|
+
It is served by the local bridge and is read-only.`);
|
|
2815
|
+
}
|
|
2816
|
+
);
|
|
2471
2817
|
registerTool(
|
|
2472
2818
|
"install_agent_hooks",
|
|
2473
2819
|
"Install the Claude Code hooks that report tool use, prompts and sessions to Loupe. Idempotent (running it twice changes nothing), backed up before writing, and it never touches another tool's hook entries. Opt-in: nothing installs these on its own.",
|
|
@@ -2604,6 +2950,7 @@ function isEntrypoint() {
|
|
|
2604
2950
|
}
|
|
2605
2951
|
if (isEntrypoint()) {
|
|
2606
2952
|
const bridge = BRIDGE_PORT ? await startHttpBridge(BRIDGE_PORT, { store, registry, presence, events, companion, bus }) : null;
|
|
2953
|
+
bridgeUrl = bridge?.url ?? null;
|
|
2607
2954
|
const self = registry.register({
|
|
2608
2955
|
name: process.env.LOUPE_AGENT_NAME || "loupe-mcp",
|
|
2609
2956
|
type: process.env.LOUPE_AGENT_TYPE || "claude-code",
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"email": "m.ashraf.saed@gmail.com",
|
|
6
6
|
"url": "https://www.linkedin.com/in/mohamedashrafelsaed/"
|
|
7
7
|
},
|
|
8
|
-
"version": "0.10.
|
|
8
|
+
"version": "0.10.27-next.74",
|
|
9
9
|
"description": "MCP server that exposes Loupe comments to Claude Code as an actionable, fully-contextualized backlog.",
|
|
10
10
|
"keywords": [
|
|
11
11
|
"loupe",
|
|
@@ -47,6 +47,6 @@
|
|
|
47
47
|
"zod": "^3.24.1"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@loupekit/shared": "0.10.
|
|
50
|
+
"@loupekit/shared": "0.10.27-next.74"
|
|
51
51
|
}
|
|
52
52
|
}
|