create-me-txt 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.
package/README.md ADDED
@@ -0,0 +1,150 @@
1
+ # create-me-txt
2
+
3
+ CLI tool to generate, validate, and fetch [me.txt](https://metxt.dev) files — the open standard for personal AI-readable identity.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ npx create-me-txt
9
+ ```
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ npm install -g create-me-txt
15
+ # or
16
+ pnpm add -g create-me-txt
17
+ ```
18
+
19
+ This installs two binaries: `create-me-txt` (generator) and `me-txt` (utility commands).
20
+
21
+ ## Usage
22
+
23
+ ### Generate a me.txt
24
+
25
+ Run the interactive wizard:
26
+
27
+ ```bash
28
+ create-me-txt
29
+ # or
30
+ create-me-txt generate
31
+ ```
32
+
33
+ Pre-fill from your GitHub profile:
34
+
35
+ ```bash
36
+ create-me-txt generate --github yourusername
37
+ ```
38
+
39
+ Include all optional sections (Writing, Talks, Optional):
40
+
41
+ ```bash
42
+ create-me-txt generate --full
43
+ ```
44
+
45
+ Skip prompts and generate from flags only:
46
+
47
+ ```bash
48
+ create-me-txt generate --github yourusername --yes
49
+ ```
50
+
51
+ Output as JSON:
52
+
53
+ ```bash
54
+ create-me-txt generate --github yourusername --yes --json
55
+ ```
56
+
57
+ Specify output path:
58
+
59
+ ```bash
60
+ create-me-txt generate -o public/me.txt
61
+ ```
62
+
63
+ ### Validate a me.txt
64
+
65
+ ```bash
66
+ me-txt lint me.txt
67
+ ```
68
+
69
+ Example output:
70
+
71
+ ```
72
+ ✓ Valid me.txt (spec v0.1)
73
+ ℹ 6 sections found: Now, Skills, Stack, Links, Preferences
74
+ ℹ Estimated token count: ~340 tokens
75
+ ```
76
+
77
+ ### Fetch someone's me.txt
78
+
79
+ ```bash
80
+ me-txt fetch example.com
81
+ ```
82
+
83
+ The fetch command tries these URLs in order:
84
+ 1. `https://example.com/me.txt`
85
+ 2. `https://example.com/.well-known/me.txt`
86
+ 3. `https://metxt.dev/api/lookup` (directory fallback)
87
+
88
+ Print full contents:
89
+
90
+ ```bash
91
+ me-txt fetch example.com --print
92
+ ```
93
+
94
+ Save to a file:
95
+
96
+ ```bash
97
+ me-txt fetch example.com --save their-me.txt
98
+ ```
99
+
100
+ ## me.txt Format
101
+
102
+ ```markdown
103
+ # Your Name
104
+
105
+ > One-line summary of who you are and what you do.
106
+
107
+ ## Now
108
+
109
+ What you're currently working on or focused on.
110
+
111
+ ## Skills
112
+
113
+ - Skill 1
114
+ - Skill 2
115
+ - Skill 3
116
+
117
+ ## Stack
118
+
119
+ - Technology 1
120
+ - Technology 2
121
+
122
+ ## Work
123
+
124
+ - [Project Name](url) - Description
125
+ - Company Name - Role
126
+
127
+ ## Links
128
+
129
+ - [GitHub](https://github.com/username): Open source projects
130
+ - [Website](https://example.com): Blog and portfolio
131
+ - [Twitter](https://twitter.com/username): Tech thoughts
132
+
133
+ ## Preferences
134
+
135
+ - Timezone: EST / UTC-5
136
+ - Contact: Email for serious inquiries
137
+ - Response time: 24-48 hours
138
+ ```
139
+
140
+ ## Programmatic API
141
+
142
+ ```typescript
143
+ import { parse } from 'create-me-txt'
144
+ ```
145
+
146
+ The package exports the parser, validator, renderer, and token estimator for use in other tools.
147
+
148
+ ## License
149
+
150
+ MIT
@@ -0,0 +1,2 @@
1
+
2
+ export { }
package/dist/index.js ADDED
@@ -0,0 +1,626 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/commands/generate.ts
7
+ import * as p from "@clack/prompts";
8
+ import pc from "picocolors";
9
+ import { writeFileSync } from "fs";
10
+ import { resolve } from "path";
11
+
12
+ // src/lib/parser.ts
13
+ var LINK_RE = /^-\s+\[([^\]]+)\]\(([^)]+)\)(?:\s*[:\u2014\u2013-]\s*(.+))?$/;
14
+ var AVATAR_RE = /^!\[([^\]]*)\]\(([^)]+)\)$/;
15
+ function parseLink(line) {
16
+ const match = line.match(LINK_RE);
17
+ if (!match) return null;
18
+ return {
19
+ title: match[1],
20
+ url: match[2],
21
+ ...match[3] ? { description: match[3].trim() } : {}
22
+ };
23
+ }
24
+ function parse(content) {
25
+ const lines = content.split("\n");
26
+ let name = "";
27
+ let summary = "";
28
+ let avatar;
29
+ const aboutLines = [];
30
+ const sections = [];
31
+ let currentSection = null;
32
+ let foundFirstH2 = false;
33
+ let foundName = false;
34
+ let foundSummary = false;
35
+ for (const line of lines) {
36
+ if (line.startsWith("# ") && !foundName) {
37
+ name = line.slice(2).trim();
38
+ foundName = true;
39
+ continue;
40
+ }
41
+ if (line.startsWith("> ") && !foundSummary && !foundFirstH2) {
42
+ summary = line.slice(2).trim();
43
+ foundSummary = true;
44
+ continue;
45
+ }
46
+ if (line.startsWith("## ")) {
47
+ foundFirstH2 = true;
48
+ if (currentSection) {
49
+ sections.push(currentSection);
50
+ }
51
+ const heading = line.slice(3).trim();
52
+ currentSection = {
53
+ heading,
54
+ content: [],
55
+ links: [],
56
+ isOptional: heading.toLowerCase() === "optional"
57
+ };
58
+ continue;
59
+ }
60
+ if (!foundFirstH2 && foundName && line.trim()) {
61
+ const avatarMatch = line.match(AVATAR_RE);
62
+ if (avatarMatch && !avatar) {
63
+ avatar = avatarMatch[2];
64
+ continue;
65
+ }
66
+ aboutLines.push(line);
67
+ continue;
68
+ }
69
+ if (currentSection && line.trim()) {
70
+ currentSection.content.push(line);
71
+ const link = parseLink(line);
72
+ if (link) {
73
+ currentSection.links.push(link);
74
+ }
75
+ }
76
+ }
77
+ if (currentSection) {
78
+ sections.push(currentSection);
79
+ }
80
+ return {
81
+ name,
82
+ summary,
83
+ ...avatar ? { avatar } : {},
84
+ ...aboutLines.length > 0 ? { about: aboutLines.join("\n") } : {},
85
+ sections,
86
+ raw: content
87
+ };
88
+ }
89
+
90
+ // src/lib/renderer.ts
91
+ function makeSection(heading, content) {
92
+ return {
93
+ heading,
94
+ content,
95
+ links: content.map((l) => parseLink(l)).filter((l) => l !== null),
96
+ isOptional: heading.toLowerCase() === "optional"
97
+ };
98
+ }
99
+ function render(options) {
100
+ const lines = [];
101
+ lines.push(`# ${options.name}`);
102
+ lines.push("");
103
+ lines.push(`> ${options.summary}`);
104
+ lines.push("");
105
+ if (options.avatar) {
106
+ lines.push(`![${options.name}](${options.avatar})`);
107
+ lines.push("");
108
+ }
109
+ for (const section of options.sections) {
110
+ lines.push(`## ${section.heading}`);
111
+ lines.push("");
112
+ for (const line of section.content) {
113
+ lines.push(line);
114
+ }
115
+ lines.push("");
116
+ }
117
+ return lines.join("\n").trim() + "\n";
118
+ }
119
+
120
+ // src/lib/github.ts
121
+ async function fetchGitHubProfile(username) {
122
+ const userResponse = await fetch(`https://api.github.com/users/${username}`, {
123
+ headers: {
124
+ "Accept": "application/vnd.github.v3+json",
125
+ "User-Agent": "create-me-txt"
126
+ }
127
+ });
128
+ if (!userResponse.ok) {
129
+ if (userResponse.status === 404) {
130
+ throw new Error(`GitHub user "${username}" not found`);
131
+ }
132
+ throw new Error(`GitHub API error: ${userResponse.status}`);
133
+ }
134
+ const user = await userResponse.json();
135
+ const reposResponse = await fetch(
136
+ `https://api.github.com/users/${username}/repos?sort=stars&per_page=10`,
137
+ {
138
+ headers: {
139
+ "Accept": "application/vnd.github.v3+json",
140
+ "User-Agent": "create-me-txt"
141
+ }
142
+ }
143
+ );
144
+ let repos = [];
145
+ if (reposResponse.ok) {
146
+ const reposData = await reposResponse.json();
147
+ repos = reposData.filter((r) => !r.fork).slice(0, 5).map((r) => ({
148
+ name: r.name,
149
+ description: r.description,
150
+ language: r.language,
151
+ stars: r.stargazers_count,
152
+ url: r.html_url
153
+ }));
154
+ }
155
+ return {
156
+ name: user.name || username,
157
+ bio: user.bio || "",
158
+ avatar: user.avatar_url,
159
+ company: user.company,
160
+ location: user.location,
161
+ blog: user.blog,
162
+ twitter: user.twitter_username,
163
+ repos
164
+ };
165
+ }
166
+ function extractLanguages(repos) {
167
+ const languages = /* @__PURE__ */ new Set();
168
+ for (const repo of repos) {
169
+ if (repo.language) {
170
+ languages.add(repo.language);
171
+ }
172
+ }
173
+ return Array.from(languages);
174
+ }
175
+ function formatRepoAsWork(repo) {
176
+ const desc = repo.description ? ` - ${repo.description}` : "";
177
+ return `- [${repo.name}](${repo.url})${desc}`;
178
+ }
179
+
180
+ // src/commands/generate.ts
181
+ async function generate(options) {
182
+ p.intro(pc.cyan("create-me-txt"));
183
+ let name = "";
184
+ let summary = "";
185
+ let prefillAvatar = "";
186
+ let prefillSkills = [];
187
+ let prefillWork = [];
188
+ let prefillLinks = [];
189
+ if (options.github) {
190
+ const spinner2 = p.spinner();
191
+ spinner2.start(`Fetching GitHub profile for ${options.github}...`);
192
+ try {
193
+ const profile = await fetchGitHubProfile(options.github);
194
+ spinner2.stop("GitHub profile loaded!");
195
+ name = profile.name;
196
+ summary = profile.bio;
197
+ if (profile.avatar) prefillAvatar = profile.avatar;
198
+ if (profile.repos.length > 0) {
199
+ prefillSkills = extractLanguages(profile.repos);
200
+ prefillWork = profile.repos.map(formatRepoAsWork);
201
+ }
202
+ if (profile.blog) {
203
+ prefillLinks.push(`- [Website](${profile.blog})`);
204
+ }
205
+ prefillLinks.push(`- [GitHub](https://github.com/${options.github})`);
206
+ if (profile.twitter) {
207
+ prefillLinks.push(`- [Twitter](https://twitter.com/${profile.twitter})`);
208
+ }
209
+ } catch (error) {
210
+ spinner2.stop(pc.red(`Failed to fetch GitHub profile: ${error.message}`));
211
+ }
212
+ }
213
+ if (options.yes) {
214
+ if (!name) name = "Your Name";
215
+ if (!summary) summary = "A short summary about yourself";
216
+ const sections2 = [];
217
+ if (prefillSkills.length > 0) {
218
+ sections2.push(makeSection("Skills", prefillSkills.map((s) => `- ${s}`)));
219
+ }
220
+ if (prefillWork.length > 0) {
221
+ sections2.push(makeSection("Work", prefillWork));
222
+ }
223
+ if (prefillLinks.length > 0) {
224
+ sections2.push(makeSection("Links", prefillLinks));
225
+ }
226
+ const avatar = prefillAvatar || void 0;
227
+ const content2 = render({ name, summary, avatar, sections: sections2 });
228
+ if (options.json) {
229
+ const parsed = parse(content2);
230
+ console.log(JSON.stringify(parsed, null, 2));
231
+ return;
232
+ }
233
+ const outputPath2 = options.output || "me.txt";
234
+ const fullPath2 = resolve(process.cwd(), outputPath2);
235
+ writeFileSync(fullPath2, content2);
236
+ p.outro(pc.green(`\u2713 Saved to ${fullPath2}`));
237
+ return;
238
+ }
239
+ const corePrompts = {
240
+ name: () => p.text({
241
+ message: "What is your name?",
242
+ placeholder: "Jane Doe",
243
+ initialValue: name,
244
+ validate: (value) => {
245
+ if (!value.trim()) return "Name is required";
246
+ }
247
+ }),
248
+ summary: () => p.text({
249
+ message: "Write a one-line summary about yourself.",
250
+ placeholder: "Full-stack developer building tools for developers.",
251
+ initialValue: summary,
252
+ validate: (value) => {
253
+ if (!value.trim()) return "Summary is required";
254
+ }
255
+ }),
256
+ avatar: () => p.text({
257
+ message: "Profile picture URL (optional, press enter to skip)",
258
+ placeholder: "https://example.com/avatar.jpg",
259
+ initialValue: prefillAvatar
260
+ }),
261
+ now: () => p.text({
262
+ message: "What are you currently working on? (Now section)",
263
+ placeholder: "Building a new project, learning Rust, etc."
264
+ }),
265
+ skills: () => p.text({
266
+ message: "List your skills (comma-separated)",
267
+ placeholder: "TypeScript, React, Node.js, PostgreSQL",
268
+ initialValue: prefillSkills.join(", ")
269
+ }),
270
+ stack: () => p.text({
271
+ message: "What is your preferred tech stack? (comma-separated)",
272
+ placeholder: "TypeScript, Next.js, Tailwind, Prisma"
273
+ }),
274
+ work: () => p.text({
275
+ message: "List notable work/projects (one per line, use - prefix)",
276
+ placeholder: "- Project Name - Description",
277
+ initialValue: prefillWork.join("\n")
278
+ }),
279
+ links: () => p.text({
280
+ message: "Add your links (one per line, use markdown format)",
281
+ placeholder: "- [GitHub](https://github.com/username)",
282
+ initialValue: prefillLinks.join("\n")
283
+ }),
284
+ timezone: () => p.text({
285
+ message: "What is your timezone?",
286
+ placeholder: "EST / UTC-5"
287
+ }),
288
+ contactPref: () => p.text({
289
+ message: "How do you prefer to be contacted?",
290
+ placeholder: "Email for serious inquiries, DM for quick questions"
291
+ }),
292
+ responseTime: () => p.text({
293
+ message: "What is your typical response time?",
294
+ placeholder: "24-48 hours"
295
+ })
296
+ };
297
+ const fullPrompts = options.full ? {
298
+ ...corePrompts,
299
+ writing: () => p.text({
300
+ message: "List your writing/publications (one per line, use markdown format)",
301
+ placeholder: "- [Blog Post Title](https://example.com/post)"
302
+ }),
303
+ talks: () => p.text({
304
+ message: "List your talks/presentations (one per line, use markdown format)",
305
+ placeholder: "- [Talk Title](https://youtube.com/...)"
306
+ }),
307
+ optional: () => p.text({
308
+ message: "Anything else? Hobbies, personal details, etc.",
309
+ placeholder: "Coffee enthusiast, dog person, amateur photographer"
310
+ })
311
+ } : corePrompts;
312
+ const answers = await p.group(fullPrompts, {
313
+ onCancel: () => {
314
+ p.cancel("Operation cancelled.");
315
+ process.exit(0);
316
+ }
317
+ });
318
+ const sections = [];
319
+ if (answers.now) {
320
+ sections.push(makeSection("Now", [answers.now]));
321
+ }
322
+ if (answers.skills) {
323
+ sections.push(makeSection("Skills", answers.skills.split(",").map((s) => `- ${s.trim()}`)));
324
+ }
325
+ if (answers.stack) {
326
+ sections.push(makeSection("Stack", answers.stack.split(",").map((s) => `- ${s.trim()}`)));
327
+ }
328
+ if (answers.work) {
329
+ sections.push(makeSection("Work", answers.work.split("\n").filter((l) => l.trim())));
330
+ }
331
+ if ("writing" in answers && answers.writing) {
332
+ sections.push(makeSection("Writing", answers.writing.split("\n").filter((l) => l.trim())));
333
+ }
334
+ if ("talks" in answers && answers.talks) {
335
+ sections.push(makeSection("Talks", answers.talks.split("\n").filter((l) => l.trim())));
336
+ }
337
+ if (answers.links) {
338
+ sections.push(makeSection("Links", answers.links.split("\n").filter((l) => l.trim())));
339
+ }
340
+ const preferences = [];
341
+ if (answers.timezone) preferences.push(`- Timezone: ${answers.timezone}`);
342
+ if (answers.contactPref) preferences.push(`- Contact: ${answers.contactPref}`);
343
+ if (answers.responseTime) preferences.push(`- Response time: ${answers.responseTime}`);
344
+ if (preferences.length > 0) {
345
+ sections.push(makeSection("Preferences", preferences));
346
+ }
347
+ if ("optional" in answers && answers.optional) {
348
+ sections.push(makeSection("Optional", [answers.optional]));
349
+ }
350
+ const content = render({
351
+ name: answers.name,
352
+ summary: answers.summary,
353
+ avatar: answers.avatar || void 0,
354
+ sections
355
+ });
356
+ if (options.json) {
357
+ const parsed = parse(content);
358
+ console.log(JSON.stringify(parsed, null, 2));
359
+ return;
360
+ }
361
+ p.note(content, "Preview");
362
+ const outputPath = options.output || "me.txt";
363
+ const shouldSave = await p.confirm({
364
+ message: `Save to ${outputPath}?`,
365
+ initialValue: true
366
+ });
367
+ if (p.isCancel(shouldSave) || !shouldSave) {
368
+ p.cancel("File not saved.");
369
+ return;
370
+ }
371
+ const fullPath = resolve(process.cwd(), outputPath);
372
+ writeFileSync(fullPath, content);
373
+ p.outro(pc.green(`\u2713 Saved to ${fullPath}`));
374
+ }
375
+
376
+ // src/commands/lint.ts
377
+ import { readFileSync } from "fs";
378
+ import { resolve as resolve2 } from "path";
379
+ import pc2 from "picocolors";
380
+
381
+ // src/types.ts
382
+ var STANDARD_SECTIONS = [
383
+ "Now",
384
+ "Skills",
385
+ "Stack",
386
+ "Work",
387
+ "Writing",
388
+ "Talks",
389
+ "Links",
390
+ "Preferences",
391
+ "Optional"
392
+ ];
393
+ var SPEC_VERSION = "0.1";
394
+
395
+ // src/lib/tokens.ts
396
+ function estimateTokens(content) {
397
+ if (!content.trim()) return 0;
398
+ const stripped = content.replace(/^#{1,6}\s/gm, "").replace(/^>\s/gm, "").replace(/^-\s/gm, "").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/\n{2,}/g, "\n");
399
+ const words = stripped.split(/\s+/).filter(Boolean);
400
+ const charCount = stripped.replace(/\s+/g, "").length;
401
+ const wordEstimate = Math.ceil(words.length * 1.3);
402
+ const charEstimate = Math.ceil(charCount / 4);
403
+ return Math.round((wordEstimate + charEstimate) / 2);
404
+ }
405
+
406
+ // src/lib/validator.ts
407
+ function validate(content) {
408
+ const parsed = parse(content);
409
+ const errors = [];
410
+ const warnings = [];
411
+ const lines = content.split("\n");
412
+ if (!parsed.name) {
413
+ errors.push({ message: "Missing name (H1 heading)", rule: "MISSING_NAME" });
414
+ }
415
+ if (!parsed.summary) {
416
+ errors.push({ message: "Missing summary (blockquote after H1)", rule: "MISSING_SUMMARY" });
417
+ }
418
+ if (parsed.sections.length === 0) {
419
+ errors.push({ message: "No sections found (H2 headings)", rule: "MISSING_SECTION" });
420
+ }
421
+ if (parsed.summary && parsed.summary.length > 200) {
422
+ warnings.push({ message: "Summary exceeds 200 characters", rule: "LONG_SUMMARY" });
423
+ }
424
+ if (lines.length > 500) {
425
+ warnings.push({
426
+ message: `File is ${lines.length} lines (recommended: under 500)`,
427
+ rule: "FILE_TOO_LONG"
428
+ });
429
+ }
430
+ for (const section of parsed.sections) {
431
+ if (section.content.length === 0) {
432
+ warnings.push({
433
+ message: `Section "${section.heading}" is empty`,
434
+ rule: "EMPTY_SECTION"
435
+ });
436
+ }
437
+ if (!STANDARD_SECTIONS.includes(section.heading)) {
438
+ warnings.push({
439
+ message: `Non-standard section "${section.heading}"`,
440
+ rule: "UNKNOWN_SECTION"
441
+ });
442
+ }
443
+ }
444
+ for (let i = 0; i < lines.length; i++) {
445
+ const line = lines[i];
446
+ if (line.startsWith("- [")) {
447
+ const linkMatch = line.match(/^-\s+\[([^\]]*)\]\(([^)]*)\)/);
448
+ if (linkMatch && !linkMatch[2]) {
449
+ errors.push({
450
+ line: i + 1,
451
+ message: `Empty URL in link "${linkMatch[1]}"`,
452
+ rule: "EMPTY_LINK_URL"
453
+ });
454
+ }
455
+ }
456
+ }
457
+ const tokens = estimateTokens(content);
458
+ return {
459
+ valid: errors.length === 0,
460
+ errors,
461
+ warnings,
462
+ info: {
463
+ sectionCount: parsed.sections.length,
464
+ sections: parsed.sections.map((s) => s.heading),
465
+ estimatedTokens: tokens,
466
+ lineCount: lines.length
467
+ }
468
+ };
469
+ }
470
+
471
+ // src/commands/lint.ts
472
+ async function lint(filePath) {
473
+ const fullPath = resolve2(process.cwd(), filePath);
474
+ let content;
475
+ try {
476
+ content = readFileSync(fullPath, "utf-8");
477
+ } catch {
478
+ console.error(pc2.red(`Error: Could not read file "${filePath}"`));
479
+ process.exit(1);
480
+ }
481
+ const result = validate(content);
482
+ console.log();
483
+ if (result.valid) {
484
+ console.log(pc2.green(` \u2713 Valid me.txt (spec v${SPEC_VERSION})`));
485
+ } else {
486
+ console.log(pc2.red(` \u2717 Invalid me.txt (spec v${SPEC_VERSION})`));
487
+ }
488
+ if (result.info.sectionCount > 0) {
489
+ console.log(pc2.cyan(` \u2139 ${result.info.sectionCount} sections found: ${result.info.sections.join(", ")}`));
490
+ }
491
+ console.log(pc2.cyan(` \u2139 Estimated token count: ~${result.info.estimatedTokens} tokens`));
492
+ if (result.errors.length > 0) {
493
+ console.log();
494
+ for (const error of result.errors) {
495
+ const loc = error.line ? ` (line ${error.line})` : "";
496
+ console.log(pc2.red(` \u2717 ${error.message}${loc}`));
497
+ }
498
+ }
499
+ if (result.warnings.length > 0) {
500
+ console.log();
501
+ for (const warning of result.warnings) {
502
+ const loc = warning.line ? ` (line ${warning.line})` : "";
503
+ console.log(pc2.yellow(` \u26A0 ${warning.message}${loc}`));
504
+ }
505
+ }
506
+ console.log();
507
+ if (!result.valid) {
508
+ process.exit(1);
509
+ }
510
+ }
511
+
512
+ // src/commands/fetch.ts
513
+ import { writeFileSync as writeFileSync2 } from "fs";
514
+ import { resolve as resolve3 } from "path";
515
+ import pc3 from "picocolors";
516
+ function buildUrls(urlOrDomain) {
517
+ let base = urlOrDomain;
518
+ if (!base.startsWith("http://") && !base.startsWith("https://")) {
519
+ base = `https://${base}`;
520
+ }
521
+ if (base.endsWith("/me.txt") || base.endsWith("/.well-known/me.txt")) {
522
+ return [base];
523
+ }
524
+ base = base.replace(/\/$/, "");
525
+ return [
526
+ `${base}/me.txt`,
527
+ `${base}/.well-known/me.txt`
528
+ ];
529
+ }
530
+ async function tryFetch(url) {
531
+ try {
532
+ const response = await fetch(url, {
533
+ headers: {
534
+ "User-Agent": "create-me-txt",
535
+ "Accept": "text/plain, text/markdown"
536
+ }
537
+ });
538
+ if (response.ok) {
539
+ return { content: await response.text(), resolvedUrl: url };
540
+ }
541
+ } catch {
542
+ }
543
+ return null;
544
+ }
545
+ async function fetchMeTxt(urlOrDomain, options) {
546
+ const urls = buildUrls(urlOrDomain);
547
+ let result = null;
548
+ for (const url of urls) {
549
+ console.log(pc3.dim(` Fetching ${url}...`));
550
+ result = await tryFetch(url);
551
+ if (result) break;
552
+ }
553
+ if (!result) {
554
+ let domain = urlOrDomain.replace(/^https?:\/\//, "").replace(/\/.*$/, "");
555
+ console.log(pc3.dim(` Trying metxt.dev directory for ${domain}...`));
556
+ try {
557
+ const apiResp = await fetch(`https://metxt.dev/api/lookup?domain=${encodeURIComponent(domain)}`, {
558
+ headers: { "User-Agent": "create-me-txt", "Accept": "application/json" }
559
+ });
560
+ if (apiResp.ok) {
561
+ const data = await apiResp.json();
562
+ if (data.raw_markdown) {
563
+ result = { content: data.raw_markdown, resolvedUrl: data.url || `https://${domain}/me.txt` };
564
+ }
565
+ }
566
+ } catch {
567
+ }
568
+ }
569
+ if (!result) {
570
+ console.error(pc3.red(`
571
+ No me.txt found for "${urlOrDomain}"`));
572
+ console.error(pc3.dim(" Tried:"));
573
+ for (const url of urls) {
574
+ console.error(pc3.dim(` - ${url}`));
575
+ }
576
+ console.error(pc3.dim(" - metxt.dev directory"));
577
+ process.exit(1);
578
+ }
579
+ const { content, resolvedUrl } = result;
580
+ const parsed = parse(content);
581
+ const validation = validate(content);
582
+ console.log();
583
+ console.log(` ${pc3.green("\u2713")} Found valid me.txt for ${pc3.bold(parsed.name)}`);
584
+ if (parsed.summary) {
585
+ console.log(` ${pc3.dim("\u2192")} ${parsed.summary}`);
586
+ }
587
+ console.log();
588
+ console.log(pc3.dim(` Sections: ${validation.info.sections.join(", ")}`));
589
+ console.log(pc3.dim(` Tokens: ~${validation.info.estimatedTokens}`));
590
+ if (!validation.valid) {
591
+ console.log();
592
+ console.log(pc3.yellow(" Warning: This me.txt has validation issues"));
593
+ for (const err of validation.errors) {
594
+ console.log(pc3.yellow(` \u2717 ${err.message}`));
595
+ }
596
+ }
597
+ const savePath = options.output || options.save;
598
+ if (savePath) {
599
+ const outputPath = resolve3(process.cwd(), savePath);
600
+ writeFileSync2(outputPath, content);
601
+ console.log();
602
+ console.log(pc3.green(` \u2713 Saved to ${outputPath}`));
603
+ } else if (options.print) {
604
+ console.log();
605
+ console.log(pc3.dim(" ---"));
606
+ console.log(content);
607
+ } else {
608
+ console.log();
609
+ console.log(pc3.dim(` Use --print to display full contents`));
610
+ console.log(pc3.dim(` Use --save <path> to save locally`));
611
+ }
612
+ }
613
+
614
+ // src/index.ts
615
+ var program = new Command();
616
+ program.name("create-me-txt").description("CLI tool to generate, validate, and fetch me.txt files").version("0.1.0");
617
+ program.command("generate", { isDefault: true }).description("Generate a new me.txt file interactively").option("-g, --github <username>", "Pre-fill from GitHub profile").option("-o, --output <path>", "Output file path", "me.txt").option("--json", "Output as JSON instead of markdown").option("-y, --yes", "Skip prompts, use defaults and flags").option("--full", "Include all optional sections in wizard").action(async (options) => {
618
+ await generate(options);
619
+ });
620
+ program.command("lint <file>").description("Validate a me.txt file").action(async (file) => {
621
+ await lint(file);
622
+ });
623
+ program.command("fetch <url>").description("Fetch and display a me.txt from a URL or domain").option("-o, --output <path>", "Save to file").option("-s, --save <path>", "Save to file (alias for --output)").option("-p, --print", "Print full contents to stdout").action(async (url, options) => {
624
+ await fetchMeTxt(url, options);
625
+ });
626
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "create-me-txt",
3
+ "version": "0.1.0",
4
+ "description": "CLI tool to generate, validate, and fetch me.txt files",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-me-txt": "./dist/index.js",
8
+ "me-txt": "./dist/index.js"
9
+ },
10
+ "exports": {
11
+ ".": {
12
+ "import": "./dist/index.js",
13
+ "types": "./dist/index.d.ts"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsup",
21
+ "dev": "tsup --watch",
22
+ "lint": "tsc --noEmit",
23
+ "test": "vitest run",
24
+ "test:watch": "vitest",
25
+ "test:coverage": "vitest run --coverage",
26
+ "prepublishOnly": "pnpm build"
27
+ },
28
+ "keywords": [
29
+ "me.txt",
30
+ "identity",
31
+ "llm",
32
+ "ai",
33
+ "personal",
34
+ "cli"
35
+ ],
36
+ "author": "",
37
+ "license": "MIT",
38
+ "dependencies": {
39
+ "@clack/prompts": "^0.7.0",
40
+ "commander": "^12.0.0",
41
+ "picocolors": "^1.0.0"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^20.0.0",
45
+ "@vitest/coverage-v8": "^1.0.0",
46
+ "tsup": "^8.0.0",
47
+ "typescript": "^5.0.0",
48
+ "vitest": "^1.0.0"
49
+ },
50
+ "engines": {
51
+ "node": ">=18"
52
+ }
53
+ }