client-handover 1.4.1 → 1.5.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.
Files changed (3) hide show
  1. package/cli.js +202 -174
  2. package/generator.js +52 -8
  3. package/package.json +2 -2
package/cli.js CHANGED
@@ -1,174 +1,202 @@
1
- #!/usr/bin/env node
2
-
3
- import { generateDoc } from './generator.js'
4
- import { technicalHandoverPrompt, nonTechnicalHandoverPrompt } from './prompts.js'
5
- import { scanProject } from './scanner.js'
6
- import chalk from 'chalk'
7
- import readline from 'readline'
8
- import fs from 'fs'
9
- import path from 'path'
10
- import os from 'os'
11
-
12
- const { version } = JSON.parse(fs.readFileSync(new URL('./package.json', import.meta.url), 'utf-8'))
13
-
14
- function normalizeCommand(cmd) {
15
- return path.basename(cmd).replace(/^\/+/, '')
16
- }
17
-
18
- function printBanner() {
19
- const t = chalk.bold.white
20
- console.log()
21
- console.log(' ' + t('█ █ ███ █ █ ████ ███ █ █ █████ ████ '))
22
- console.log(' ' + t('█ █ █ █ ██ █ █ █ █ █ █ █ █ █ █'))
23
- console.log(' ' + t('█████ █████ █ █ █ █ █ █ █ █ █ ████ ████ '))
24
- console.log(' ' + t('█ █ █ █ █ ██ █ █ █ █ █ █ █ █ █ '))
25
- console.log(' ' + t('█ █ █ █ █ █ ████ ███ █ █████ █ █ '))
26
- console.log()
27
- console.log(chalk.dim(' Website handover documents for clients & developers') + chalk.dim(' · v' + version))
28
- console.log()
29
- }
30
-
31
- function printHelp() {
32
- printBanner()
33
- console.log(chalk.dim(' Usage:'))
34
- console.log(' handover /create Generate technical & non-technical handover documents\n')
35
- console.log(chalk.dim(' Example:'))
36
- console.log(' cd my-client-project')
37
- console.log(' handover /create\n')
38
- }
39
-
40
- function ask(rl, question) {
41
- return new Promise(resolve => rl.question(question, answer => resolve(answer.trim())))
42
- }
43
-
44
-
45
- async function runSetup() {
46
- const configDir = path.join(os.homedir(), '.handover')
47
- const configPath = path.join(configDir, 'config.json')
48
-
49
- let config = {}
50
- try {
51
- if (fs.existsSync(configPath)) config = JSON.parse(fs.readFileSync(configPath, 'utf-8'))
52
- } catch {}
53
-
54
- const hasDeveloperInfo = !!config.developerName
55
-
56
- if (hasDeveloperInfo) {
57
- return {
58
- name: config.developerName,
59
- company: config.developerCompany || '',
60
- email: config.developerEmail || '',
61
- phone: config.developerPhone || '',
62
- }
63
- }
64
-
65
- console.log(chalk.bold('\n──────────────────────────────────────────'))
66
- console.log(chalk.bold(' client-handover — first time setup'))
67
- console.log(chalk.bold('──────────────────────────────────────────\n'))
68
-
69
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
70
-
71
- if (!hasDeveloperInfo) {
72
- console.log(' Your details will appear in all generated handover documents.\n')
73
- const name = await ask(rl, ' Developer name: ')
74
- if (name) config.developerName = name
75
-
76
- const company = await ask(rl, ' Company name (press Enter if not applicable): ')
77
- if (company) config.developerCompany = company
78
-
79
- const email = await ask(rl, ' Email address: ')
80
- if (email) config.developerEmail = email
81
-
82
- const phone = await ask(rl, ' Phone number (optional, press Enter to skip): ')
83
- if (phone) config.developerPhone = phone
84
-
85
- console.log()
86
- }
87
-
88
- rl.close()
89
-
90
- if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true })
91
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8')
92
- console.log(chalk.green(' ✓ Setup saved.\n'))
93
-
94
- return {
95
- name: config.developerName || '',
96
- company: config.developerCompany || '',
97
- email: config.developerEmail || '',
98
- phone: config.developerPhone || '',
99
- }
100
- }
101
-
102
- async function runCreate() {
103
- printBanner()
104
- const developerInfo = await runSetup()
105
-
106
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
107
- let docType = ''
108
- while (!['1', '2', '3'].includes(docType)) {
109
- docType = await ask(rl, ' What would you like to generate?\n 1 - Technical handover\n 2 - Non-technical handover\n 3 - Both\n\n Enter 1, 2 or 3: ')
110
- if (!['1', '2', '3'].includes(docType)) console.log(chalk.yellow('\n Please enter 1, 2 or 3.\n'))
111
- }
112
- rl.close()
113
- console.log()
114
-
115
- console.log(chalk.dim(' Scanning project files...'))
116
- const projectInfo = scanProject(process.cwd())
117
-
118
- const techDir = './technical-handover'
119
- const clientDir = './client-handover'
120
-
121
- if (docType === '1' || docType === '3') {
122
- console.log(chalk.bold('\n Generating Technical Handover Document...'))
123
- const techPrompt = technicalHandoverPrompt(projectInfo, developerInfo)
124
- await generateDoc(techPrompt, 'technical-handover', techDir)
125
- }
126
-
127
- if (docType === '2' || docType === '3') {
128
- console.log(chalk.bold('\n Generating Client Handover Document...'))
129
- const nonTechPrompt = nonTechnicalHandoverPrompt(projectInfo, developerInfo)
130
- await generateDoc(nonTechPrompt, 'client-handover', clientDir)
131
- }
132
-
133
- console.log(chalk.green.bold('\n Handover documents created successfully!\n'))
134
- if (docType === '1' || docType === '3') {
135
- console.log(` ${chalk.cyan('./technical-handover/')}`)
136
- console.log(` technical-handover.md`)
137
- console.log(` technical-handover.txt`)
138
- console.log(` technical-handover.docx`)
139
- console.log()
140
- }
141
- if (docType === '2' || docType === '3') {
142
- console.log(` ${chalk.cyan('./client-handover/')}`)
143
- console.log(` client-handover.md`)
144
- console.log(` client-handover.txt`)
145
- console.log(` client-handover.docx`)
146
- console.log()
147
- }
148
- }
149
-
150
- async function main() {
151
- const [,, rawCommand, secondArg] = process.argv
152
-
153
- if (!rawCommand || rawCommand === '--help' || rawCommand === '-h') {
154
- printHelp()
155
- process.exit(0)
156
- }
157
-
158
- const command = normalizeCommand(rawCommand)
159
-
160
- if (command !== 'create') {
161
- console.error(chalk.red(`\n ❌ Unknown command: ${rawCommand}\n`))
162
- printHelp()
163
- process.exit(1)
164
- }
165
-
166
- try {
167
- await runCreate()
168
- } catch (err) {
169
- console.error(chalk.red(`\n ❌ Error: ${err.message}\n`))
170
- process.exit(1)
171
- }
172
- }
173
-
174
- main()
1
+ #!/usr/bin/env node
2
+
3
+ import { generateDoc, resolveApiKey, saveApiKey } from './generator.js'
4
+ import { technicalHandoverPrompt, nonTechnicalHandoverPrompt } from './prompts.js'
5
+ import { scanProject } from './scanner.js'
6
+ import chalk from 'chalk'
7
+ import readline from 'readline'
8
+ import fs from 'fs'
9
+ import path from 'path'
10
+ import os from 'os'
11
+
12
+ const { version } = JSON.parse(fs.readFileSync(new URL('./package.json', import.meta.url), 'utf-8'))
13
+
14
+ function normalizeCommand(cmd) {
15
+ return path.basename(cmd).replace(/^\/+/, '')
16
+ }
17
+
18
+ function printBanner() {
19
+ const t = chalk.bold.white
20
+ console.log()
21
+ console.log(' ' + t('█ █ ███ █ █ ████ ███ █ █ █████ ████ '))
22
+ console.log(' ' + t('█ █ █ █ ██ █ █ █ █ █ █ █ █ █ █'))
23
+ console.log(' ' + t('█████ █████ █ █ █ █ █ █ █ █ █ ████ ████ '))
24
+ console.log(' ' + t('█ █ █ █ █ ██ █ █ █ █ █ █ █ █ █ '))
25
+ console.log(' ' + t('█ █ █ █ █ █ ████ ███ █ █████ █ █ '))
26
+ console.log()
27
+ console.log(chalk.dim(' Website handover documents for clients & developers') + chalk.dim(' · v' + version))
28
+ console.log()
29
+ }
30
+
31
+ function printHelp() {
32
+ printBanner()
33
+ console.log(chalk.dim(' Usage:'))
34
+ console.log(' handover /create Generate technical & non-technical handover documents')
35
+ console.log(' handover key <key> Save your Anthropic API key\n')
36
+ console.log(chalk.dim(' Example:'))
37
+ console.log(' cd my-client-project')
38
+ console.log(' handover /create\n')
39
+ }
40
+
41
+ function ask(rl, question) {
42
+ return new Promise(resolve => rl.question(question, answer => resolve(answer.trim())))
43
+ }
44
+
45
+
46
+ async function runSetup() {
47
+ const configDir = path.join(os.homedir(), '.handover')
48
+ const configPath = path.join(configDir, 'config.json')
49
+
50
+ let config = {}
51
+ try {
52
+ if (fs.existsSync(configPath)) config = JSON.parse(fs.readFileSync(configPath, 'utf-8'))
53
+ } catch {}
54
+
55
+ const hasApiKey = !!resolveApiKey(config)
56
+ const hasDeveloperInfo = !!config.developerName
57
+
58
+ if (hasApiKey && hasDeveloperInfo) {
59
+ return {
60
+ name: config.developerName,
61
+ company: config.developerCompany || '',
62
+ email: config.developerEmail || '',
63
+ phone: config.developerPhone || '',
64
+ }
65
+ }
66
+
67
+ console.log(chalk.bold('\n──────────────────────────────────────────'))
68
+ console.log(chalk.bold(' client-handover — first time setup'))
69
+ console.log(chalk.bold('──────────────────────────────────────────\n'))
70
+
71
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
72
+
73
+ if (!hasApiKey) {
74
+ console.log(' To generate documents you need an Anthropic API key.')
75
+ console.log(chalk.dim(' Get one free at: https://console.anthropic.com\n'))
76
+ let key = ''
77
+ while (!key) {
78
+ key = await ask(rl, ' Enter your Anthropic API key: ')
79
+ if (!key) console.log(chalk.yellow(' API key is required to continue.\n'))
80
+ }
81
+ config.apiKey = key
82
+ console.log(chalk.green(' API key saved.\n'))
83
+ }
84
+
85
+ if (!hasDeveloperInfo) {
86
+ console.log(' Your details will appear in all generated handover documents.\n')
87
+ const name = await ask(rl, ' Developer name: ')
88
+ if (name) config.developerName = name
89
+
90
+ const company = await ask(rl, ' Company name (press Enter if not applicable): ')
91
+ if (company) config.developerCompany = company
92
+
93
+ const email = await ask(rl, ' Email address: ')
94
+ if (email) config.developerEmail = email
95
+
96
+ const phone = await ask(rl, ' Phone number (optional, press Enter to skip): ')
97
+ if (phone) config.developerPhone = phone
98
+
99
+ console.log()
100
+ }
101
+
102
+ rl.close()
103
+
104
+ if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true })
105
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8')
106
+ console.log(chalk.green(' Setup saved.\n'))
107
+
108
+ return {
109
+ name: config.developerName || '',
110
+ company: config.developerCompany || '',
111
+ email: config.developerEmail || '',
112
+ phone: config.developerPhone || '',
113
+ }
114
+ }
115
+
116
+ async function runCreate() {
117
+ printBanner()
118
+ const developerInfo = await runSetup()
119
+
120
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
121
+ let docType = ''
122
+ while (!['1', '2', '3'].includes(docType)) {
123
+ docType = await ask(rl, ' What would you like to generate?\n 1 - Technical handover\n 2 - Non-technical handover\n 3 - Both\n\n Enter 1, 2 or 3: ')
124
+ if (!['1', '2', '3'].includes(docType)) console.log(chalk.yellow('\n Please enter 1, 2 or 3.\n'))
125
+ }
126
+ rl.close()
127
+ console.log()
128
+
129
+ console.log(chalk.dim(' Scanning project files...'))
130
+ const projectInfo = scanProject(process.cwd())
131
+
132
+ const techDir = './technical-handover'
133
+ const clientDir = './client-handover'
134
+
135
+ if (docType === '1' || docType === '3') {
136
+ console.log(chalk.bold('\n Generating Technical Handover Document...'))
137
+ const techPrompt = technicalHandoverPrompt(projectInfo, developerInfo)
138
+ await generateDoc(techPrompt, 'technical-handover', techDir)
139
+ }
140
+
141
+ if (docType === '2' || docType === '3') {
142
+ console.log(chalk.bold('\n Generating Client Handover Document...'))
143
+ const nonTechPrompt = nonTechnicalHandoverPrompt(projectInfo, developerInfo)
144
+ await generateDoc(nonTechPrompt, 'client-handover', clientDir)
145
+ }
146
+
147
+ console.log(chalk.green.bold('\n ✅ Handover documents created successfully!\n'))
148
+ if (docType === '1' || docType === '3') {
149
+ console.log(` ${chalk.cyan('./technical-handover/')}`)
150
+ console.log(` technical-handover.md`)
151
+ console.log(` technical-handover.txt`)
152
+ console.log(` technical-handover.docx`)
153
+ console.log()
154
+ }
155
+ if (docType === '2' || docType === '3') {
156
+ console.log(` ${chalk.cyan('./client-handover/')}`)
157
+ console.log(` client-handover.md`)
158
+ console.log(` client-handover.txt`)
159
+ console.log(` client-handover.docx`)
160
+ console.log()
161
+ }
162
+ }
163
+
164
+ async function main() {
165
+ const [,, rawCommand, secondArg] = process.argv
166
+
167
+ if (!rawCommand || rawCommand === '--help' || rawCommand === '-h') {
168
+ printHelp()
169
+ process.exit(0)
170
+ }
171
+
172
+ const command = normalizeCommand(rawCommand)
173
+
174
+ if (command === 'key') {
175
+ if (!secondArg) {
176
+ console.error(chalk.red('\n ❌ Usage: handover key <your-api-key>\n'))
177
+ process.exit(1)
178
+ }
179
+ saveApiKey(secondArg)
180
+ console.log(chalk.green('\n ✅ API key saved. Run "handover /create" to get started.\n'))
181
+ process.exit(0)
182
+ }
183
+
184
+ if (command !== 'create') {
185
+ console.error(chalk.red(`\n ❌ Unknown command: ${rawCommand}\n`))
186
+ printHelp()
187
+ process.exit(1)
188
+ }
189
+
190
+ try {
191
+ await runCreate()
192
+ } catch (err) {
193
+ if (err.status === 401) {
194
+ console.error(chalk.red('\n ❌ Authentication failed. Run "handover key <your-api-key>" to set your API key.\n'))
195
+ } else {
196
+ console.error(chalk.red(`\n ❌ Error: ${err.message}\n`))
197
+ }
198
+ process.exit(1)
199
+ }
200
+ }
201
+
202
+ main()
package/generator.js CHANGED
@@ -1,4 +1,4 @@
1
- import { query } from '@anthropic-ai/claude-code'
1
+ import Anthropic from '@anthropic-ai/sdk'
2
2
  import {
3
3
  Document, Packer, Paragraph, TextRun, HeadingLevel,
4
4
  AlignmentType, BorderStyle, TableRow, TableCell, Table,
@@ -6,6 +6,51 @@ import {
6
6
  } from 'docx'
7
7
  import fs from 'fs'
8
8
  import path from 'path'
9
+ import os from 'os'
10
+
11
+ export function resolveApiKey(config = null) {
12
+ if (process.env.ANTHROPIC_API_KEY) return process.env.ANTHROPIC_API_KEY
13
+
14
+ const cfg = config || (() => {
15
+ try {
16
+ const p = path.join(os.homedir(), '.handover', 'config.json')
17
+ if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf-8'))
18
+ } catch {}
19
+ return {}
20
+ })()
21
+
22
+ if (cfg?.apiKey) return cfg.apiKey
23
+
24
+ const credentialsPath = path.join(os.homedir(), '.claude', '.credentials.json')
25
+ try {
26
+ if (fs.existsSync(credentialsPath)) {
27
+ const creds = JSON.parse(fs.readFileSync(credentialsPath, 'utf-8'))
28
+ const token = creds?.claudeAiOauth?.accessToken
29
+ const expiresAt = creds?.claudeAiOauth?.expiresAt
30
+ if (token && (!expiresAt || expiresAt > Date.now())) return token
31
+ }
32
+ } catch {}
33
+
34
+ return null
35
+ }
36
+
37
+ export function saveApiKey(key) {
38
+ const configDir = path.join(os.homedir(), '.handover')
39
+ const configPath = path.join(configDir, 'config.json')
40
+ if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true })
41
+ let config = {}
42
+ try { config = JSON.parse(fs.readFileSync(configPath, 'utf-8')) } catch {}
43
+ config.apiKey = key
44
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8')
45
+ }
46
+
47
+ const apiKey = resolveApiKey()
48
+ if (!apiKey) {
49
+ console.error('\n ❌ No API key found. Run "handover key <your-api-key>" or install Claude Code.\n')
50
+ process.exit(1)
51
+ }
52
+
53
+ const client = new Anthropic({ apiKey })
9
54
 
10
55
  // ---------------------------------------------------------------------------
11
56
  // Inline text parser — splits a line into TextRun segments handling **bold**,
@@ -253,14 +298,13 @@ function buildDocx(markdown) {
253
298
  export async function generateDoc(prompt, outputName = 'handover', outputDir = './output') {
254
299
  console.log('⏳ Generating document with Claude...\n')
255
300
 
256
- let markdown = ''
257
- for await (const message of query({ prompt, options: { maxTurns: 1 } })) {
258
- if (message.type === 'result' && message.subtype === 'success') {
259
- markdown = message.result
260
- }
261
- }
301
+ const message = await client.messages.create({
302
+ model: 'claude-sonnet-4-6',
303
+ max_tokens: 4096,
304
+ messages: [{ role: 'user', content: prompt }]
305
+ })
262
306
 
263
- if (!markdown) throw new Error('No response received from Claude')
307
+ const markdown = message.content[0].text
264
308
 
265
309
  if (!fs.existsSync(outputDir)) {
266
310
  fs.mkdirSync(outputDir, { recursive: true })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "client-handover",
3
- "version": "1.4.1",
3
+ "version": "1.5.0",
4
4
  "description": "AI-powered handover document generator for frontend developers handing off client websites",
5
5
  "type": "module",
6
6
  "bin": {
@@ -29,7 +29,7 @@
29
29
  "node": ">=18"
30
30
  },
31
31
  "dependencies": {
32
- "@anthropic-ai/claude-code": "*",
32
+ "@anthropic-ai/sdk": "^0.39.0",
33
33
  "chalk": "^5.3.0",
34
34
  "docx": "^8.5.0"
35
35
  }