client-handover 1.0.5 → 1.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.

Potentially problematic release.


This version of client-handover might be problematic. Click here for more details.

package/generator.js CHANGED
@@ -1,5 +1,9 @@
1
1
  import Anthropic from '@anthropic-ai/sdk'
2
- import { marked } from 'marked'
2
+ import {
3
+ Document, Packer, Paragraph, TextRun, HeadingLevel,
4
+ AlignmentType, BorderStyle, TableRow, TableCell, Table,
5
+ WidthType, ShadingType
6
+ } from 'docx'
3
7
  import fs from 'fs'
4
8
  import path from 'path'
5
9
  import os from 'os'
@@ -7,15 +11,12 @@ import os from 'os'
7
11
  function resolveApiKey() {
8
12
  if (process.env.ANTHROPIC_API_KEY) return process.env.ANTHROPIC_API_KEY
9
13
 
10
- // Check saved key from `handover key <your-key>`
11
14
  const configPath = path.join(os.homedir(), '.handover', 'config.json')
12
15
  if (fs.existsSync(configPath)) {
13
16
  try {
14
17
  const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'))
15
18
  if (config?.apiKey) return config.apiKey
16
- } catch {
17
- // ignore malformed config file
18
- }
19
+ } catch {}
19
20
  }
20
21
 
21
22
  const credentialsPath = path.join(os.homedir(), '.claude', '.credentials.json')
@@ -24,42 +25,293 @@ function resolveApiKey() {
24
25
  const creds = JSON.parse(fs.readFileSync(credentialsPath, 'utf-8'))
25
26
  const token = creds?.claudeAiOauth?.accessToken
26
27
  const expiresAt = creds?.claudeAiOauth?.expiresAt
27
- if (token && (!expiresAt || expiresAt > Date.now())) {
28
- return token
29
- }
30
- } catch {
31
- // ignore malformed credentials file
32
- }
28
+ if (token && (!expiresAt || expiresAt > Date.now())) return token
29
+ } catch {}
33
30
  }
34
31
 
35
32
  return null
36
33
  }
37
34
 
35
+ export function loadDeveloperInfo() {
36
+ const configPath = path.join(os.homedir(), '.handover', 'config.json')
37
+ try {
38
+ if (fs.existsSync(configPath)) {
39
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'))
40
+ return {
41
+ name: config.developerName || '',
42
+ company: config.developerCompany || '',
43
+ email: config.developerEmail || '',
44
+ phone: config.developerPhone || '',
45
+ }
46
+ }
47
+ } catch {}
48
+ return { name: '', company: '', email: '', phone: '' }
49
+ }
50
+
38
51
  export function saveApiKey(key) {
39
52
  const configDir = path.join(os.homedir(), '.handover')
40
53
  const configPath = path.join(configDir, 'config.json')
41
54
  if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true })
42
- fs.writeFileSync(configPath, JSON.stringify({ apiKey: key }, null, 2), 'utf-8')
55
+ let config = {}
56
+ try { config = JSON.parse(fs.readFileSync(configPath, 'utf-8')) } catch {}
57
+ config.apiKey = key
58
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8')
43
59
  }
44
60
 
45
61
  const apiKey = resolveApiKey()
46
62
  if (!apiKey) {
47
- console.error('\n❌ No API key found. Set ANTHROPIC_API_KEY or install Claude Code (code.visualstudio.com/download).\n')
63
+ console.error('\n❌ No API key found. Run "handover key <your-api-key>" or install Claude Code.\n')
48
64
  process.exit(1)
49
65
  }
50
66
 
51
67
  const client = new Anthropic({ apiKey })
52
68
 
53
- /**
54
- * Generate a handover document using the Claude API
55
- * @param {string} prompt - The command prompt to send
56
- * @param {string} outputName - Base filename (without extension)
57
- * @param {string} outputDir - Directory to save files
58
- */
69
+ // ---------------------------------------------------------------------------
70
+ // Inline text parser — splits a line into TextRun segments handling **bold**,
71
+ // *italic*, `code`, and [link text](url) just the text
72
+ // ---------------------------------------------------------------------------
73
+ function parseInline(text) {
74
+ const runs = []
75
+ // Strip markdown links to plain text
76
+ text = text.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
77
+
78
+ const pattern = /(\*\*(.+?)\*\*|\*(.+?)\*|`(.+?)`)/g
79
+ let lastIndex = 0
80
+ let match
81
+
82
+ while ((match = pattern.exec(text)) !== null) {
83
+ if (match.index > lastIndex) {
84
+ runs.push(new TextRun({ text: text.slice(lastIndex, match.index) }))
85
+ }
86
+ if (match[2] !== undefined) {
87
+ runs.push(new TextRun({ text: match[2], bold: true }))
88
+ } else if (match[3] !== undefined) {
89
+ runs.push(new TextRun({ text: match[3], italics: true }))
90
+ } else if (match[4] !== undefined) {
91
+ runs.push(new TextRun({
92
+ text: match[4],
93
+ font: 'Courier New',
94
+ size: 18,
95
+ shading: { type: ShadingType.CLEAR, fill: 'F4F4F4' },
96
+ }))
97
+ }
98
+ lastIndex = match.index + match[0].length
99
+ }
100
+
101
+ if (lastIndex < text.length) {
102
+ runs.push(new TextRun({ text: text.slice(lastIndex) }))
103
+ }
104
+
105
+ return runs.length ? runs : [new TextRun({ text })]
106
+ }
107
+
108
+ // ---------------------------------------------------------------------------
109
+ // Convert markdown string to an array of docx Paragraph/Table objects
110
+ // ---------------------------------------------------------------------------
111
+ function markdownToDocxChildren(markdown) {
112
+ const children = []
113
+ const lines = markdown.split('\n')
114
+ let i = 0
115
+
116
+ while (i < lines.length) {
117
+ const line = lines[i]
118
+
119
+ // Fenced code block
120
+ if (line.trim().startsWith('```')) {
121
+ const codeLines = []
122
+ i++
123
+ while (i < lines.length && !lines[i].trim().startsWith('```')) {
124
+ codeLines.push(lines[i])
125
+ i++
126
+ }
127
+ for (const codeLine of codeLines) {
128
+ children.push(new Paragraph({
129
+ children: [new TextRun({ text: codeLine || ' ', font: 'Courier New', size: 18 })],
130
+ shading: { type: ShadingType.CLEAR, fill: 'F4F4F4' },
131
+ spacing: { before: 0, after: 0 },
132
+ indent: { left: 360 },
133
+ }))
134
+ }
135
+ // Add small gap after code block
136
+ children.push(new Paragraph({ text: '' }))
137
+ i++
138
+ continue
139
+ }
140
+
141
+ // Horizontal rule
142
+ if (/^---+$/.test(line.trim())) {
143
+ children.push(new Paragraph({
144
+ text: '',
145
+ border: { bottom: { style: BorderStyle.SINGLE, size: 6, color: 'CCCCCC' } },
146
+ spacing: { before: 200, after: 200 },
147
+ }))
148
+ i++
149
+ continue
150
+ }
151
+
152
+ // Headings
153
+ const h1 = line.match(/^# (.+)/)
154
+ const h2 = line.match(/^## (.+)/)
155
+ const h3 = line.match(/^### (.+)/)
156
+ const h4 = line.match(/^#{4,6} (.+)/)
157
+
158
+ if (h1) {
159
+ children.push(new Paragraph({
160
+ children: parseInline(h1[1]),
161
+ heading: HeadingLevel.HEADING_1,
162
+ spacing: { before: 400, after: 160 },
163
+ }))
164
+ i++; continue
165
+ }
166
+ if (h2) {
167
+ children.push(new Paragraph({
168
+ children: parseInline(h2[1]),
169
+ heading: HeadingLevel.HEADING_2,
170
+ spacing: { before: 320, after: 120 },
171
+ }))
172
+ i++; continue
173
+ }
174
+ if (h3) {
175
+ children.push(new Paragraph({
176
+ children: parseInline(h3[1]),
177
+ heading: HeadingLevel.HEADING_3,
178
+ spacing: { before: 240, after: 80 },
179
+ }))
180
+ i++; continue
181
+ }
182
+ if (h4) {
183
+ children.push(new Paragraph({
184
+ children: [new TextRun({ text: h4[1], bold: true })],
185
+ spacing: { before: 160, after: 80 },
186
+ }))
187
+ i++; continue
188
+ }
189
+
190
+ // Checkbox list items - [ ] or - [x]
191
+ const checkbox = line.match(/^(\s*)- \[(x| )\] (.+)/i)
192
+ if (checkbox) {
193
+ const checked = checkbox[2].toLowerCase() === 'x'
194
+ children.push(new Paragraph({
195
+ children: [
196
+ new TextRun({ text: checked ? '☑ ' : '☐ ' }),
197
+ ...parseInline(checkbox[3]),
198
+ ],
199
+ indent: { left: 360 },
200
+ spacing: { before: 40, after: 40 },
201
+ }))
202
+ i++; continue
203
+ }
204
+
205
+ // Bullet list items - or *
206
+ const bullet = line.match(/^(\s*)[-*+] (.+)/)
207
+ if (bullet) {
208
+ const indent = bullet[1].length > 0 ? 720 : 360
209
+ children.push(new Paragraph({
210
+ children: [new TextRun({ text: '• ' }), ...parseInline(bullet[2])],
211
+ indent: { left: indent },
212
+ spacing: { before: 40, after: 40 },
213
+ }))
214
+ i++; continue
215
+ }
216
+
217
+ // Numbered list items
218
+ const numbered = line.match(/^\d+\. (.+)/)
219
+ if (numbered) {
220
+ children.push(new Paragraph({
221
+ children: parseInline(numbered[1]),
222
+ numbering: { reference: 'default-numbering', level: 0 },
223
+ indent: { left: 360 },
224
+ spacing: { before: 40, after: 40 },
225
+ }))
226
+ i++; continue
227
+ }
228
+
229
+ // Bold-only line (often used as a label/key line like **Key:** value)
230
+ // Handled by parseInline already — fall through to normal paragraph
231
+
232
+ // Empty line
233
+ if (line.trim() === '') {
234
+ children.push(new Paragraph({ text: '', spacing: { before: 0, after: 80 } }))
235
+ i++; continue
236
+ }
237
+
238
+ // Normal paragraph
239
+ children.push(new Paragraph({
240
+ children: parseInline(line),
241
+ spacing: { before: 0, after: 80 },
242
+ }))
243
+ i++
244
+ }
245
+
246
+ return children
247
+ }
248
+
249
+ // ---------------------------------------------------------------------------
250
+ // Build a docx Document from markdown
251
+ // ---------------------------------------------------------------------------
252
+ function buildDocx(markdown) {
253
+ const children = markdownToDocxChildren(markdown)
254
+
255
+ return new Document({
256
+ numbering: {
257
+ config: [{
258
+ reference: 'default-numbering',
259
+ levels: [{
260
+ level: 0,
261
+ format: 'decimal',
262
+ text: '%1.',
263
+ alignment: AlignmentType.LEFT,
264
+ style: { paragraph: { indent: { left: 360, hanging: 260 } } },
265
+ }],
266
+ }],
267
+ },
268
+ styles: {
269
+ default: {
270
+ document: {
271
+ run: { font: 'Calibri', size: 22 },
272
+ paragraph: { spacing: { line: 276 } },
273
+ },
274
+ },
275
+ paragraphStyles: [
276
+ {
277
+ id: 'Heading1',
278
+ name: 'Heading 1',
279
+ basedOn: 'Normal',
280
+ next: 'Normal',
281
+ run: { bold: true, size: 36, color: '1a1a1a' },
282
+ paragraph: { spacing: { before: 400, after: 160 } },
283
+ },
284
+ {
285
+ id: 'Heading2',
286
+ name: 'Heading 2',
287
+ basedOn: 'Normal',
288
+ next: 'Normal',
289
+ run: { bold: true, size: 28, color: '2c2c2c' },
290
+ paragraph: {
291
+ spacing: { before: 320, after: 120 },
292
+ border: { bottom: { style: BorderStyle.SINGLE, size: 4, color: 'E0E0E0' } },
293
+ },
294
+ },
295
+ {
296
+ id: 'Heading3',
297
+ name: 'Heading 3',
298
+ basedOn: 'Normal',
299
+ next: 'Normal',
300
+ run: { bold: true, size: 24, color: '444444' },
301
+ paragraph: { spacing: { before: 240, after: 80 } },
302
+ },
303
+ ],
304
+ },
305
+ sections: [{ properties: {}, children }],
306
+ })
307
+ }
308
+
309
+ // ---------------------------------------------------------------------------
310
+ // Main export
311
+ // ---------------------------------------------------------------------------
59
312
  export async function generateDoc(prompt, outputName = 'handover', outputDir = './output') {
60
313
  console.log('⏳ Generating document with Claude...\n')
61
314
 
62
- // Call Claude API
63
315
  const message = await client.messages.create({
64
316
  model: 'claude-sonnet-4-20250514',
65
317
  max_tokens: 4096,
@@ -68,105 +320,38 @@ export async function generateDoc(prompt, outputName = 'handover', outputDir = '
68
320
 
69
321
  const markdown = message.content[0].text
70
322
 
71
- // Ensure output directory exists
72
323
  if (!fs.existsSync(outputDir)) {
73
324
  fs.mkdirSync(outputDir, { recursive: true })
74
325
  }
75
326
 
76
327
  const basePath = path.join(outputDir, outputName)
77
328
 
78
- // --- 1. Save as Markdown ---
329
+ // Save as Markdown
79
330
  const mdPath = `${basePath}.md`
80
331
  fs.writeFileSync(mdPath, markdown, 'utf-8')
81
- console.log(`✅ Markdown saved: ${mdPath}`)
332
+ console.log(`✅ Markdown saved: ${mdPath}`)
82
333
 
83
- // --- 2. Save as Plain Text ---
334
+ // Save as plain text
84
335
  const plainText = markdown
85
- .replace(/#{1,6}\s+/g, '') // Remove headings
86
- .replace(/\*\*(.*?)\*\*/g, '$1') // Remove bold
87
- .replace(/\*(.*?)\*/g, '$1') // Remove italic
88
- .replace(/`{1,3}[^`]*`{1,3}/g, (m) => m.replace(/`/g, '')) // Remove code ticks
89
- .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // Remove links, keep text
90
- .replace(/[-*+] /g, '• ') // Convert bullets
91
- .replace(/\n{3,}/g, '\n\n') // Clean extra blank lines
336
+ .replace(/#{1,6}\s+/g, '')
337
+ .replace(/\*\*(.*?)\*\*/g, '$1')
338
+ .replace(/\*(.*?)\*/g, '$1')
339
+ .replace(/`{1,3}[^`]*`{1,3}/g, m => m.replace(/`/g, ''))
340
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
341
+ .replace(/[-*+] /g, '• ')
342
+ .replace(/\n{3,}/g, '\n\n')
92
343
  .trim()
93
344
 
94
345
  const txtPath = `${basePath}.txt`
95
346
  fs.writeFileSync(txtPath, plainText, 'utf-8')
96
347
  console.log(`✅ Plain text saved: ${txtPath}`)
97
348
 
98
- // --- 3. Save as HTML ---
99
- const htmlBody = marked(markdown)
100
- const html = `<!DOCTYPE html>
101
- <html lang="en">
102
- <head>
103
- <meta charset="UTF-8" />
104
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
105
- <title>Handover Document</title>
106
- <style>
107
- body {
108
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
109
- max-width: 860px;
110
- margin: 0 auto;
111
- padding: 2rem;
112
- color: #1a1a1a;
113
- line-height: 1.7;
114
- }
115
- h1 { border-bottom: 2px solid #e0e0e0; padding-bottom: 0.5rem; }
116
- h2 { margin-top: 2.5rem; color: #2c2c2c; }
117
- h3 { color: #444; }
118
- code {
119
- background: #f4f4f4;
120
- padding: 0.2em 0.4em;
121
- border-radius: 4px;
122
- font-size: 0.9em;
123
- }
124
- pre {
125
- background: #f4f4f4;
126
- padding: 1rem;
127
- border-radius: 6px;
128
- overflow-x: auto;
129
- }
130
- pre code { background: none; padding: 0; }
131
- table {
132
- width: 100%;
133
- border-collapse: collapse;
134
- margin: 1rem 0;
135
- }
136
- th, td {
137
- border: 1px solid #ddd;
138
- padding: 0.6rem 0.8rem;
139
- text-align: left;
140
- }
141
- th { background: #f0f0f0; font-weight: 600; }
142
- blockquote {
143
- border-left: 4px solid #ccc;
144
- margin: 0;
145
- padding-left: 1rem;
146
- color: #555;
147
- }
148
- .badge {
149
- display: inline-block;
150
- background: #e8f4fd;
151
- color: #0969da;
152
- padding: 0.2em 0.6em;
153
- border-radius: 4px;
154
- font-size: 0.8em;
155
- font-weight: 600;
156
- }
157
- input[type="checkbox"] { margin-right: 0.4rem; }
158
- </style>
159
- </head>
160
- <body>
161
- ${htmlBody}
162
- </body>
163
- </html>`
164
-
165
- const htmlPath = `${basePath}.html`
166
- fs.writeFileSync(htmlPath, html, 'utf-8')
167
- console.log(`✅ HTML saved: ${htmlPath}`)
168
-
169
- console.log('\n🎉 All formats generated successfully!\n')
349
+ // Save as Word document
350
+ const doc = buildDocx(markdown)
351
+ const docxBuffer = await Packer.toBuffer(doc)
352
+ const docxPath = `${basePath}.docx`
353
+ fs.writeFileSync(docxPath, docxBuffer)
354
+ console.log(`✅ Word doc saved: ${docxPath}`)
170
355
 
171
- return { markdown, plainText, html }
356
+ return { markdown, plainText }
172
357
  }
package/license.js CHANGED
@@ -2,8 +2,9 @@ export function license(projectInfo = '') {
2
2
  return `
3
3
  You are a technical documentation writer creating a LICENSING & ATTRIBUTION section for a frontend website handover document.
4
4
 
5
- Using the following project information:
6
- ${projectInfo || '[No project info provided — use placeholder examples]'}
5
+ The following context has been automatically scanned from the developer's actual project files. Use the real dependencies, license file, and detected libraries to generate licensing documentation specific to THIS project — do not use generic placeholder libraries.
6
+
7
+ ${projectInfo || '[No project data available — use placeholder examples]'}
7
8
 
8
9
  Generate a LICENSING & ATTRIBUTION section that includes:
9
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "client-handover",
3
- "version": "1.0.5",
3
+ "version": "1.1.0",
4
4
  "description": "AI-powered handover document generator for frontend developers handing off client websites",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -11,11 +11,8 @@
11
11
  "cli.js",
12
12
  "index.js",
13
13
  "generator.js",
14
- "handover.js",
15
- "setup.js",
16
- "deploy.js",
17
- "credentials.js",
18
- "license.js",
14
+ "prompts.js",
15
+ "scanner.js",
19
16
  "postinstall.js",
20
17
  "README.md"
21
18
  ],
@@ -40,7 +37,7 @@
40
37
  "dependencies": {
41
38
  "@anthropic-ai/sdk": "^0.39.0",
42
39
  "chalk": "^5.3.0",
43
- "client-handover": "^1.0.2",
44
- "marked": "^12.0.0"
40
+ "client-handover": "^1.0.6",
41
+ "docx": "^8.5.0"
45
42
  }
46
43
  }
package/postinstall.js CHANGED
@@ -9,56 +9,87 @@ const configDir = path.join(os.homedir(), '.handover')
9
9
  const configPath = path.join(configDir, 'config.json')
10
10
  const claudeCredsPath = path.join(os.homedir(), '.claude', '.credentials.json')
11
11
 
12
- function alreadyConfigured() {
13
- if (process.env.ANTHROPIC_API_KEY) return true
12
+ function loadConfig() {
13
+ try {
14
+ if (fs.existsSync(configPath)) {
15
+ return JSON.parse(fs.readFileSync(configPath, 'utf-8'))
16
+ }
17
+ } catch {}
18
+ return {}
19
+ }
14
20
 
15
- if (fs.existsSync(configPath)) {
16
- try {
17
- const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'))
18
- if (config?.apiKey) return true
19
- } catch {}
20
- }
21
+ function saveConfig(config) {
22
+ if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true })
23
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8')
24
+ }
21
25
 
22
- if (fs.existsSync(claudeCredsPath)) {
23
- try {
26
+ function hasApiKey(config) {
27
+ if (process.env.ANTHROPIC_API_KEY) return true
28
+ if (config?.apiKey) return true
29
+ try {
30
+ if (fs.existsSync(claudeCredsPath)) {
24
31
  const creds = JSON.parse(fs.readFileSync(claudeCredsPath, 'utf-8'))
25
32
  const token = creds?.claudeAiOauth?.accessToken
26
33
  const expiresAt = creds?.claudeAiOauth?.expiresAt
27
34
  if (token && (!expiresAt || expiresAt > Date.now())) return true
28
- } catch {}
29
- }
30
-
35
+ }
36
+ } catch {}
31
37
  return false
32
38
  }
33
39
 
34
- function saveKey(key) {
35
- if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true })
36
- fs.writeFileSync(configPath, JSON.stringify({ apiKey: key }, null, 2), 'utf-8')
37
- }
40
+ if (!process.stdin.isTTY) process.exit(0)
38
41
 
39
- // Skip if already set up or not in an interactive terminal
40
- if (alreadyConfigured() || !process.stdin.isTTY) {
41
- process.exit(0)
42
- }
42
+ const config = loadConfig()
43
+ const needsApiKey = !hasApiKey(config)
44
+ const needsDeveloperInfo = !config.developerName
45
+
46
+ if (!needsApiKey && !needsDeveloperInfo) process.exit(0)
43
47
 
44
48
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
45
49
 
50
+ function ask(question) {
51
+ return new Promise(resolve => rl.question(question, answer => resolve(answer.trim())))
52
+ }
53
+
46
54
  console.log('\n──────────────────────────────────────────')
47
55
  console.log(' client-handover setup')
48
56
  console.log('──────────────────────────────────────────')
49
- console.log(' To generate documents, you need an Anthropic API key.')
50
- console.log(' Get one free at: https://console.anthropic.com\n')
57
+ console.log(' This tool generates professional handover documents for client websites.\n')
51
58
 
52
- rl.question(' Enter your Anthropic API key (or press Enter to skip): ', (answer) => {
53
- rl.close()
54
- const key = answer.trim()
59
+ async function run() {
60
+ if (needsApiKey) {
61
+ console.log(' To generate documents you need an Anthropic API key.')
62
+ console.log(' Get one free at: https://console.anthropic.com\n')
63
+ const key = await ask(' Enter your Anthropic API key (or press Enter to skip): ')
64
+ if (key) {
65
+ config.apiKey = key
66
+ console.log(' ✓ API key saved.\n')
67
+ } else {
68
+ console.log(' Skipped. Run "handover key <your-api-key>" at any time.\n')
69
+ }
70
+ }
71
+
72
+ if (needsDeveloperInfo) {
73
+ console.log(' Developer details will appear in all generated handover documents.\n')
74
+ const name = await ask(' Your full name (developer): ')
75
+ if (name) config.developerName = name
55
76
 
56
- if (!key) {
57
- console.log('\n Skipped. Run "handover key <your-api-key>" at any time to set it.\n')
58
- process.exit(0)
77
+ const company = await ask(' Your company name (or press Enter if not applicable): ')
78
+ if (company) config.developerCompany = company
79
+
80
+ const email = await ask(' Your email address: ')
81
+ if (email) config.developerEmail = email
82
+
83
+ const phone = await ask(' Your phone number (optional, press Enter to skip): ')
84
+ if (phone) config.developerPhone = phone
85
+
86
+ console.log()
59
87
  }
60
88
 
61
- saveKey(key)
62
- console.log('\n✅ API key saved. Run "handover handover" to get started.\n')
89
+ rl.close()
90
+ saveConfig(config)
91
+ console.log(' ✅ Setup complete. Run "handover /create" in any project folder to generate handover documents.\n')
63
92
  process.exit(0)
64
- })
93
+ }
94
+
95
+ run()