hone-ai 0.2.0 → 0.9.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 +47 -2
- package/package.json +8 -5
- package/src/agent-client.integration.test.ts +57 -59
- package/src/agent-client.test.ts +27 -27
- package/src/agent-client.ts +109 -77
- package/src/agent.test.ts +16 -16
- package/src/agent.ts +103 -103
- package/src/agents-md-generator.test.ts +360 -0
- package/src/agents-md-generator.ts +900 -0
- package/src/config.test.ts +209 -224
- package/src/config.ts +84 -83
- package/src/errors.test.ts +211 -208
- package/src/errors.ts +107 -101
- package/src/index.integration.test.ts +327 -223
- package/src/index.ts +163 -100
- package/src/integration-test.ts +168 -137
- package/src/logger.test.ts +67 -67
- package/src/logger.ts +8 -8
- package/src/prd-generator.integration.test.ts +50 -50
- package/src/prd-generator.test.ts +66 -25
- package/src/prd-generator.ts +280 -194
- package/src/prds.test.ts +60 -65
- package/src/prds.ts +64 -62
- package/src/prompt.test.ts +154 -155
- package/src/prompt.ts +63 -65
- package/src/run.ts +147 -147
- package/src/status.test.ts +80 -80
- package/src/status.ts +40 -42
- package/src/task-generator.test.ts +93 -66
- package/src/task-generator.ts +125 -112
package/src/errors.ts
CHANGED
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
export class HoneError extends Error {
|
|
6
|
-
constructor(
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
constructor(
|
|
7
|
+
message: string,
|
|
8
|
+
public readonly exitCode: number = 1
|
|
9
|
+
) {
|
|
10
|
+
super(message)
|
|
11
|
+
this.name = 'HoneError'
|
|
9
12
|
}
|
|
10
13
|
}
|
|
11
14
|
|
|
@@ -13,11 +16,11 @@ export class HoneError extends Error {
|
|
|
13
16
|
* Format error message in hone style with ✗ symbol
|
|
14
17
|
*/
|
|
15
18
|
export function formatError(message: string, details?: string): string {
|
|
16
|
-
let output = `✗ ${message}
|
|
19
|
+
let output = `✗ ${message}`
|
|
17
20
|
if (details) {
|
|
18
|
-
output += `\n\n${details}
|
|
21
|
+
output += `\n\n${details}`
|
|
19
22
|
}
|
|
20
|
-
return output
|
|
23
|
+
return output
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
/**
|
|
@@ -25,27 +28,27 @@ export function formatError(message: string, details?: string): string {
|
|
|
25
28
|
* In test mode (NODE_ENV=test or BUN_ENV=test), throws instead of exiting
|
|
26
29
|
*/
|
|
27
30
|
export function exitWithError(message: string, details?: string): never {
|
|
28
|
-
const fullMessage = formatError(message, details)
|
|
29
|
-
|
|
31
|
+
const fullMessage = formatError(message, details)
|
|
32
|
+
|
|
30
33
|
// In test mode, throw instead of exit to allow testing
|
|
31
34
|
if (process.env.NODE_ENV === 'test' || process.env.BUN_ENV === 'test') {
|
|
32
|
-
throw new HoneError(fullMessage)
|
|
35
|
+
throw new HoneError(fullMessage)
|
|
33
36
|
}
|
|
34
|
-
|
|
35
|
-
console.error(fullMessage)
|
|
36
|
-
process.exit(1)
|
|
37
|
+
|
|
38
|
+
console.error(fullMessage)
|
|
39
|
+
process.exit(1)
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
/**
|
|
40
43
|
* Check if error is a network-related error
|
|
41
44
|
*/
|
|
42
45
|
export function isNetworkError(error: unknown): boolean {
|
|
43
|
-
if (!error || typeof error !== 'object') return false
|
|
44
|
-
|
|
45
|
-
const err = error as any
|
|
46
|
-
const message = err.message?.toLowerCase() || ''
|
|
47
|
-
const code = err.code?.toLowerCase() || ''
|
|
48
|
-
|
|
46
|
+
if (!error || typeof error !== 'object') return false
|
|
47
|
+
|
|
48
|
+
const err = error as any
|
|
49
|
+
const message = err.message?.toLowerCase() || ''
|
|
50
|
+
const code = err.code?.toLowerCase() || ''
|
|
51
|
+
|
|
49
52
|
// Common network error codes and messages
|
|
50
53
|
const networkIndicators = [
|
|
51
54
|
'econnrefused',
|
|
@@ -56,36 +59,36 @@ export function isNetworkError(error: unknown): boolean {
|
|
|
56
59
|
'network',
|
|
57
60
|
'timeout',
|
|
58
61
|
'fetch failed',
|
|
59
|
-
'socket hang up'
|
|
60
|
-
]
|
|
61
|
-
|
|
62
|
-
return networkIndicators.some(
|
|
63
|
-
message.includes(indicator) || code.includes(indicator)
|
|
64
|
-
)
|
|
62
|
+
'socket hang up',
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
return networkIndicators.some(
|
|
66
|
+
indicator => message.includes(indicator) || code.includes(indicator)
|
|
67
|
+
)
|
|
65
68
|
}
|
|
66
69
|
|
|
67
70
|
/**
|
|
68
71
|
* Check if error indicates rate limiting
|
|
69
72
|
*/
|
|
70
73
|
export function isRateLimitError(errorText: string): boolean {
|
|
71
|
-
const lowerError = errorText.toLowerCase()
|
|
74
|
+
const lowerError = errorText.toLowerCase()
|
|
72
75
|
const rateLimitIndicators = [
|
|
73
76
|
'rate limit',
|
|
74
77
|
'rate_limit',
|
|
75
78
|
'too many requests',
|
|
76
79
|
'429',
|
|
77
80
|
'quota exceeded',
|
|
78
|
-
'rate exceeded'
|
|
79
|
-
]
|
|
80
|
-
|
|
81
|
-
return rateLimitIndicators.some(indicator => lowerError.includes(indicator))
|
|
81
|
+
'rate exceeded',
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
return rateLimitIndicators.some(indicator => lowerError.includes(indicator))
|
|
82
85
|
}
|
|
83
86
|
|
|
84
87
|
/**
|
|
85
88
|
* Check if error indicates model unavailability
|
|
86
89
|
*/
|
|
87
90
|
export function isModelUnavailableError(errorText: string): boolean {
|
|
88
|
-
const lowerError = errorText.toLowerCase()
|
|
91
|
+
const lowerError = errorText.toLowerCase()
|
|
89
92
|
const modelErrorIndicators = [
|
|
90
93
|
'model not found',
|
|
91
94
|
'model unavailable',
|
|
@@ -93,48 +96,48 @@ export function isModelUnavailableError(errorText: string): boolean {
|
|
|
93
96
|
'invalid model',
|
|
94
97
|
'unknown model',
|
|
95
98
|
'404',
|
|
96
|
-
'not found'
|
|
97
|
-
]
|
|
98
|
-
|
|
99
|
-
return modelErrorIndicators.some(indicator => lowerError.includes(indicator))
|
|
99
|
+
'not found',
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
return modelErrorIndicators.some(indicator => lowerError.includes(indicator))
|
|
100
103
|
}
|
|
101
104
|
|
|
102
105
|
/**
|
|
103
106
|
* Parse structured error information from agent stderr
|
|
104
107
|
*/
|
|
105
108
|
export interface AgentErrorInfo {
|
|
106
|
-
type: 'network' | 'rate_limit' | 'model_unavailable' | 'spawn_failed' | 'timeout' | 'unknown'
|
|
107
|
-
retryable: boolean
|
|
108
|
-
retryAfter?: number
|
|
109
|
+
type: 'network' | 'rate_limit' | 'model_unavailable' | 'spawn_failed' | 'timeout' | 'unknown'
|
|
110
|
+
retryable: boolean
|
|
111
|
+
retryAfter?: number
|
|
109
112
|
}
|
|
110
113
|
|
|
111
114
|
export function parseAgentError(stderr: string, exitCode?: number): AgentErrorInfo {
|
|
112
115
|
if (isNetworkError({ message: stderr })) {
|
|
113
|
-
return { type: 'network', retryable: true }
|
|
116
|
+
return { type: 'network', retryable: true }
|
|
114
117
|
}
|
|
115
|
-
|
|
118
|
+
|
|
116
119
|
if (isRateLimitError(stderr)) {
|
|
117
120
|
// Try to extract retry-after time from stderr
|
|
118
|
-
const retryMatch = stderr.match(/retry[- ]after[:\s]+(\d+)/i)
|
|
119
|
-
const retryAfter = retryMatch && retryMatch[1] ? parseInt(retryMatch[1], 10) : undefined
|
|
120
|
-
return { type: 'rate_limit', retryable: false, retryAfter }
|
|
121
|
+
const retryMatch = stderr.match(/retry[- ]after[:\s]+(\d+)/i)
|
|
122
|
+
const retryAfter = retryMatch && retryMatch[1] ? parseInt(retryMatch[1], 10) : undefined
|
|
123
|
+
return { type: 'rate_limit', retryable: false, retryAfter }
|
|
121
124
|
}
|
|
122
|
-
|
|
125
|
+
|
|
123
126
|
if (isModelUnavailableError(stderr)) {
|
|
124
|
-
return { type: 'model_unavailable', retryable: false }
|
|
127
|
+
return { type: 'model_unavailable', retryable: false }
|
|
125
128
|
}
|
|
126
|
-
|
|
129
|
+
|
|
127
130
|
// Check for timeout (exit code 124 or timeout in stderr)
|
|
128
131
|
if (exitCode === 124 || stderr.toLowerCase().includes('timed out')) {
|
|
129
|
-
return { type: 'timeout', retryable: false }
|
|
132
|
+
return { type: 'timeout', retryable: false }
|
|
130
133
|
}
|
|
131
|
-
|
|
134
|
+
|
|
132
135
|
// Check for spawn-related failures (typically exit code undefined or ENOENT)
|
|
133
136
|
if (exitCode === undefined || stderr.toLowerCase().includes('enoent')) {
|
|
134
|
-
return { type: 'spawn_failed', retryable: false }
|
|
137
|
+
return { type: 'spawn_failed', retryable: false }
|
|
135
138
|
}
|
|
136
|
-
|
|
137
|
-
return { type: 'unknown', retryable: false }
|
|
139
|
+
|
|
140
|
+
return { type: 'unknown', retryable: false }
|
|
138
141
|
}
|
|
139
142
|
|
|
140
143
|
/**
|
|
@@ -143,44 +146,46 @@ export function parseAgentError(stderr: string, exitCode?: number): AgentErrorIn
|
|
|
143
146
|
export async function retryWithBackoff<T>(
|
|
144
147
|
fn: () => Promise<T>,
|
|
145
148
|
options: {
|
|
146
|
-
maxRetries?: number
|
|
147
|
-
initialDelay?: number
|
|
148
|
-
maxDelay?: number
|
|
149
|
-
shouldRetry?: (error: unknown) => boolean
|
|
149
|
+
maxRetries?: number
|
|
150
|
+
initialDelay?: number
|
|
151
|
+
maxDelay?: number
|
|
152
|
+
shouldRetry?: (error: unknown) => boolean
|
|
150
153
|
} = {}
|
|
151
154
|
): Promise<T> {
|
|
152
155
|
const {
|
|
153
156
|
maxRetries = 3,
|
|
154
157
|
initialDelay = 1000,
|
|
155
158
|
maxDelay = 10000,
|
|
156
|
-
shouldRetry = isNetworkError
|
|
157
|
-
} = options
|
|
159
|
+
shouldRetry = isNetworkError,
|
|
160
|
+
} = options
|
|
161
|
+
|
|
162
|
+
let lastError: unknown
|
|
158
163
|
|
|
159
|
-
let lastError: unknown;
|
|
160
|
-
|
|
161
164
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
162
165
|
try {
|
|
163
|
-
return await fn()
|
|
166
|
+
return await fn()
|
|
164
167
|
} catch (error) {
|
|
165
|
-
lastError = error
|
|
166
|
-
|
|
168
|
+
lastError = error
|
|
169
|
+
|
|
167
170
|
// Don't retry if not a network error or if we're out of retries
|
|
168
171
|
if (!shouldRetry(error) || attempt === maxRetries) {
|
|
169
|
-
throw error
|
|
172
|
+
throw error
|
|
170
173
|
}
|
|
171
|
-
|
|
174
|
+
|
|
172
175
|
// Calculate delay with exponential backoff
|
|
173
|
-
const delay = Math.min(initialDelay * Math.pow(2, attempt), maxDelay)
|
|
174
|
-
|
|
175
|
-
console.error(
|
|
176
|
-
|
|
176
|
+
const delay = Math.min(initialDelay * Math.pow(2, attempt), maxDelay)
|
|
177
|
+
|
|
178
|
+
console.error(
|
|
179
|
+
`Network error, retrying in ${delay}ms... (attempt ${attempt + 1}/${maxRetries})`
|
|
180
|
+
)
|
|
181
|
+
|
|
177
182
|
// Wait before retrying
|
|
178
|
-
await new Promise(resolve => setTimeout(resolve, delay))
|
|
183
|
+
await new Promise(resolve => setTimeout(resolve, delay))
|
|
179
184
|
}
|
|
180
185
|
}
|
|
181
|
-
|
|
186
|
+
|
|
182
187
|
// Should never reach here, but TypeScript needs it
|
|
183
|
-
throw lastError
|
|
188
|
+
throw lastError
|
|
184
189
|
}
|
|
185
190
|
|
|
186
191
|
/**
|
|
@@ -192,64 +197,65 @@ export const ErrorMessages = {
|
|
|
192
197
|
details: `Please create a .env file in your project root with:
|
|
193
198
|
ANTHROPIC_API_KEY=your-api-key-here
|
|
194
199
|
|
|
195
|
-
Get your API key at: https://console.anthropic.com
|
|
200
|
+
Get your API key at: https://console.anthropic.com/`,
|
|
196
201
|
},
|
|
197
|
-
|
|
202
|
+
|
|
198
203
|
FILE_NOT_FOUND: (path: string) => ({
|
|
199
204
|
message: `Error: File not found`,
|
|
200
205
|
details: `Could not find file: ${path}
|
|
201
206
|
|
|
202
|
-
Please check the path and try again
|
|
207
|
+
Please check the path and try again.`,
|
|
203
208
|
}),
|
|
204
|
-
|
|
209
|
+
|
|
205
210
|
AGENT_NOT_FOUND: (agent: string) => ({
|
|
206
211
|
message: `Error: ${agent} command not found`,
|
|
207
|
-
details:
|
|
208
|
-
|
|
212
|
+
details:
|
|
213
|
+
agent === 'claude'
|
|
214
|
+
? `Please install Claude Code CLI:
|
|
209
215
|
npm install -g @anthropic-ai/claude-code
|
|
210
216
|
|
|
211
217
|
Or visit: https://docs.anthropic.com/en/docs/claude-code`
|
|
212
|
-
|
|
218
|
+
: `Please install OpenCode CLI:
|
|
213
219
|
npm install -g @opencode/cli
|
|
214
220
|
|
|
215
|
-
Or visit: https://opencode.ai/docs/installation
|
|
221
|
+
Or visit: https://opencode.ai/docs/installation`,
|
|
216
222
|
}),
|
|
217
|
-
|
|
223
|
+
|
|
218
224
|
GIT_NOT_INITIALIZED: {
|
|
219
225
|
message: 'Error: Git repository not initialized',
|
|
220
226
|
details: `Please initialize git first:
|
|
221
|
-
git init
|
|
227
|
+
git init`,
|
|
222
228
|
},
|
|
223
|
-
|
|
229
|
+
|
|
224
230
|
INVALID_TASK_FILE: (path: string, reason: string) => ({
|
|
225
231
|
message: 'Error: Invalid task file format',
|
|
226
232
|
details: `File: ${path}
|
|
227
233
|
Reason: ${reason}
|
|
228
234
|
|
|
229
|
-
Please ensure the task file follows the correct YAML schema
|
|
235
|
+
Please ensure the task file follows the correct YAML schema.`,
|
|
230
236
|
}),
|
|
231
|
-
|
|
237
|
+
|
|
232
238
|
NETWORK_ERROR_FINAL: (error: unknown) => {
|
|
233
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
239
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
234
240
|
return {
|
|
235
241
|
message: 'Error: Network request failed after retries',
|
|
236
242
|
details: `Failed to connect to Anthropic API after multiple attempts.
|
|
237
243
|
|
|
238
244
|
Error: ${message}
|
|
239
245
|
|
|
240
|
-
Please check your internet connection and try again
|
|
241
|
-
}
|
|
246
|
+
Please check your internet connection and try again.`,
|
|
247
|
+
}
|
|
242
248
|
},
|
|
243
|
-
|
|
249
|
+
|
|
244
250
|
AGENT_SPAWN_FAILED: (agent: string, error: string) => ({
|
|
245
251
|
message: `Error: Failed to start ${agent}`,
|
|
246
252
|
details: `Could not spawn ${agent} agent process.
|
|
247
253
|
|
|
248
254
|
Error: ${error}
|
|
249
255
|
|
|
250
|
-
Please ensure ${agent} is properly installed and in your PATH
|
|
256
|
+
Please ensure ${agent} is properly installed and in your PATH.`,
|
|
251
257
|
}),
|
|
252
|
-
|
|
258
|
+
|
|
253
259
|
MODEL_UNAVAILABLE: (model: string, agent: string) => ({
|
|
254
260
|
message: `Error: Model not available`,
|
|
255
261
|
details: `The model "${model}" is not available for agent "${agent}".
|
|
@@ -260,14 +266,14 @@ Please check:
|
|
|
260
266
|
• Your ${agent} CLI is up to date
|
|
261
267
|
|
|
262
268
|
Supported tiers: sonnet, opus
|
|
263
|
-
Example: claude-sonnet-4-20250514
|
|
269
|
+
Example: claude-sonnet-4-20250514`,
|
|
264
270
|
}),
|
|
265
|
-
|
|
271
|
+
|
|
266
272
|
RATE_LIMIT_ERROR: (agent: string, retryAfter?: number) => {
|
|
267
|
-
const retryMsg = retryAfter
|
|
273
|
+
const retryMsg = retryAfter
|
|
268
274
|
? `Please retry after ${retryAfter} seconds.`
|
|
269
|
-
: 'Please wait a few minutes before retrying.'
|
|
270
|
-
|
|
275
|
+
: 'Please wait a few minutes before retrying.'
|
|
276
|
+
|
|
271
277
|
return {
|
|
272
278
|
message: 'Error: Rate limit exceeded',
|
|
273
279
|
details: `The ${agent} agent has exceeded its rate limit.
|
|
@@ -277,10 +283,10 @@ ${retryMsg}
|
|
|
277
283
|
Consider:
|
|
278
284
|
• Spacing out your requests
|
|
279
285
|
• Using a different model if available
|
|
280
|
-
• Checking your API usage dashboard
|
|
281
|
-
}
|
|
286
|
+
• Checking your API usage dashboard`,
|
|
287
|
+
}
|
|
282
288
|
},
|
|
283
|
-
|
|
289
|
+
|
|
284
290
|
AGENT_TIMEOUT: (agent: string, timeout: number) => ({
|
|
285
291
|
message: `Error: ${agent} agent timed out`,
|
|
286
292
|
details: `The ${agent} agent did not respond within ${Math.round(timeout / 1000)} seconds.
|
|
@@ -293,7 +299,7 @@ This may indicate:
|
|
|
293
299
|
Try:
|
|
294
300
|
• Simplifying your request
|
|
295
301
|
• Checking your internet connection
|
|
296
|
-
• Retrying in a few minutes
|
|
302
|
+
• Retrying in a few minutes`,
|
|
297
303
|
}),
|
|
298
304
|
|
|
299
305
|
AGENT_ERROR: (agent: string, exitCode: number, stderr: string) => ({
|
|
@@ -308,6 +314,6 @@ This may indicate:
|
|
|
308
314
|
• Model configuration issue
|
|
309
315
|
• Agent internal error
|
|
310
316
|
|
|
311
|
-
Review the error output above for specific details
|
|
312
|
-
})
|
|
313
|
-
}
|
|
317
|
+
Review the error output above for specific details.`,
|
|
318
|
+
}),
|
|
319
|
+
}
|