@ha7ch/cv-pro 0.5.1

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,117 @@
1
+ # cv-pro
2
+
3
+ Turn a PDF resume into a living personal site at **cv.ha7ch.com/{handle}**.
4
+
5
+ Drop a PDF, paste text, or describe a change — an AI agent does the rest.
6
+
7
+ ---
8
+
9
+ ## If you are an AI agent helping a user set up cv-pro
10
+
11
+ Follow these steps exactly, in order. No browser required.
12
+
13
+ **Step 1 — Ask the user one question before doing anything else:**
14
+
15
+ > What handle do you want? Your resume will be live at `cv.ha7ch.com/{handle}`.
16
+
17
+ **Step 2 — Register and log in (one command)**
18
+
19
+ ```bash
20
+ npx cv-pro@latest register {handle}
21
+ ```
22
+
23
+ This claims the handle and saves the token automatically. Output: `Logged in as @{handle}`.
24
+
25
+ If the handle is taken, the error will say so — ask the user to pick another one and retry.
26
+
27
+ **Step 3 — Publish first resume (optional)**
28
+
29
+ Ask: *Do you have a resume PDF or any content to publish now?*
30
+
31
+ - **PDF** — read the file, extract all fields into JSON, save to `/tmp/resume.json`, then:
32
+ ```bash
33
+ npx cv-pro@latest update /tmp/resume.json
34
+ ```
35
+ - **Text / paste** — same flow, extract from pasted content.
36
+ - **Later** — skip. The page is already live with an empty template.
37
+
38
+ **Step 4 — Confirm**
39
+
40
+ ```bash
41
+ npx cv-pro@latest whoami
42
+ ```
43
+
44
+ Tell the user their live URL: **https://cv.ha7ch.com/{handle}**
45
+
46
+ For future edits: drop a PDF or describe changes. Sections: `header`, `personalInfo`, `experience`, `education`, `projectsRecent`, `projectsDetailed`, `skills`, `contact`.
47
+
48
+ Each `experience` and `projectsDetailed` entry takes a `bullets: string[]` field — these render as the role's bullet points on the page. Skipping them leaves the entry as just a header line.
49
+
50
+ ---
51
+
52
+ ## CLI reference
53
+
54
+ ```bash
55
+ npx cv-pro@latest register <handle> # claim handle + auto-login (no browser)
56
+ npx cv-pro@latest login cv_pat_... # save an existing token
57
+ npx cv-pro@latest whoami # show handle + page URL
58
+ npx cv-pro@latest get # print current resume JSON
59
+ npx cv-pro@latest get --variant=openai # print one stored variant JSON
60
+ npx cv-pro@latest update resume.json # replace full resume
61
+ npx cv-pro@latest update-section <section> data.json
62
+ npx cv-pro@latest open # open live page in browser
63
+ npx cv-pro@latest open --json # open the public JSON view
64
+ ```
65
+
66
+ ## Public JSON URL
67
+
68
+ Every resume is also served as raw JSON at **`https://cv.ha7ch.com/{handle}.json`** — no auth, CORS-open, fetch-friendly for any AI agent.
69
+
70
+ `CV_TOKEN` env var overrides saved credentials.
71
+
72
+ ## MCP
73
+
74
+ Replace `cv_pat_...` with your token.
75
+
76
+ **Claude Code**
77
+ ```bash
78
+ claude mcp add cv --transport http https://cv.ha7ch.com/api/mcp \
79
+ --header "Authorization: Bearer cv_pat_..."
80
+ ```
81
+
82
+ **Cursor** — `~/.cursor/mcp.json`
83
+ ```json
84
+ {
85
+ "mcpServers": {
86
+ "cv": {
87
+ "type": "http",
88
+ "url": "https://cv.ha7ch.com/api/mcp",
89
+ "headers": { "Authorization": "Bearer cv_pat_..." }
90
+ }
91
+ }
92
+ }
93
+ ```
94
+
95
+ **Codex** — `~/.codex/config.json`
96
+ ```json
97
+ {
98
+ "mcpServers": {
99
+ "cv": {
100
+ "type": "http",
101
+ "url": "https://cv.ha7ch.com/api/mcp",
102
+ "headers": { "Authorization": "Bearer cv_pat_..." }
103
+ }
104
+ }
105
+ }
106
+ ```
107
+
108
+ Tools: `get_resume` · `update_resume` · `update_section`
109
+
110
+ ## Troubleshooting
111
+
112
+ **Sandboxed agents fail to connect.** Some hosted AI agents (Claude Code Cloud, ChatGPT Code Interpreter, etc.) run inside an egress allowlist that blocks `cv.ha7ch.com`. The CLI surfaces this as `Sandbox blocked egress to ... (host_not_allowed)`. There is no client-side workaround — run from local Claude Code, or attach the MCP server (`/api/mcp`) as a Custom Connector on claude.ai, which uses a different egress.
113
+
114
+ ## Links
115
+
116
+ - Site: [cv.ha7ch.com](https://cv.ha7ch.com)
117
+ - Repo: [github.com/LAWTED/cv-pro](https://github.com/LAWTED/cv-pro)
package/dist/api.js ADDED
@@ -0,0 +1,129 @@
1
+ async function throwServerError(res) {
2
+ const body = (await res.json().catch(() => ({})));
3
+ let msg = body.error ?? res.statusText;
4
+ if (body.issues?.length) {
5
+ msg +=
6
+ "\n" +
7
+ body.issues
8
+ .map((i) => ` - ${i.path || "(root)"}: ${i.message}`)
9
+ .join("\n");
10
+ }
11
+ if (body.allowed?.length) {
12
+ msg += `\n Allowed: ${body.allowed.join(", ")}`;
13
+ }
14
+ throw new Error(msg);
15
+ }
16
+ // Some hosted AI agents (Claude Code Cloud, ChatGPT Code Interpreter)
17
+ // run inside sandboxes whose egress proxy denies arbitrary hosts and
18
+ // returns 403 with `x-deny-reason: host_not_allowed` BEFORE the request
19
+ // reaches our server. Surface this clearly instead of letting it look
20
+ // like an auth failure.
21
+ function checkEgressBlock(cfg, res) {
22
+ if (res.status !== 403)
23
+ return;
24
+ const denyReason = res.headers.get("x-deny-reason");
25
+ if (denyReason !== "host_not_allowed")
26
+ return;
27
+ throw new Error(`Sandbox blocked egress to ${cfg.apiBase} (x-deny-reason: host_not_allowed).\n` +
28
+ `This environment cannot reach cv-pro. Workarounds:\n` +
29
+ ` • Run from local Claude Code (CLI on your own machine)\n` +
30
+ ` • Use claude.ai with a Custom Connector → https://cv.ha7ch.com/api/mcp\n` +
31
+ ` • Ask the agent host to allowlist cv.ha7ch.com`);
32
+ }
33
+ async function req(cfg, path, opts = {}) {
34
+ const url = `${cfg.apiBase}${path}`;
35
+ const res = await fetch(url, {
36
+ ...opts,
37
+ headers: {
38
+ "Authorization": `Bearer ${cfg.token}`,
39
+ "Content-Type": "application/json",
40
+ ...(opts.headers ?? {}),
41
+ },
42
+ });
43
+ checkEgressBlock(cfg, res);
44
+ return res;
45
+ }
46
+ export async function register(handle, apiBase) {
47
+ const url = `${apiBase}/api/register`;
48
+ const res = await fetch(url, {
49
+ method: "POST",
50
+ headers: { "Content-Type": "application/json" },
51
+ body: JSON.stringify({ handle }),
52
+ });
53
+ checkEgressBlock({ token: "", apiBase }, res);
54
+ const data = await res.json();
55
+ if (!res.ok)
56
+ throw new Error(data.error ?? res.statusText);
57
+ return { handle: data.handle, token: data.token };
58
+ }
59
+ export async function whoami(cfg) {
60
+ const res = await req(cfg, "/api/v1/resume");
61
+ if (res.status === 404)
62
+ return { ok: false, reason: "no-resume" };
63
+ if (res.status === 401 || res.status === 403) {
64
+ return { ok: false, reason: "unauthorized" };
65
+ }
66
+ if (!res.ok)
67
+ return { ok: false, reason: "error" };
68
+ const data = await res.json();
69
+ return { ok: true, username: data.username };
70
+ }
71
+ export async function getResume(cfg) {
72
+ const res = await req(cfg, "/api/v1/resume");
73
+ if (!res.ok)
74
+ await throwServerError(res);
75
+ return res.json();
76
+ }
77
+ export async function putResume(cfg, data) {
78
+ const res = await req(cfg, "/api/v1/resume", {
79
+ method: "PUT",
80
+ body: JSON.stringify(data),
81
+ });
82
+ if (!res.ok)
83
+ await throwServerError(res);
84
+ return res.json();
85
+ }
86
+ export async function patchSection(cfg, section, value) {
87
+ const res = await req(cfg, "/api/v1/resume", {
88
+ method: "PATCH",
89
+ body: JSON.stringify({ section, value }),
90
+ });
91
+ if (!res.ok)
92
+ await throwServerError(res);
93
+ return res.json();
94
+ }
95
+ export async function getSchema(apiBase) {
96
+ const res = await fetch(`${apiBase}/api/v1/schema`);
97
+ checkEgressBlock({ token: "", apiBase }, res);
98
+ if (!res.ok)
99
+ await throwServerError(res);
100
+ return res.json();
101
+ }
102
+ export async function listVariants(cfg) {
103
+ const res = await req(cfg, "/api/v1/variants");
104
+ if (!res.ok)
105
+ await throwServerError(res);
106
+ return res.json();
107
+ }
108
+ export async function getVariant(cfg, audience) {
109
+ const res = await req(cfg, `/api/v1/variants/${encodeURIComponent(audience)}`);
110
+ if (!res.ok)
111
+ await throwServerError(res);
112
+ return res.json();
113
+ }
114
+ export async function putVariant(cfg, audience, data) {
115
+ const res = await req(cfg, `/api/v1/variants/${encodeURIComponent(audience)}`, {
116
+ method: "PUT",
117
+ body: JSON.stringify(data),
118
+ });
119
+ if (!res.ok)
120
+ await throwServerError(res);
121
+ return res.json();
122
+ }
123
+ export async function deleteVariant(cfg, audience) {
124
+ const res = await req(cfg, `/api/v1/variants/${encodeURIComponent(audience)}`, {
125
+ method: "DELETE",
126
+ });
127
+ if (!res.ok)
128
+ await throwServerError(res);
129
+ }
package/dist/config.js ADDED
@@ -0,0 +1,33 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
4
+ const CONFIG_DIR = join(homedir(), ".cv");
5
+ const CONFIG_FILE = join(CONFIG_DIR, "config.json");
6
+ export const DEFAULT_API = "https://cv.ha7ch.com";
7
+ export function loadConfig() {
8
+ const envToken = process.env.CV_TOKEN;
9
+ if (envToken) {
10
+ return {
11
+ token: envToken,
12
+ handle: process.env.CV_HANDLE,
13
+ apiBase: process.env.CV_API ?? DEFAULT_API,
14
+ };
15
+ }
16
+ if (!existsSync(CONFIG_FILE))
17
+ return null;
18
+ try {
19
+ return JSON.parse(readFileSync(CONFIG_FILE, "utf8"));
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ }
25
+ export function saveConfig(config) {
26
+ if (!existsSync(CONFIG_DIR))
27
+ mkdirSync(CONFIG_DIR, { recursive: true });
28
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), "utf8");
29
+ }
30
+ export function clearConfig() {
31
+ if (existsSync(CONFIG_FILE))
32
+ writeFileSync(CONFIG_FILE, "{}", "utf8");
33
+ }
package/dist/index.js ADDED
@@ -0,0 +1,278 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from "node:fs";
3
+ import { loadConfig, saveConfig, clearConfig, DEFAULT_API } from "./config.js";
4
+ import { register, whoami, getResume, putResume, patchSection, getSchema, listVariants, getVariant, putVariant, deleteVariant } from "./api.js";
5
+ const VERSION = "0.5.0";
6
+ const VARIANT_FLAG = "--variant=";
7
+ function printVariantReminder(apiBase, handle) {
8
+ if (!handle)
9
+ return;
10
+ console.log("");
11
+ console.log("Tip — create tailored variants for each audience:");
12
+ console.log("");
13
+ console.log(" • Company variant → share via");
14
+ console.log(` ${apiBase}/${handle}?company=openai`);
15
+ console.log(" • Role variant → share via");
16
+ console.log(` ${apiBase}/${handle}?role=designer`);
17
+ console.log(" • Language variant → share via");
18
+ console.log(` ${apiBase}/${handle}?lang=en`);
19
+ console.log("");
20
+ console.log(`Use 'cv-pro set-variant <key> [file]' to store a tailored version.`);
21
+ console.log(`See ${apiBase}/llms.txt for the full variant workflow.`);
22
+ }
23
+ const HELP = `
24
+ aicv — AI-native resume CLI (cv.ha7ch.com)
25
+
26
+ USAGE
27
+ cv-pro <command> [options]
28
+
29
+ COMMANDS
30
+ register <handle> Claim a handle and get a token (no browser needed)
31
+ login <token> Save an existing personal access token
32
+ logout Remove saved credentials
33
+ whoami Show authenticated handle
34
+ schema Show resume schema (section names + field shapes)
35
+ get [--variant=<key>] Print current resume (or one variant) as JSON
36
+ update [file] Replace entire resume from a JSON file (or stdin)
37
+ update-section <section> Update one section from JSON file (or stdin)
38
+ variants List all stored variants with links
39
+ get-variant <key> Print variant JSON
40
+ set-variant <key> [file] Create/update a variant from file or stdin
41
+ delete-variant <key> Delete a variant
42
+ open [--json] [--variant=<key>] Open resume page in browser
43
+
44
+ SECTIONS
45
+ header, personalInfo, experience, education,
46
+ projectsRecent, projectsDetailed, skills, contact
47
+
48
+ Run 'cv-pro schema' for the full field-level structure.
49
+
50
+ EXAMPLES
51
+ cv-pro register lawted
52
+ cv-pro whoami
53
+ cv-pro get
54
+ cv-pro get --variant=openai
55
+ cv-pro update resume.json
56
+ cv-pro update-section experience experience.json
57
+ echo '{"name":"Lawted"}' | cv update-section header
58
+ cv-pro set-variant openai resume_openai.json
59
+ cv-pro variants
60
+ cv-pro open --variant=openai
61
+
62
+ ENV
63
+ CV_TOKEN token (overrides saved config)
64
+ CV_HANDLE handle (required when using CV_TOKEN)
65
+ CV_API API base URL (default: https://cv.ha7ch.com)
66
+ `.trim();
67
+ async function main() {
68
+ const args = process.argv.slice(2);
69
+ const cmd = args[0];
70
+ if (!cmd || cmd === "--help" || cmd === "help" || cmd === "-h") {
71
+ console.log(HELP);
72
+ return;
73
+ }
74
+ if (cmd === "--version" || cmd === "-v") {
75
+ console.log(VERSION);
76
+ return;
77
+ }
78
+ // register
79
+ if (cmd === "register") {
80
+ const handle = args[1]?.toLowerCase().trim();
81
+ if (!handle)
82
+ die("Usage: cv-pro register <handle>");
83
+ process.stdout.write(`Registering @${handle}… `);
84
+ const apiBase = process.env.CV_API ?? DEFAULT_API;
85
+ const result = await register(handle, apiBase);
86
+ console.log("✓");
87
+ saveConfig({ token: result.token, handle: result.handle, apiBase });
88
+ console.log(`Logged in as @${result.handle}`);
89
+ console.log(`Page: ${apiBase}/${result.handle}`);
90
+ console.log(`JSON: ${apiBase}/${result.handle}.json (public, AI-readable)`);
91
+ return;
92
+ }
93
+ // login
94
+ if (cmd === "login") {
95
+ const token = args[1];
96
+ if (!token) {
97
+ die("Usage: cv login <token>\nGet a token at cv.ha7ch.com");
98
+ }
99
+ if (!token.startsWith("cv_pat_")) {
100
+ die("Token must start with cv_pat_");
101
+ }
102
+ // verify token by calling whoami
103
+ const cfg = { token, apiBase: DEFAULT_API };
104
+ process.stdout.write("Verifying token… ");
105
+ const result = await whoami(cfg);
106
+ if (!result.ok) {
107
+ if (result.reason === "no-resume") {
108
+ die("\nToken valid, but no resume on file. Run: cv-pro register <handle>");
109
+ }
110
+ if (result.reason === "unauthorized")
111
+ die("\nInvalid token.");
112
+ die("\nServer error. Try again.");
113
+ }
114
+ console.log(`✓\nLogged in as @${result.username}`);
115
+ saveConfig({ token, handle: result.username, apiBase: DEFAULT_API });
116
+ return;
117
+ }
118
+ // logout
119
+ if (cmd === "logout") {
120
+ clearConfig();
121
+ console.log("Logged out.");
122
+ return;
123
+ }
124
+ // schema — public, no auth needed
125
+ if (cmd === "schema") {
126
+ const apiBase = loadConfig()?.apiBase ?? process.env.CV_API ?? DEFAULT_API;
127
+ const wantJson = args.slice(1).includes("--json");
128
+ const result = await getSchema(apiBase);
129
+ console.log(wantJson ? JSON.stringify(result.json, null, 2) : result.text);
130
+ return;
131
+ }
132
+ // commands that need auth
133
+ const config = loadConfig();
134
+ if (!config?.token) {
135
+ die("Not logged in. Run: cv-pro login <token>\nGet a token at cv.ha7ch.com");
136
+ }
137
+ if (cmd === "whoami") {
138
+ let handle = config.handle;
139
+ if (!handle) {
140
+ const result = await whoami(config);
141
+ if (!result.ok) {
142
+ if (result.reason === "unauthorized")
143
+ die("Invalid token.");
144
+ if (result.reason === "no-resume") {
145
+ die("Token valid, but no resume on file. Run: cv-pro register <handle>");
146
+ }
147
+ die("Server unreachable.");
148
+ }
149
+ handle = result.username;
150
+ }
151
+ console.log(`@${handle}`);
152
+ console.log(`Page: ${config.apiBase}/${handle}`);
153
+ console.log(`JSON: ${config.apiBase}/${handle}.json (public, AI-readable)`);
154
+ return;
155
+ }
156
+ if (cmd === "get") {
157
+ const variantArg = args.find((arg) => arg.startsWith(VARIANT_FLAG));
158
+ const variantKey = variantArg?.slice(VARIANT_FLAG.length);
159
+ const data = variantKey ? await getVariant(config, variantKey) : await getResume(config);
160
+ console.log(JSON.stringify(data, null, 2));
161
+ return;
162
+ }
163
+ if (cmd === "update") {
164
+ const data = readJsonArg(args[1], "resume data");
165
+ process.stdout.write("Updating resume… ");
166
+ await putResume(config, data);
167
+ console.log(`✓\nView at ${config.apiBase}/${config.handle}`);
168
+ printVariantReminder(config.apiBase, config.handle);
169
+ return;
170
+ }
171
+ if (cmd === "update-section") {
172
+ const section = args[1];
173
+ if (!section)
174
+ die("Usage: cv update-section <section> [file]");
175
+ const value = readJsonArg(args[2], `section '${section}'`);
176
+ process.stdout.write(`Updating ${section}… `);
177
+ await patchSection(config, section, value);
178
+ console.log(`✓\nView at ${config.apiBase}/${config.handle}`);
179
+ if (section === "experience" || section === "projectsRecent" || section === "projectsDetailed") {
180
+ printVariantReminder(config.apiBase, config.handle);
181
+ }
182
+ return;
183
+ }
184
+ if (cmd === "open") {
185
+ const wantJson = args.slice(1).includes("--json");
186
+ const variantArg = args.slice(1).find((arg) => arg.startsWith(VARIANT_FLAG));
187
+ const variantKey = variantArg?.slice(VARIANT_FLAG.length);
188
+ let url;
189
+ if (variantKey) {
190
+ url = `${config.apiBase}/${config.handle}?company=${variantKey}`;
191
+ }
192
+ else if (wantJson) {
193
+ url = `${config.apiBase}/${config.handle}.json`;
194
+ }
195
+ else {
196
+ url = `${config.apiBase}/${config.handle}`;
197
+ }
198
+ const { execSync } = await import("node:child_process");
199
+ const opener = process.platform === "darwin" ? "open" :
200
+ process.platform === "win32" ? "start" : "xdg-open";
201
+ execSync(`${opener} "${url}"`);
202
+ console.log(url);
203
+ return;
204
+ }
205
+ if (cmd === "variants") {
206
+ const variants = await listVariants(config);
207
+ if (variants.length === 0) {
208
+ console.log("No variants yet. Use 'cv-pro set-variant <key> [file]' to create one.");
209
+ return;
210
+ }
211
+ console.log("Stored variants:\n");
212
+ for (const { audience, updatedAt } of variants) {
213
+ const date = updatedAt.slice(0, 10);
214
+ console.log(` ${audience.padEnd(16)} updated ${date}`);
215
+ console.log(` ?company=${audience} → ${config.apiBase}/${config.handle}?company=${audience}`);
216
+ console.log(` ?role=${audience} → ${config.apiBase}/${config.handle}?role=${audience}`);
217
+ console.log(` ?lang=${audience} → ${config.apiBase}/${config.handle}?lang=${audience}`);
218
+ console.log("");
219
+ }
220
+ return;
221
+ }
222
+ if (cmd === "get-variant") {
223
+ const key = args[1];
224
+ if (!key)
225
+ die("Usage: cv-pro get-variant <key>");
226
+ const data = await getVariant(config, key);
227
+ console.log(JSON.stringify(data, null, 2));
228
+ return;
229
+ }
230
+ if (cmd === "set-variant") {
231
+ const key = args[1];
232
+ if (!key)
233
+ die("Usage: cv-pro set-variant <key> [file]");
234
+ const data = readJsonArg(args[2], `variant '${key}'`);
235
+ process.stdout.write(`Saving variant '${key}'… `);
236
+ await putVariant(config, key, data);
237
+ console.log(`✓`);
238
+ console.log(`View at ${config.apiBase}/${config.handle}?company=${key}`);
239
+ console.log(`(Also works with ?role=${key} or ?lang=${key})`);
240
+ return;
241
+ }
242
+ if (cmd === "delete-variant") {
243
+ const key = args[1];
244
+ if (!key)
245
+ die("Usage: cv-pro delete-variant <key>");
246
+ process.stdout.write(`Deleting variant '${key}'… `);
247
+ await deleteVariant(config, key);
248
+ console.log("✓");
249
+ return;
250
+ }
251
+ die(`Unknown command: ${cmd}\nRun 'cv-pro help' to see available commands.`);
252
+ }
253
+ function readJsonArg(filePath, label) {
254
+ if (filePath) {
255
+ try {
256
+ return JSON.parse(readFileSync(filePath, "utf8"));
257
+ }
258
+ catch {
259
+ die(`Could not read ${label} from file: ${filePath}`);
260
+ }
261
+ }
262
+ // try stdin
263
+ try {
264
+ const stdin = readFileSync("/dev/stdin", "utf8").trim();
265
+ if (stdin)
266
+ return JSON.parse(stdin);
267
+ }
268
+ catch { }
269
+ die(`Provide ${label} as a file path or pipe JSON via stdin.`);
270
+ }
271
+ function die(msg) {
272
+ console.error(`Error: ${msg}`);
273
+ process.exit(1);
274
+ }
275
+ main().catch((err) => {
276
+ console.error("Error:", err instanceof Error ? err.message : err);
277
+ process.exit(1);
278
+ });
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@ha7ch/cv-pro",
3
+ "version": "0.5.1",
4
+ "description": "AI-native resume CLI — update your living resume at cv.ha7ch.com from Claude Code or any terminal",
5
+ "homepage": "https://cv.ha7ch.com",
6
+ "repository": "https://github.com/LAWTED/cv-pro",
7
+ "license": "MIT",
8
+ "type": "module",
9
+ "bin": {
10
+ "cv-pro": "./dist/index.js"
11
+ },
12
+ "files": [
13
+ "dist"
14
+ ],
15
+ "publishConfig": {
16
+ "access": "public",
17
+ "registry": "https://registry.npmjs.org/"
18
+ },
19
+ "scripts": {
20
+ "build": "tsc",
21
+ "dev": "tsx src/index.ts",
22
+ "prepublishOnly": "npm run build"
23
+ },
24
+ "devDependencies": {
25
+ "@types/node": "^20",
26
+ "typescript": "^5",
27
+ "tsx": "^4"
28
+ },
29
+ "engines": {
30
+ "node": ">=18"
31
+ }
32
+ }