me3-protocol 2.0.0 → 2.2.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/README.md CHANGED
@@ -6,7 +6,7 @@ It treats your online identity as a **"Digital Business Card"**—a single JSON
6
6
 
7
7
  ## Why it exists (brief)
8
8
 
9
- People increasingly ask AI to *find* a person (to hire, collaborate with, or learn from). Scraping arbitrary personal sites is a failure mode for agents: it’s brittle, slow, and ambiguous. `me.json` is a small, predictable identity endpoint that makes discovery and summarization reliable.
9
+ People increasingly ask AI to _find_ a person (to hire, collaborate with, or learn from). Scraping arbitrary personal sites is a failure mode for agents: it’s brittle, slow, and ambiguous. `me.json` is a small, predictable identity endpoint that makes discovery and summarization reliable.
10
10
 
11
11
  ## 1. The Specification
12
12
 
@@ -36,18 +36,19 @@ Your `me.json` defines who you are and where to find you. It is strictly typed t
36
36
 
37
37
  ### Core Structure (`Me3Profile`)
38
38
 
39
- | Field | Type | Required | Description |
40
- | :-------- | :------- | :------- | :------------------------------------------------ |
41
- | `version` | `string` | **Yes** | Protocol version (currently "0.1"). |
42
- | `name` | `string` | **Yes** | Your display name. |
43
- | `handle` | `string` | No | Your preferred username/handle. |
44
- | `bio` | `string` | No | Short bio (max 500 chars). |
45
- | `avatar` | `string` | No | URL to your profile picture. |
46
- | `banner` | `string` | No | URL to a header/banner image. |
47
- | `location`| `string` | No | Freeform location string (e.g. "Remote"). |
48
- | `links` | `object` | No | Social links (website, github, twitter, etc.). |
49
- | `buttons` | `array` | No | Primary actions (e.g., "Book Call", "Subscribe"). |
50
- | `pages` | `array` | No | Custom content pages. |
39
+ | Field | Type | Required | Description |
40
+ | :--------- | :------- | :------- | :------------------------------------------------ |
41
+ | `version` | `string` | **Yes** | Protocol version (currently "0.1"). |
42
+ | `name` | `string` | **Yes** | Your display name. |
43
+ | `handle` | `string` | No | Your preferred username/handle. |
44
+ | `bio` | `string` | No | Short bio (max 500 chars). |
45
+ | `avatar` | `string` | No | URL to your profile picture. |
46
+ | `banner` | `string` | No | URL to a header/banner image. |
47
+ | `location` | `string` | No | Freeform location string (e.g. "Remote"). |
48
+ | `links` | `object` | No | Social links (website, github, twitter, etc.). |
49
+ | `buttons` | `array` | No | Primary actions (e.g., "Book Call", "Subscribe"). |
50
+ | `pages` | `array` | No | Custom content pages. |
51
+ | `footer` | `object` | No | Custom footer config (or `false` to hide). |
51
52
 
52
53
  ### Examples
53
54
 
package/dist/index.d.ts CHANGED
@@ -35,6 +35,60 @@ export interface Me3Button {
35
35
  /** Optional icon (emoji or icon identifier) */
36
36
  icon?: string;
37
37
  }
38
+ export interface Me3FooterLink {
39
+ /** Link text */
40
+ text: string;
41
+ /** URL to open when clicked */
42
+ url: string;
43
+ }
44
+ export interface Me3Footer {
45
+ /** Custom footer text (e.g. "Built by Jane") */
46
+ text?: string;
47
+ /** Optional custom footer link */
48
+ link?: Me3FooterLink;
49
+ }
50
+ /**
51
+ * Newsletter subscription intent.
52
+ * When enabled, the site accepts email subscriptions via POST /api/subscribe
53
+ */
54
+ export interface Me3IntentSubscribe {
55
+ /** Whether newsletter signups are enabled */
56
+ enabled: boolean;
57
+ /** Newsletter title (e.g., "AI Weekly") - for agents to present context */
58
+ title?: string;
59
+ /** What subscribers will receive - for agents to explain the value */
60
+ description?: string;
61
+ /** How often subscribers will hear from you */
62
+ frequency?: "daily" | "weekly" | "monthly" | "irregular";
63
+ }
64
+ /**
65
+ * Booking/scheduling intent.
66
+ * Declares that the person accepts meeting bookings.
67
+ */
68
+ export interface Me3IntentBook {
69
+ /** Whether booking is enabled */
70
+ enabled: boolean;
71
+ /** Meeting title (e.g., "30-min Consultation") */
72
+ title?: string;
73
+ /** What the meeting is about */
74
+ description?: string;
75
+ /** Meeting duration in minutes */
76
+ duration?: number;
77
+ /** Booking provider (e.g., "cal.com", "calendly") */
78
+ provider?: string;
79
+ /** Direct booking URL */
80
+ url: string;
81
+ }
82
+ /**
83
+ * Intents object - declares what actions visitors/agents can take.
84
+ * This is the machine-readable API contract for interacting with a person.
85
+ */
86
+ export interface Me3Intents {
87
+ /** Newsletter subscription */
88
+ subscribe?: Me3IntentSubscribe;
89
+ /** Meeting booking */
90
+ book?: Me3IntentBook;
91
+ }
38
92
  export interface Me3Profile {
39
93
  /** Protocol version */
40
94
  version: string;
@@ -56,6 +110,17 @@ export interface Me3Profile {
56
110
  buttons?: Me3Button[];
57
111
  /** Custom pages (markdown) */
58
112
  pages?: Me3Page[];
113
+ /**
114
+ * Custom footer configuration.
115
+ * - `undefined`: default footer behavior (renderer-defined)
116
+ * - `false`: hide footer (renderer may restrict this to Pro tiers)
117
+ */
118
+ footer?: Me3Footer | false;
119
+ /**
120
+ * Intents - machine-readable actions that visitors/agents can take.
121
+ * This is the API contract for interacting with the person.
122
+ */
123
+ intents?: Me3Intents;
59
124
  }
60
125
  export interface ValidationError {
61
126
  field: string;
package/dist/index.js CHANGED
@@ -19,6 +19,11 @@ const MAX_BUTTONS = 3;
19
19
  const MAX_BUTTON_TEXT_LENGTH = 30;
20
20
  const VALID_BUTTON_STYLES = ["primary", "secondary", "outline"];
21
21
  const URL_REGEX = /^https?:\/\/.+/i;
22
+ const MAX_FOOTER_TEXT_LENGTH = 200;
23
+ const MAX_FOOTER_LINK_TEXT_LENGTH = 60;
24
+ const MAX_INTENT_TITLE_LENGTH = 100;
25
+ const MAX_INTENT_DESCRIPTION_LENGTH = 300;
26
+ const VALID_FREQUENCIES = ["daily", "weekly", "monthly", "irregular"];
22
27
  /**
23
28
  * Validate a me3 profile object
24
29
  */
@@ -167,6 +172,70 @@ function validateProfile(data) {
167
172
  });
168
173
  }
169
174
  }
175
+ // Footer (optional)
176
+ if (profile.footer !== undefined) {
177
+ if (profile.footer === false) {
178
+ // ok (renderer may enforce tier restrictions)
179
+ }
180
+ else if (typeof profile.footer !== "object" || profile.footer === null) {
181
+ errors.push({
182
+ field: "footer",
183
+ message: "Footer must be an object or false",
184
+ });
185
+ }
186
+ else {
187
+ const footer = profile.footer;
188
+ if (footer.text !== undefined) {
189
+ if (typeof footer.text !== "string") {
190
+ errors.push({
191
+ field: "footer.text",
192
+ message: "Footer text must be a string",
193
+ });
194
+ }
195
+ else if (footer.text.length > MAX_FOOTER_TEXT_LENGTH) {
196
+ errors.push({
197
+ field: "footer.text",
198
+ message: `Footer text must be ${MAX_FOOTER_TEXT_LENGTH} characters or less`,
199
+ });
200
+ }
201
+ }
202
+ if (footer.link !== undefined) {
203
+ if (typeof footer.link !== "object" || footer.link === null) {
204
+ errors.push({
205
+ field: "footer.link",
206
+ message: "Footer link must be an object",
207
+ });
208
+ }
209
+ else {
210
+ const link = footer.link;
211
+ if (!link.text || typeof link.text !== "string") {
212
+ errors.push({
213
+ field: "footer.link.text",
214
+ message: "Footer link text is required",
215
+ });
216
+ }
217
+ else if (link.text.length > MAX_FOOTER_LINK_TEXT_LENGTH) {
218
+ errors.push({
219
+ field: "footer.link.text",
220
+ message: `Footer link text must be ${MAX_FOOTER_LINK_TEXT_LENGTH} characters or less`,
221
+ });
222
+ }
223
+ if (!link.url || typeof link.url !== "string") {
224
+ errors.push({
225
+ field: "footer.link.url",
226
+ message: "Footer link URL is required",
227
+ });
228
+ }
229
+ else if (!URL_REGEX.test(link.url)) {
230
+ errors.push({
231
+ field: "footer.link.url",
232
+ message: "Footer link URL must be a valid URL starting with http:// or https://",
233
+ });
234
+ }
235
+ }
236
+ }
237
+ }
238
+ }
170
239
  // Pages (optional)
171
240
  if (profile.pages !== undefined) {
172
241
  if (!Array.isArray(profile.pages)) {
@@ -208,6 +277,141 @@ function validateProfile(data) {
208
277
  });
209
278
  }
210
279
  }
280
+ // Intents (optional)
281
+ if (profile.intents !== undefined) {
282
+ if (typeof profile.intents !== "object" || profile.intents === null) {
283
+ errors.push({ field: "intents", message: "Intents must be an object" });
284
+ }
285
+ else {
286
+ const intents = profile.intents;
287
+ // Validate subscribe intent
288
+ if (intents.subscribe !== undefined) {
289
+ if (typeof intents.subscribe !== "object" ||
290
+ intents.subscribe === null) {
291
+ errors.push({
292
+ field: "intents.subscribe",
293
+ message: "Subscribe intent must be an object",
294
+ });
295
+ }
296
+ else {
297
+ const subscribe = intents.subscribe;
298
+ if (typeof subscribe.enabled !== "boolean") {
299
+ errors.push({
300
+ field: "intents.subscribe.enabled",
301
+ message: "Subscribe enabled must be a boolean",
302
+ });
303
+ }
304
+ if (subscribe.title !== undefined) {
305
+ if (typeof subscribe.title !== "string") {
306
+ errors.push({
307
+ field: "intents.subscribe.title",
308
+ message: "Subscribe title must be a string",
309
+ });
310
+ }
311
+ else if (subscribe.title.length > MAX_INTENT_TITLE_LENGTH) {
312
+ errors.push({
313
+ field: "intents.subscribe.title",
314
+ message: `Subscribe title must be ${MAX_INTENT_TITLE_LENGTH} characters or less`,
315
+ });
316
+ }
317
+ }
318
+ if (subscribe.description !== undefined) {
319
+ if (typeof subscribe.description !== "string") {
320
+ errors.push({
321
+ field: "intents.subscribe.description",
322
+ message: "Subscribe description must be a string",
323
+ });
324
+ }
325
+ else if (subscribe.description.length > MAX_INTENT_DESCRIPTION_LENGTH) {
326
+ errors.push({
327
+ field: "intents.subscribe.description",
328
+ message: `Subscribe description must be ${MAX_INTENT_DESCRIPTION_LENGTH} characters or less`,
329
+ });
330
+ }
331
+ }
332
+ if (subscribe.frequency !== undefined &&
333
+ !VALID_FREQUENCIES.includes(subscribe.frequency)) {
334
+ errors.push({
335
+ field: "intents.subscribe.frequency",
336
+ message: `Subscribe frequency must be one of: ${VALID_FREQUENCIES.join(", ")}`,
337
+ });
338
+ }
339
+ }
340
+ }
341
+ // Validate book intent
342
+ if (intents.book !== undefined) {
343
+ if (typeof intents.book !== "object" || intents.book === null) {
344
+ errors.push({
345
+ field: "intents.book",
346
+ message: "Book intent must be an object",
347
+ });
348
+ }
349
+ else {
350
+ const book = intents.book;
351
+ if (typeof book.enabled !== "boolean") {
352
+ errors.push({
353
+ field: "intents.book.enabled",
354
+ message: "Book enabled must be a boolean",
355
+ });
356
+ }
357
+ if (book.title !== undefined) {
358
+ if (typeof book.title !== "string") {
359
+ errors.push({
360
+ field: "intents.book.title",
361
+ message: "Book title must be a string",
362
+ });
363
+ }
364
+ else if (book.title.length > MAX_INTENT_TITLE_LENGTH) {
365
+ errors.push({
366
+ field: "intents.book.title",
367
+ message: `Book title must be ${MAX_INTENT_TITLE_LENGTH} characters or less`,
368
+ });
369
+ }
370
+ }
371
+ if (book.description !== undefined) {
372
+ if (typeof book.description !== "string") {
373
+ errors.push({
374
+ field: "intents.book.description",
375
+ message: "Book description must be a string",
376
+ });
377
+ }
378
+ else if (book.description.length > MAX_INTENT_DESCRIPTION_LENGTH) {
379
+ errors.push({
380
+ field: "intents.book.description",
381
+ message: `Book description must be ${MAX_INTENT_DESCRIPTION_LENGTH} characters or less`,
382
+ });
383
+ }
384
+ }
385
+ if (book.duration !== undefined &&
386
+ typeof book.duration !== "number") {
387
+ errors.push({
388
+ field: "intents.book.duration",
389
+ message: "Book duration must be a number (minutes)",
390
+ });
391
+ }
392
+ if (book.provider !== undefined &&
393
+ typeof book.provider !== "string") {
394
+ errors.push({
395
+ field: "intents.book.provider",
396
+ message: "Book provider must be a string",
397
+ });
398
+ }
399
+ if (!book.url || typeof book.url !== "string") {
400
+ errors.push({
401
+ field: "intents.book.url",
402
+ message: "Book URL is required",
403
+ });
404
+ }
405
+ else if (!URL_REGEX.test(book.url)) {
406
+ errors.push({
407
+ field: "intents.book.url",
408
+ message: "Book URL must be a valid URL starting with http:// or https://",
409
+ });
410
+ }
411
+ }
412
+ }
413
+ }
414
+ }
211
415
  if (errors.length > 0) {
212
416
  return { valid: false, errors };
213
417
  }
@@ -1,31 +1,47 @@
1
1
  {
2
2
  "version": "0.1",
3
- "name": "Kieran Butler",
4
- "handle": "kieran",
5
- "location": "Austin, TX",
6
- "bio": "Intuitive. Coach. Coder. Creative. Building Soulink.",
7
- "avatar": "https://api.dicebear.com/7.x/avataaars/svg?seed=kieran",
3
+ "name": "Example Name",
4
+ "handle": "example",
5
+ "location": "City, Country",
6
+ "bio": "Short example bio describing what this person does.",
7
+ "avatar": "https://api.dicebear.com/7.x/avataaars/svg?seed=example",
8
8
  "banner": "https://images.unsplash.com/photo-1519681393784-d120267933ba?w=1200&h=400&fit=crop",
9
9
  "links": {
10
- "website": "https://kieranbutler.com",
11
- "github": "kieranbutler",
12
- "soulink": "kieran"
10
+ "website": "https://example.com",
11
+ "github": "example",
12
+ "soulink": "example"
13
13
  },
14
14
  "buttons": [
15
15
  {
16
- "text": "Book a Discovery Call",
17
- "url": "https://cal.com/kieran",
16
+ "text": "Primary Call To Action",
17
+ "url": "https://cal.com/example",
18
18
  "style": "primary",
19
19
  "icon": "📅"
20
20
  },
21
21
  {
22
- "text": "Buy me a coffee",
23
- "url": "https://buymeacoffee.com/kieran",
22
+ "text": "Secondary Action",
23
+ "url": "https://buymeacoffee.com/example",
24
24
  "style": "secondary",
25
25
  "icon": "☕"
26
26
  }
27
27
  ],
28
28
  "pages": [
29
29
  { "slug": "about", "title": "About", "file": "about.md", "visible": true }
30
- ]
30
+ ],
31
+ "intents": {
32
+ "subscribe": {
33
+ "enabled": true,
34
+ "title": "Weekly Insights",
35
+ "description": "Curated thoughts on design, tech, and creativity every Sunday",
36
+ "frequency": "weekly"
37
+ },
38
+ "book": {
39
+ "enabled": true,
40
+ "title": "30-min Consultation",
41
+ "description": "Let's discuss your project",
42
+ "duration": 30,
43
+ "provider": "cal.com",
44
+ "url": "https://cal.com/example"
45
+ }
46
+ }
31
47
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "me3-protocol",
3
- "version": "2.0.0",
3
+ "version": "2.2.0",
4
4
  "description": "",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
package/schema.json CHANGED
@@ -33,6 +33,120 @@
33
33
  ],
34
34
  "type": "object"
35
35
  },
36
+ "Me3Footer": {
37
+ "additionalProperties": false,
38
+ "properties": {
39
+ "link": {
40
+ "$ref": "#/definitions/Me3FooterLink",
41
+ "description": "Optional custom footer link"
42
+ },
43
+ "text": {
44
+ "description": "Custom footer text (e.g. \"Built by Jane\")",
45
+ "type": "string"
46
+ }
47
+ },
48
+ "type": "object"
49
+ },
50
+ "Me3FooterLink": {
51
+ "additionalProperties": false,
52
+ "properties": {
53
+ "text": {
54
+ "description": "Link text",
55
+ "type": "string"
56
+ },
57
+ "url": {
58
+ "description": "URL to open when clicked",
59
+ "type": "string"
60
+ }
61
+ },
62
+ "required": [
63
+ "text",
64
+ "url"
65
+ ],
66
+ "type": "object"
67
+ },
68
+ "Me3IntentBook": {
69
+ "additionalProperties": false,
70
+ "description": "Booking/scheduling intent. Declares that the person accepts meeting bookings.",
71
+ "properties": {
72
+ "description": {
73
+ "description": "What the meeting is about",
74
+ "type": "string"
75
+ },
76
+ "duration": {
77
+ "description": "Meeting duration in minutes",
78
+ "type": "number"
79
+ },
80
+ "enabled": {
81
+ "description": "Whether booking is enabled",
82
+ "type": "boolean"
83
+ },
84
+ "provider": {
85
+ "description": "Booking provider (e.g., \"cal.com\", \"calendly\")",
86
+ "type": "string"
87
+ },
88
+ "title": {
89
+ "description": "Meeting title (e.g., \"30-min Consultation\")",
90
+ "type": "string"
91
+ },
92
+ "url": {
93
+ "description": "Direct booking URL",
94
+ "type": "string"
95
+ }
96
+ },
97
+ "required": [
98
+ "enabled",
99
+ "url"
100
+ ],
101
+ "type": "object"
102
+ },
103
+ "Me3IntentSubscribe": {
104
+ "additionalProperties": false,
105
+ "description": "Newsletter subscription intent. When enabled, the site accepts email subscriptions via POST /api/subscribe",
106
+ "properties": {
107
+ "description": {
108
+ "description": "What subscribers will receive - for agents to explain the value",
109
+ "type": "string"
110
+ },
111
+ "enabled": {
112
+ "description": "Whether newsletter signups are enabled",
113
+ "type": "boolean"
114
+ },
115
+ "frequency": {
116
+ "description": "How often subscribers will hear from you",
117
+ "enum": [
118
+ "daily",
119
+ "weekly",
120
+ "monthly",
121
+ "irregular"
122
+ ],
123
+ "type": "string"
124
+ },
125
+ "title": {
126
+ "description": "Newsletter title (e.g., \"AI Weekly\") - for agents to present context",
127
+ "type": "string"
128
+ }
129
+ },
130
+ "required": [
131
+ "enabled"
132
+ ],
133
+ "type": "object"
134
+ },
135
+ "Me3Intents": {
136
+ "additionalProperties": false,
137
+ "description": "Intents object - declares what actions visitors/agents can take. This is the machine-readable API contract for interacting with a person.",
138
+ "properties": {
139
+ "book": {
140
+ "$ref": "#/definitions/Me3IntentBook",
141
+ "description": "Meeting booking"
142
+ },
143
+ "subscribe": {
144
+ "$ref": "#/definitions/Me3IntentSubscribe",
145
+ "description": "Newsletter subscription"
146
+ }
147
+ },
148
+ "type": "object"
149
+ },
36
150
  "Me3Links": {
37
151
  "additionalProperties": {
38
152
  "anyOf": [
@@ -123,10 +237,26 @@
123
237
  },
124
238
  "type": "array"
125
239
  },
240
+ "footer": {
241
+ "anyOf": [
242
+ {
243
+ "$ref": "#/definitions/Me3Footer"
244
+ },
245
+ {
246
+ "const": false,
247
+ "type": "boolean"
248
+ }
249
+ ],
250
+ "description": "Custom footer configuration.\n- `undefined`: default footer behavior (renderer-defined)\n- `false`: hide footer (renderer may restrict this to Pro tiers)"
251
+ },
126
252
  "handle": {
127
253
  "description": "Username/handle",
128
254
  "type": "string"
129
255
  },
256
+ "intents": {
257
+ "$ref": "#/definitions/Me3Intents",
258
+ "description": "Intents - machine-readable actions that visitors/agents can take. This is the API contract for interacting with the person."
259
+ },
130
260
  "links": {
131
261
  "$ref": "#/definitions/Me3Links",
132
262
  "description": "Social and external links"