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 +150 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +626 -0
- package/package.json +53 -0
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
|
package/dist/index.d.ts
ADDED
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(``);
|
|
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
|
+
}
|