prjct-cli 0.9.2 → 0.10.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/CHANGELOG.md +142 -0
- package/core/__tests__/agentic/memory-system.test.js +263 -0
- package/core/__tests__/agentic/plan-mode.test.js +336 -0
- package/core/agentic/agent-router.js +253 -186
- package/core/agentic/chain-of-thought.js +578 -0
- package/core/agentic/command-executor.js +299 -17
- package/core/agentic/context-builder.js +208 -8
- package/core/agentic/context-filter.js +83 -83
- package/core/agentic/ground-truth.js +591 -0
- package/core/agentic/loop-detector.js +406 -0
- package/core/agentic/memory-system.js +850 -0
- package/core/agentic/parallel-tools.js +366 -0
- package/core/agentic/plan-mode.js +572 -0
- package/core/agentic/prompt-builder.js +127 -2
- package/core/agentic/response-templates.js +290 -0
- package/core/agentic/semantic-compression.js +517 -0
- package/core/agentic/think-blocks.js +657 -0
- package/core/agentic/tool-registry.js +32 -0
- package/core/agentic/validation-rules.js +380 -0
- package/core/command-registry.js +48 -0
- package/core/commands.js +128 -60
- package/core/context-sync.js +183 -0
- package/core/domain/agent-generator.js +77 -46
- package/core/domain/agent-loader.js +183 -0
- package/core/domain/agent-matcher.js +217 -0
- package/core/domain/agent-validator.js +217 -0
- package/core/domain/context-estimator.js +175 -0
- package/core/domain/product-standards.js +92 -0
- package/core/domain/smart-cache.js +157 -0
- package/core/domain/task-analyzer.js +353 -0
- package/core/domain/tech-detector.js +365 -0
- package/package.json +8 -16
- package/templates/commands/done.md +7 -0
- package/templates/commands/feature.md +8 -0
- package/templates/commands/ship.md +8 -0
- package/templates/commands/spec.md +128 -0
- package/templates/global/CLAUDE.md +17 -0
- package/core/__tests__/agentic/agent-router.test.js +0 -398
- package/core/__tests__/agentic/command-executor.test.js +0 -223
- package/core/__tests__/agentic/context-builder.test.js +0 -160
- package/core/__tests__/agentic/context-filter.test.js +0 -494
- package/core/__tests__/agentic/prompt-builder.test.js +0 -212
- package/core/__tests__/agentic/template-loader.test.js +0 -164
- package/core/__tests__/agentic/tool-registry.test.js +0 -243
- package/core/__tests__/domain/agent-generator.test.js +0 -296
- package/core/__tests__/domain/analyzer.test.js +0 -324
- package/core/__tests__/infrastructure/author-detector.test.js +0 -103
- package/core/__tests__/infrastructure/config-manager.test.js +0 -454
- package/core/__tests__/infrastructure/path-manager.test.js +0 -412
- package/core/__tests__/setup.test.js +0 -15
- package/core/__tests__/utils/date-helper.test.js +0 -169
- package/core/__tests__/utils/file-helper.test.js +0 -258
- package/core/__tests__/utils/jsonl-helper.test.js +0 -387
|
@@ -2,18 +2,59 @@
|
|
|
2
2
|
* Prompt Builder
|
|
3
3
|
* Builds prompts for Claude based on templates and context
|
|
4
4
|
* Claude decides what to do - NO if/else logic here
|
|
5
|
+
*
|
|
6
|
+
* P1.1: Includes learned patterns from memory system
|
|
7
|
+
* P3.1: Includes think blocks for anti-hallucination
|
|
8
|
+
* P3.3: Includes relevant memories from semantic database
|
|
9
|
+
* P3.4: Includes plan mode instructions
|
|
5
10
|
*/
|
|
6
11
|
|
|
7
12
|
class PromptBuilder {
|
|
8
13
|
/**
|
|
9
14
|
* Build concise prompt - only essentials
|
|
15
|
+
* CRITICAL: Includes full agent content if agent is provided
|
|
16
|
+
* P1.1: Includes learned patterns to avoid repetitive questions
|
|
17
|
+
* P3.1: Includes think blocks for critical decisions
|
|
18
|
+
* P3.3: Includes relevant memories from semantic database
|
|
19
|
+
* P3.4: Includes plan mode status and constraints
|
|
10
20
|
*/
|
|
11
|
-
build(template, context, state, agent = null) {
|
|
21
|
+
build(template, context, state, agent = null, learnedPatterns = null, thinkBlock = null, relevantMemories = null, planInfo = null) {
|
|
12
22
|
const parts = []
|
|
13
23
|
|
|
14
24
|
// Agent assignment (if applicable)
|
|
25
|
+
// CRITICAL: Include full agent content, not just name
|
|
15
26
|
if (agent) {
|
|
16
|
-
parts.push(
|
|
27
|
+
parts.push(`# AGENT ASSIGNMENT\n`)
|
|
28
|
+
parts.push(`Agent: ${agent.name}\n`)
|
|
29
|
+
|
|
30
|
+
// Include role if available
|
|
31
|
+
if (agent.role) {
|
|
32
|
+
parts.push(`Role: ${agent.role}\n`)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Include domain if available
|
|
36
|
+
if (agent.domain) {
|
|
37
|
+
parts.push(`Domain: ${agent.domain}\n`)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Include skills if available
|
|
41
|
+
if (agent.skills && agent.skills.length > 0) {
|
|
42
|
+
parts.push(`Skills: ${agent.skills.join(', ')}\n`)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
parts.push(`\n## AGENT INSTRUCTIONS\n`)
|
|
46
|
+
|
|
47
|
+
// CRITICAL: Include full agent content
|
|
48
|
+
// This is the specialized knowledge for this project
|
|
49
|
+
if (agent.content) {
|
|
50
|
+
parts.push(agent.content)
|
|
51
|
+
parts.push(`\n`)
|
|
52
|
+
} else if (agent.name) {
|
|
53
|
+
// Fallback if content not loaded
|
|
54
|
+
parts.push(`You are the ${agent.name} agent for this project.\n`)
|
|
55
|
+
parts.push(`Apply your specialized expertise to complete the task.\n\n`)
|
|
56
|
+
}
|
|
57
|
+
|
|
17
58
|
parts.push(`CONTEXT: ${context.filteredSize || 'all'} files (${context.reduction || 0}% reduced)\n\n`)
|
|
18
59
|
}
|
|
19
60
|
|
|
@@ -49,6 +90,75 @@ class PromptBuilder {
|
|
|
49
90
|
parts.push('\n')
|
|
50
91
|
}
|
|
51
92
|
|
|
93
|
+
// Enforcement (Strict Mode)
|
|
94
|
+
parts.push(this.buildEnforcement());
|
|
95
|
+
|
|
96
|
+
// P1.1: Learned Patterns (avoid asking user questions we already know)
|
|
97
|
+
if (learnedPatterns && Object.keys(learnedPatterns).some(k => learnedPatterns[k])) {
|
|
98
|
+
parts.push('\n## LEARNED PATTERNS (use these, do NOT ask user)\n')
|
|
99
|
+
for (const [key, value] of Object.entries(learnedPatterns)) {
|
|
100
|
+
if (value) {
|
|
101
|
+
parts.push(`- ${key}: ${value}\n`)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// P3.1: Think Block (reasoning before action)
|
|
107
|
+
if (thinkBlock && thinkBlock.plan && thinkBlock.plan.length > 0) {
|
|
108
|
+
parts.push('\n## THINK FIRST (reasoning from analysis)\n')
|
|
109
|
+
if (thinkBlock.conclusions && thinkBlock.conclusions.length > 0) {
|
|
110
|
+
parts.push('Conclusions:\n')
|
|
111
|
+
thinkBlock.conclusions.forEach(c => parts.push(` → ${c}\n`))
|
|
112
|
+
}
|
|
113
|
+
parts.push('Plan:\n')
|
|
114
|
+
thinkBlock.plan.forEach((p, i) => parts.push(` ${i + 1}. ${p}\n`))
|
|
115
|
+
parts.push(`Confidence: ${Math.round((thinkBlock.confidence || 0.5) * 100)}%\n`)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// P3.3: Relevant Memories (context from past decisions)
|
|
119
|
+
if (relevantMemories && relevantMemories.length > 0) {
|
|
120
|
+
parts.push('\n## RELEVANT MEMORIES (apply these learnings)\n')
|
|
121
|
+
for (const memory of relevantMemories) {
|
|
122
|
+
parts.push(`- **${memory.title}**: ${memory.content}\n`)
|
|
123
|
+
if (memory.tags && memory.tags.length > 0) {
|
|
124
|
+
parts.push(` Tags: ${memory.tags.join(', ')}\n`)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// P3.4: Plan Mode instructions
|
|
130
|
+
if (planInfo) {
|
|
131
|
+
if (planInfo.isPlanning) {
|
|
132
|
+
parts.push('\n## PLAN MODE ACTIVE\n')
|
|
133
|
+
parts.push('You are in PLANNING mode. Follow these constraints:\n')
|
|
134
|
+
parts.push('1. **READ-ONLY**: Only use read tools (Read, Glob, Grep)\n')
|
|
135
|
+
parts.push('2. **GATHER INFO**: Collect all necessary information\n')
|
|
136
|
+
parts.push('3. **ANALYZE**: Understand the context and implications\n')
|
|
137
|
+
parts.push('4. **PROPOSE**: Generate a plan with clear steps\n')
|
|
138
|
+
parts.push('5. **WAIT FOR APPROVAL**: Do NOT execute until user approves\n')
|
|
139
|
+
|
|
140
|
+
if (planInfo.active) {
|
|
141
|
+
parts.push(`\nCurrent Status: ${planInfo.active.status}\n`)
|
|
142
|
+
if (planInfo.active.gatheredInfo?.length > 0) {
|
|
143
|
+
parts.push(`Info gathered: ${planInfo.active.gatheredInfo.length} items\n`)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (planInfo.requiresApproval) {
|
|
149
|
+
parts.push('\n## APPROVAL REQUIRED\n')
|
|
150
|
+
parts.push('This command is DESTRUCTIVE. You MUST:\n')
|
|
151
|
+
parts.push('1. Show exactly what will change\n')
|
|
152
|
+
parts.push('2. List affected files/resources\n')
|
|
153
|
+
parts.push('3. Ask for explicit user confirmation (y/n)\n')
|
|
154
|
+
parts.push('4. Only proceed after approval\n')
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (planInfo.allowedTools) {
|
|
158
|
+
parts.push(`\nAllowed tools: ${planInfo.allowedTools.join(', ')}\n`)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
52
162
|
// Simple execution directive
|
|
53
163
|
parts.push('\nEXECUTE: Follow flow. Use tools. Decide.\n')
|
|
54
164
|
|
|
@@ -91,6 +201,21 @@ class PromptBuilder {
|
|
|
91
201
|
|
|
92
202
|
return parts.join('')
|
|
93
203
|
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Build enforcement section
|
|
207
|
+
* Forces Claude to follow the process strictly
|
|
208
|
+
*/
|
|
209
|
+
buildEnforcement() {
|
|
210
|
+
return `
|
|
211
|
+
## PROCESS ENFORCEMENT
|
|
212
|
+
1. FOLLOW the Flow strictly. Do not skip steps.
|
|
213
|
+
2. USE the allowed tools only.
|
|
214
|
+
3. IF you are stuck, use the "Ask" tool or stop.
|
|
215
|
+
4. DO NOT hallucinate files or commands.
|
|
216
|
+
5. ALWAYS verify your changes.
|
|
217
|
+
`;
|
|
218
|
+
}
|
|
94
219
|
}
|
|
95
220
|
|
|
96
221
|
module.exports = new PromptBuilder()
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response Templates
|
|
3
|
+
* Minimal output templates for all commands
|
|
4
|
+
* Rule: < 4 lines, always actionable
|
|
5
|
+
*
|
|
6
|
+
* OPTIMIZATION (P0.3): Minimal Output
|
|
7
|
+
* - Concise responses (< 4 lines)
|
|
8
|
+
* - Always suggest next action
|
|
9
|
+
* - Use symbols for status, not words
|
|
10
|
+
*
|
|
11
|
+
* Source: Claude Code, Kiro patterns
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Format duration from milliseconds or ISO strings
|
|
16
|
+
* @param {number|string|Date} start - Start time
|
|
17
|
+
* @param {number|string|Date} end - End time (defaults to now)
|
|
18
|
+
* @returns {string} Human-readable duration
|
|
19
|
+
*/
|
|
20
|
+
function formatDuration(start, end = new Date()) {
|
|
21
|
+
const startMs = new Date(start).getTime()
|
|
22
|
+
const endMs = new Date(end).getTime()
|
|
23
|
+
const diffMs = endMs - startMs
|
|
24
|
+
|
|
25
|
+
const hours = Math.floor(diffMs / (1000 * 60 * 60))
|
|
26
|
+
const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60))
|
|
27
|
+
|
|
28
|
+
if (hours > 0) {
|
|
29
|
+
return `${hours}h ${minutes}m`
|
|
30
|
+
}
|
|
31
|
+
return `${minutes}m`
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Truncate text to max length with ellipsis
|
|
36
|
+
* @param {string} text - Text to truncate
|
|
37
|
+
* @param {number} maxLength - Maximum length
|
|
38
|
+
* @returns {string}
|
|
39
|
+
*/
|
|
40
|
+
function truncate(text, maxLength = 40) {
|
|
41
|
+
if (!text) return ''
|
|
42
|
+
if (text.length <= maxLength) return text
|
|
43
|
+
return text.substring(0, maxLength - 3) + '...'
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Response templates for each command
|
|
48
|
+
* Each template is a function that returns minimal formatted output
|
|
49
|
+
*/
|
|
50
|
+
const templates = {
|
|
51
|
+
/**
|
|
52
|
+
* /p:done - Task completed
|
|
53
|
+
*/
|
|
54
|
+
done: ({ task, duration, nextTask }) => {
|
|
55
|
+
let output = `✓ '${truncate(task)}' (${duration})`
|
|
56
|
+
if (nextTask) {
|
|
57
|
+
output += `\n→ Next: '${truncate(nextTask)}'`
|
|
58
|
+
}
|
|
59
|
+
output += '\n\n/p:ship to release'
|
|
60
|
+
return output
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* /p:now - Current task set/shown
|
|
65
|
+
*/
|
|
66
|
+
now: ({ task, started, isNew }) => {
|
|
67
|
+
if (!task) {
|
|
68
|
+
return `No active task\n→ /p:now "task" to start`
|
|
69
|
+
}
|
|
70
|
+
if (isNew) {
|
|
71
|
+
return `🎯 Started: '${truncate(task)}'\n→ /p:done when complete`
|
|
72
|
+
}
|
|
73
|
+
return `🎯 Working on: '${truncate(task)}'\n⏱️ ${started || 'just now'}\n→ /p:done when complete`
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* /p:next - Priority queue
|
|
78
|
+
*/
|
|
79
|
+
next: ({ tasks, total }) => {
|
|
80
|
+
if (!tasks || tasks.length === 0) {
|
|
81
|
+
return `Queue empty\n→ /p:feature or /p:idea to add`
|
|
82
|
+
}
|
|
83
|
+
const top3 = tasks.slice(0, 3).map((t, i) =>
|
|
84
|
+
`${i + 1}. ${truncate(t.name, 35)}`
|
|
85
|
+
).join('\n')
|
|
86
|
+
const more = total > 3 ? `\n+${total - 3} more` : ''
|
|
87
|
+
return `${top3}${more}\n\n/p:now 1 to start`
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* /p:ship - Feature shipped
|
|
92
|
+
*/
|
|
93
|
+
ship: ({ feature, agent, duration, version }) => {
|
|
94
|
+
let output = `🚀 Shipped: '${truncate(feature)}'`
|
|
95
|
+
if (agent) output += ` (${agent})`
|
|
96
|
+
if (duration) output += ` | ${duration}`
|
|
97
|
+
if (version) output += ` | v${version}`
|
|
98
|
+
output += '\n→ /compact recommended'
|
|
99
|
+
return output
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* /p:idea - Idea captured
|
|
104
|
+
*/
|
|
105
|
+
idea: ({ idea, addedToQueue }) => {
|
|
106
|
+
let output = `💡 Captured: '${truncate(idea)}'`
|
|
107
|
+
if (addedToQueue) {
|
|
108
|
+
output += '\n→ Added to queue'
|
|
109
|
+
}
|
|
110
|
+
output += '\n\n/p:next to see queue'
|
|
111
|
+
return output
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* /p:feature - Feature added
|
|
116
|
+
*/
|
|
117
|
+
feature: ({ feature, tasks, impact, effort }) => {
|
|
118
|
+
let output = `📋 Feature: '${truncate(feature)}'`
|
|
119
|
+
if (tasks) output += ` (${tasks} tasks)`
|
|
120
|
+
if (impact) output += `\nImpact: ${impact}`
|
|
121
|
+
if (effort) output += ` | Effort: ${effort}`
|
|
122
|
+
output += '\n\n/p:now to start'
|
|
123
|
+
return output
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* /p:bug - Bug reported
|
|
128
|
+
*/
|
|
129
|
+
bug: ({ description, priority, addedAt }) => {
|
|
130
|
+
const priorityIcon = {
|
|
131
|
+
'critical': '🔴',
|
|
132
|
+
'high': '🟠',
|
|
133
|
+
'medium': '🟡',
|
|
134
|
+
'low': '🟢'
|
|
135
|
+
}[priority] || '🟡'
|
|
136
|
+
|
|
137
|
+
let output = `${priorityIcon} Bug: '${truncate(description)}'\nPriority: ${priority}`
|
|
138
|
+
if (addedAt) {
|
|
139
|
+
output += ` | Added: ${addedAt}`
|
|
140
|
+
}
|
|
141
|
+
output += '\n\n/p:now to fix'
|
|
142
|
+
return output
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* /p:pause - Task paused
|
|
147
|
+
*/
|
|
148
|
+
pause: ({ task, duration }) => {
|
|
149
|
+
return `⏸️ Paused: '${truncate(task)}' (${duration})\n→ /p:resume to continue`
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* /p:resume - Task resumed
|
|
154
|
+
*/
|
|
155
|
+
resume: ({ task, pausedFor }) => {
|
|
156
|
+
return `▶️ Resumed: '${truncate(task)}'\nPaused for: ${pausedFor}\n→ /p:done when complete`
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* /p:recap - Project overview
|
|
161
|
+
*/
|
|
162
|
+
recap: ({ shipped, inProgress, queued, momentum }) => {
|
|
163
|
+
const momentumIcon = {
|
|
164
|
+
'high': '🔥',
|
|
165
|
+
'medium': '✨',
|
|
166
|
+
'low': '💤'
|
|
167
|
+
}[momentum] || '✨'
|
|
168
|
+
|
|
169
|
+
return `${momentumIcon} ${shipped} shipped | ${inProgress ? '1 active' : '0 active'} | ${queued} queued`
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* /p:progress - Progress metrics
|
|
174
|
+
*/
|
|
175
|
+
progress: ({ period, shipped, velocity, trend }) => {
|
|
176
|
+
const trendIcon = trend > 0 ? '↑' : trend < 0 ? '↓' : '→'
|
|
177
|
+
return `📊 ${period}: ${shipped} shipped\nVelocity: ${velocity}/week ${trendIcon}`
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* /p:analyze - Analysis complete
|
|
182
|
+
*/
|
|
183
|
+
analyze: ({ stack, files, agents }) => {
|
|
184
|
+
return `🔍 Analyzed: ${stack}\n${files} files | ${agents} agents generated\n\n/p:sync to update`
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* /p:sync - Sync complete
|
|
189
|
+
*/
|
|
190
|
+
sync: ({ updated, agents }) => {
|
|
191
|
+
return `🔄 Synced: ${updated} files updated\n${agents} agents refreshed`
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* /p:help - Help shown
|
|
196
|
+
*/
|
|
197
|
+
help: ({ context, suggestions }) => {
|
|
198
|
+
const sugs = suggestions.slice(0, 3).map(s => `• ${s}`).join('\n')
|
|
199
|
+
return `📚 ${context}\n\n${sugs}`
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* /p:suggest - Suggestions
|
|
204
|
+
*/
|
|
205
|
+
suggest: ({ urgency, suggestion, command }) => {
|
|
206
|
+
const urgencyIcon = {
|
|
207
|
+
'high': '🔥',
|
|
208
|
+
'medium': '💡',
|
|
209
|
+
'low': '✨'
|
|
210
|
+
}[urgency] || '💡'
|
|
211
|
+
|
|
212
|
+
return `${urgencyIcon} ${suggestion}\n→ ${command}`
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* /p:spec - Spec created/updated
|
|
217
|
+
*/
|
|
218
|
+
spec: ({ name, status, tasks, requirements, isNew }) => {
|
|
219
|
+
let output = isNew
|
|
220
|
+
? `📋 Created spec: '${truncate(name)}'`
|
|
221
|
+
: `📋 Updated spec: '${truncate(name)}'`
|
|
222
|
+
|
|
223
|
+
if (requirements) output += `\n${requirements} requirements`
|
|
224
|
+
if (tasks) output += ` | ${tasks} tasks`
|
|
225
|
+
if (status) output += ` | Status: ${status}`
|
|
226
|
+
|
|
227
|
+
output += '\n\n→ Review and approve to start'
|
|
228
|
+
return output
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Generic success response
|
|
233
|
+
*/
|
|
234
|
+
success: ({ message, nextAction }) => {
|
|
235
|
+
let output = `✓ ${message}`
|
|
236
|
+
if (nextAction) {
|
|
237
|
+
output += `\n→ ${nextAction}`
|
|
238
|
+
}
|
|
239
|
+
return output
|
|
240
|
+
},
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Generic error response
|
|
244
|
+
*/
|
|
245
|
+
error: ({ error, suggestion }) => {
|
|
246
|
+
let output = `❌ ${error}`
|
|
247
|
+
if (suggestion) {
|
|
248
|
+
output += `\n→ ${suggestion}`
|
|
249
|
+
}
|
|
250
|
+
return output
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Format a response using the appropriate template
|
|
256
|
+
*
|
|
257
|
+
* @param {string} commandName - Command name
|
|
258
|
+
* @param {Object} data - Data for the template
|
|
259
|
+
* @returns {string} Formatted response
|
|
260
|
+
*/
|
|
261
|
+
function format(commandName, data) {
|
|
262
|
+
const template = templates[commandName]
|
|
263
|
+
if (!template) {
|
|
264
|
+
// Fallback to generic success/error
|
|
265
|
+
if (data.error) {
|
|
266
|
+
return templates.error(data)
|
|
267
|
+
}
|
|
268
|
+
return templates.success(data)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return template(data)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Check if response exceeds recommended length
|
|
276
|
+
* @param {string} response - Response text
|
|
277
|
+
* @returns {boolean} True if too long
|
|
278
|
+
*/
|
|
279
|
+
function isTooLong(response) {
|
|
280
|
+
const lines = response.split('\n').filter(l => l.trim())
|
|
281
|
+
return lines.length > 4
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
module.exports = {
|
|
285
|
+
format,
|
|
286
|
+
templates,
|
|
287
|
+
formatDuration,
|
|
288
|
+
truncate,
|
|
289
|
+
isTooLong
|
|
290
|
+
}
|