@msg91comm/m91 0.0.0-stage → 1.1.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.
Files changed (70) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/LICENSE +21 -0
  3. package/README.md +425 -2
  4. package/dist/src/client.d.ts +165 -0
  5. package/dist/src/client.d.ts.map +1 -0
  6. package/dist/src/client.js +221 -0
  7. package/dist/src/client.js.map +1 -0
  8. package/dist/src/exceptions.d.ts +114 -0
  9. package/dist/src/exceptions.d.ts.map +1 -0
  10. package/dist/src/exceptions.js +199 -0
  11. package/dist/src/exceptions.js.map +1 -0
  12. package/dist/src/http.d.ts +71 -0
  13. package/dist/src/http.d.ts.map +1 -0
  14. package/dist/src/http.js +150 -0
  15. package/dist/src/http.js.map +1 -0
  16. package/dist/src/index.d.ts +49 -0
  17. package/dist/src/index.d.ts.map +1 -0
  18. package/dist/src/index.js +45 -0
  19. package/dist/src/index.js.map +1 -0
  20. package/dist/src/integrations/index.d.ts +22 -0
  21. package/dist/src/integrations/index.d.ts.map +1 -0
  22. package/dist/src/integrations/index.js +21 -0
  23. package/dist/src/integrations/index.js.map +1 -0
  24. package/dist/src/integrations/langchain.d.ts +43 -0
  25. package/dist/src/integrations/langchain.d.ts.map +1 -0
  26. package/dist/src/integrations/langchain.js +75 -0
  27. package/dist/src/integrations/langchain.js.map +1 -0
  28. package/dist/src/integrations/langgraph.d.ts +79 -0
  29. package/dist/src/integrations/langgraph.d.ts.map +1 -0
  30. package/dist/src/integrations/langgraph.js +114 -0
  31. package/dist/src/integrations/langgraph.js.map +1 -0
  32. package/dist/src/integrations/specs.d.ts +131 -0
  33. package/dist/src/integrations/specs.d.ts.map +1 -0
  34. package/dist/src/integrations/specs.js +156 -0
  35. package/dist/src/integrations/specs.js.map +1 -0
  36. package/dist/src/integrations/vercel-ai.d.ts +36 -0
  37. package/dist/src/integrations/vercel-ai.d.ts.map +1 -0
  38. package/dist/src/integrations/vercel-ai.js +67 -0
  39. package/dist/src/integrations/vercel-ai.js.map +1 -0
  40. package/dist/src/models.d.ts +274 -0
  41. package/dist/src/models.d.ts.map +1 -0
  42. package/dist/src/models.js +383 -0
  43. package/dist/src/models.js.map +1 -0
  44. package/dist/src/platform.d.ts +176 -0
  45. package/dist/src/platform.d.ts.map +1 -0
  46. package/dist/src/platform.js +286 -0
  47. package/dist/src/platform.js.map +1 -0
  48. package/dist/src/url-builder.d.ts +65 -0
  49. package/dist/src/url-builder.d.ts.map +1 -0
  50. package/dist/src/url-builder.js +167 -0
  51. package/dist/src/url-builder.js.map +1 -0
  52. package/examples/approval-gate.ts +35 -0
  53. package/examples/basic-alert.ts +53 -0
  54. package/examples/langchain-agent.ts +65 -0
  55. package/examples/langgraph-hitl.ts +70 -0
  56. package/examples/vercel-ai-agent.ts +34 -0
  57. package/examples/webhook-urls.ts +44 -0
  58. package/package.json +83 -4
  59. package/src/client.ts +324 -0
  60. package/src/exceptions.ts +229 -0
  61. package/src/http.ts +174 -0
  62. package/src/index.ts +91 -0
  63. package/src/integrations/index.ts +23 -0
  64. package/src/integrations/langchain.ts +104 -0
  65. package/src/integrations/langgraph.ts +142 -0
  66. package/src/integrations/specs.ts +224 -0
  67. package/src/integrations/vercel-ai.ts +99 -0
  68. package/src/models.ts +660 -0
  69. package/src/platform.ts +343 -0
  70. package/src/url-builder.ts +225 -0
@@ -0,0 +1,167 @@
1
+ /**
2
+ * The GET form of a send: an alert spelled as a URL.
3
+ *
4
+ * This is the integration surface that needs no client at all. It goes in a
5
+ * browser bar, a phone shortcut, an `<img>` tag, a cron `curl`, a smart button,
6
+ * or any field in any tool labelled "URL to call". Somebody who has never made
7
+ * an HTTP request in their life can raise an alert by tapping a bookmark, and
8
+ * that is the difference between M91 being wired into something today and being
9
+ * wired in never.
10
+ *
11
+ * **A bare send link raises nothing.** It is the connection test: it answers
12
+ * with the channel's name so whoever pasted it can see they picked the right
13
+ * one. Only a URL with `?title=` fires. That is not a default nobody got round
14
+ * to — the token is in the path, so link previews, crawlers and mail scanners
15
+ * fetch these routinely, and `title` having no default is the only thing
16
+ * between a pasted link and a woken team.
17
+ */
18
+ import { M91ValidationError } from './exceptions.js';
19
+ import { CUSTOM_ID_MAX, DESCRIPTION_MAX, LABEL_MAX, MAX_LINK_RESPONSE_OPTIONS, TITLE_MAX, TITLE_MIN, checkSeverity, collapse, } from './models.js';
20
+ /**
21
+ * Validate a send link and strip anything after the token.
22
+ *
23
+ * Callers paste these out of the app, out of a create-channel response, or out
24
+ * of a colleague's message, so a trailing slash, a stray query string or an
25
+ * already-built alert URL all arrive here. All of them normalise to the bare
26
+ * link.
27
+ */
28
+ export function normalizeSendLink(sendLink) {
29
+ if (!sendLink || typeof sendLink !== 'string') {
30
+ throw new M91ValidationError('sendLink is required — the full https://…/s/<token> URL');
31
+ }
32
+ let parsed;
33
+ try {
34
+ parsed = new URL(sendLink.trim());
35
+ }
36
+ catch {
37
+ throw new M91ValidationError(`sendLink must be a full URL beginning http:// or https://, got '${sendLink}'`);
38
+ }
39
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
40
+ throw new M91ValidationError(`sendLink must be a full URL beginning http:// or https://, got '${sendLink}'`);
41
+ }
42
+ const segments = parsed.pathname.split('/').filter(Boolean);
43
+ // Checked because the failure it prevents is silent: a URL without a token
44
+ // authenticates as nothing and every call 401s, which reads like a revoked
45
+ // link rather than a mistyped one.
46
+ if (segments.length < 2 || segments[segments.length - 2] !== 's') {
47
+ throw new M91ValidationError(`sendLink must look like https://m91.msg91.com/s/<token> — ` +
48
+ `no /s/<token> found in '${parsed.pathname}'`);
49
+ }
50
+ return `${parsed.origin}/${segments.join('/')}`;
51
+ }
52
+ /**
53
+ * Render `respond=` from labels or response options.
54
+ *
55
+ * The compact syntax exists because `[{label, effect}]` cannot survive a query
56
+ * string without becoming unreadable. So: a comma-separated list where the
57
+ * **first label is SHARED and the rest are PERSONAL** — the shape almost every
58
+ * real alert has, one button that takes the job for the whole group and some
59
+ * number of "seen" buttons that do not. A trailing `!` forces SHARED on any
60
+ * label, for the caller who wants two people able to claim.
61
+ *
62
+ * Two things a link cannot express, both of which throw here rather than
63
+ * producing a URL that quietly does something else: a **PERSONAL first option**,
64
+ * and a label that genuinely **ends in `!`**. Both are reachable with a POST.
65
+ */
66
+ export function buildRespond(labels) {
67
+ const rendered = labels.map((entry, index) => {
68
+ let label;
69
+ let marker = '';
70
+ if (typeof entry === 'string') {
71
+ label = collapse(entry);
72
+ }
73
+ else {
74
+ label = collapse(entry?.label ?? '');
75
+ const shared = (entry?.effect ?? 'SHARED') === 'SHARED';
76
+ if (index === 0 && !shared) {
77
+ throw new M91ValidationError(`the first response option in a link is always SHARED — '${label}' cannot be ` +
78
+ 'PERSONAL here. Use POST with responseOptions.');
79
+ }
80
+ marker = shared && index > 0 ? '!' : '';
81
+ }
82
+ if (!label)
83
+ throw new M91ValidationError('respond labels cannot be empty');
84
+ if (label.includes(',')) {
85
+ // The separator is a comma, so a label containing one would silently
86
+ // become two buttons — an alert offering "Restart" and "then page Ana"
87
+ // as separate answers.
88
+ throw new M91ValidationError(`a respond label cannot contain a comma — it is the separator. ` +
89
+ `Use POST with responseOptions for '${label}'.`);
90
+ }
91
+ const bare = label.replace(/!+$/, '').trim();
92
+ if (bare.length > LABEL_MAX) {
93
+ throw new M91ValidationError(`response label must be ${LABEL_MAX} characters or fewer, got '${label}'`);
94
+ }
95
+ return { rendered: label + marker, key: bare.toLowerCase() };
96
+ });
97
+ if (rendered.length > MAX_LINK_RESPONSE_OPTIONS) {
98
+ throw new M91ValidationError(`a link can carry at most ${MAX_LINK_RESPONSE_OPTIONS} response labels, ` +
99
+ `got ${rendered.length}. Use POST for more.`);
100
+ }
101
+ const seen = new Set();
102
+ for (const { rendered: text, key } of rendered) {
103
+ if (seen.has(key)) {
104
+ throw new M91ValidationError(`cannot repeat the same response label twice: '${text}'`);
105
+ }
106
+ seen.add(key);
107
+ }
108
+ return rendered.map((r) => r.rendered).join(',');
109
+ }
110
+ /** Builds ready-to-paste alert URLs for tools whose only field is "URL to call". */
111
+ export class M91URLBuilder {
112
+ sendLink;
113
+ constructor(sendLink) {
114
+ this.sendLink = normalizeSendLink(sendLink);
115
+ }
116
+ /** The bare link. Answers with the channel name and **raises nothing**. */
117
+ testUrl() {
118
+ return this.sendLink;
119
+ }
120
+ /** A full URL that raises this alert when anything opens it. */
121
+ build(title, options = {}) {
122
+ const cleanTitle = collapse(title ?? '');
123
+ if (!cleanTitle || cleanTitle.length < TITLE_MIN) {
124
+ throw new M91ValidationError(`title must be at least ${TITLE_MIN} characters, got '${title}'`);
125
+ }
126
+ if (cleanTitle.length > TITLE_MAX) {
127
+ throw new M91ValidationError(`title must be ${TITLE_MAX} characters or fewer, got ${cleanTitle.length}`);
128
+ }
129
+ const severity = checkSeverity(options.severity ?? 'MEDIUM');
130
+ const query = new URLSearchParams();
131
+ query.set('title', cleanTitle);
132
+ if (options.description !== undefined && options.description !== null) {
133
+ const description = collapse(options.description);
134
+ if (description.length > DESCRIPTION_MAX) {
135
+ throw new M91ValidationError(`description must be ${DESCRIPTION_MAX} characters or fewer, got ${description.length}`);
136
+ }
137
+ query.set('description', description);
138
+ }
139
+ // MEDIUM is the server's default, so spelling it costs URL length and buys
140
+ // nothing. These get pasted into text boxes and retyped by hand.
141
+ if (severity !== 'MEDIUM')
142
+ query.set('severity', severity);
143
+ if (options.customId !== undefined && options.customId !== null) {
144
+ const customId = collapse(String(options.customId));
145
+ if (customId.length > CUSTOM_ID_MAX) {
146
+ throw new M91ValidationError(`customId must be ${CUSTOM_ID_MAX} characters or fewer, got ${customId.length}`);
147
+ }
148
+ query.set('customId', customId);
149
+ }
150
+ if (options.respond?.length)
151
+ query.set('respond', buildRespond(options.respond));
152
+ if (options.isAutoClose !== undefined) {
153
+ query.set('isAutoClose', options.isAutoClose ? 'true' : 'false');
154
+ }
155
+ return `${this.sendLink}?${query.toString()}`;
156
+ }
157
+ /** One URL per spec. Each takes the same options as `build`. */
158
+ buildMany(alerts) {
159
+ return alerts.map(({ title, ...options }) => {
160
+ if (title === undefined) {
161
+ throw new M91ValidationError("every alert in buildMany needs a 'title'");
162
+ }
163
+ return this.build(title, options);
164
+ });
165
+ }
166
+ }
167
+ //# sourceMappingURL=url-builder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"url-builder.js","sourceRoot":"","sources":["../../src/url-builder.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * The approval gate: ask a human, on their phone, and wait.
3
+ *
4
+ * This is the shape of every human-in-the-loop decision — raise the question,
5
+ * block, act on the answer. One call does all of it, including closing the
6
+ * alert afterwards so nobody is left with a live question that has been
7
+ * answered.
8
+ */
9
+
10
+ import { M91Client } from '@msg91comm/m91';
11
+
12
+ const client = new M91Client(process.env.M91_SEND_LINK!);
13
+
14
+ const approved = await client.requestApproval(
15
+ 'Agent wants to process a refund of ₹48,000 for order ORD-40122',
16
+ {
17
+ description:
18
+ 'Customer says the item never arrived. The courier\'s tracking shows delivered, ' +
19
+ "signed for by 'R. Kumar'. The customer's name is Rahul Kumar. " +
20
+ 'Approving pays out immediately and cannot be reversed.',
21
+ customId: 'refund-ORD-40122', // stable: the order id, never a timestamp
22
+ severity: 'HIGH',
23
+ approveLabel: 'Refund',
24
+ rejectLabel: 'Hold',
25
+ timeoutMs: 300_000,
26
+ },
27
+ );
28
+
29
+ if (approved) {
30
+ console.log('A human approved it. Processing the refund.');
31
+ } else {
32
+ // Rejected, or nobody answered in five minutes. Both mean: do not pay out.
33
+ // There is no configuration that makes silence mean yes.
34
+ console.log('Not approved. Leaving the refund for the morning queue.');
35
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Raise an alert, then close it when the condition heals.
3
+ *
4
+ * export M91_SEND_LINK="https://m91.msg91.com/s/your_token"
5
+ * npx tsx examples/basic-alert.ts
6
+ *
7
+ * The send link is the whole credential. It goes in the environment, never in
8
+ * the repository — the token is in the URL path, so the URL *is* the password.
9
+ */
10
+
11
+ import { M91Client, M91DeliveryError } from '@msg91comm/m91';
12
+
13
+ const client = new M91Client(process.env.M91_SEND_LINK!);
14
+
15
+ // Raises nothing. Worth doing on startup: a mistyped link otherwise looks fine
16
+ // until the night it has to work.
17
+ const channel = await client.verify();
18
+ console.log(`Connected to '${channel.name}' (${channel.visibility})`);
19
+
20
+ const alert = await client.send('Database disk 90% full on node-7', {
21
+ description: '/var/lib/postgresql at 90%. Growing ~2GB/hour. ~5 hours left.',
22
+ severity: 'HIGH', // HIGH also places a voice call after 45s of silence
23
+ // Stable: the SAME string every time this condition is true. A check running
24
+ // every minute then raises one alert, not sixty — and this is what closes it.
25
+ customId: 'disk-full-node7',
26
+ responseOptions: [
27
+ { label: 'On it', effect: 'SHARED' }, // takes it on, clears for everyone
28
+ { label: 'Seen', effect: 'PERSONAL' }, // acknowledges, stays with the team
29
+ ],
30
+ });
31
+
32
+ // Not "did the request succeed" — `delivered` is phones actually reached.
33
+ console.log(`Delivered to ${alert.delivered} phones (alert ${alert.id})`);
34
+ if (alert.deduplicated) {
35
+ console.log('An alert for this was already open — nobody was woken a second time.');
36
+ }
37
+
38
+ // ...later, when the disk has been cleared:
39
+ await client.close({ customId: 'disk-full-node7' });
40
+
41
+ // The failure worth catching by name: a 2xx where nobody's phone rang.
42
+ try {
43
+ await client.send('Heartbeat check from the example', { customId: 'example-heartbeat' });
44
+ } catch (error) {
45
+ if (error instanceof M91DeliveryError) {
46
+ console.error(
47
+ `Nobody was reachable: ${error.unregistered.length} unregistered, ` +
48
+ `${error.noDevice} without a device`,
49
+ );
50
+ } else {
51
+ throw error;
52
+ }
53
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * A LangChain agent that has to ask before it spends money.
3
+ *
4
+ * The interesting part is not the wiring — it is the system prompt. A tool the
5
+ * model *can* call is not a tool the model *will* call, and "ask a human before
6
+ * anything irreversible" has to be stated as a rule with a list, not implied.
7
+ */
8
+
9
+ import { ChatAnthropic } from '@langchain/anthropic';
10
+ import { createReactAgent } from '@langchain/langgraph/prebuilt';
11
+
12
+ import { M91Client } from '@msg91comm/m91';
13
+ import { createM91AlertTool, createM91ApprovalTool } from '@msg91comm/m91/integrations/langchain';
14
+
15
+ const client = new M91Client(process.env.M91_SEND_LINK!);
16
+
17
+ const tools = [
18
+ await createM91ApprovalTool(client), // blocks until a human answers
19
+ await createM91AlertTool(client), // wakes somebody and carries on
20
+ // ...your own tools: issueRefund, deploy, deleteAccount, and so on.
21
+ ];
22
+
23
+ const SYSTEM = `You are an operations agent with access to production systems.
24
+
25
+ Before ANY of the following, you must call m91_request_approval and wait for the
26
+ answer:
27
+ - moving money: refunds, payouts, charges, credits
28
+ - deleting or overwriting anything a customer owns
29
+ - deploying, rolling back, or changing production configuration
30
+ - sending a message to a customer on the company's behalf
31
+
32
+ Rules for that call:
33
+ - \`title\` is what somebody reads on a lock screen at 3am. Name the action and
34
+ the amount or the target: "Refund 48,000 on ORD-40122?" — not "approve action".
35
+ - \`description\` is everything they need to answer without asking you anything.
36
+ - \`customId\` is derived from the thing being decided — the order id, the
37
+ commit sha, the account id. Never a timestamp, never a UUID: it is what
38
+ stops a retry raising a second alert, and what closes the alert afterwards.
39
+ - The result is "approved" or "rejected". If it is not exactly "approved",
40
+ do not do the thing. Say what you were going to do and stop.
41
+
42
+ Use m91_send_alert, which does not wait, when something has already gone wrong
43
+ and a human needs to know now — a failed job, a breach, a stuck queue. Do not
44
+ use it for anything that could wait until morning: it wakes people up.`;
45
+
46
+ const agent = createReactAgent({
47
+ llm: new ChatAnthropic({ model: 'claude-sonnet-5' }),
48
+ tools,
49
+ prompt: SYSTEM,
50
+ });
51
+
52
+ // The agent pauses here — a phone rings, somebody taps Approve or Reject, and
53
+ // the run continues with their answer in the transcript.
54
+ const result = await agent.invoke({
55
+ messages: [
56
+ {
57
+ role: 'user',
58
+ content:
59
+ 'Customer on order ORD-40122 says their item never arrived. ' +
60
+ 'Look into it and refund them if it checks out.',
61
+ },
62
+ ],
63
+ });
64
+
65
+ console.log(result.messages.at(-1)?.content);
@@ -0,0 +1,70 @@
1
+ /**
2
+ * LangGraph's `interrupt()` pattern, with M91 filling the gap.
3
+ *
4
+ * LangGraph knows how to stop a graph and resume it. What it has no opinion
5
+ * about is how the human finds out it stopped — and the default answer is that
6
+ * somebody notices a paused run in a web UI. At 2am, for a graph that paused
7
+ * because it wants to move money, nobody does.
8
+ *
9
+ * M91 is the part between the pause and the resume.
10
+ */
11
+
12
+ import { Annotation, Command, END, MemorySaver, START, StateGraph, interrupt } from '@langchain/langgraph';
13
+
14
+ import { M91Client } from '@msg91comm/m91';
15
+ import { M91InterruptNotifier } from '@msg91comm/m91/integrations/langgraph';
16
+
17
+ const client = new M91Client(process.env.M91_SEND_LINK!);
18
+ const notifier = new M91InterruptNotifier(client, { severity: 'HIGH', timeoutMs: 600_000 });
19
+
20
+ const State = Annotation.Root({
21
+ id: Annotation<string>,
22
+ amount: Annotation<number>,
23
+ result: Annotation<string>,
24
+ });
25
+
26
+ const builder = new StateGraph(State)
27
+ .addNode('riskyAction', (state) => {
28
+ if (state.amount > 10_000) {
29
+ // The graph stops here and hands the payload out to the orchestrator.
30
+ const decision = interrupt({
31
+ question: `Approve payment of ₹${state.amount.toLocaleString()} on order ${state.id}?`,
32
+ customId: `payment-${state.id}`,
33
+ });
34
+ if (decision !== 'approved') return { result: 'payment_rejected' };
35
+ }
36
+ // await executePayment(state);
37
+ return { result: 'payment_completed' };
38
+ })
39
+ .addEdge(START, 'riskyAction')
40
+ .addEdge('riskyAction', END);
41
+
42
+ const graph = builder.compile({ checkpointer: new MemorySaver() });
43
+ const config = { configurable: { thread_id: 'order-40122' } };
44
+
45
+ let result = await graph.invoke({ id: 'ORD-40122', amount: 48_000 }, config);
46
+
47
+ if (result.__interrupt__) {
48
+ const data = result.__interrupt__[0].value as { question: string; customId: string };
49
+
50
+ // A phone rings at full volume, through Do Not Disturb, until somebody
51
+ // answers — and gets a voice call if 45 seconds pass in silence.
52
+ const decision = await notifier.notify({
53
+ title: data.question,
54
+ customId: data.customId, // the graph chose it, and it is stable
55
+ });
56
+
57
+ result = await graph.invoke(new Command({ resume: decision }), config);
58
+ }
59
+
60
+ console.log(result.result);
61
+
62
+ // The same thing as a node, when you would rather the graph own it:
63
+ //
64
+ // import { createM91ApprovalNode } from '@msg91comm/m91/integrations/langgraph';
65
+ // builder.addNode('approveRefund', createM91ApprovalNode(client, {
66
+ // approveLabel: 'Process Refund', rejectLabel: 'Hold Refund', timeoutMs: 600_000,
67
+ // }));
68
+ //
69
+ // It reads `title`/`question` and `customId` from state, and writes
70
+ // { decision, approved } back for the next edge to branch on.
@@ -0,0 +1,34 @@
1
+ /**
2
+ * An agent on the Vercel AI SDK that asks before it deletes anything.
3
+ */
4
+
5
+ import { openai } from '@ai-sdk/openai';
6
+ import { generateText, stepCountIs } from 'ai';
7
+
8
+ import { M91Client } from '@msg91comm/m91';
9
+ import { createM91AlertTool, createM91ApprovalTool } from '@msg91comm/m91/integrations/vercel-ai';
10
+
11
+ const client = new M91Client(process.env.M91_SEND_LINK!);
12
+
13
+ const { text } = await generateText({
14
+ model: openai('gpt-4o'),
15
+ system:
16
+ 'You are an operations agent. Before anything irreversible — deleting customer ' +
17
+ 'data, moving money, deploying to production — you must call m91_request_approval ' +
18
+ 'and wait. Give it a title naming the action and its target, a description with ' +
19
+ 'everything the person needs to decide, and a customId derived from what is being ' +
20
+ 'decided (never a timestamp). Proceed only on exactly "approved". ' +
21
+ 'Use m91_send_alert, which does not wait, to report something that has already ' +
22
+ 'gone wrong and needs somebody now.',
23
+ tools: {
24
+ m91_request_approval: await createM91ApprovalTool(client),
25
+ m91_send_alert: await createM91AlertTool(client),
26
+ // ...your own tools
27
+ },
28
+ stopWhen: stepCountIs(10),
29
+ prompt:
30
+ 'Account acct_8821 has asked to be deleted under GDPR. Verify the request and, ' +
31
+ 'if it is valid, delete the account.',
32
+ });
33
+
34
+ console.log(text);
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Alert URLs for tools whose only integration field is "URL to call".
3
+ *
4
+ * Grafana, Sentry, UptimeRobot, Zapier, a GitHub Action, a cron `curl`, an iOS
5
+ * Shortcut, a browser bookmark, an `<img>` tag in a status page. No client, no
6
+ * headers, no code — which is the point: this is the difference between M91
7
+ * being wired into something this afternoon and being wired in never.
8
+ */
9
+
10
+ import { M91URLBuilder } from '@msg91comm/m91';
11
+
12
+ const builder = new M91URLBuilder(process.env.M91_SEND_LINK!);
13
+
14
+ // Paste this into Grafana's webhook URL field.
15
+ console.log(
16
+ builder.build('CPU above 95% on prod-server-3', {
17
+ severity: 'HIGH',
18
+ customId: 'cpu-high-prod3',
19
+ // First label is SHARED — one person takes it on and it clears for the rest.
20
+ // The others are PERSONAL: an acknowledgement that leaves it looking for an owner.
21
+ respond: ['On it', 'Escalate'],
22
+ }),
23
+ );
24
+
25
+ // The bare link. Opening it RAISES NOTHING and answers with the channel's name,
26
+ // so whoever is pasting it can see they picked the right team. That is also why
27
+ // a link preview, a crawler or a mail scanner touching the URL wakes nobody.
28
+ console.log(builder.testUrl());
29
+
30
+ // A batch, for generating a dashboard's worth at once.
31
+ for (const url of builder.buildMany([
32
+ { title: 'Disk above 90% on prod-3', customId: 'disk-high-prod3', severity: 'MEDIUM' },
33
+ { title: 'Certificate expires in 48 hours', customId: 'cert-expiry', severity: 'LOW' },
34
+ { title: 'Payment gateway unreachable', customId: 'gateway-down', severity: 'CRITICAL' },
35
+ ])) {
36
+ console.log(url);
37
+ }
38
+
39
+ // In a crontab, where there is no Node at all:
40
+ //
41
+ // 0 9 * * * curl -fsS "https://m91.msg91.com/s/TOKEN?title=Daily+backup+did+not+run&severity=HIGH"
42
+ //
43
+ // The URL contains the token, so it is a credential. Keep it out of anything
44
+ // public, and out of anything that ends up in a screenshot.
package/package.json CHANGED
@@ -1,6 +1,85 @@
1
1
  {
2
2
  "name": "@msg91comm/m91",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.1.0",
4
+ "description": "Critical alerting and human-in-the-loop approval for AI agents and on-call systems",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "MSG91",
8
+ "publishConfig": {
9
+ "access": "public"
10
+ },
11
+ "keywords": [
12
+ "alerting",
13
+ "on-call",
14
+ "paging",
15
+ "human-in-the-loop",
16
+ "agent",
17
+ "approval",
18
+ "escalation"
19
+ ],
20
+ "homepage": "https://m91.msg91.com",
21
+ "repository": { "type": "git", "url": "git+https://github.com/MSG91/m91-sdk.git" },
22
+ "bugs": { "url": "https://github.com/MSG91/m91-sdk/issues" },
23
+ "main": "./dist/src/index.js",
24
+ "types": "./dist/src/index.d.ts",
25
+ "exports": {
26
+ "./package.json": "./package.json",
27
+ ".": {
28
+ "types": "./dist/src/index.d.ts",
29
+ "default": "./dist/src/index.js"
30
+ },
31
+ "./integrations/langchain": {
32
+ "types": "./dist/src/integrations/langchain.d.ts",
33
+ "default": "./dist/src/integrations/langchain.js"
34
+ },
35
+ "./integrations/langgraph": {
36
+ "types": "./dist/src/integrations/langgraph.d.ts",
37
+ "default": "./dist/src/integrations/langgraph.js"
38
+ },
39
+ "./integrations/vercel-ai": {
40
+ "types": "./dist/src/integrations/vercel-ai.d.ts",
41
+ "default": "./dist/src/integrations/vercel-ai.js"
42
+ }
43
+ },
44
+ "files": [
45
+ "dist/src",
46
+ "src",
47
+ "examples",
48
+ "README.md",
49
+ "CHANGELOG.md",
50
+ "LICENSE"
51
+ ],
52
+ "engines": {
53
+ "node": ">=18.0.0"
54
+ },
55
+ "scripts": {
56
+ "build": "tsc",
57
+ "test": "tsc && node --test dist/tests/*.test.js",
58
+ "prepublishOnly": "npm run build"
59
+ },
60
+ "dependencies": {},
61
+ "devDependencies": {
62
+ "@types/node": "^22.0.0",
63
+ "typescript": "^5.4.0"
64
+ },
65
+ "peerDependencies": {
66
+ "@langchain/core": ">=0.2.0",
67
+ "@langchain/langgraph": ">=0.2.0",
68
+ "ai": ">=3.0.0",
69
+ "zod": ">=3.0.0"
70
+ },
71
+ "peerDependenciesMeta": {
72
+ "@langchain/core": {
73
+ "optional": true
74
+ },
75
+ "@langchain/langgraph": {
76
+ "optional": true
77
+ },
78
+ "ai": {
79
+ "optional": true
80
+ },
81
+ "zod": {
82
+ "optional": true
83
+ }
84
+ }
85
+ }