@ha7ch/cv-pro 0.5.1 → 0.5.2

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
@@ -17,7 +17,7 @@ Follow these steps exactly, in order. No browser required.
17
17
  **Step 2 — Register and log in (one command)**
18
18
 
19
19
  ```bash
20
- npx cv-pro@latest register {handle}
20
+ npx @ha7ch/cv-pro@latest register {handle}
21
21
  ```
22
22
 
23
23
  This claims the handle and saves the token automatically. Output: `Logged in as @{handle}`.
@@ -30,7 +30,7 @@ Ask: *Do you have a resume PDF or any content to publish now?*
30
30
 
31
31
  - **PDF** — read the file, extract all fields into JSON, save to `/tmp/resume.json`, then:
32
32
  ```bash
33
- npx cv-pro@latest update /tmp/resume.json
33
+ npx @ha7ch/cv-pro@latest update /tmp/resume.json
34
34
  ```
35
35
  - **Text / paste** — same flow, extract from pasted content.
36
36
  - **Later** — skip. The page is already live with an empty template.
@@ -38,7 +38,7 @@ Ask: *Do you have a resume PDF or any content to publish now?*
38
38
  **Step 4 — Confirm**
39
39
 
40
40
  ```bash
41
- npx cv-pro@latest whoami
41
+ npx @ha7ch/cv-pro@latest whoami
42
42
  ```
43
43
 
44
44
  Tell the user their live URL: **https://cv.ha7ch.com/{handle}**
@@ -52,15 +52,15 @@ Each `experience` and `projectsDetailed` entry takes a `bullets: string[]` field
52
52
  ## CLI reference
53
53
 
54
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
55
+ npx @ha7ch/cv-pro@latest register <handle> # claim handle + auto-login (no browser)
56
+ npx @ha7ch/cv-pro@latest login cv_pat_... # save an existing token
57
+ npx @ha7ch/cv-pro@latest whoami # show handle + page URL
58
+ npx @ha7ch/cv-pro@latest get # print current resume JSON
59
+ npx @ha7ch/cv-pro@latest get --variant=openai # print one stored variant JSON
60
+ npx @ha7ch/cv-pro@latest update resume.json # replace full resume
61
+ npx @ha7ch/cv-pro@latest update-section <section> data.json
62
+ npx @ha7ch/cv-pro@latest open # open live page in browser
63
+ npx @ha7ch/cv-pro@latest open --json # open the public JSON view
64
64
  ```
65
65
 
66
66
  ## Public JSON URL
@@ -105,7 +105,7 @@ claude mcp add cv --transport http https://cv.ha7ch.com/api/mcp \
105
105
  }
106
106
  ```
107
107
 
108
- Tools: `get_resume` · `update_resume` · `update_section`
108
+ Tools: `get_schema` · `get_resume` · `update_resume` · `update_section` · `list_variants` · `get_variant` · `set_variant` · `delete_variant`
109
109
 
110
110
  ## Troubleshooting
111
111
 
@@ -114,4 +114,4 @@ Tools: `get_resume` · `update_resume` · `update_section`
114
114
  ## Links
115
115
 
116
116
  - Site: [cv.ha7ch.com](https://cv.ha7ch.com)
117
- - Repo: [github.com/LAWTED/cv-pro](https://github.com/LAWTED/cv-pro)
117
+ - Repo: [github.com/HA7CH/cv-pro](https://github.com/HA7CH/cv-pro)
package/dist/api.js CHANGED
@@ -1,5 +1,29 @@
1
+ async function parseJson(res) {
2
+ const ct = res.headers.get("content-type") ?? "";
3
+ const raw = await res.text().catch(() => "");
4
+ if (!ct.toLowerCase().includes("json")) {
5
+ throw new Error(`Server returned non-JSON (status ${res.status}): ${raw.slice(0, 200)}`);
6
+ }
7
+ try {
8
+ return JSON.parse(raw);
9
+ }
10
+ catch {
11
+ throw new Error(`Server returned non-JSON (status ${res.status}): ${raw.slice(0, 200)}`);
12
+ }
13
+ }
1
14
  async function throwServerError(res) {
2
- const body = (await res.json().catch(() => ({})));
15
+ const ct = res.headers.get("content-type") ?? "";
16
+ const raw = await res.text().catch(() => "");
17
+ if (!ct.toLowerCase().includes("json")) {
18
+ throw new Error(`Server returned non-JSON (status ${res.status}): ${raw.slice(0, 200)}`);
19
+ }
20
+ let body = {};
21
+ try {
22
+ body = JSON.parse(raw);
23
+ }
24
+ catch {
25
+ // fall through with empty body
26
+ }
3
27
  let msg = body.error ?? res.statusText;
4
28
  if (body.issues?.length) {
5
29
  msg +=
@@ -51,7 +75,7 @@ export async function register(handle, apiBase) {
51
75
  body: JSON.stringify({ handle }),
52
76
  });
53
77
  checkEgressBlock({ token: "", apiBase }, res);
54
- const data = await res.json();
78
+ const data = await parseJson(res);
55
79
  if (!res.ok)
56
80
  throw new Error(data.error ?? res.statusText);
57
81
  return { handle: data.handle, token: data.token };
@@ -65,14 +89,14 @@ export async function whoami(cfg) {
65
89
  }
66
90
  if (!res.ok)
67
91
  return { ok: false, reason: "error" };
68
- const data = await res.json();
92
+ const data = await parseJson(res);
69
93
  return { ok: true, username: data.username };
70
94
  }
71
95
  export async function getResume(cfg) {
72
96
  const res = await req(cfg, "/api/v1/resume");
73
97
  if (!res.ok)
74
98
  await throwServerError(res);
75
- return res.json();
99
+ return parseJson(res);
76
100
  }
77
101
  export async function putResume(cfg, data) {
78
102
  const res = await req(cfg, "/api/v1/resume", {
@@ -81,7 +105,7 @@ export async function putResume(cfg, data) {
81
105
  });
82
106
  if (!res.ok)
83
107
  await throwServerError(res);
84
- return res.json();
108
+ return parseJson(res);
85
109
  }
86
110
  export async function patchSection(cfg, section, value) {
87
111
  const res = await req(cfg, "/api/v1/resume", {
@@ -90,26 +114,26 @@ export async function patchSection(cfg, section, value) {
90
114
  });
91
115
  if (!res.ok)
92
116
  await throwServerError(res);
93
- return res.json();
117
+ return parseJson(res);
94
118
  }
95
119
  export async function getSchema(apiBase) {
96
120
  const res = await fetch(`${apiBase}/api/v1/schema`);
97
121
  checkEgressBlock({ token: "", apiBase }, res);
98
122
  if (!res.ok)
99
123
  await throwServerError(res);
100
- return res.json();
124
+ return parseJson(res);
101
125
  }
102
126
  export async function listVariants(cfg) {
103
127
  const res = await req(cfg, "/api/v1/variants");
104
128
  if (!res.ok)
105
129
  await throwServerError(res);
106
- return res.json();
130
+ return parseJson(res);
107
131
  }
108
132
  export async function getVariant(cfg, audience) {
109
133
  const res = await req(cfg, `/api/v1/variants/${encodeURIComponent(audience)}`);
110
134
  if (!res.ok)
111
135
  await throwServerError(res);
112
- return res.json();
136
+ return parseJson(res);
113
137
  }
114
138
  export async function putVariant(cfg, audience, data) {
115
139
  const res = await req(cfg, `/api/v1/variants/${encodeURIComponent(audience)}`, {
@@ -118,7 +142,7 @@ export async function putVariant(cfg, audience, data) {
118
142
  });
119
143
  if (!res.ok)
120
144
  await throwServerError(res);
121
- return res.json();
145
+ return parseJson(res);
122
146
  }
123
147
  export async function deleteVariant(cfg, audience) {
124
148
  const res = await req(cfg, `/api/v1/variants/${encodeURIComponent(audience)}`, {
@@ -127,3 +151,15 @@ export async function deleteVariant(cfg, audience) {
127
151
  if (!res.ok)
128
152
  await throwServerError(res);
129
153
  }
154
+ // Plain-text resume is public (served at /:handle.txt), so no auth is needed —
155
+ // just the handle. A variant key maps onto ?company= (the highest-precedence
156
+ // variant param, which also resolves ?role=/?lang= keys server-side).
157
+ export async function getResumeText(apiBase, handle, variant) {
158
+ const query = variant ? `?company=${encodeURIComponent(variant)}` : "";
159
+ const res = await fetch(`${apiBase}/${handle}.txt${query}`);
160
+ checkEgressBlock({ token: "", apiBase }, res);
161
+ if (!res.ok) {
162
+ throw new Error(`Could not fetch text resume (status ${res.status})`);
163
+ }
164
+ return res.text();
165
+ }
package/dist/index.js CHANGED
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import { readFileSync } from "node:fs";
2
+ import { readFileSync, writeFileSync } from "node:fs";
3
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";
4
+ import { register, whoami, getResume, putResume, patchSection, getSchema, listVariants, getVariant, putVariant, deleteVariant, getResumeText } from "./api.js";
5
+ const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
6
+ const VERSION = pkg.version;
6
7
  const VARIANT_FLAG = "--variant=";
7
8
  function printVariantReminder(apiBase, handle) {
8
9
  if (!handle)
@@ -21,7 +22,7 @@ function printVariantReminder(apiBase, handle) {
21
22
  console.log(`See ${apiBase}/llms.txt for the full variant workflow.`);
22
23
  }
23
24
  const HELP = `
24
- aicvAI-native resume CLI (cv.ha7ch.com)
25
+ cv-proyour resume as a living site (cv.ha7ch.com)
25
26
 
26
27
  USAGE
27
28
  cv-pro <command> [options]
@@ -33,6 +34,8 @@ COMMANDS
33
34
  whoami Show authenticated handle
34
35
  schema Show resume schema (section names + field shapes)
35
36
  get [--variant=<key>] Print current resume (or one variant) as JSON
37
+ export [--txt|--json] Save resume as plain text (default) or JSON
38
+ [--variant=<key>] [-o <file>]
36
39
  update [file] Replace entire resume from a JSON file (or stdin)
37
40
  update-section <section> Update one section from JSON file (or stdin)
38
41
  variants List all stored variants with links
@@ -52,9 +55,11 @@ EXAMPLES
52
55
  cv-pro whoami
53
56
  cv-pro get
54
57
  cv-pro get --variant=openai
58
+ cv-pro export -o resume.txt
59
+ cv-pro export --json --variant=openai -o resume_openai.json
55
60
  cv-pro update resume.json
56
61
  cv-pro update-section experience experience.json
57
- echo '{"name":"Lawted"}' | cv update-section header
62
+ echo '{"name":"Lawted"}' | cv-pro update-section header
58
63
  cv-pro set-variant openai resume_openai.json
59
64
  cv-pro variants
60
65
  cv-pro open --variant=openai
@@ -92,9 +97,9 @@ async function main() {
92
97
  }
93
98
  // login
94
99
  if (cmd === "login") {
95
- const token = args[1];
100
+ const token = args[1]?.trim();
96
101
  if (!token) {
97
- die("Usage: cv login <token>\nGet a token at cv.ha7ch.com");
102
+ die("Usage: cv-pro login <token>\nGet a token at cv.ha7ch.com");
98
103
  }
99
104
  if (!token.startsWith("cv_pat_")) {
100
105
  die("Token must start with cv_pat_");
@@ -160,24 +165,54 @@ async function main() {
160
165
  console.log(JSON.stringify(data, null, 2));
161
166
  return;
162
167
  }
168
+ if (cmd === "export") {
169
+ const rest = args.slice(1);
170
+ const wantJson = rest.includes("--json");
171
+ const variantArg = rest.find((arg) => arg.startsWith(VARIANT_FLAG));
172
+ const variantKey = variantArg?.slice(VARIANT_FLAG.length);
173
+ const outIdx = rest.findIndex((arg) => arg === "-o" || arg === "--out");
174
+ const outFile = outIdx >= 0 ? rest[outIdx + 1] : undefined;
175
+ if (outIdx >= 0 && !outFile) {
176
+ die("Usage: cv-pro export [--txt|--json] [--variant=<key>] [-o <file>]");
177
+ }
178
+ const handle = await resolveHandle(config);
179
+ let content;
180
+ if (wantJson) {
181
+ const data = variantKey ? await getVariant(config, variantKey) : await getResume(config);
182
+ content = JSON.stringify(data, null, 2) + "\n";
183
+ }
184
+ else {
185
+ content = await getResumeText(config.apiBase, handle, variantKey);
186
+ }
187
+ if (outFile) {
188
+ writeFileSync(outFile, content);
189
+ console.log(`Saved ${outFile}`);
190
+ }
191
+ else {
192
+ process.stdout.write(content);
193
+ }
194
+ return;
195
+ }
163
196
  if (cmd === "update") {
164
197
  const data = readJsonArg(args[1], "resume data");
165
198
  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);
199
+ const saved = await putResume(config, data);
200
+ const handle = await resolveHandle(config, saved.username);
201
+ console.log(`✓\nView at ${config.apiBase}/${handle}`);
202
+ printVariantReminder(config.apiBase, handle);
169
203
  return;
170
204
  }
171
205
  if (cmd === "update-section") {
172
206
  const section = args[1];
173
207
  if (!section)
174
- die("Usage: cv update-section <section> [file]");
208
+ die("Usage: cv-pro update-section <section> [file]");
175
209
  const value = readJsonArg(args[2], `section '${section}'`);
176
210
  process.stdout.write(`Updating ${section}… `);
177
- await patchSection(config, section, value);
178
- console.log(`✓\nView at ${config.apiBase}/${config.handle}`);
211
+ const saved = await patchSection(config, section, value);
212
+ const handle = await resolveHandle(config, saved.username);
213
+ console.log(`✓\nView at ${config.apiBase}/${handle}`);
179
214
  if (section === "experience" || section === "projectsRecent" || section === "projectsDetailed") {
180
- printVariantReminder(config.apiBase, config.handle);
215
+ printVariantReminder(config.apiBase, handle);
181
216
  }
182
217
  return;
183
218
  }
@@ -233,9 +268,10 @@ async function main() {
233
268
  die("Usage: cv-pro set-variant <key> [file]");
234
269
  const data = readJsonArg(args[2], `variant '${key}'`);
235
270
  process.stdout.write(`Saving variant '${key}'… `);
236
- await putVariant(config, key, data);
271
+ const saved = await putVariant(config, key, data);
272
+ const handle = await resolveHandle(config, saved.username);
237
273
  console.log(`✓`);
238
- console.log(`View at ${config.apiBase}/${config.handle}?company=${key}`);
274
+ console.log(`View at ${config.apiBase}/${handle}?company=${key}`);
239
275
  console.log(`(Also works with ?role=${key} or ?lang=${key})`);
240
276
  return;
241
277
  }
@@ -250,6 +286,16 @@ async function main() {
250
286
  }
251
287
  die(`Unknown command: ${cmd}\nRun 'cv-pro help' to see available commands.`);
252
288
  }
289
+ async function resolveHandle(cfg, fromResponse) {
290
+ if (cfg.handle)
291
+ return cfg.handle;
292
+ if (fromResponse)
293
+ return fromResponse;
294
+ const result = await whoami(cfg);
295
+ if (result.ok)
296
+ return result.username;
297
+ die("Could not determine handle. Set CV_HANDLE or run 'cv-pro login <token>'.");
298
+ }
253
299
  function readJsonArg(filePath, label) {
254
300
  if (filePath) {
255
301
  try {
@@ -259,7 +305,11 @@ function readJsonArg(filePath, label) {
259
305
  die(`Could not read ${label} from file: ${filePath}`);
260
306
  }
261
307
  }
262
- // try stdin
308
+ // try stdin, but only if it's actually piped — otherwise readFileSync("/dev/stdin")
309
+ // blocks the terminal forever waiting for input
310
+ if (process.stdin.isTTY) {
311
+ die(`Provide ${label} as a file path or pipe JSON via stdin.`);
312
+ }
263
313
  try {
264
314
  const stdin = readFileSync("/dev/stdin", "utf8").trim();
265
315
  if (stdin)
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@ha7ch/cv-pro",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "description": "AI-native resume CLI — update your living resume at cv.ha7ch.com from Claude Code or any terminal",
5
5
  "homepage": "https://cv.ha7ch.com",
6
- "repository": "https://github.com/LAWTED/cv-pro",
6
+ "repository": "https://github.com/HA7CH/cv-pro",
7
7
  "license": "MIT",
8
8
  "type": "module",
9
9
  "bin": {