@howlil/ez-agents 3.4.1 → 3.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 (162) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +84 -20
  3. package/agents/ez-observer-agent.md +260 -0
  4. package/agents/ez-release-agent.md +333 -0
  5. package/agents/ez-requirements-agent.md +377 -0
  6. package/agents/ez-scrum-master-agent.md +242 -0
  7. package/agents/ez-tech-lead-agent.md +267 -0
  8. package/bin/install.js +3221 -3230
  9. package/commands/ez/arch-review.md +102 -0
  10. package/commands/ez/execute-phase.md +11 -0
  11. package/commands/ez/export-session.md +79 -0
  12. package/commands/ez/gather-requirements.md +117 -0
  13. package/commands/ez/git-workflow.md +72 -0
  14. package/commands/ez/hotfix.md +120 -0
  15. package/commands/ez/import-session.md +82 -0
  16. package/commands/ez/join-discord.md +18 -18
  17. package/commands/ez/list-sessions.md +96 -0
  18. package/commands/ez/package-manager.md +316 -0
  19. package/commands/ez/plan-phase.md +9 -1
  20. package/commands/ez/preflight.md +79 -0
  21. package/commands/ez/progress.md +13 -1
  22. package/commands/ez/release.md +153 -0
  23. package/commands/ez/resume.md +107 -0
  24. package/commands/ez/standup.md +85 -0
  25. package/ez-agents/bin/ez-tools.cjs +1095 -716
  26. package/ez-agents/bin/lib/assistant-adapter.cjs +264 -264
  27. package/ez-agents/bin/lib/audit-exec.cjs +7 -2
  28. package/ez-agents/bin/lib/bdd-validator.cjs +622 -0
  29. package/ez-agents/bin/lib/circuit-breaker.cjs +118 -118
  30. package/ez-agents/bin/lib/config.cjs +190 -190
  31. package/ez-agents/bin/lib/content-scanner.cjs +238 -0
  32. package/ez-agents/bin/lib/context-cache.cjs +154 -0
  33. package/ez-agents/bin/lib/context-errors.cjs +71 -0
  34. package/ez-agents/bin/lib/context-manager.cjs +220 -0
  35. package/ez-agents/bin/lib/discussion-synthesizer.cjs +458 -0
  36. package/ez-agents/bin/lib/file-access.cjs +207 -0
  37. package/ez-agents/bin/lib/file-lock.cjs +236 -236
  38. package/ez-agents/bin/lib/frontmatter.cjs +299 -299
  39. package/ez-agents/bin/lib/fs-utils.cjs +153 -153
  40. package/ez-agents/bin/lib/git-errors.cjs +83 -0
  41. package/ez-agents/bin/lib/git-utils.cjs +118 -0
  42. package/ez-agents/bin/lib/git-workflow-engine.cjs +1157 -0
  43. package/ez-agents/bin/lib/index.cjs +157 -113
  44. package/ez-agents/bin/lib/init.cjs +757 -757
  45. package/ez-agents/bin/lib/lockfile-validator.cjs +227 -0
  46. package/ez-agents/bin/lib/logger.cjs +124 -124
  47. package/ez-agents/bin/lib/memory-compression.cjs +256 -0
  48. package/ez-agents/bin/lib/metrics-tracker.cjs +406 -0
  49. package/ez-agents/bin/lib/milestone.cjs +241 -241
  50. package/ez-agents/bin/lib/model-provider.cjs +241 -241
  51. package/ez-agents/bin/lib/package-manager-detector.cjs +203 -0
  52. package/ez-agents/bin/lib/package-manager-executor.cjs +385 -0
  53. package/ez-agents/bin/lib/package-manager-service.cjs +216 -0
  54. package/ez-agents/bin/lib/phase.cjs +925 -925
  55. package/ez-agents/bin/lib/planning-write.cjs +107 -107
  56. package/ez-agents/bin/lib/release-validator.cjs +614 -0
  57. package/ez-agents/bin/lib/retry.cjs +119 -119
  58. package/ez-agents/bin/lib/roadmap.cjs +306 -306
  59. package/ez-agents/bin/lib/safe-exec.cjs +128 -128
  60. package/ez-agents/bin/lib/safe-path.cjs +130 -130
  61. package/ez-agents/bin/lib/session-chain.cjs +304 -0
  62. package/ez-agents/bin/lib/session-errors.cjs +81 -0
  63. package/ez-agents/bin/lib/session-export.cjs +251 -0
  64. package/ez-agents/bin/lib/session-import.cjs +262 -0
  65. package/ez-agents/bin/lib/session-manager.cjs +280 -0
  66. package/ez-agents/bin/lib/state.cjs +736 -736
  67. package/ez-agents/bin/lib/temp-file.cjs +239 -239
  68. package/ez-agents/bin/lib/template.cjs +223 -223
  69. package/ez-agents/bin/lib/test-file-lock.cjs +112 -112
  70. package/ez-agents/bin/lib/test-graceful.cjs +93 -93
  71. package/ez-agents/bin/lib/test-logger.cjs +60 -60
  72. package/ez-agents/bin/lib/test-safe-exec.cjs +38 -38
  73. package/ez-agents/bin/lib/test-safe-path.cjs +33 -33
  74. package/ez-agents/bin/lib/test-temp-file.cjs +125 -125
  75. package/ez-agents/bin/lib/tier-manager.cjs +428 -0
  76. package/ez-agents/bin/lib/timeout-exec.cjs +63 -63
  77. package/ez-agents/bin/lib/url-fetch.cjs +170 -0
  78. package/ez-agents/bin/lib/verify.cjs +15 -1
  79. package/ez-agents/references/checkpoints.md +776 -776
  80. package/ez-agents/references/continuation-format.md +249 -249
  81. package/ez-agents/references/metrics-schema.md +118 -0
  82. package/ez-agents/references/planning-config.md +140 -0
  83. package/ez-agents/references/questioning.md +162 -162
  84. package/ez-agents/references/tdd.md +263 -263
  85. package/ez-agents/references/tier-strategy.md +103 -0
  86. package/ez-agents/templates/bdd-feature.md +173 -0
  87. package/ez-agents/templates/codebase/concerns.md +310 -310
  88. package/ez-agents/templates/codebase/conventions.md +307 -307
  89. package/ez-agents/templates/codebase/integrations.md +280 -280
  90. package/ez-agents/templates/codebase/stack.md +186 -186
  91. package/ez-agents/templates/codebase/testing.md +480 -480
  92. package/ez-agents/templates/config.json +37 -37
  93. package/ez-agents/templates/continue-here.md +78 -78
  94. package/ez-agents/templates/discussion.md +68 -0
  95. package/ez-agents/templates/incident-runbook.md +205 -0
  96. package/ez-agents/templates/milestone-archive.md +123 -123
  97. package/ez-agents/templates/milestone.md +115 -115
  98. package/ez-agents/templates/release-checklist.md +133 -0
  99. package/ez-agents/templates/requirements.md +231 -231
  100. package/ez-agents/templates/research-project/ARCHITECTURE.md +204 -204
  101. package/ez-agents/templates/research-project/FEATURES.md +147 -147
  102. package/ez-agents/templates/research-project/PITFALLS.md +200 -200
  103. package/ez-agents/templates/research-project/STACK.md +120 -120
  104. package/ez-agents/templates/research-project/SUMMARY.md +170 -170
  105. package/ez-agents/templates/retrospective.md +54 -54
  106. package/ez-agents/templates/roadmap.md +202 -202
  107. package/ez-agents/templates/rollback-plan.md +201 -0
  108. package/ez-agents/templates/summary-minimal.md +41 -41
  109. package/ez-agents/templates/summary-standard.md +48 -48
  110. package/ez-agents/templates/summary.md +248 -248
  111. package/ez-agents/templates/user-setup.md +311 -311
  112. package/ez-agents/templates/verification-report.md +322 -322
  113. package/ez-agents/workflows/add-phase.md +112 -112
  114. package/ez-agents/workflows/add-tests.md +351 -351
  115. package/ez-agents/workflows/add-todo.md +158 -158
  116. package/ez-agents/workflows/arch-review.md +54 -0
  117. package/ez-agents/workflows/audit-milestone.md +332 -332
  118. package/ez-agents/workflows/autonomous.md +131 -30
  119. package/ez-agents/workflows/check-todos.md +177 -177
  120. package/ez-agents/workflows/cleanup.md +152 -152
  121. package/ez-agents/workflows/complete-milestone.md +766 -766
  122. package/ez-agents/workflows/diagnose-issues.md +219 -219
  123. package/ez-agents/workflows/discovery-phase.md +289 -289
  124. package/ez-agents/workflows/discuss-phase.md +762 -762
  125. package/ez-agents/workflows/execute-phase.md +513 -468
  126. package/ez-agents/workflows/execute-plan.md +483 -483
  127. package/ez-agents/workflows/export-session.md +255 -0
  128. package/ez-agents/workflows/gather-requirements.md +206 -0
  129. package/ez-agents/workflows/health.md +159 -159
  130. package/ez-agents/workflows/help.md +584 -492
  131. package/ez-agents/workflows/hotfix.md +291 -0
  132. package/ez-agents/workflows/import-session.md +303 -0
  133. package/ez-agents/workflows/insert-phase.md +130 -130
  134. package/ez-agents/workflows/list-phase-assumptions.md +178 -178
  135. package/ez-agents/workflows/map-codebase.md +316 -316
  136. package/ez-agents/workflows/new-milestone.md +339 -10
  137. package/ez-agents/workflows/new-project.md +293 -299
  138. package/ez-agents/workflows/node-repair.md +92 -92
  139. package/ez-agents/workflows/pause-work.md +122 -122
  140. package/ez-agents/workflows/plan-milestone-gaps.md +274 -274
  141. package/ez-agents/workflows/plan-phase.md +673 -651
  142. package/ez-agents/workflows/progress.md +372 -382
  143. package/ez-agents/workflows/quick.md +610 -610
  144. package/ez-agents/workflows/release.md +253 -0
  145. package/ez-agents/workflows/remove-phase.md +155 -155
  146. package/ez-agents/workflows/research-phase.md +74 -74
  147. package/ez-agents/workflows/resume-project.md +307 -307
  148. package/ez-agents/workflows/resume-session.md +215 -0
  149. package/ez-agents/workflows/set-profile.md +81 -81
  150. package/ez-agents/workflows/settings.md +242 -242
  151. package/ez-agents/workflows/standup.md +64 -0
  152. package/ez-agents/workflows/stats.md +57 -57
  153. package/ez-agents/workflows/transition.md +544 -544
  154. package/ez-agents/workflows/ui-phase.md +290 -290
  155. package/ez-agents/workflows/ui-review.md +157 -157
  156. package/ez-agents/workflows/update.md +320 -320
  157. package/ez-agents/workflows/validate-phase.md +167 -167
  158. package/ez-agents/workflows/verify-phase.md +243 -243
  159. package/ez-agents/workflows/verify-work.md +584 -584
  160. package/package.json +10 -4
  161. package/scripts/build-hooks.js +43 -43
  162. package/scripts/run-tests.cjs +29 -29
@@ -1,241 +1,241 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * EZ Model Provider — Unified API for multiple AI providers
5
- *
6
- * Supports: Anthropic, Moonshot (Kimi), Alibaba (Qwen), OpenAI
7
- */
8
-
9
- const https = require('https');
10
- const { URL } = require('url');
11
- const Logger = require('./logger.cjs');
12
- const logger = new Logger();
13
-
14
- class ModelProvider {
15
- /**
16
- * Create model provider
17
- * @param {Object} config - Configuration
18
- */
19
- constructor(config) {
20
- this.provider = config.provider || 'anthropic';
21
- this.model = config.model || 'sonnet';
22
- this.apiKey = config.apiKey || process.env[`${this.provider.toUpperCase()}_API_KEY`];
23
-
24
- if (!this.apiKey && this.provider !== 'anthropic') {
25
- logger.warn('API key not configured', { provider: this.provider });
26
- }
27
- }
28
-
29
- /**
30
- * Helper for HTTP requests
31
- */
32
- _httpRequest(options, data) {
33
- return new Promise((resolve, reject) => {
34
- const req = https.request(options, (res) => {
35
- let body = '';
36
- res.on('data', (chunk) => body += chunk);
37
- res.on('end', () => {
38
- if (res.statusCode >= 200 && res.statusCode < 300) {
39
- try {
40
- resolve(JSON.parse(body));
41
- } catch (e) {
42
- resolve(body);
43
- }
44
- } else {
45
- reject(new Error(`HTTP ${res.statusCode}: ${body}`));
46
- }
47
- });
48
- });
49
- req.on('error', reject);
50
- if (data) req.write(JSON.stringify(data));
51
- req.end();
52
- });
53
- }
54
-
55
- /**
56
- * Send chat message
57
- * @param {Object[]} messages - Chat messages
58
- * @param {Object} options - Chat options
59
- * @returns {Promise<Object>} - Response
60
- */
61
- async chat(messages, options = {}) {
62
- logger.info('Chat request', {
63
- provider: this.provider,
64
- model: this.model,
65
- messageCount: messages.length
66
- });
67
-
68
- switch (this.provider) {
69
- case 'anthropic':
70
- return this._chatAnthropic(messages, options);
71
- case 'moonshot':
72
- return this._chatMoonshot(messages, options);
73
- case 'alibaba':
74
- case 'qwen':
75
- return this._chatQwen(messages, options);
76
- case 'openai':
77
- return this._chatOpenAI(messages, options);
78
- default:
79
- throw new Error(`Unsupported provider: ${this.provider}`);
80
- }
81
- }
82
-
83
- /**
84
- * Anthropic Claude API
85
- */
86
- async _chatAnthropic(messages, options) {
87
- // Anthropic usually requires their SDK or complex headers
88
- logger.debug('Anthropic chat', { model: this.model });
89
- return {
90
- content: '[Anthropic response placeholder - requires SDK]',
91
- provider: 'anthropic',
92
- model: this.model
93
- };
94
- }
95
-
96
- /**
97
- * Moonshot (Kimi) API
98
- */
99
- async _chatMoonshot(messages, options) {
100
- const modelName = this.model === 'sonnet' ? 'moonshot-v1-8k' : this.model;
101
- const data = {
102
- model: modelName,
103
- messages: messages,
104
- temperature: options.temperature || 0.3
105
- };
106
-
107
- const reqOptions = {
108
- hostname: 'api.moonshot.cn',
109
- path: '/v1/chat/completions',
110
- method: 'POST',
111
- headers: {
112
- 'Content-Type': 'application/json',
113
- 'Authorization': `Bearer ${this.apiKey}`
114
- }
115
- };
116
-
117
- try {
118
- const response = await this._httpRequest(reqOptions, data);
119
- return {
120
- content: response.choices[0].message.content,
121
- provider: 'moonshot',
122
- model: modelName
123
- };
124
- } catch (error) {
125
- logger.error('Moonshot API error', { error: error.message });
126
- throw error;
127
- }
128
- }
129
-
130
- /**
131
- * Alibaba Qwen API (DashScope)
132
- */
133
- async _chatQwen(messages, options) {
134
- // Map generic model names to Qwen specific ones
135
- let modelName = this.model;
136
- if (modelName === 'sonnet' || modelName === 'gpt-4') modelName = 'qwen-max';
137
- if (modelName === 'haiku' || modelName === 'gpt-3.5-turbo') modelName = 'qwen-plus';
138
-
139
- const data = {
140
- model: modelName,
141
- input: {
142
- messages: messages
143
- },
144
- parameters: {
145
- result_format: 'message',
146
- temperature: options.temperature || 0.3
147
- }
148
- };
149
-
150
- const reqOptions = {
151
- hostname: 'dashscope.aliyuncs.com',
152
- path: '/api/v1/services/aigc/text-generation/generation',
153
- method: 'POST',
154
- headers: {
155
- 'Content-Type': 'application/json',
156
- 'Authorization': `Bearer ${this.apiKey}`
157
- }
158
- };
159
-
160
- try {
161
- const response = await this._httpRequest(reqOptions, data);
162
- return {
163
- content: response.output.choices[0].message.content,
164
- provider: 'alibaba',
165
- model: modelName
166
- };
167
- } catch (error) {
168
- logger.error('Qwen API error', { error: error.message });
169
- throw error;
170
- }
171
- }
172
-
173
- /**
174
- * OpenAI API
175
- */
176
- async _chatOpenAI(messages, options) {
177
- const modelName = this.model === 'sonnet' ? 'gpt-4-turbo' : this.model;
178
- const data = {
179
- model: modelName,
180
- messages: messages,
181
- temperature: options.temperature || 0.3
182
- };
183
-
184
- const reqOptions = {
185
- hostname: 'api.openai.com',
186
- path: '/v1/chat/completions',
187
- method: 'POST',
188
- headers: {
189
- 'Content-Type': 'application/json',
190
- 'Authorization': `Bearer ${this.apiKey}`
191
- }
192
- };
193
-
194
- try {
195
- const response = await this._httpRequest(reqOptions, data);
196
- return {
197
- content: response.choices[0].message.content,
198
- provider: 'openai',
199
- model: modelName
200
- };
201
- } catch (error) {
202
- logger.error('OpenAI API error', { error: error.message });
203
- throw error;
204
- }
205
- }
206
-
207
- /**
208
- * Count tokens (approximate)
209
- * @param {string} text - Text to count
210
- * @returns {number} - Approximate token count
211
- */
212
- countTokens(text) {
213
- return Math.ceil(text.length / 4);
214
- }
215
-
216
- /**
217
- * Get provider info
218
- * @returns {Object} - Provider information
219
- */
220
- getInfo() {
221
- return {
222
- provider: this.provider,
223
- model: this.model,
224
- hasApiKey: !!this.apiKey
225
- };
226
- }
227
- }
228
-
229
- /**
230
- * Create provider from config
231
- * @param {Object} config - Provider config
232
- * @returns {ModelProvider} - Model provider instance
233
- */
234
- function createProvider(config) {
235
- return new ModelProvider(config);
236
- }
237
-
238
- module.exports = {
239
- ModelProvider,
240
- createProvider
241
- };
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * EZ Model Provider — Unified API for multiple AI providers
5
+ *
6
+ * Supports: Anthropic, Moonshot (Kimi), Alibaba (Qwen), OpenAI
7
+ */
8
+
9
+ const https = require('https');
10
+ const { URL } = require('url');
11
+ const Logger = require('./logger.cjs');
12
+ const logger = new Logger();
13
+
14
+ class ModelProvider {
15
+ /**
16
+ * Create model provider
17
+ * @param {Object} config - Configuration
18
+ */
19
+ constructor(config) {
20
+ this.provider = config.provider || 'anthropic';
21
+ this.model = config.model || 'sonnet';
22
+ this.apiKey = config.apiKey || process.env[`${this.provider.toUpperCase()}_API_KEY`];
23
+
24
+ if (!this.apiKey && this.provider !== 'anthropic') {
25
+ logger.warn('API key not configured', { provider: this.provider });
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Helper for HTTP requests
31
+ */
32
+ _httpRequest(options, data) {
33
+ return new Promise((resolve, reject) => {
34
+ const req = https.request(options, (res) => {
35
+ let body = '';
36
+ res.on('data', (chunk) => body += chunk);
37
+ res.on('end', () => {
38
+ if (res.statusCode >= 200 && res.statusCode < 300) {
39
+ try {
40
+ resolve(JSON.parse(body));
41
+ } catch (e) {
42
+ resolve(body);
43
+ }
44
+ } else {
45
+ reject(new Error(`HTTP ${res.statusCode}: ${body}`));
46
+ }
47
+ });
48
+ });
49
+ req.on('error', reject);
50
+ if (data) req.write(JSON.stringify(data));
51
+ req.end();
52
+ });
53
+ }
54
+
55
+ /**
56
+ * Send chat message
57
+ * @param {Object[]} messages - Chat messages
58
+ * @param {Object} options - Chat options
59
+ * @returns {Promise<Object>} - Response
60
+ */
61
+ async chat(messages, options = {}) {
62
+ logger.info('Chat request', {
63
+ provider: this.provider,
64
+ model: this.model,
65
+ messageCount: messages.length
66
+ });
67
+
68
+ switch (this.provider) {
69
+ case 'anthropic':
70
+ return this._chatAnthropic(messages, options);
71
+ case 'moonshot':
72
+ return this._chatMoonshot(messages, options);
73
+ case 'alibaba':
74
+ case 'qwen':
75
+ return this._chatQwen(messages, options);
76
+ case 'openai':
77
+ return this._chatOpenAI(messages, options);
78
+ default:
79
+ throw new Error(`Unsupported provider: ${this.provider}`);
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Anthropic Claude API
85
+ */
86
+ async _chatAnthropic(messages, options) {
87
+ // Anthropic usually requires their SDK or complex headers
88
+ logger.debug('Anthropic chat', { model: this.model });
89
+ return {
90
+ content: '[Anthropic response placeholder - requires SDK]',
91
+ provider: 'anthropic',
92
+ model: this.model
93
+ };
94
+ }
95
+
96
+ /**
97
+ * Moonshot (Kimi) API
98
+ */
99
+ async _chatMoonshot(messages, options) {
100
+ const modelName = this.model === 'sonnet' ? 'moonshot-v1-8k' : this.model;
101
+ const data = {
102
+ model: modelName,
103
+ messages: messages,
104
+ temperature: options.temperature || 0.3
105
+ };
106
+
107
+ const reqOptions = {
108
+ hostname: 'api.moonshot.cn',
109
+ path: '/v1/chat/completions',
110
+ method: 'POST',
111
+ headers: {
112
+ 'Content-Type': 'application/json',
113
+ 'Authorization': `Bearer ${this.apiKey}`
114
+ }
115
+ };
116
+
117
+ try {
118
+ const response = await this._httpRequest(reqOptions, data);
119
+ return {
120
+ content: response.choices[0].message.content,
121
+ provider: 'moonshot',
122
+ model: modelName
123
+ };
124
+ } catch (error) {
125
+ logger.error('Moonshot API error', { error: error.message });
126
+ throw error;
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Alibaba Qwen API (DashScope)
132
+ */
133
+ async _chatQwen(messages, options) {
134
+ // Map generic model names to Qwen specific ones
135
+ let modelName = this.model;
136
+ if (modelName === 'sonnet' || modelName === 'gpt-4') modelName = 'qwen-max';
137
+ if (modelName === 'haiku' || modelName === 'gpt-3.5-turbo') modelName = 'qwen-plus';
138
+
139
+ const data = {
140
+ model: modelName,
141
+ input: {
142
+ messages: messages
143
+ },
144
+ parameters: {
145
+ result_format: 'message',
146
+ temperature: options.temperature || 0.3
147
+ }
148
+ };
149
+
150
+ const reqOptions = {
151
+ hostname: 'dashscope.aliyuncs.com',
152
+ path: '/api/v1/services/aigc/text-generation/generation',
153
+ method: 'POST',
154
+ headers: {
155
+ 'Content-Type': 'application/json',
156
+ 'Authorization': `Bearer ${this.apiKey}`
157
+ }
158
+ };
159
+
160
+ try {
161
+ const response = await this._httpRequest(reqOptions, data);
162
+ return {
163
+ content: response.output.choices[0].message.content,
164
+ provider: 'alibaba',
165
+ model: modelName
166
+ };
167
+ } catch (error) {
168
+ logger.error('Qwen API error', { error: error.message });
169
+ throw error;
170
+ }
171
+ }
172
+
173
+ /**
174
+ * OpenAI API
175
+ */
176
+ async _chatOpenAI(messages, options) {
177
+ const modelName = this.model === 'sonnet' ? 'gpt-4-turbo' : this.model;
178
+ const data = {
179
+ model: modelName,
180
+ messages: messages,
181
+ temperature: options.temperature || 0.3
182
+ };
183
+
184
+ const reqOptions = {
185
+ hostname: 'api.openai.com',
186
+ path: '/v1/chat/completions',
187
+ method: 'POST',
188
+ headers: {
189
+ 'Content-Type': 'application/json',
190
+ 'Authorization': `Bearer ${this.apiKey}`
191
+ }
192
+ };
193
+
194
+ try {
195
+ const response = await this._httpRequest(reqOptions, data);
196
+ return {
197
+ content: response.choices[0].message.content,
198
+ provider: 'openai',
199
+ model: modelName
200
+ };
201
+ } catch (error) {
202
+ logger.error('OpenAI API error', { error: error.message });
203
+ throw error;
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Count tokens (approximate)
209
+ * @param {string} text - Text to count
210
+ * @returns {number} - Approximate token count
211
+ */
212
+ countTokens(text) {
213
+ return Math.ceil(text.length / 4);
214
+ }
215
+
216
+ /**
217
+ * Get provider info
218
+ * @returns {Object} - Provider information
219
+ */
220
+ getInfo() {
221
+ return {
222
+ provider: this.provider,
223
+ model: this.model,
224
+ hasApiKey: !!this.apiKey
225
+ };
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Create provider from config
231
+ * @param {Object} config - Provider config
232
+ * @returns {ModelProvider} - Model provider instance
233
+ */
234
+ function createProvider(config) {
235
+ return new ModelProvider(config);
236
+ }
237
+
238
+ module.exports = {
239
+ ModelProvider,
240
+ createProvider
241
+ };
@@ -0,0 +1,203 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Package Manager Detector — Auto-detect available package managers
5
+ *
6
+ * Detects package managers (npm, yarn, pnpm) using a multi-layer strategy:
7
+ * 1. Configuration override (.planning/config.json)
8
+ * 2. Lockfile presence (pnpm-lock.yaml, yarn.lock, package-lock.json)
9
+ * 3. System availability (which pnpm/yarn/npm)
10
+ * 4. Fallback to npm (always available with Node)
11
+ *
12
+ * Usage:
13
+ * const PackageManagerDetector = require('./package-manager-detector.cjs');
14
+ * const detector = new PackageManagerDetector(cwd);
15
+ * const result = detector.detect();
16
+ * // Returns: { manager, source, confidence, lockfilePath? }
17
+ */
18
+
19
+ const { execFileSync } = require('child_process');
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+ const Logger = require('./logger.cjs');
23
+
24
+ /**
25
+ * Package Manager Detector class
26
+ * Detects available package managers with priority-based strategy
27
+ */
28
+ class PackageManagerDetector {
29
+ /**
30
+ * Create a PackageManagerDetector instance
31
+ * @param {string} cwd - Working directory (default: process.cwd())
32
+ */
33
+ constructor(cwd = process.cwd()) {
34
+ this.cwd = cwd;
35
+ this.logger = new Logger();
36
+ this.config = this.loadConfig();
37
+ }
38
+
39
+ /**
40
+ * Detect package manager with priority-based strategy
41
+ * @returns {Object} Detection result { manager, source, confidence, lockfilePath? }
42
+ */
43
+ detect() {
44
+ this.logger.info('Starting package manager detection', { cwd: this.cwd });
45
+
46
+ // Layer 1: Configuration override
47
+ const configManager = this._detectFromConfig();
48
+ if (configManager) {
49
+ this.logger.info('Package manager detected from config', { manager: configManager });
50
+ return {
51
+ manager: configManager,
52
+ source: 'config',
53
+ confidence: 'high',
54
+ configPath: '.planning/config.json'
55
+ };
56
+ }
57
+
58
+ // Layer 2: Lockfile detection
59
+ const lockfileManager = this.detectFromLockfile();
60
+ if (lockfileManager) {
61
+ const lockfilePath = this.getLockfilePath(lockfileManager);
62
+ this.logger.info('Package manager detected from lockfile', {
63
+ manager: lockfileManager,
64
+ lockfilePath
65
+ });
66
+ return {
67
+ manager: lockfileManager,
68
+ source: 'lockfile',
69
+ confidence: 'high',
70
+ lockfilePath
71
+ };
72
+ }
73
+
74
+ // Layer 3: System availability
75
+ const availableManagers = this.getAvailableManagers();
76
+ if (availableManagers.length > 0) {
77
+ // Prefer pnpm > yarn > npm (performance order)
78
+ const preferred = availableManagers.find(m => m === 'pnpm') ||
79
+ availableManagers.find(m => m === 'yarn') ||
80
+ availableManagers[0];
81
+ this.logger.info('Package manager detected from system', {
82
+ manager: preferred,
83
+ available: availableManagers
84
+ });
85
+ return {
86
+ manager: preferred,
87
+ source: 'system',
88
+ confidence: 'medium',
89
+ available: availableManagers
90
+ };
91
+ }
92
+
93
+ // Layer 4: Fallback to npm
94
+ this.logger.warn('No package manager detected, falling back to npm');
95
+ return {
96
+ manager: 'npm',
97
+ source: 'fallback',
98
+ confidence: 'low',
99
+ reason: 'No other package manager detected'
100
+ };
101
+ }
102
+
103
+ /**
104
+ * Detect package manager from configuration
105
+ * @private
106
+ * @returns {string|null} Package manager name or null
107
+ */
108
+ _detectFromConfig() {
109
+ const configManager = this.config?.packageManager?.default;
110
+ if (configManager && this.isPackageManagerInstalled(configManager)) {
111
+ return configManager;
112
+ }
113
+ return null;
114
+ }
115
+
116
+ /**
117
+ * Detect package manager from lockfile presence
118
+ * @returns {string|null} Package manager name or null
119
+ */
120
+ detectFromLockfile() {
121
+ const lockfiles = {
122
+ 'pnpm-lock.yaml': 'pnpm',
123
+ 'yarn.lock': 'yarn',
124
+ 'package-lock.json': 'npm'
125
+ };
126
+
127
+ for (const [lockfile, manager] of Object.entries(lockfiles)) {
128
+ const lockfilePath = path.join(this.cwd, lockfile);
129
+ if (fs.existsSync(lockfilePath)) {
130
+ return manager;
131
+ }
132
+ }
133
+ return null;
134
+ }
135
+
136
+ /**
137
+ * Get all available package managers installed on the system
138
+ * @returns {string[]} Array of available manager names
139
+ */
140
+ getAvailableManagers() {
141
+ const managers = ['pnpm', 'yarn', 'npm'];
142
+ const available = [];
143
+
144
+ for (const manager of managers) {
145
+ if (this.isPackageManagerInstalled(manager)) {
146
+ available.push(manager);
147
+ }
148
+ }
149
+
150
+ return available;
151
+ }
152
+
153
+ /**
154
+ * Check if a specific package manager is installed
155
+ * @param {string} manager - Package manager name
156
+ * @returns {boolean} True if installed
157
+ */
158
+ isPackageManagerInstalled(manager) {
159
+ try {
160
+ execFileSync(manager, ['--version'], {
161
+ stdio: 'pipe',
162
+ shell: false
163
+ });
164
+ return true;
165
+ } catch (err) {
166
+ this.logger.debug('Package manager not installed', { manager, error: err.message });
167
+ return false;
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Get the lockfile path for a specific package manager
173
+ * @param {string} manager - Package manager name
174
+ * @returns {string} Full path to lockfile
175
+ */
176
+ getLockfilePath(manager) {
177
+ const lockfiles = {
178
+ 'pnpm': 'pnpm-lock.yaml',
179
+ 'yarn': 'yarn.lock',
180
+ 'npm': 'package-lock.json'
181
+ };
182
+ return path.join(this.cwd, lockfiles[manager] || 'package-lock.json');
183
+ }
184
+
185
+ /**
186
+ * Load configuration from .planning/config.json
187
+ * @returns {Object} Configuration object
188
+ */
189
+ loadConfig() {
190
+ const configPath = path.join(this.cwd, '.planning', 'config.json');
191
+ try {
192
+ if (fs.existsSync(configPath)) {
193
+ const content = fs.readFileSync(configPath, 'utf-8');
194
+ return JSON.parse(content);
195
+ }
196
+ } catch (err) {
197
+ this.logger.warn('Failed to load config', { path: configPath, error: err.message });
198
+ }
199
+ return {};
200
+ }
201
+ }
202
+
203
+ module.exports = PackageManagerDetector;