@secondlayer/sentinel 0.5.0 → 0.6.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 CHANGED
@@ -198,10 +198,30 @@ function mutedUntilOf(w, now = Date.now()) {
198
198
  const at = w.delivery.mutedUntil;
199
199
  return at && Date.parse(at) > now ? at : null;
200
200
  }
201
+ function changeText(c) {
202
+ const name = c.subject.kind === "asset" ? subjectName(c.subject, c.units) : c.watch.title;
203
+ const rule = (r) => ruleSummary(r, c.units);
204
+ switch (c.action) {
205
+ case "on":
206
+ return `${c.watch.title} turned on`;
207
+ case "off":
208
+ return `${c.watch.title} turned off`;
209
+ case "muted":
210
+ case "cooldown":
211
+ return `${c.watch.title}: ${c.action === "muted" ? "mute" : "cooldown"} ${c.note ?? ""}`.trim();
212
+ case "applied":
213
+ if (c.prevRule && c.rule)
214
+ return `${name} rule changed: ${rule(c.prevRule)} → ${rule(c.rule)}`;
215
+ return c.rule ? `${name} rule set: ${rule(c.rule)}` : `${name} rule set`;
216
+ }
217
+ }
201
218
  function createClient(opts) {
202
219
  const baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
203
220
  const doFetch = opts.fetch ?? globalThis.fetch;
204
- const authHeader = () => ({ authorization: `Bearer ${opts.token}` });
221
+ const authHeader = () => ({
222
+ authorization: `Bearer ${opts.token}`,
223
+ ...opts.client ? { "x-sentinel-client": opts.client } : {}
224
+ });
205
225
  async function call(method, path, body) {
206
226
  const res = await doFetch(`${baseUrl}${path}`, {
207
227
  method,
@@ -304,7 +324,8 @@ function createClient(opts) {
304
324
  setProjectDestinations: (projectId, dest) => call("POST", `${appProject(projectId)}/destinations`, dest),
305
325
  setProjectMonitoring: (projectId, on) => call("POST", `${appProject(projectId)}/monitoring`, { on }),
306
326
  projectWatches: (projectId) => call("GET", `${appProject(projectId)}/watches`),
307
- projectActivity: (projectId, intentId, opts = {}) => call("GET", `${appProject(projectId)}/watches/${encodeURIComponent(intentId)}/activity${query(opts)}`)
327
+ projectActivity: (projectId, intentId, opts = {}) => call("GET", `${appProject(projectId)}/watches/${encodeURIComponent(intentId)}/activity${query(opts)}`),
328
+ projectChanges: (projectId, opts = {}) => call("GET", `${appProject(projectId)}/changes${query(opts)}`)
308
329
  };
309
330
  }
310
331
  function query(params) {
@@ -873,6 +894,7 @@ projects (the contracts of one protocol; a write only happens with --yes):
873
894
  projects remove <project> --confirm "<name>" [--yes] every contract you added in it, and the project
874
895
  projects watches <project> [--json] the project's watches as one list
875
896
  projects activity <project> <watch> [--contract <id>] [--window 7|90] [--json]
897
+ projects changes <project> [--watch <watch>] [--mutes] [--limit N] [--cursor C] [--json] who turned watches on/off or changed rules
876
898
  projects destinations <project> [--email on|off] [--webhook URL] [--slack URL] [--yes] a URL left out is cleared
877
899
  projects monitoring <project> on|off [--yes]
878
900
 
@@ -917,7 +939,10 @@ var VALUE_FLAGS = new Set([
917
939
  "--commit",
918
940
  "--into",
919
941
  "--window",
920
- "--example"
942
+ "--example",
943
+ "--watch",
944
+ "--limit",
945
+ "--cursor"
921
946
  ]);
922
947
  var cell2 = (v, empty = "—") => v === null || v === undefined || v === "" ? empty : String(v);
923
948
  var table = (rows) => {
@@ -1844,6 +1869,7 @@ async function projects(pos, flags, values, io, usage) {
1844
1869
  "remove",
1845
1870
  "watches",
1846
1871
  "activity",
1872
+ "changes",
1847
1873
  "destinations",
1848
1874
  "monitoring"
1849
1875
  ];
@@ -1998,6 +2024,30 @@ async function projects(pos, flags, values, io, usage) {
1998
2024
  printActivity(io, intent.title, a);
1999
2025
  return 0;
2000
2026
  }
2027
+ case "changes": {
2028
+ const limit = values["--limit"] === undefined ? undefined : Number(values["--limit"]);
2029
+ if (limit !== undefined && !(Number.isInteger(limit) && limit > 0))
2030
+ return usage("--limit takes a whole number above 0");
2031
+ const watchName = values["--watch"];
2032
+ const watch = watchName ? pickIntent((await io.client.projectWatches(id)).intents, watchName).id : undefined;
2033
+ const r = await io.client.projectChanges(id, {
2034
+ limit,
2035
+ cursor: values["--cursor"],
2036
+ watch,
2037
+ ...flags.includes("--mutes") ? { filter: "mutes" } : {}
2038
+ });
2039
+ if (json) {
2040
+ io.out(JSON.stringify(r, null, 2));
2041
+ return 0;
2042
+ }
2043
+ if (!r.events.length)
2044
+ io.out("No changes yet.");
2045
+ for (const c of r.events)
2046
+ io.out([c.at, changeText(c), c.source ?? ""].join(" ").trimEnd());
2047
+ if (r.next)
2048
+ io.out(`More: --cursor ${r.next}`);
2049
+ return 0;
2050
+ }
2001
2051
  case "destinations": {
2002
2052
  const email = values["--email"];
2003
2053
  if (email !== undefined && onOff(email) === null)
@@ -2075,7 +2125,7 @@ async function main() {
2075
2125
  return 2;
2076
2126
  }
2077
2127
  const baseUrl = process.env.SENTINEL_API_URL;
2078
- const clientFor = (token) => createClient({ token, baseUrl });
2128
+ const clientFor = (token) => createClient({ token, baseUrl, client: "cli" });
2079
2129
  return run(argv, {
2080
2130
  client: clientFor(credential?.token ?? ""),
2081
2131
  out: console.log,
package/dist/index.js CHANGED
@@ -213,10 +213,30 @@ function mutedUntilOf(w, now = Date.now()) {
213
213
  const at = w.delivery.mutedUntil;
214
214
  return at && Date.parse(at) > now ? at : null;
215
215
  }
216
+ function changeText(c) {
217
+ const name = c.subject.kind === "asset" ? subjectName(c.subject, c.units) : c.watch.title;
218
+ const rule = (r) => ruleSummary(r, c.units);
219
+ switch (c.action) {
220
+ case "on":
221
+ return `${c.watch.title} turned on`;
222
+ case "off":
223
+ return `${c.watch.title} turned off`;
224
+ case "muted":
225
+ case "cooldown":
226
+ return `${c.watch.title}: ${c.action === "muted" ? "mute" : "cooldown"} ${c.note ?? ""}`.trim();
227
+ case "applied":
228
+ if (c.prevRule && c.rule)
229
+ return `${name} rule changed: ${rule(c.prevRule)} → ${rule(c.rule)}`;
230
+ return c.rule ? `${name} rule set: ${rule(c.rule)}` : `${name} rule set`;
231
+ }
232
+ }
216
233
  function createClient(opts) {
217
234
  const baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
218
235
  const doFetch = opts.fetch ?? globalThis.fetch;
219
- const authHeader = () => ({ authorization: `Bearer ${opts.token}` });
236
+ const authHeader = () => ({
237
+ authorization: `Bearer ${opts.token}`,
238
+ ...opts.client ? { "x-sentinel-client": opts.client } : {}
239
+ });
220
240
  async function call(method, path, body) {
221
241
  const res = await doFetch(`${baseUrl}${path}`, {
222
242
  method,
@@ -319,7 +339,8 @@ function createClient(opts) {
319
339
  setProjectDestinations: (projectId, dest) => call("POST", `${appProject(projectId)}/destinations`, dest),
320
340
  setProjectMonitoring: (projectId, on) => call("POST", `${appProject(projectId)}/monitoring`, { on }),
321
341
  projectWatches: (projectId) => call("GET", `${appProject(projectId)}/watches`),
322
- projectActivity: (projectId, intentId, opts = {}) => call("GET", `${appProject(projectId)}/watches/${encodeURIComponent(intentId)}/activity${query(opts)}`)
342
+ projectActivity: (projectId, intentId, opts = {}) => call("GET", `${appProject(projectId)}/watches/${encodeURIComponent(intentId)}/activity${query(opts)}`),
343
+ projectChanges: (projectId, opts = {}) => call("GET", `${appProject(projectId)}/changes${query(opts)}`)
323
344
  };
324
345
  }
325
346
  function query(params) {
@@ -365,6 +386,7 @@ export {
365
386
  SentinelError,
366
387
  SubjectError,
367
388
  assetClause,
389
+ changeText,
368
390
  clearSession,
369
391
  clearTokenFile,
370
392
  createClient,
package/dist/mcp.js CHANGED
@@ -12781,7 +12781,10 @@ function mutedUntilOf(w, now = Date.now()) {
12781
12781
  function createClient(opts) {
12782
12782
  const baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
12783
12783
  const doFetch = opts.fetch ?? globalThis.fetch;
12784
- const authHeader = () => ({ authorization: `Bearer ${opts.token}` });
12784
+ const authHeader = () => ({
12785
+ authorization: `Bearer ${opts.token}`,
12786
+ ...opts.client ? { "x-sentinel-client": opts.client } : {}
12787
+ });
12785
12788
  async function call(method, path, body) {
12786
12789
  const res = await doFetch(`${baseUrl}${path}`, {
12787
12790
  method,
@@ -12884,7 +12887,8 @@ function createClient(opts) {
12884
12887
  setProjectDestinations: (projectId, dest) => call("POST", `${appProject(projectId)}/destinations`, dest),
12885
12888
  setProjectMonitoring: (projectId, on) => call("POST", `${appProject(projectId)}/monitoring`, { on }),
12886
12889
  projectWatches: (projectId) => call("GET", `${appProject(projectId)}/watches`),
12887
- projectActivity: (projectId, intentId, opts = {}) => call("GET", `${appProject(projectId)}/watches/${encodeURIComponent(intentId)}/activity${query(opts)}`)
12890
+ projectActivity: (projectId, intentId, opts = {}) => call("GET", `${appProject(projectId)}/watches/${encodeURIComponent(intentId)}/activity${query(opts)}`),
12891
+ projectChanges: (projectId, opts = {}) => call("GET", `${appProject(projectId)}/changes${query(opts)}`)
12888
12892
  };
12889
12893
  }
12890
12894
  function query(params) {
@@ -21577,6 +21581,22 @@ function buildTools(client) {
21577
21581
  window: a.window
21578
21582
  })
21579
21583
  },
21584
+ list_project_changes: {
21585
+ description: 'The account\'s changes to what Sentinel watches across a project, newest first: each watch turned on or off, rule edits (rule and prevRule, the one it replaced), mutes and cooldowns, with how each was made (source) and when. `watch` keeps one watch (an id from list_project_watches); filter "mutes" keeps mutes and cooldowns; pass `next` back as cursor for older ones.',
21586
+ input: {
21587
+ projectId,
21588
+ watch: string2().optional(),
21589
+ filter: literal("mutes").optional(),
21590
+ limit: number2().int().min(1).max(200).optional(),
21591
+ cursor: string2().optional()
21592
+ },
21593
+ run: async (a) => client.projectChanges(a.projectId, {
21594
+ watch: a.watch,
21595
+ filter: a.filter,
21596
+ limit: a.limit,
21597
+ cursor: a.cursor
21598
+ })
21599
+ },
21580
21600
  set_project_destinations: {
21581
21601
  description: "Where a project's alerts go, set on each contract you added in it: email on or off, a webhook URL, a Slack URL. A URL left out is cleared. Without confirm it only describes.",
21582
21602
  input: {
@@ -21622,7 +21642,7 @@ async function runTool(tool, args) {
21622
21642
  }
21623
21643
  }
21624
21644
  function buildServer(client) {
21625
- const server = new McpServer({ name: "sentinel", version: "0.5.0" });
21645
+ const server = new McpServer({ name: "sentinel", version: "0.6.0" });
21626
21646
  for (const [name, tool] of Object.entries(buildTools(client))) {
21627
21647
  server.registerTool(name, { description: tool.description, inputSchema: tool.input }, (args) => runTool(tool, args));
21628
21648
  }
@@ -21637,6 +21657,7 @@ if (!credential) {
21637
21657
  }
21638
21658
  var client = createClient({
21639
21659
  token: credential.token,
21640
- baseUrl: process.env.SENTINEL_API_URL
21660
+ baseUrl: process.env.SENTINEL_API_URL,
21661
+ client: "mcp"
21641
21662
  });
21642
21663
  await buildServer(client).connect(new StdioServerTransport);
@@ -542,6 +542,35 @@ export type ProjectWatchesView = {
542
542
  locked: number;
543
543
  [field: string]: unknown;
544
544
  };
545
+ /** One change to what Sentinel watches: turned on or off, a rule edit (`prevRule` is what it replaced), a mute or
546
+ * a cooldown. `source` is how it was made (`web`, `key "<name>"`, …), null when not known. */
547
+ export type WatchChange = {
548
+ id: string;
549
+ at: string;
550
+ action: "applied" | "on" | "off" | "muted" | "cooldown";
551
+ contractId: string;
552
+ watch: {
553
+ id: string;
554
+ title: string;
555
+ };
556
+ subject: WatchSubject;
557
+ units: Units | null;
558
+ rule: WatchRule | null;
559
+ prevRule: WatchRule | null;
560
+ note: string | null;
561
+ by: "you";
562
+ source: string | null;
563
+ };
564
+ export type ChangesOptions = {
565
+ limit?: number;
566
+ cursor?: string | null;
567
+ /** One intent watch's id (from projectWatches). */
568
+ watch?: string;
569
+ /** `mutes`: only mutes and cooldowns. */
570
+ filter?: "mutes";
571
+ };
572
+ /** One change as a line: `Money out turned on`, `sBTC rule changed: ≥ 5 → ≥ 2`. */
573
+ export declare function changeText(c: WatchChange): string;
545
574
  /** One intent watch's activity, as the web app draws it: points, the history page, counts. */
546
575
  export type WatchActivity = {
547
576
  window: 7 | 90;
@@ -633,6 +662,8 @@ export type ClientOptions = {
633
662
  baseUrl?: string;
634
663
  token: string;
635
664
  fetch?: typeof globalThis.fetch;
665
+ /** Which tool is calling (sent as `x-sentinel-client`), so a change's history can say "sentinel CLI" or "MCP". */
666
+ client?: "cli" | "mcp";
636
667
  };
637
668
  export declare function createClient(opts: ClientOptions): {
638
669
  me: () => Promise<Record<string, unknown>>;
@@ -890,6 +921,11 @@ export declare function createClient(opts: ClientOptions): {
890
921
  contract?: string;
891
922
  fn?: string;
892
923
  }) => Promise<WatchActivity>;
924
+ /** The account's changes to what Sentinel watches across the project, newest first; `next` pages back. */
925
+ projectChanges: (projectId: string, opts?: ChangesOptions) => Promise<{
926
+ events: WatchChange[];
927
+ next: string | null;
928
+ }>;
893
929
  };
894
930
  export type SentinelClient = ReturnType<typeof createClient>;
895
931
  /** A mute end `hours` out, a minute inside the 7-day limit so clock skew can't tip it over. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@secondlayer/sentinel",
3
- "version": "0.5.0",
3
+ "version": "0.6.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",