@nothingmail/nmp 0.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.
@@ -0,0 +1,238 @@
1
+ import { p as NmpChannelConfig, y as NmpType, x as NmpStatus, v as NmpPriority, r as NmpContext, w as NmpSource, s as NmpError, z as TokenPermission } from '../types-BBAct0Gd.js';
2
+
3
+ /** POST /api/messages/send */
4
+ interface SendRequest {
5
+ to: string;
6
+ text: string;
7
+ subject?: string;
8
+ type?: NmpType;
9
+ project?: string;
10
+ labels?: string[];
11
+ priority?: NmpPriority;
12
+ files?: string[];
13
+ require?: string[];
14
+ reply_schema?: Record<string, unknown>;
15
+ ack?: boolean;
16
+ }
17
+ interface SendResponse {
18
+ success: boolean;
19
+ message_id: string;
20
+ status: NmpStatus;
21
+ }
22
+ /** GET /api/messages/inbox */
23
+ interface InboxQuery {
24
+ unread?: boolean;
25
+ project?: string;
26
+ label?: string;
27
+ limit?: number;
28
+ offset?: number;
29
+ }
30
+ interface InboxResponse {
31
+ messages: MessageSummary[];
32
+ total_unread: number;
33
+ }
34
+ /** GET /api/messages/sent */
35
+ interface SentQuery {
36
+ project?: string;
37
+ limit?: number;
38
+ offset?: number;
39
+ }
40
+ interface SentResponse {
41
+ messages: MessageSummary[];
42
+ }
43
+ /** GET /api/messages/:id */
44
+ interface MessageDetail {
45
+ id: string;
46
+ from: string;
47
+ to: string;
48
+ subject: string;
49
+ date: string;
50
+ type: NmpType;
51
+ content: string;
52
+ project?: string;
53
+ labels?: string[];
54
+ priority?: NmpPriority;
55
+ context?: NmpContext;
56
+ channel?: ChannelRef;
57
+ status?: NmpStatus;
58
+ source?: NmpSource;
59
+ error?: NmpError;
60
+ attachments?: AttachmentInfo[];
61
+ thread?: ThreadMessage[];
62
+ }
63
+ /** POST /api/messages/:id/reply */
64
+ interface ReplyRequest {
65
+ text: string;
66
+ files?: string[];
67
+ }
68
+ interface ReplyResponse {
69
+ success: boolean;
70
+ message_id: string;
71
+ status: NmpStatus;
72
+ }
73
+ /** GET /api/projects */
74
+ interface ProjectInfo {
75
+ name: string;
76
+ total: number;
77
+ unread: number;
78
+ last_activity: string;
79
+ }
80
+ interface ProjectsResponse {
81
+ projects: ProjectInfo[];
82
+ }
83
+ /** Shared types */
84
+ interface MessageSummary {
85
+ id: string;
86
+ from: string;
87
+ to: string;
88
+ subject: string;
89
+ preview: string;
90
+ date: string;
91
+ type: NmpType;
92
+ channel?: ChannelRef;
93
+ status?: NmpStatus;
94
+ unread?: boolean;
95
+ has_attachments: boolean;
96
+ project?: string;
97
+ labels?: string[];
98
+ thread_count?: number;
99
+ }
100
+ /** Channel reference in message responses (lightweight, not full config) */
101
+ interface ChannelRef {
102
+ id: string;
103
+ name: string;
104
+ email: string;
105
+ }
106
+ interface AttachmentInfo {
107
+ filename: string;
108
+ size: number;
109
+ url: string;
110
+ }
111
+ interface ThreadMessage {
112
+ id: string;
113
+ from: string;
114
+ preview: string;
115
+ date: string;
116
+ }
117
+ /** GET /api/channels */
118
+ interface ChannelsResponse {
119
+ channels: ChannelInfo[];
120
+ }
121
+ interface ChannelInfo {
122
+ id: string;
123
+ name: string;
124
+ type: NmpChannelConfig['type'];
125
+ email: string;
126
+ is_primary: boolean;
127
+ is_active: boolean;
128
+ created_at: string;
129
+ }
130
+ /** POST /api/channels */
131
+ interface CreateChannelRequest {
132
+ name: string;
133
+ type: NmpChannelConfig['type'];
134
+ email: string;
135
+ smtp_host?: string;
136
+ smtp_port?: number;
137
+ imap_host?: string;
138
+ imap_port?: number;
139
+ password?: string;
140
+ is_primary?: boolean;
141
+ }
142
+ /** POST /api/channels/:id/test */
143
+ interface TestChannelResponse {
144
+ smtp: boolean;
145
+ imap: boolean;
146
+ }
147
+ /** GET /api/reports */
148
+ interface ReportQuery {
149
+ period?: 'today' | 'week' | 'month';
150
+ project?: string;
151
+ }
152
+ interface ReportResponse {
153
+ period: {
154
+ start: string;
155
+ end: string;
156
+ label: string;
157
+ };
158
+ summary: {
159
+ sent: number;
160
+ received: number;
161
+ replied: number;
162
+ failed: number;
163
+ };
164
+ projects: {
165
+ name: string;
166
+ messages: number;
167
+ threads: number;
168
+ resolved: number;
169
+ }[];
170
+ needs_reply: {
171
+ id: string;
172
+ from: string;
173
+ subject: string;
174
+ date: string;
175
+ project?: string;
176
+ }[];
177
+ top_threads: {
178
+ thread_id: string;
179
+ subject: string;
180
+ message_count: number;
181
+ status: string;
182
+ last_activity: string;
183
+ }[];
184
+ }
185
+
186
+ /** Account and token types for Nothing API */
187
+ /** GET /api/account */
188
+ interface AccountInfo {
189
+ id: string;
190
+ handle: string;
191
+ email: string;
192
+ display_name?: string;
193
+ github_username: string;
194
+ created_at: string;
195
+ }
196
+ /** GET /api/account/usage */
197
+ interface UsageInfo {
198
+ messages_today: number;
199
+ messages_limit: number;
200
+ storage_used: number;
201
+ storage_limit: number;
202
+ tokens_count: number;
203
+ tokens_limit: number;
204
+ }
205
+ /** GET /api/account/tokens */
206
+ interface TokenInfo {
207
+ id: string;
208
+ name: string;
209
+ token_preview: string;
210
+ permissions: TokenPermission[];
211
+ last_used?: string;
212
+ expires_at?: string;
213
+ created_at: string;
214
+ revoked: boolean;
215
+ }
216
+ /** POST /api/account/tokens */
217
+ interface CreateTokenRequest {
218
+ name: string;
219
+ permissions: TokenPermission[];
220
+ expires_at?: string;
221
+ }
222
+ interface CreateTokenResponse {
223
+ id: string;
224
+ token: string;
225
+ name: string;
226
+ permissions: TokenPermission[];
227
+ }
228
+ /** POST /api/account/register */
229
+ interface RegisterRequest {
230
+ handle: string;
231
+ }
232
+ interface RegisterResponse {
233
+ handle: string;
234
+ email: string;
235
+ master_token: string;
236
+ }
237
+
238
+ export { type AccountInfo, type AttachmentInfo, type ChannelInfo, type ChannelRef, type ChannelsResponse, type CreateChannelRequest, type CreateTokenRequest, type CreateTokenResponse, type InboxQuery, type InboxResponse, type MessageDetail, type MessageSummary, type ProjectInfo, type ProjectsResponse, type RegisterRequest, type RegisterResponse, type ReplyRequest, type ReplyResponse, type ReportQuery, type ReportResponse, type SendRequest, type SendResponse, type SentQuery, type SentResponse, type TestChannelResponse, type ThreadMessage, type TokenInfo, TokenPermission, type UsageInfo };
File without changes
@@ -0,0 +1,224 @@
1
+ import { N as NmpPayload, a as NmpMarkdown, b as NmpComplianceLevel } from './types-BBAct0Gd.js';
2
+ export { c as NMP_ATTACHMENT_NAME, d as NMP_CHANNEL_TYPES, e as NMP_COMPLIANCE, f as NMP_DEFAULTS, g as NMP_ERROR_CODES, h as NMP_HEADERS, i as NMP_LIMITS, j as NMP_PRIORITIES, k as NMP_SOURCES, l as NMP_STATUSES, m as NMP_TYPES, n as NMP_VERSION, o as NmpAttachment, p as NmpChannelConfig, q as NmpChannelType, r as NmpContext, s as NmpError, t as NmpErrorCode, u as NmpMessage, v as NmpPriority, w as NmpSource, x as NmpStatus, y as NmpType, T as TOKEN_PERMISSIONS, z as TokenPermission } from './types-BBAct0Gd.js';
3
+
4
+ /** Generic registry for extensible collections */
5
+ declare class Registry<T> {
6
+ private items;
7
+ register(key: string, value: T): void;
8
+ get(key: string): T | undefined;
9
+ has(key: string): boolean;
10
+ keys(): string[];
11
+ values(): T[];
12
+ entries(): [string, T][];
13
+ unregister(key: string): boolean;
14
+ get size(): number;
15
+ }
16
+
17
+ /** Generate nmp.md content from payload + message body */
18
+ declare function generateMarkdown(content: string, payload: NmpPayload): string;
19
+ /** Generate plain text (Part 1) from content + payload */
20
+ declare function generatePlainText(content: string, payload: NmpPayload): string;
21
+ /** Parse nmp.md content into structured data */
22
+ declare function parseMarkdown(md: string): NmpMarkdown | null;
23
+
24
+ /** Schema registry — extensible collection of Reply Schemas */
25
+ declare const schemaRegistry: Registry<Record<string, unknown>>;
26
+ /** Resolve a schema reference (e.g., "nmp:code-review") */
27
+ declare function resolveSchema(ref: string): Record<string, unknown> | null;
28
+
29
+ interface ValidationResult {
30
+ valid: boolean;
31
+ errors: string[];
32
+ level: NmpComplianceLevel;
33
+ }
34
+ /** Validate an NMP JSON payload */
35
+ declare function validatePayload(payload: unknown): ValidationResult;
36
+ /** Validate nmp.md content string */
37
+ declare function validateMarkdown(md: string): ValidationResult;
38
+
39
+ /** MCP tool definitions for Nothing — rich descriptions for Agent understanding */
40
+ declare const NMP_TOOLS: {
41
+ readonly nothing_send: {
42
+ readonly name: "nothing_send";
43
+ readonly description: "Send an email or NMP message. Use when the user wants to notify someone, share code, ask a question, or delegate a task. Supports file attachments and project tagging. Example triggers: \"send this to bob\", \"email alice about the bug\", \"notify the team\".";
44
+ readonly inputSchema: {
45
+ readonly type: "object";
46
+ readonly properties: {
47
+ readonly to: {
48
+ readonly type: "string";
49
+ readonly description: "Recipient email address (e.g., bob@nothing.email, alice@gmail.com)";
50
+ };
51
+ readonly text: {
52
+ readonly type: "string";
53
+ readonly description: "Message body. For code discussions, include file paths and line numbers.";
54
+ };
55
+ readonly agent: {
56
+ readonly type: "string";
57
+ readonly description: "Your agent identity (e.g., \"claude-code\", \"cursor\"). Auto-detected from MCP context.";
58
+ };
59
+ readonly subject: {
60
+ readonly type: "string";
61
+ readonly description: "Subject line. Auto-generated from first 50 chars if omitted.";
62
+ };
63
+ readonly type: {
64
+ readonly type: "string";
65
+ readonly enum: readonly ["share", "question", "reply", "notify"];
66
+ readonly description: "Message intent: share (FYI), question (expects reply), reply, notify (no reply expected)";
67
+ };
68
+ readonly project: {
69
+ readonly type: "string";
70
+ readonly description: "Project name for grouping (e.g., \"backend-refactor\"). Messages with same project are grouped together.";
71
+ };
72
+ readonly labels: {
73
+ readonly type: "array";
74
+ readonly items: {
75
+ readonly type: "string";
76
+ };
77
+ readonly description: "Tags for categorization (e.g., [\"code-review\", \"urgent\"])";
78
+ };
79
+ readonly files: {
80
+ readonly type: "array";
81
+ readonly items: {
82
+ readonly type: "string";
83
+ };
84
+ readonly description: "Absolute file paths to attach (e.g., [\"/path/to/file.ts\"])";
85
+ };
86
+ readonly require: {
87
+ readonly type: "array";
88
+ readonly items: {
89
+ readonly type: "string";
90
+ };
91
+ readonly description: "Capabilities the recipient must have (e.g., [\"code-review\"]). Recipient can reject if they lack the capability.";
92
+ };
93
+ readonly priority: {
94
+ readonly type: "string";
95
+ readonly enum: readonly ["urgent", "normal", "low"];
96
+ readonly description: "Urgency level. Use \"urgent\" sparingly.";
97
+ };
98
+ };
99
+ readonly required: readonly ["to", "text"];
100
+ };
101
+ };
102
+ readonly nothing_inbox: {
103
+ readonly name: "nothing_inbox";
104
+ readonly description: "Check inbox for received messages. Use when the user asks \"any new messages?\", \"check my mail\", \"what did bob send?\", or \"show messages from gmail\". Supports filtering by channel (gmail/qq/nothing), sender, project, and labels.";
105
+ readonly inputSchema: {
106
+ readonly type: "object";
107
+ readonly properties: {
108
+ readonly unread: {
109
+ readonly type: "boolean";
110
+ readonly description: "Only unread messages (default: true). Set false to see all.";
111
+ };
112
+ readonly project: {
113
+ readonly type: "string";
114
+ readonly description: "Filter by project name";
115
+ };
116
+ readonly label: {
117
+ readonly type: "string";
118
+ readonly description: "Filter by label";
119
+ };
120
+ readonly channel: {
121
+ readonly type: "string";
122
+ readonly description: "Filter by email channel: gmail, qq, outlook, nothing, local";
123
+ };
124
+ readonly source: {
125
+ readonly type: "string";
126
+ readonly enum: readonly ["nmp", "external"];
127
+ readonly description: "Filter by message type: \"nmp\" for Agent messages, \"external\" for regular emails";
128
+ };
129
+ readonly agent: {
130
+ readonly type: "string";
131
+ readonly description: "Filter by sending agent name (exact match, e.g., \"claude-code\", \"cursor\", \"codex\")";
132
+ };
133
+ readonly limit: {
134
+ readonly type: "number";
135
+ readonly description: "Max messages to return (default: 20)";
136
+ };
137
+ };
138
+ };
139
+ };
140
+ readonly nothing_sent: {
141
+ readonly name: "nothing_sent";
142
+ readonly description: "Check sent messages and their delivery status. Use when the user asks \"did my message arrive?\", \"delivery status\", or \"what did I send?\". Shows 6 statuses: queued → sent → delivered → read → replied / failed.";
143
+ readonly inputSchema: {
144
+ readonly type: "object";
145
+ readonly properties: {
146
+ readonly limit: {
147
+ readonly type: "number";
148
+ readonly description: "Max messages to return (default: 20)";
149
+ };
150
+ readonly project: {
151
+ readonly type: "string";
152
+ readonly description: "Filter by project";
153
+ };
154
+ };
155
+ };
156
+ };
157
+ readonly nothing_read: {
158
+ readonly name: "nothing_read";
159
+ readonly description: "Read a specific message in full. Use when the user says \"open that message\", \"read it\", \"show me the details\". Returns complete content, code context, attachments, and thread history. Marks message as read.";
160
+ readonly inputSchema: {
161
+ readonly type: "object";
162
+ readonly properties: {
163
+ readonly id: {
164
+ readonly type: "string";
165
+ readonly description: "Message ID (from inbox or sent listing)";
166
+ };
167
+ };
168
+ readonly required: readonly ["id"];
169
+ };
170
+ };
171
+ readonly nothing_reply: {
172
+ readonly name: "nothing_reply";
173
+ readonly description: "Reply to a message within its thread. Use when the user says \"reply to that\", \"tell them yes\", \"respond with...\". Automatically inherits project, labels, and thread context from the original message.";
174
+ readonly inputSchema: {
175
+ readonly type: "object";
176
+ readonly properties: {
177
+ readonly id: {
178
+ readonly type: "string";
179
+ readonly description: "Message ID to reply to";
180
+ };
181
+ readonly text: {
182
+ readonly type: "string";
183
+ readonly description: "Reply body";
184
+ };
185
+ readonly files: {
186
+ readonly type: "array";
187
+ readonly items: {
188
+ readonly type: "string";
189
+ };
190
+ readonly description: "File paths to attach";
191
+ };
192
+ };
193
+ readonly required: readonly ["id", "text"];
194
+ };
195
+ };
196
+ readonly nothing_projects: {
197
+ readonly name: "nothing_projects";
198
+ readonly description: "List all projects with message counts and unread status. Use when the user asks \"what projects do I have?\", \"project overview\", or \"which projects have unread messages?\".";
199
+ readonly inputSchema: {
200
+ readonly type: "object";
201
+ readonly properties: {};
202
+ };
203
+ };
204
+ readonly nothing_report: {
205
+ readonly name: "nothing_report";
206
+ readonly description: "Generate an activity report showing messages sent/received, project breakdown, items needing reply, and top threads. Use when the user asks \"weekly summary\", \"what happened this week?\", \"give me a report\", or \"any messages I missed?\".";
207
+ readonly inputSchema: {
208
+ readonly type: "object";
209
+ readonly properties: {
210
+ readonly period: {
211
+ readonly type: "string";
212
+ readonly enum: readonly ["today", "week", "month"];
213
+ readonly description: "Time period (default: week)";
214
+ };
215
+ readonly project: {
216
+ readonly type: "string";
217
+ readonly description: "Filter report to a specific project";
218
+ };
219
+ };
220
+ };
221
+ };
222
+ };
223
+
224
+ export { NMP_TOOLS, NmpComplianceLevel, NmpMarkdown, NmpPayload, Registry, type ValidationResult, generateMarkdown, generatePlainText, parseMarkdown, resolveSchema, schemaRegistry, validateMarkdown, validatePayload };
package/dist/index.js ADDED
@@ -0,0 +1,419 @@
1
+ // src/types.ts
2
+ var NMP_VERSION = 1;
3
+ var NMP_TYPES = ["share", "question", "reply", "notify"];
4
+ var NMP_PRIORITIES = ["urgent", "normal", "low"];
5
+ var NMP_STATUSES = ["queued", "sent", "delivered", "read", "replied", "failed"];
6
+ var NMP_SOURCES = ["nmp", "external"];
7
+ var NMP_CHANNEL_TYPES = ["nothing", "smtp", "stalwart", "local"];
8
+ var TOKEN_PERMISSIONS = ["send", "inbox", "read", "reply", "manage"];
9
+ var NMP_ERROR_CODES = [
10
+ "capability_not_supported",
11
+ "schema_mismatch",
12
+ "rate_limited",
13
+ "rejected",
14
+ "expired",
15
+ "version_unsupported",
16
+ "payload_too_large"
17
+ ];
18
+ var NMP_COMPLIANCE = {
19
+ NONE: 0,
20
+ BASIC: 1,
21
+ FULL: 2
22
+ };
23
+ var NMP_DEFAULTS = {
24
+ subjectMaxLength: 50,
25
+ messageLimit: 20,
26
+ dailyMessageLimit: 50,
27
+ tokenRateLimit: 10,
28
+ priority: "normal",
29
+ type: "share"
30
+ };
31
+ var NMP_LIMITS = {
32
+ maxEmailSize: 10 * 1024 * 1024,
33
+ maxAttachments: 5,
34
+ maxJsonSize: 256 * 1024,
35
+ maxMarkdownSize: 64 * 1024,
36
+ maxPlainTextSize: 64 * 1024
37
+ };
38
+ var NMP_HEADERS = {
39
+ version: "X-NMP-Version",
40
+ type: "X-NMP-Type",
41
+ agent: "X-NMP-Agent",
42
+ project: "X-NMP-Project",
43
+ labels: "X-NMP-Labels",
44
+ priority: "X-NMP-Priority",
45
+ expires: "X-NMP-Expires",
46
+ capabilities: "X-NMP-Capabilities",
47
+ require: "X-NMP-Require",
48
+ replySchema: "X-NMP-Reply-Schema",
49
+ signature: "X-NMP-Signature"
50
+ };
51
+ var NMP_ATTACHMENT_NAME = "nmp.md";
52
+
53
+ // src/registry.ts
54
+ var Registry = class {
55
+ items = /* @__PURE__ */ new Map();
56
+ register(key, value) {
57
+ this.items.set(key, value);
58
+ }
59
+ get(key) {
60
+ return this.items.get(key);
61
+ }
62
+ has(key) {
63
+ return this.items.has(key);
64
+ }
65
+ keys() {
66
+ return [...this.items.keys()];
67
+ }
68
+ values() {
69
+ return [...this.items.values()];
70
+ }
71
+ entries() {
72
+ return [...this.items.entries()];
73
+ }
74
+ unregister(key) {
75
+ return this.items.delete(key);
76
+ }
77
+ get size() {
78
+ return this.items.size;
79
+ }
80
+ };
81
+
82
+ // src/markdown.ts
83
+ function generateMarkdown(content, payload) {
84
+ const sections = [];
85
+ const messageParts = [];
86
+ messageParts.push(`- type: ${payload.type}`);
87
+ if (payload.agent) messageParts.push(`- agent: ${payload.agent}`);
88
+ if (payload.project) messageParts.push(`- project: ${payload.project}`);
89
+ if (payload.labels?.length) messageParts.push(`- labels: ${payload.labels.join(", ")}`);
90
+ if (payload.priority && payload.priority !== "normal") messageParts.push(`- priority: ${payload.priority}`);
91
+ if (payload.expires) messageParts.push(`- expires: ${payload.expires}`);
92
+ sections.push(`## Message
93
+
94
+ ${messageParts.join("\n")}`);
95
+ sections.push(`## Content
96
+
97
+ ${content}`);
98
+ if (payload.context) {
99
+ const ctx = payload.context;
100
+ const ctxParts = [];
101
+ if (ctx.repo) ctxParts.push(`- repo: ${ctx.repo}`);
102
+ if (ctx.file) ctxParts.push(`- file: ${ctx.file}`);
103
+ if (ctx.lines) ctxParts.push(`- lines: ${ctx.lines}`);
104
+ if (ctx.language) ctxParts.push(`- language: ${ctx.language}`);
105
+ if (ctxParts.length) sections.push(`## Context
106
+
107
+ ${ctxParts.join("\n")}`);
108
+ }
109
+ if (payload.capabilities?.length || payload.require?.length) {
110
+ const capParts = [];
111
+ if (payload.capabilities?.length) capParts.push(`- has: ${payload.capabilities.join(", ")}`);
112
+ if (payload.require?.length) capParts.push(`- require: ${payload.require.join(", ")}`);
113
+ sections.push(`## Capabilities
114
+
115
+ ${capParts.join("\n")}`);
116
+ }
117
+ if (payload.files?.length) {
118
+ const fileParts = payload.files.map((f) => `- ${f}`);
119
+ sections.push(`## Attachments
120
+
121
+ ${fileParts.join("\n")}`);
122
+ }
123
+ if (payload.reply_schema) {
124
+ const schemaLines = generateSchemaMarkdown(payload.reply_schema);
125
+ if (schemaLines.length) sections.push(`## Reply Schema
126
+
127
+ ${schemaLines.join("\n")}`);
128
+ }
129
+ return sections.join("\n\n") + "\n";
130
+ }
131
+ function generatePlainText(content, payload) {
132
+ const parts = [content];
133
+ const meta = [];
134
+ if (payload.context?.file) meta.push(`\u6587\u4EF6: ${payload.context.file}`);
135
+ if (payload.context?.lines) meta.push(`\u884C\u53F7: ${payload.context.lines}`);
136
+ if (payload.context?.repo) meta.push(`\u4ED3\u5E93: ${payload.context.repo}`);
137
+ if (payload.project) meta.push(`\u9879\u76EE: ${payload.project}`);
138
+ if (meta.length) {
139
+ parts.push("---");
140
+ parts.push(meta.join("\n"));
141
+ }
142
+ return parts.join("\n\n") + "\n";
143
+ }
144
+ function parseMarkdown(md) {
145
+ const sections = parseSections(md);
146
+ if (!sections["Message"] || !sections["Content"]) return null;
147
+ const message = parseKV(sections["Message"]);
148
+ const context = sections["Context"] ? parseContextKV(sections["Context"]) : void 0;
149
+ const capabilities = sections["Capabilities"] ? parseKV(sections["Capabilities"]) : void 0;
150
+ return {
151
+ type: message["type"] || "share",
152
+ agent: message["agent"],
153
+ project: message["project"],
154
+ labels: message["labels"]?.split(",").map((l) => l.trim()),
155
+ priority: message["priority"],
156
+ expires: message["expires"],
157
+ content: sections["Content"].trim(),
158
+ context,
159
+ capabilities: capabilities?.["has"]?.split(",").map((c) => c.trim()),
160
+ require: capabilities?.["require"]?.split(",").map((c) => c.trim()),
161
+ attachments: sections["Attachments"] ? sections["Attachments"].split("\n").filter((l) => l.startsWith("- ")).map((l) => l.slice(2).trim()) : void 0,
162
+ replySchema: sections["Reply Schema"] ? sections["Reply Schema"].split("\n").filter((l) => l.startsWith("- ")).map((l) => l.slice(2).trim()) : void 0
163
+ };
164
+ }
165
+ function parseSections(md) {
166
+ const sections = {};
167
+ const lines = md.split("\n");
168
+ let currentSection = "";
169
+ for (const line of lines) {
170
+ const heading = line.match(/^## (.+)$/);
171
+ if (heading) {
172
+ currentSection = heading[1].trim();
173
+ sections[currentSection] = "";
174
+ } else if (currentSection) {
175
+ sections[currentSection] += line + "\n";
176
+ }
177
+ }
178
+ return sections;
179
+ }
180
+ function parseKV(text) {
181
+ const result = {};
182
+ for (const line of text.split("\n")) {
183
+ const match = line.match(/^- (\w[\w\s]*?):\s*(.+)$/);
184
+ if (match) result[match[1].trim()] = match[2].trim();
185
+ }
186
+ return result;
187
+ }
188
+ function parseContextKV(text) {
189
+ const kv = parseKV(text);
190
+ return {
191
+ repo: kv["repo"],
192
+ file: kv["file"],
193
+ lines: kv["lines"],
194
+ language: kv["language"]
195
+ };
196
+ }
197
+ function generateSchemaMarkdown(schema) {
198
+ const props = schema["properties"];
199
+ const required = schema["required"] || [];
200
+ if (!props) return [];
201
+ return Object.entries(props).map(([key, def]) => {
202
+ const type = def["enum"] ? def["enum"].join(" | ") : def["type"] || "string";
203
+ const desc = def["description"] ? ` (${def["description"]})` : "";
204
+ const req = required.includes(key) ? " [\u5FC5\u586B]" : "";
205
+ return `- ${key}: ${type}${desc}${req}`;
206
+ });
207
+ }
208
+
209
+ // src/schemas.ts
210
+ var schemaRegistry = new Registry();
211
+ var RISK_LEVELS = ["low", "medium", "high"];
212
+ var SEVERITY_LEVELS = ["low", "medium", "high", "critical"];
213
+ schemaRegistry.register("nmp:code-review", {
214
+ type: "object",
215
+ properties: {
216
+ approved: { type: "boolean", description: "Approve merge" },
217
+ risk_level: { type: "string", enum: [...RISK_LEVELS], description: "Risk level" },
218
+ comments: { type: "string", description: "Review comments" },
219
+ suggestions: { type: "string", description: "Improvement suggestions" }
220
+ },
221
+ required: ["approved", "risk_level", "comments"]
222
+ });
223
+ schemaRegistry.register("nmp:approval", {
224
+ type: "object",
225
+ properties: {
226
+ approved: { type: "boolean", description: "Approved" },
227
+ reason: { type: "string", description: "Reason" }
228
+ },
229
+ required: ["approved", "reason"]
230
+ });
231
+ schemaRegistry.register("nmp:bug-report", {
232
+ type: "object",
233
+ properties: {
234
+ severity: { type: "string", enum: [...SEVERITY_LEVELS], description: "Severity" },
235
+ steps: { type: "string", description: "Steps to reproduce" },
236
+ expected: { type: "string", description: "Expected behavior" },
237
+ actual: { type: "string", description: "Actual behavior" }
238
+ },
239
+ required: ["severity", "steps", "expected", "actual"]
240
+ });
241
+ schemaRegistry.register("nmp:translation", {
242
+ type: "object",
243
+ properties: {
244
+ translated_text: { type: "string", description: "Translated text" },
245
+ source_language: { type: "string", description: "Source language" },
246
+ target_language: { type: "string", description: "Target language" }
247
+ },
248
+ required: ["translated_text", "source_language", "target_language"]
249
+ });
250
+ function resolveSchema(ref) {
251
+ return schemaRegistry.get(ref) ?? null;
252
+ }
253
+
254
+ // src/validate.ts
255
+ function validatePayload(payload) {
256
+ const errors = [];
257
+ if (!payload || typeof payload !== "object") {
258
+ return { valid: false, errors: ["Payload is not an object"], level: NMP_COMPLIANCE.NONE };
259
+ }
260
+ const p = payload;
261
+ if (p["nmp"] !== NMP_VERSION) {
262
+ errors.push(`Invalid nmp version: ${p["nmp"]}, expected ${NMP_VERSION}`);
263
+ }
264
+ if (!p["type"] || !NMP_TYPES.includes(p["type"])) {
265
+ errors.push(`Invalid type: ${p["type"]}, expected one of ${NMP_TYPES.join(", ")}`);
266
+ }
267
+ if (p["priority"] && !NMP_PRIORITIES.includes(p["priority"])) {
268
+ errors.push(`Invalid priority: ${p["priority"]}, expected one of ${NMP_PRIORITIES.join(", ")}`);
269
+ }
270
+ if (p["expires"] && isNaN(Date.parse(p["expires"]))) {
271
+ errors.push(`Invalid expires: ${p["expires"]}, expected ISO 8601 date`);
272
+ }
273
+ if (p["labels"] && !Array.isArray(p["labels"])) {
274
+ errors.push("labels must be a string array");
275
+ }
276
+ if (p["files"] && !Array.isArray(p["files"])) {
277
+ errors.push("files must be a string array");
278
+ }
279
+ let level = NMP_COMPLIANCE.NONE;
280
+ if (errors.length === 0) {
281
+ level = NMP_COMPLIANCE.BASIC;
282
+ if (p["capabilities"] || p["require"] || p["reply_schema"]) {
283
+ level = NMP_COMPLIANCE.FULL;
284
+ }
285
+ }
286
+ return { valid: errors.length === 0, errors, level };
287
+ }
288
+ function validateMarkdown(md) {
289
+ const errors = [];
290
+ if (md.length > NMP_LIMITS.maxMarkdownSize) {
291
+ errors.push(`Markdown exceeds ${NMP_LIMITS.maxMarkdownSize} bytes limit`);
292
+ }
293
+ if (!md.includes("## Message")) {
294
+ errors.push("Missing required ## Message section");
295
+ }
296
+ if (!md.includes("## Content")) {
297
+ errors.push("Missing required ## Content section");
298
+ }
299
+ const level = errors.length === 0 ? md.includes("## Capabilities") || md.includes("## Reply Schema") ? NMP_COMPLIANCE.FULL : NMP_COMPLIANCE.BASIC : NMP_COMPLIANCE.NONE;
300
+ return { valid: errors.length === 0, errors, level };
301
+ }
302
+
303
+ // src/tools.ts
304
+ var NMP_TOOLS = {
305
+ nothing_send: {
306
+ name: "nothing_send",
307
+ description: 'Send an email or NMP message. Use when the user wants to notify someone, share code, ask a question, or delegate a task. Supports file attachments and project tagging. Example triggers: "send this to bob", "email alice about the bug", "notify the team".',
308
+ inputSchema: {
309
+ type: "object",
310
+ properties: {
311
+ to: { type: "string", description: "Recipient email address (e.g., bob@nothing.email, alice@gmail.com)" },
312
+ text: { type: "string", description: "Message body. For code discussions, include file paths and line numbers." },
313
+ agent: { type: "string", description: 'Your agent identity (e.g., "claude-code", "cursor"). Auto-detected from MCP context.' },
314
+ subject: { type: "string", description: `Subject line. Auto-generated from first ${NMP_DEFAULTS.subjectMaxLength} chars if omitted.` },
315
+ type: { type: "string", enum: [...NMP_TYPES], description: "Message intent: share (FYI), question (expects reply), reply, notify (no reply expected)" },
316
+ project: { type: "string", description: 'Project name for grouping (e.g., "backend-refactor"). Messages with same project are grouped together.' },
317
+ labels: { type: "array", items: { type: "string" }, description: 'Tags for categorization (e.g., ["code-review", "urgent"])' },
318
+ files: { type: "array", items: { type: "string" }, description: 'Absolute file paths to attach (e.g., ["/path/to/file.ts"])' },
319
+ require: { type: "array", items: { type: "string" }, description: 'Capabilities the recipient must have (e.g., ["code-review"]). Recipient can reject if they lack the capability.' },
320
+ priority: { type: "string", enum: [...NMP_PRIORITIES], description: 'Urgency level. Use "urgent" sparingly.' }
321
+ },
322
+ required: ["to", "text"]
323
+ }
324
+ },
325
+ nothing_inbox: {
326
+ name: "nothing_inbox",
327
+ description: 'Check inbox for received messages. Use when the user asks "any new messages?", "check my mail", "what did bob send?", or "show messages from gmail". Supports filtering by channel (gmail/qq/nothing), sender, project, and labels.',
328
+ inputSchema: {
329
+ type: "object",
330
+ properties: {
331
+ unread: { type: "boolean", description: "Only unread messages (default: true). Set false to see all." },
332
+ project: { type: "string", description: "Filter by project name" },
333
+ label: { type: "string", description: "Filter by label" },
334
+ channel: { type: "string", description: "Filter by email channel: gmail, qq, outlook, nothing, local" },
335
+ source: { type: "string", enum: ["nmp", "external"], description: 'Filter by message type: "nmp" for Agent messages, "external" for regular emails' },
336
+ agent: { type: "string", description: 'Filter by sending agent name (exact match, e.g., "claude-code", "cursor", "codex")' },
337
+ limit: { type: "number", description: `Max messages to return (default: ${NMP_DEFAULTS.messageLimit})` }
338
+ }
339
+ }
340
+ },
341
+ nothing_sent: {
342
+ name: "nothing_sent",
343
+ description: 'Check sent messages and their delivery status. Use when the user asks "did my message arrive?", "delivery status", or "what did I send?". Shows 6 statuses: queued \u2192 sent \u2192 delivered \u2192 read \u2192 replied / failed.',
344
+ inputSchema: {
345
+ type: "object",
346
+ properties: {
347
+ limit: { type: "number", description: `Max messages to return (default: ${NMP_DEFAULTS.messageLimit})` },
348
+ project: { type: "string", description: "Filter by project" }
349
+ }
350
+ }
351
+ },
352
+ nothing_read: {
353
+ name: "nothing_read",
354
+ description: 'Read a specific message in full. Use when the user says "open that message", "read it", "show me the details". Returns complete content, code context, attachments, and thread history. Marks message as read.',
355
+ inputSchema: {
356
+ type: "object",
357
+ properties: {
358
+ id: { type: "string", description: "Message ID (from inbox or sent listing)" }
359
+ },
360
+ required: ["id"]
361
+ }
362
+ },
363
+ nothing_reply: {
364
+ name: "nothing_reply",
365
+ description: 'Reply to a message within its thread. Use when the user says "reply to that", "tell them yes", "respond with...". Automatically inherits project, labels, and thread context from the original message.',
366
+ inputSchema: {
367
+ type: "object",
368
+ properties: {
369
+ id: { type: "string", description: "Message ID to reply to" },
370
+ text: { type: "string", description: "Reply body" },
371
+ files: { type: "array", items: { type: "string" }, description: "File paths to attach" }
372
+ },
373
+ required: ["id", "text"]
374
+ }
375
+ },
376
+ nothing_projects: {
377
+ name: "nothing_projects",
378
+ description: 'List all projects with message counts and unread status. Use when the user asks "what projects do I have?", "project overview", or "which projects have unread messages?".',
379
+ inputSchema: {
380
+ type: "object",
381
+ properties: {}
382
+ }
383
+ },
384
+ nothing_report: {
385
+ name: "nothing_report",
386
+ description: 'Generate an activity report showing messages sent/received, project breakdown, items needing reply, and top threads. Use when the user asks "weekly summary", "what happened this week?", "give me a report", or "any messages I missed?".',
387
+ inputSchema: {
388
+ type: "object",
389
+ properties: {
390
+ period: { type: "string", enum: ["today", "week", "month"], description: "Time period (default: week)" },
391
+ project: { type: "string", description: "Filter report to a specific project" }
392
+ }
393
+ }
394
+ }
395
+ };
396
+ export {
397
+ NMP_ATTACHMENT_NAME,
398
+ NMP_CHANNEL_TYPES,
399
+ NMP_COMPLIANCE,
400
+ NMP_DEFAULTS,
401
+ NMP_ERROR_CODES,
402
+ NMP_HEADERS,
403
+ NMP_LIMITS,
404
+ NMP_PRIORITIES,
405
+ NMP_SOURCES,
406
+ NMP_STATUSES,
407
+ NMP_TOOLS,
408
+ NMP_TYPES,
409
+ NMP_VERSION,
410
+ Registry,
411
+ TOKEN_PERMISSIONS,
412
+ generateMarkdown,
413
+ generatePlainText,
414
+ parseMarkdown,
415
+ resolveSchema,
416
+ schemaRegistry,
417
+ validateMarkdown,
418
+ validatePayload
419
+ };
@@ -0,0 +1,140 @@
1
+ /** NMP protocol version */
2
+ declare const NMP_VERSION = 1;
3
+ /** Message types */
4
+ declare const NMP_TYPES: readonly ["share", "question", "reply", "notify"];
5
+ type NmpType = typeof NMP_TYPES[number];
6
+ /** Priority levels */
7
+ declare const NMP_PRIORITIES: readonly ["urgent", "normal", "low"];
8
+ type NmpPriority = typeof NMP_PRIORITIES[number];
9
+ /** Message statuses */
10
+ declare const NMP_STATUSES: readonly ["queued", "sent", "delivered", "read", "replied", "failed"];
11
+ type NmpStatus = typeof NMP_STATUSES[number];
12
+ /** Message sources */
13
+ declare const NMP_SOURCES: readonly ["nmp", "external"];
14
+ type NmpSource = typeof NMP_SOURCES[number];
15
+ /** Channel backend types */
16
+ declare const NMP_CHANNEL_TYPES: readonly ["nothing", "smtp", "stalwart", "local"];
17
+ type NmpChannelType = typeof NMP_CHANNEL_TYPES[number];
18
+ /** Token permissions */
19
+ declare const TOKEN_PERMISSIONS: readonly ["send", "inbox", "read", "reply", "manage"];
20
+ type TokenPermission = typeof TOKEN_PERMISSIONS[number];
21
+ /** Error codes */
22
+ declare const NMP_ERROR_CODES: readonly ["capability_not_supported", "schema_mismatch", "rate_limited", "rejected", "expired", "version_unsupported", "payload_too_large"];
23
+ type NmpErrorCode = typeof NMP_ERROR_CODES[number];
24
+ /** Compliance levels */
25
+ declare const NMP_COMPLIANCE: {
26
+ readonly NONE: 0;
27
+ readonly BASIC: 1;
28
+ readonly FULL: 2;
29
+ };
30
+ type NmpComplianceLevel = typeof NMP_COMPLIANCE[keyof typeof NMP_COMPLIANCE];
31
+ declare const NMP_DEFAULTS: {
32
+ readonly subjectMaxLength: 50;
33
+ readonly messageLimit: 20;
34
+ readonly dailyMessageLimit: 50;
35
+ readonly tokenRateLimit: 10;
36
+ readonly priority: NmpPriority;
37
+ readonly type: NmpType;
38
+ };
39
+ declare const NMP_LIMITS: {
40
+ readonly maxEmailSize: number;
41
+ readonly maxAttachments: 5;
42
+ readonly maxJsonSize: number;
43
+ readonly maxMarkdownSize: number;
44
+ readonly maxPlainTextSize: number;
45
+ };
46
+ declare const NMP_HEADERS: {
47
+ readonly version: "X-NMP-Version";
48
+ readonly type: "X-NMP-Type";
49
+ readonly agent: "X-NMP-Agent";
50
+ readonly project: "X-NMP-Project";
51
+ readonly labels: "X-NMP-Labels";
52
+ readonly priority: "X-NMP-Priority";
53
+ readonly expires: "X-NMP-Expires";
54
+ readonly capabilities: "X-NMP-Capabilities";
55
+ readonly require: "X-NMP-Require";
56
+ readonly replySchema: "X-NMP-Reply-Schema";
57
+ readonly signature: "X-NMP-Signature";
58
+ };
59
+ /** Reserved attachment filename for NMP markdown */
60
+ declare const NMP_ATTACHMENT_NAME = "nmp.md";
61
+ /** Channel configuration */
62
+ interface NmpChannelConfig {
63
+ id: string;
64
+ name: string;
65
+ type: NmpChannelType;
66
+ email: string;
67
+ smtp_host?: string;
68
+ smtp_port?: number;
69
+ imap_host?: string;
70
+ imap_port?: number;
71
+ is_primary: boolean;
72
+ is_active: boolean;
73
+ }
74
+ /** Code context attached to a message */
75
+ interface NmpContext {
76
+ repo?: string;
77
+ file?: string;
78
+ lines?: string;
79
+ language?: string;
80
+ }
81
+ /** Error returned in reply messages */
82
+ interface NmpError {
83
+ code: NmpErrorCode;
84
+ message: string;
85
+ supported?: string[];
86
+ }
87
+ /** NMP JSON payload (Part 3) — metadata only, no message body */
88
+ interface NmpPayload {
89
+ nmp: number;
90
+ type: NmpType;
91
+ agent?: string;
92
+ project?: string;
93
+ labels?: string[];
94
+ priority?: NmpPriority;
95
+ expires?: string;
96
+ ack?: boolean;
97
+ context?: NmpContext;
98
+ files?: string[];
99
+ capabilities?: string[];
100
+ require?: string[];
101
+ reply_schema?: Record<string, unknown>;
102
+ source?: NmpSource;
103
+ error?: NmpError;
104
+ }
105
+ /** Parsed nmp.md content (Part 2) */
106
+ interface NmpMarkdown {
107
+ type: NmpType;
108
+ agent?: string;
109
+ project?: string;
110
+ labels?: string[];
111
+ priority?: NmpPriority;
112
+ expires?: string;
113
+ content: string;
114
+ context?: NmpContext;
115
+ capabilities?: string[];
116
+ require?: string[];
117
+ attachments?: string[];
118
+ replySchema?: string[];
119
+ }
120
+ /** Full NMP message (combined from all parts) */
121
+ interface NmpMessage {
122
+ id: string;
123
+ from: string;
124
+ to: string;
125
+ subject: string;
126
+ date: string;
127
+ inReplyTo?: string;
128
+ references?: string[];
129
+ payload: NmpPayload;
130
+ content: string;
131
+ attachments?: NmpAttachment[];
132
+ }
133
+ /** Attachment metadata */
134
+ interface NmpAttachment {
135
+ filename: string;
136
+ size: number;
137
+ contentType: string;
138
+ }
139
+
140
+ export { type NmpPayload as N, TOKEN_PERMISSIONS as T, type NmpMarkdown as a, type NmpComplianceLevel as b, NMP_ATTACHMENT_NAME as c, NMP_CHANNEL_TYPES as d, NMP_COMPLIANCE as e, NMP_DEFAULTS as f, NMP_ERROR_CODES as g, NMP_HEADERS as h, NMP_LIMITS as i, NMP_PRIORITIES as j, NMP_SOURCES as k, NMP_STATUSES as l, NMP_TYPES as m, NMP_VERSION as n, type NmpAttachment as o, type NmpChannelConfig as p, type NmpChannelType as q, type NmpContext as r, type NmpError as s, type NmpErrorCode as t, type NmpMessage as u, type NmpPriority as v, type NmpSource as w, type NmpStatus as x, type NmpType as y, type TokenPermission as z };
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@nothingmail/nmp",
3
+ "version": "0.1.0",
4
+ "description": "Nothing Message Protocol — AI-native communication protocol",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./api": {
14
+ "types": "./dist/api/index.d.ts",
15
+ "import": "./dist/api/index.js"
16
+ }
17
+ },
18
+ "files": ["dist"],
19
+ "scripts": {
20
+ "build": "tsup",
21
+ "dev": "tsup --watch",
22
+ "test": "vitest run",
23
+ "test:watch": "vitest",
24
+ "lint": "tsc --noEmit"
25
+ },
26
+ "devDependencies": {
27
+ "tsup": "^8.4.0",
28
+ "typescript": "^5.8.3",
29
+ "vitest": "^3.2.1"
30
+ },
31
+ "license": "MIT"
32
+ }