@rubytech/create-maxy-code 0.1.267 → 0.1.268

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy-code",
3
- "version": "0.1.267",
3
+ "version": "0.1.268",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy-code": "./dist/index.js"
@@ -0,0 +1,40 @@
1
+ export interface InboundPayload {
2
+ /** The WhatsApp sender this payload is for (E.164 digits or JID). */
3
+ senderId: string;
4
+ /** The message body, verbatim — multi-line content is preserved. */
5
+ text: string;
6
+ /** The WhatsApp message id, for dedup/observability. */
7
+ waMessageId: string;
8
+ }
9
+ /**
10
+ * Shape an inbound WhatsApp payload as a Claude Code channel notification.
11
+ * `content` becomes the `<channel>` body; each `meta` entry becomes a tag
12
+ * attribute, so keys must be identifier-safe ([A-Za-z0-9_]) — hyphenated
13
+ * keys are silently dropped by Claude Code.
14
+ */
15
+ export declare function buildInboundNotification(p: InboundPayload): {
16
+ method: "notifications/claude/channel";
17
+ params: {
18
+ content: string;
19
+ meta: Record<string, string>;
20
+ };
21
+ };
22
+ /**
23
+ * Defence in depth: this server serves exactly one sender. Even though the
24
+ * gateway only streams that sender's messages, refuse anything else before
25
+ * it reaches the session.
26
+ */
27
+ export declare function acceptsSender(mySenderId: string, p: {
28
+ senderId: string;
29
+ }): boolean;
30
+ /**
31
+ * Build the body for POST /wa-channel/reply. The reply tool exposes only
32
+ * `text` to the model; the senderId is the server's fixed sender (from spawn
33
+ * env), never trusted from the model, so a compromised turn cannot redirect a
34
+ * reply to a different WhatsApp recipient.
35
+ */
36
+ export declare function buildReplyPostBody(mySenderId: string, text: string): {
37
+ senderId: string;
38
+ text: string;
39
+ };
40
+ //# sourceMappingURL=notification.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notification.d.ts","sourceRoot":"","sources":["../src/notification.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,cAAc;IAC7B,qEAAqE;IACrE,QAAQ,EAAE,MAAM,CAAA;IAChB,oEAAoE;IACpE,IAAI,EAAE,MAAM,CAAA;IACZ,wDAAwD;IACxD,WAAW,EAAE,MAAM,CAAA;CACpB;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,CAAC,EAAE,cAAc;;;;cAKa,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;;EAG5F;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,CAAC,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAElF;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAEvG"}
@@ -0,0 +1,36 @@
1
+ // Pure helpers for the WhatsApp channel server. No MCP SDK import here, so
2
+ // these can be unit-tested without the SDK installed and so the server's
3
+ // transport wiring (server.ts) stays separable from its message shaping.
4
+ /**
5
+ * Shape an inbound WhatsApp payload as a Claude Code channel notification.
6
+ * `content` becomes the `<channel>` body; each `meta` entry becomes a tag
7
+ * attribute, so keys must be identifier-safe ([A-Za-z0-9_]) — hyphenated
8
+ * keys are silently dropped by Claude Code.
9
+ */
10
+ export function buildInboundNotification(p) {
11
+ return {
12
+ method: 'notifications/claude/channel',
13
+ params: {
14
+ content: p.text,
15
+ meta: { sender_id: p.senderId, wa_message_id: p.waMessageId },
16
+ },
17
+ };
18
+ }
19
+ /**
20
+ * Defence in depth: this server serves exactly one sender. Even though the
21
+ * gateway only streams that sender's messages, refuse anything else before
22
+ * it reaches the session.
23
+ */
24
+ export function acceptsSender(mySenderId, p) {
25
+ return p.senderId === mySenderId;
26
+ }
27
+ /**
28
+ * Build the body for POST /wa-channel/reply. The reply tool exposes only
29
+ * `text` to the model; the senderId is the server's fixed sender (from spawn
30
+ * env), never trusted from the model, so a compromised turn cannot redirect a
31
+ * reply to a different WhatsApp recipient.
32
+ */
33
+ export function buildReplyPostBody(mySenderId, text) {
34
+ return { senderId: mySenderId, text };
35
+ }
36
+ //# sourceMappingURL=notification.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notification.js","sourceRoot":"","sources":["../src/notification.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,yEAAyE;AACzE,yEAAyE;AAWzE;;;;;GAKG;AACH,MAAM,UAAU,wBAAwB,CAAC,CAAiB;IACxD,OAAO;QACL,MAAM,EAAE,8BAAuC;QAC/C,MAAM,EAAE;YACN,OAAO,EAAE,CAAC,CAAC,IAAI;YACf,IAAI,EAAE,EAAE,SAAS,EAAE,CAAC,CAAC,QAAQ,EAAE,aAAa,EAAE,CAAC,CAAC,WAAW,EAA4B;SACxF;KACF,CAAA;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,UAAkB,EAAE,CAAuB;IACvE,OAAO,CAAC,CAAC,QAAQ,KAAK,UAAU,CAAA;AAClC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,UAAkB,EAAE,IAAY;IACjE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,IAAI,EAAE,CAAA;AACvC,CAAC"}
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":""}
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env node
2
+ // Per-sender WhatsApp channel MCP server. Spawned as a stdio child of one
3
+ // `claude` session (so it dies when that session dies — no bound port, no
4
+ // orphan). It:
5
+ // - subscribes to the gateway for ITS sender's inbound (SSE) and emits each
6
+ // message as a notifications/claude/channel event;
7
+ // - exposes a `reply` tool that posts the agent's reply back to the gateway,
8
+ // which sends it to WhatsApp.
9
+ //
10
+ // Transport glue only — message shaping and the reply body are the unit-tested
11
+ // pure helpers in ./notification. Verified end-to-end at deploy (Task 7).
12
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
13
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
14
+ import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
15
+ import { acceptsSender, buildInboundNotification, buildReplyPostBody } from './notification.js';
16
+ function requireEnv(name) {
17
+ const v = process.env[name];
18
+ if (!v)
19
+ throw new Error(`whatsapp-channel: missing required env ${name}`);
20
+ return v;
21
+ }
22
+ const SENDER_ID = requireEnv('WA_CHANNEL_SENDER_ID');
23
+ const GATEWAY_URL = requireEnv('WA_CHANNEL_GATEWAY_URL').replace(/\/$/, '');
24
+ // Co-visibility: the operator watches the same session in claude.ai/code,
25
+ // where the reply tool call shows but its text does not. Instruct the agent to
26
+ // also write its answer as normal assistant text so the operator can read it
27
+ // in the web view, AND relay that same text via the reply tool to WhatsApp.
28
+ const INSTRUCTIONS = `Messages from the WhatsApp sender arrive as <channel source="whatsapp" sender_id="...">. ` +
29
+ `Always do two things with your response, in this order: (1) write your answer as a normal ` +
30
+ `assistant message so it is visible in the session, then (2) call the reply tool with that ` +
31
+ `same text to send it to WhatsApp. Do not skip either step.`;
32
+ const mcp = new Server({ name: 'whatsapp', version: '0.1.0' }, {
33
+ capabilities: { experimental: { 'claude/channel': {} }, tools: {} },
34
+ instructions: INSTRUCTIONS,
35
+ });
36
+ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
37
+ tools: [
38
+ {
39
+ name: 'reply',
40
+ description: 'Send your reply text to the WhatsApp sender of this conversation.',
41
+ inputSchema: {
42
+ type: 'object',
43
+ properties: { text: { type: 'string', description: 'The message to send to WhatsApp' } },
44
+ required: ['text'],
45
+ },
46
+ },
47
+ ],
48
+ }));
49
+ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
50
+ if (req.params.name !== 'reply')
51
+ throw new Error(`unknown tool: ${req.params.name}`);
52
+ const text = req.params.arguments?.text;
53
+ if (typeof text !== 'string')
54
+ throw new Error('reply: text must be a string');
55
+ const res = await fetch(`${GATEWAY_URL}/wa-channel/reply`, {
56
+ method: 'POST',
57
+ headers: { 'content-type': 'application/json' },
58
+ body: JSON.stringify(buildReplyPostBody(SENDER_ID, text)),
59
+ });
60
+ if (!res.ok)
61
+ throw new Error(`reply: gateway returned ${res.status}`);
62
+ return { content: [{ type: 'text', text: 'sent' }] };
63
+ });
64
+ // Subscribe to this sender's inbound stream and emit each message as an event.
65
+ async function subscribeInbound() {
66
+ const url = `${GATEWAY_URL}/wa-channel/inbound?senderId=${encodeURIComponent(SENDER_ID)}`;
67
+ const res = await fetch(url, { headers: { accept: 'text/event-stream' } });
68
+ if (!res.ok || !res.body)
69
+ throw new Error(`inbound subscribe failed: ${res.status}`);
70
+ const reader = res.body.getReader();
71
+ const decoder = new TextDecoder();
72
+ let buffer = '';
73
+ for (;;) {
74
+ const { value, done } = await reader.read();
75
+ if (done)
76
+ break;
77
+ buffer += decoder.decode(value, { stream: true });
78
+ const frames = buffer.split('\n\n');
79
+ buffer = frames.pop() ?? '';
80
+ for (const frame of frames) {
81
+ const dataLine = frame.split('\n').find((l) => l.startsWith('data:'));
82
+ if (!dataLine)
83
+ continue;
84
+ const data = dataLine.slice('data:'.length).trim();
85
+ if (!data)
86
+ continue; // ping
87
+ let payload;
88
+ try {
89
+ payload = JSON.parse(data);
90
+ }
91
+ catch {
92
+ continue;
93
+ }
94
+ if (!acceptsSender(SENDER_ID, payload))
95
+ continue;
96
+ await mcp.notification(buildInboundNotification(payload));
97
+ }
98
+ }
99
+ }
100
+ async function main() {
101
+ await mcp.connect(new StdioServerTransport());
102
+ // Tell the gateway this sender's channel is live so any cold-start queue drains.
103
+ await fetch(`${GATEWAY_URL}/wa-channel/ready`, {
104
+ method: 'POST',
105
+ headers: { 'content-type': 'application/json' },
106
+ body: JSON.stringify({ senderId: SENDER_ID }),
107
+ }).catch(() => { });
108
+ await subscribeInbound();
109
+ }
110
+ main().catch((err) => {
111
+ console.error(`[whatsapp-channel] fatal: ${err instanceof Error ? err.message : String(err)}`);
112
+ process.exit(1);
113
+ });
114
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}