@secondlayer/sentinel 0.6.0 → 0.7.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/cli.js +24 -0
- package/dist/index.js +5 -0
- package/dist/mcp.js +27 -1
- package/dist/sentinel-client.d.ts +12 -4
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -92,6 +92,7 @@ function runPhase(run) {
|
|
|
92
92
|
const last = run.events.at(-1);
|
|
93
93
|
return last ? [last.name ?? last.kind, last.state].filter(Boolean).join(" ") : "starting";
|
|
94
94
|
}
|
|
95
|
+
var WATCH_NOTE_MAX = 500;
|
|
95
96
|
var rowsOf = (intents) => intents.flatMap((i) => i.rows);
|
|
96
97
|
var subjectKey = (s) => s.kind === "fn" ? `fn:${s.fn}` : s.kind === "group" ? `group:${s.group}` : `asset:${s.asset}`;
|
|
97
98
|
var CONTROL_NAME = "Control changes";
|
|
@@ -209,6 +210,8 @@ function changeText(c) {
|
|
|
209
210
|
case "muted":
|
|
210
211
|
case "cooldown":
|
|
211
212
|
return `${c.watch.title}: ${c.action === "muted" ? "mute" : "cooldown"} ${c.note ?? ""}`.trim();
|
|
213
|
+
case "note":
|
|
214
|
+
return `${c.watch.title}: note ${c.note ?? "set"}`;
|
|
212
215
|
case "applied":
|
|
213
216
|
if (c.prevRule && c.rule)
|
|
214
217
|
return `${name} rule changed: ${rule(c.prevRule)} → ${rule(c.rule)}`;
|
|
@@ -265,6 +268,7 @@ function createClient(opts) {
|
|
|
265
268
|
on
|
|
266
269
|
}),
|
|
267
270
|
mute: (planId, watchId, mutedUntil) => call("POST", `${appPlan(planId)}/watches/${encodeURIComponent(watchId)}/delivery`, { mutedUntil }),
|
|
271
|
+
setWatchNote: (planId, watchId, note) => call("POST", `${appPlan(planId)}/watches/${encodeURIComponent(watchId)}/delivery`, { note }),
|
|
268
272
|
muteAlert: (alertId, hours) => call("POST", `/alerts/${encodeURIComponent(alertId)}/mute`, { hours }),
|
|
269
273
|
parseRule: (planId, subject, text) => call("POST", `${appPlan(planId)}/rules/parse`, { subject, text }),
|
|
270
274
|
monitorSettings: (planId, settings) => call("POST", `${appPlan(planId)}/monitor`, settings),
|
|
@@ -906,6 +910,7 @@ monitoring (a write only happens with --yes; without it you see what would chang
|
|
|
906
910
|
watches <plan> [--json] subjects, rules, status, mutes
|
|
907
911
|
rule <plan> <subject> "<sentence>" [--yes] read the sentence; --yes adds or edits the rules it found
|
|
908
912
|
watch on|off <plan> <subject> [--yes] on and off apply now
|
|
913
|
+
watch-note <plan> <subject> "<text>"|--clear [--yes] the note sent with this watch's alerts (500 characters)
|
|
909
914
|
mute <plan> <subject> [1h|24h|7d] [--yes] default 24h
|
|
910
915
|
unmute <plan> <subject> [--yes]
|
|
911
916
|
mute-alert <alert> [1h|24h|7d] [--yes] mute the watch behind an alert
|
|
@@ -1241,6 +1246,7 @@ No run command: no PoC is stored for this finding.`);
|
|
|
1241
1246
|
return await monitoringMore(cmd, args.slice(1), flags, values, io, usage);
|
|
1242
1247
|
case "watches":
|
|
1243
1248
|
case "rule":
|
|
1249
|
+
case "watch-note":
|
|
1244
1250
|
case "mute":
|
|
1245
1251
|
case "unmute":
|
|
1246
1252
|
case "mute-alert":
|
|
@@ -1434,6 +1440,9 @@ async function monitoring(cmd, pos, on, flags, values, io, usage) {
|
|
|
1434
1440
|
io.out([i.title, i.tag, i.on ? "on" : "off", intentLine(i), muted ? `muted until ${muted}` : ""].filter(Boolean).join(" "));
|
|
1435
1441
|
for (const w of i.rows)
|
|
1436
1442
|
io.out(` ${w.status.padEnd(14)}${ruleSummary(w.rule, w.units)}`);
|
|
1443
|
+
const note = i.rows.map((w) => w.delivery.note).find(Boolean);
|
|
1444
|
+
if (note)
|
|
1445
|
+
io.out(` note: ${note}`);
|
|
1437
1446
|
}
|
|
1438
1447
|
return 0;
|
|
1439
1448
|
}
|
|
@@ -1507,6 +1516,21 @@ async function monitoring(cmd, pos, on, flags, values, io, usage) {
|
|
|
1507
1516
|
io.out(`${willDo} ${effect}`);
|
|
1508
1517
|
return 0;
|
|
1509
1518
|
}
|
|
1519
|
+
if (cmd === "watch-note") {
|
|
1520
|
+
const clear = flags.includes("--clear");
|
|
1521
|
+
if (clear === (third !== undefined))
|
|
1522
|
+
return usage("watch-note takes a note in quotes, or --clear");
|
|
1523
|
+
const note = clear ? null : (third ?? "").trim();
|
|
1524
|
+
if (note !== null && note.length > WATCH_NOTE_MAX)
|
|
1525
|
+
return usage(`Keep the note under ${WATCH_NOTE_MAX} characters`);
|
|
1526
|
+
const willDo = note ? `Send this note with ${g.name}'s alerts: ${note}` : `Clear the note on ${g.name}.`;
|
|
1527
|
+
if (previewOnly(io, flags, willDo, "Takes effect now."))
|
|
1528
|
+
return 0;
|
|
1529
|
+
for (const w of g.watches)
|
|
1530
|
+
await io.client.setWatchNote(planId, w.id, note);
|
|
1531
|
+
io.out(note ? `Note set on ${g.name}.` : `Note cleared on ${g.name}.`);
|
|
1532
|
+
return 0;
|
|
1533
|
+
}
|
|
1510
1534
|
const mute = cmd === "mute";
|
|
1511
1535
|
const dur = third ?? "24h";
|
|
1512
1536
|
if (mute && !(dur in DURATIONS))
|
package/dist/index.js
CHANGED
|
@@ -86,6 +86,7 @@ function runPhase(run) {
|
|
|
86
86
|
const last = run.events.at(-1);
|
|
87
87
|
return last ? [last.name ?? last.kind, last.state].filter(Boolean).join(" ") : "starting";
|
|
88
88
|
}
|
|
89
|
+
var WATCH_NOTE_MAX = 500;
|
|
89
90
|
var rowsOf = (intents) => intents.flatMap((i) => i.rows);
|
|
90
91
|
var subjectKey = (s) => s.kind === "fn" ? `fn:${s.fn}` : s.kind === "group" ? `group:${s.group}` : `asset:${s.asset}`;
|
|
91
92
|
var CONTROL_NAME = "Control changes";
|
|
@@ -224,6 +225,8 @@ function changeText(c) {
|
|
|
224
225
|
case "muted":
|
|
225
226
|
case "cooldown":
|
|
226
227
|
return `${c.watch.title}: ${c.action === "muted" ? "mute" : "cooldown"} ${c.note ?? ""}`.trim();
|
|
228
|
+
case "note":
|
|
229
|
+
return `${c.watch.title}: note ${c.note ?? "set"}`;
|
|
227
230
|
case "applied":
|
|
228
231
|
if (c.prevRule && c.rule)
|
|
229
232
|
return `${name} rule changed: ${rule(c.prevRule)} → ${rule(c.rule)}`;
|
|
@@ -280,6 +283,7 @@ function createClient(opts) {
|
|
|
280
283
|
on
|
|
281
284
|
}),
|
|
282
285
|
mute: (planId, watchId, mutedUntil) => call("POST", `${appPlan(planId)}/watches/${encodeURIComponent(watchId)}/delivery`, { mutedUntil }),
|
|
286
|
+
setWatchNote: (planId, watchId, note) => call("POST", `${appPlan(planId)}/watches/${encodeURIComponent(watchId)}/delivery`, { note }),
|
|
283
287
|
muteAlert: (alertId, hours) => call("POST", `/alerts/${encodeURIComponent(alertId)}/mute`, { hours }),
|
|
284
288
|
parseRule: (planId, subject, text) => call("POST", `${appPlan(planId)}/rules/parse`, { subject, text }),
|
|
285
289
|
monitorSettings: (planId, settings) => call("POST", `${appPlan(planId)}/monitor`, settings),
|
|
@@ -385,6 +389,7 @@ export {
|
|
|
385
389
|
NOT_LOGGED_IN,
|
|
386
390
|
SentinelError,
|
|
387
391
|
SubjectError,
|
|
392
|
+
WATCH_NOTE_MAX,
|
|
388
393
|
assetClause,
|
|
389
394
|
changeText,
|
|
390
395
|
clearSession,
|
package/dist/mcp.js
CHANGED
|
@@ -12666,6 +12666,7 @@ function runPhase(run) {
|
|
|
12666
12666
|
const last = run.events.at(-1);
|
|
12667
12667
|
return last ? [last.name ?? last.kind, last.state].filter(Boolean).join(" ") : "starting";
|
|
12668
12668
|
}
|
|
12669
|
+
var WATCH_NOTE_MAX = 500;
|
|
12669
12670
|
var rowsOf = (intents) => intents.flatMap((i) => i.rows);
|
|
12670
12671
|
var subjectKey = (s) => s.kind === "fn" ? `fn:${s.fn}` : s.kind === "group" ? `group:${s.group}` : `asset:${s.asset}`;
|
|
12671
12672
|
var CONTROL_NAME = "Control changes";
|
|
@@ -12828,6 +12829,7 @@ function createClient(opts) {
|
|
|
12828
12829
|
on
|
|
12829
12830
|
}),
|
|
12830
12831
|
mute: (planId, watchId, mutedUntil) => call("POST", `${appPlan(planId)}/watches/${encodeURIComponent(watchId)}/delivery`, { mutedUntil }),
|
|
12832
|
+
setWatchNote: (planId, watchId, note) => call("POST", `${appPlan(planId)}/watches/${encodeURIComponent(watchId)}/delivery`, { note }),
|
|
12831
12833
|
muteAlert: (alertId, hours) => call("POST", `/alerts/${encodeURIComponent(alertId)}/mute`, { hours }),
|
|
12832
12834
|
parseRule: (planId, subject, text) => call("POST", `${appPlan(planId)}/rules/parse`, { subject, text }),
|
|
12833
12835
|
monitorSettings: (planId, settings) => call("POST", `${appPlan(planId)}/monitor`, settings),
|
|
@@ -20729,6 +20731,7 @@ function intentView(i) {
|
|
|
20729
20731
|
...i.alsoChecks ? { alsoChecks: i.alsoChecks } : {},
|
|
20730
20732
|
status: subjectStatus(i.rows),
|
|
20731
20733
|
mutedUntil: i.rows.map((w) => mutedUntilOf(w)).find(Boolean) ?? null,
|
|
20734
|
+
note: i.rows.map((w) => w.delivery.note).find(Boolean) ?? null,
|
|
20732
20735
|
rules: i.rows.map((w) => ({
|
|
20733
20736
|
watchId: w.id,
|
|
20734
20737
|
kind: w.rule.kind,
|
|
@@ -20752,6 +20755,19 @@ async function muteSubject(client, a, hours) {
|
|
|
20752
20755
|
await client.mute(ctx.planId, w.id, mutedUntil);
|
|
20753
20756
|
return done(willDo, "Takes effect now.", { mutedUntil });
|
|
20754
20757
|
}
|
|
20758
|
+
async function noteSubject(client, a) {
|
|
20759
|
+
const text = typeof a.note === "string" ? a.note.trim() : "";
|
|
20760
|
+
if (text.length > WATCH_NOTE_MAX)
|
|
20761
|
+
throw new SentinelError(400, `Keep the note under ${WATCH_NOTE_MAX} characters`);
|
|
20762
|
+
const ctx = await watchContext(client, a.planId);
|
|
20763
|
+
const g = resolveSubject(ctx.watches, a.subject);
|
|
20764
|
+
const willDo = text ? `Send this note with ${g.name}'s alerts: ${text}` : `Clear the note on ${g.name}.`;
|
|
20765
|
+
if (a.confirm !== true)
|
|
20766
|
+
return preview(willDo, "Takes effect now.");
|
|
20767
|
+
for (const w of g.watches)
|
|
20768
|
+
await client.setWatchNote(ctx.planId, w.id, text || null);
|
|
20769
|
+
return done(willDo, "Takes effect now.");
|
|
20770
|
+
}
|
|
20755
20771
|
function buildTools(client) {
|
|
20756
20772
|
const planId = string2().describe("Plan id, from list_contracts");
|
|
20757
20773
|
const subject = string2().describe("A watched subject: a function name or a token symbol, from list_watches");
|
|
@@ -21084,6 +21100,16 @@ function buildTools(client) {
|
|
|
21084
21100
|
input: { planId, subject, hours: MUTE_HOURS, confirm },
|
|
21085
21101
|
run: async (a) => muteSubject(client, a, a.hours)
|
|
21086
21102
|
},
|
|
21103
|
+
set_watch_note: {
|
|
21104
|
+
description: "Set the note sent with a subject's alerts: your team's own words (who holds the key, where to post first), plain text, 500 characters at most. An empty note clears it. Sentinel adds nothing to it. Takes effect at once. Without confirm it only describes.",
|
|
21105
|
+
input: {
|
|
21106
|
+
planId,
|
|
21107
|
+
subject,
|
|
21108
|
+
note: string2().max(WATCH_NOTE_MAX).describe("The note; empty clears it"),
|
|
21109
|
+
confirm
|
|
21110
|
+
},
|
|
21111
|
+
run: async (a) => noteSubject(client, a)
|
|
21112
|
+
},
|
|
21087
21113
|
unmute_watch: {
|
|
21088
21114
|
description: "Clear a subject's mute. Takes effect at once. Without confirm it only describes.",
|
|
21089
21115
|
input: { planId, subject, confirm },
|
|
@@ -21642,7 +21668,7 @@ async function runTool(tool, args) {
|
|
|
21642
21668
|
}
|
|
21643
21669
|
}
|
|
21644
21670
|
function buildServer(client) {
|
|
21645
|
-
const server = new McpServer({ name: "sentinel", version: "0.
|
|
21671
|
+
const server = new McpServer({ name: "sentinel", version: "0.7.0" });
|
|
21646
21672
|
for (const [name, tool] of Object.entries(buildTools(client))) {
|
|
21647
21673
|
server.registerTool(name, { description: tool.description, inputSchema: tool.input }, (args) => runTool(tool, args));
|
|
21648
21674
|
}
|
|
@@ -157,7 +157,11 @@ export type WatchDelivery = {
|
|
|
157
157
|
routes: string[];
|
|
158
158
|
cooldownBlocks: number;
|
|
159
159
|
mutedUntil: string | null;
|
|
160
|
+
/** The team's own words, sent with every alert this watch fires. Absent from a server before notes. */
|
|
161
|
+
note?: string | null;
|
|
160
162
|
};
|
|
163
|
+
/** The longest watch note the server takes (monitoring/watch.ts `WATCH_NOTE_MAX`). */
|
|
164
|
+
export declare const WATCH_NOTE_MAX = 500;
|
|
161
165
|
/** One of the caller's own watches on a plan's contract, as `GET /plan/:id/watches` returns it. */
|
|
162
166
|
export type StoredWatch = {
|
|
163
167
|
id: string;
|
|
@@ -251,7 +255,7 @@ export type WatchesView = {
|
|
|
251
255
|
events: {
|
|
252
256
|
id: string;
|
|
253
257
|
watchId: string;
|
|
254
|
-
action: "proposed" | "applied" | "rejected" | "muted" | "cooldown";
|
|
258
|
+
action: "proposed" | "applied" | "rejected" | "muted" | "cooldown" | "note";
|
|
255
259
|
rule: WatchRule | null;
|
|
256
260
|
note: string | null;
|
|
257
261
|
createdAt: string;
|
|
@@ -542,12 +546,12 @@ export type ProjectWatchesView = {
|
|
|
542
546
|
locked: number;
|
|
543
547
|
[field: string]: unknown;
|
|
544
548
|
};
|
|
545
|
-
/** One change to what Sentinel watches: turned on or off, a rule edit (`prevRule` is what it replaced), a mute
|
|
546
|
-
* a
|
|
549
|
+
/** One change to what Sentinel watches: turned on or off, a rule edit (`prevRule` is what it replaced), a mute, a
|
|
550
|
+
* cooldown or a note set or cleared (never its text). `source` is how it was made (`web`, `key "<name>"`, …), null when not known. */
|
|
547
551
|
export type WatchChange = {
|
|
548
552
|
id: string;
|
|
549
553
|
at: string;
|
|
550
|
-
action: "applied" | "on" | "off" | "muted" | "cooldown";
|
|
554
|
+
action: "applied" | "on" | "off" | "muted" | "cooldown" | "note";
|
|
551
555
|
contractId: string;
|
|
552
556
|
watch: {
|
|
553
557
|
id: string;
|
|
@@ -716,6 +720,10 @@ export declare function createClient(opts: ClientOptions): {
|
|
|
716
720
|
mute: (planId: string, watchId: string, mutedUntil: string | null) => Promise<{
|
|
717
721
|
watch: StoredWatch;
|
|
718
722
|
}>;
|
|
723
|
+
/** Set the team's note on a watch (shown with every alert it sends), or clear it with null. Plain text, 500 characters at most. */
|
|
724
|
+
setWatchNote: (planId: string, watchId: string, note: string | null) => Promise<{
|
|
725
|
+
watch: StoredWatch;
|
|
726
|
+
}>;
|
|
719
727
|
/** Mute the watch behind an alert for 1, 24 or 168 hours; 0 clears. */
|
|
720
728
|
muteAlert: (alertId: string, hours: 0 | 1 | 24 | 168) => Promise<{
|
|
721
729
|
mutedUntil: string | null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@secondlayer/sentinel",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Sentinel from a terminal or an agent: the CLI, an MCP server and a client for your Stacks contract monitoring.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": "https://runsentinel.app/docs",
|