@howlil/ez-agents 2.0.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 (183) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +845 -0
  3. package/README.zh-CN.md +702 -0
  4. package/agents/ez-codebase-mapper.md +770 -0
  5. package/agents/ez-debugger.md +1255 -0
  6. package/agents/ez-executor.md +487 -0
  7. package/agents/ez-integration-checker.md +443 -0
  8. package/agents/ez-nyquist-auditor.md +176 -0
  9. package/agents/ez-phase-researcher.md +553 -0
  10. package/agents/ez-plan-checker.md +706 -0
  11. package/agents/ez-planner.md +1307 -0
  12. package/agents/ez-project-researcher.md +629 -0
  13. package/agents/ez-research-synthesizer.md +247 -0
  14. package/agents/ez-roadmapper.md +650 -0
  15. package/agents/ez-ui-auditor.md +441 -0
  16. package/agents/ez-ui-checker.md +302 -0
  17. package/agents/ez-ui-researcher.md +355 -0
  18. package/agents/ez-verifier.md +579 -0
  19. package/bin/install.js +2862 -0
  20. package/bin/update.js +214 -0
  21. package/commands/ez/add-phase.md +43 -0
  22. package/commands/ez/add-tests.md +41 -0
  23. package/commands/ez/add-todo.md +47 -0
  24. package/commands/ez/audit-milestone.md +36 -0
  25. package/commands/ez/autonomous.md +41 -0
  26. package/commands/ez/check-todos.md +45 -0
  27. package/commands/ez/cleanup.md +18 -0
  28. package/commands/ez/complete-milestone.md +136 -0
  29. package/commands/ez/debug.md +168 -0
  30. package/commands/ez/discuss-phase.md +90 -0
  31. package/commands/ez/execute-phase.md +41 -0
  32. package/commands/ez/health.md +22 -0
  33. package/commands/ez/help.md +22 -0
  34. package/commands/ez/insert-phase.md +32 -0
  35. package/commands/ez/join-discord.md +18 -0
  36. package/commands/ez/list-phase-assumptions.md +46 -0
  37. package/commands/ez/map-codebase.md +71 -0
  38. package/commands/ez/new-milestone.md +44 -0
  39. package/commands/ez/new-project.md +42 -0
  40. package/commands/ez/pause-work.md +38 -0
  41. package/commands/ez/plan-milestone-gaps.md +34 -0
  42. package/commands/ez/plan-phase.md +45 -0
  43. package/commands/ez/progress.md +24 -0
  44. package/commands/ez/quick.md +45 -0
  45. package/commands/ez/reapply-patches.md +124 -0
  46. package/commands/ez/remove-phase.md +31 -0
  47. package/commands/ez/research-phase.md +190 -0
  48. package/commands/ez/resume-work.md +40 -0
  49. package/commands/ez/set-profile.md +34 -0
  50. package/commands/ez/settings.md +36 -0
  51. package/commands/ez/stats.md +18 -0
  52. package/commands/ez/ui-phase.md +34 -0
  53. package/commands/ez/ui-review.md +32 -0
  54. package/commands/ez/update.md +37 -0
  55. package/commands/ez/validate-phase.md +35 -0
  56. package/commands/ez/verify-work.md +38 -0
  57. package/get-shit-done/bin/ez-tools.cjs +598 -0
  58. package/get-shit-done/bin/lib/assistant-adapter.cjs +205 -0
  59. package/get-shit-done/bin/lib/audit-exec.cjs +150 -0
  60. package/get-shit-done/bin/lib/auth.cjs +175 -0
  61. package/get-shit-done/bin/lib/circuit-breaker.cjs +118 -0
  62. package/get-shit-done/bin/lib/commands.cjs +666 -0
  63. package/get-shit-done/bin/lib/config.cjs +183 -0
  64. package/get-shit-done/bin/lib/core.cjs +495 -0
  65. package/get-shit-done/bin/lib/file-lock.cjs +236 -0
  66. package/get-shit-done/bin/lib/frontmatter.cjs +299 -0
  67. package/get-shit-done/bin/lib/fs-utils.cjs +153 -0
  68. package/get-shit-done/bin/lib/git-utils.cjs +203 -0
  69. package/get-shit-done/bin/lib/health-check.cjs +163 -0
  70. package/get-shit-done/bin/lib/index.cjs +113 -0
  71. package/get-shit-done/bin/lib/init.cjs +710 -0
  72. package/get-shit-done/bin/lib/logger.cjs +117 -0
  73. package/get-shit-done/bin/lib/milestone.cjs +241 -0
  74. package/get-shit-done/bin/lib/model-provider.cjs +146 -0
  75. package/get-shit-done/bin/lib/phase.cjs +908 -0
  76. package/get-shit-done/bin/lib/retry.cjs +119 -0
  77. package/get-shit-done/bin/lib/roadmap.cjs +305 -0
  78. package/get-shit-done/bin/lib/safe-exec.cjs +128 -0
  79. package/get-shit-done/bin/lib/safe-path.cjs +130 -0
  80. package/get-shit-done/bin/lib/state.cjs +721 -0
  81. package/get-shit-done/bin/lib/temp-file.cjs +239 -0
  82. package/get-shit-done/bin/lib/template.cjs +222 -0
  83. package/get-shit-done/bin/lib/test-file-lock.cjs +112 -0
  84. package/get-shit-done/bin/lib/test-graceful.cjs +93 -0
  85. package/get-shit-done/bin/lib/test-logger.cjs +60 -0
  86. package/get-shit-done/bin/lib/test-safe-exec.cjs +38 -0
  87. package/get-shit-done/bin/lib/test-safe-path.cjs +33 -0
  88. package/get-shit-done/bin/lib/test-temp-file.cjs +125 -0
  89. package/get-shit-done/bin/lib/timeout-exec.cjs +62 -0
  90. package/get-shit-done/bin/lib/verify.cjs +820 -0
  91. package/get-shit-done/references/checkpoints.md +776 -0
  92. package/get-shit-done/references/continuation-format.md +249 -0
  93. package/get-shit-done/references/decimal-phase-calculation.md +65 -0
  94. package/get-shit-done/references/git-integration.md +248 -0
  95. package/get-shit-done/references/git-planning-commit.md +38 -0
  96. package/get-shit-done/references/model-profile-resolution.md +34 -0
  97. package/get-shit-done/references/model-profiles.md +93 -0
  98. package/get-shit-done/references/phase-argument-parsing.md +61 -0
  99. package/get-shit-done/references/planning-config.md +200 -0
  100. package/get-shit-done/references/questioning.md +162 -0
  101. package/get-shit-done/references/tdd.md +263 -0
  102. package/get-shit-done/references/ui-brand.md +160 -0
  103. package/get-shit-done/references/verification-patterns.md +612 -0
  104. package/get-shit-done/templates/DEBUG.md +164 -0
  105. package/get-shit-done/templates/UAT.md +247 -0
  106. package/get-shit-done/templates/UI-SPEC.md +100 -0
  107. package/get-shit-done/templates/VALIDATION.md +76 -0
  108. package/get-shit-done/templates/codebase/architecture.md +255 -0
  109. package/get-shit-done/templates/codebase/concerns.md +310 -0
  110. package/get-shit-done/templates/codebase/conventions.md +307 -0
  111. package/get-shit-done/templates/codebase/integrations.md +280 -0
  112. package/get-shit-done/templates/codebase/stack.md +186 -0
  113. package/get-shit-done/templates/codebase/structure.md +285 -0
  114. package/get-shit-done/templates/codebase/testing.md +480 -0
  115. package/get-shit-done/templates/config.json +37 -0
  116. package/get-shit-done/templates/context.md +352 -0
  117. package/get-shit-done/templates/continue-here.md +78 -0
  118. package/get-shit-done/templates/copilot-instructions.md +7 -0
  119. package/get-shit-done/templates/debug-subagent-prompt.md +91 -0
  120. package/get-shit-done/templates/discovery.md +146 -0
  121. package/get-shit-done/templates/milestone-archive.md +123 -0
  122. package/get-shit-done/templates/milestone.md +115 -0
  123. package/get-shit-done/templates/phase-prompt.md +610 -0
  124. package/get-shit-done/templates/planner-subagent-prompt.md +117 -0
  125. package/get-shit-done/templates/project.md +184 -0
  126. package/get-shit-done/templates/requirements.md +231 -0
  127. package/get-shit-done/templates/research-project/ARCHITECTURE.md +204 -0
  128. package/get-shit-done/templates/research-project/FEATURES.md +147 -0
  129. package/get-shit-done/templates/research-project/PITFALLS.md +200 -0
  130. package/get-shit-done/templates/research-project/STACK.md +120 -0
  131. package/get-shit-done/templates/research-project/SUMMARY.md +170 -0
  132. package/get-shit-done/templates/research.md +552 -0
  133. package/get-shit-done/templates/retrospective.md +54 -0
  134. package/get-shit-done/templates/roadmap.md +202 -0
  135. package/get-shit-done/templates/state.md +176 -0
  136. package/get-shit-done/templates/summary-complex.md +59 -0
  137. package/get-shit-done/templates/summary-minimal.md +41 -0
  138. package/get-shit-done/templates/summary-standard.md +48 -0
  139. package/get-shit-done/templates/summary.md +248 -0
  140. package/get-shit-done/templates/user-setup.md +311 -0
  141. package/get-shit-done/templates/verification-report.md +322 -0
  142. package/get-shit-done/workflows/add-phase.md +112 -0
  143. package/get-shit-done/workflows/add-tests.md +351 -0
  144. package/get-shit-done/workflows/add-todo.md +158 -0
  145. package/get-shit-done/workflows/audit-milestone.md +332 -0
  146. package/get-shit-done/workflows/autonomous.md +743 -0
  147. package/get-shit-done/workflows/check-todos.md +177 -0
  148. package/get-shit-done/workflows/cleanup.md +152 -0
  149. package/get-shit-done/workflows/complete-milestone.md +766 -0
  150. package/get-shit-done/workflows/diagnose-issues.md +219 -0
  151. package/get-shit-done/workflows/discovery-phase.md +289 -0
  152. package/get-shit-done/workflows/discuss-phase.md +762 -0
  153. package/get-shit-done/workflows/execute-phase.md +468 -0
  154. package/get-shit-done/workflows/execute-plan.md +483 -0
  155. package/get-shit-done/workflows/health.md +159 -0
  156. package/get-shit-done/workflows/help.md +492 -0
  157. package/get-shit-done/workflows/insert-phase.md +130 -0
  158. package/get-shit-done/workflows/list-phase-assumptions.md +178 -0
  159. package/get-shit-done/workflows/map-codebase.md +316 -0
  160. package/get-shit-done/workflows/new-milestone.md +384 -0
  161. package/get-shit-done/workflows/new-project.md +1111 -0
  162. package/get-shit-done/workflows/node-repair.md +92 -0
  163. package/get-shit-done/workflows/pause-work.md +122 -0
  164. package/get-shit-done/workflows/plan-milestone-gaps.md +274 -0
  165. package/get-shit-done/workflows/plan-phase.md +651 -0
  166. package/get-shit-done/workflows/progress.md +382 -0
  167. package/get-shit-done/workflows/quick.md +610 -0
  168. package/get-shit-done/workflows/remove-phase.md +155 -0
  169. package/get-shit-done/workflows/research-phase.md +74 -0
  170. package/get-shit-done/workflows/resume-project.md +307 -0
  171. package/get-shit-done/workflows/set-profile.md +81 -0
  172. package/get-shit-done/workflows/settings.md +242 -0
  173. package/get-shit-done/workflows/stats.md +57 -0
  174. package/get-shit-done/workflows/transition.md +544 -0
  175. package/get-shit-done/workflows/ui-phase.md +290 -0
  176. package/get-shit-done/workflows/ui-review.md +157 -0
  177. package/get-shit-done/workflows/update.md +320 -0
  178. package/get-shit-done/workflows/validate-phase.md +167 -0
  179. package/get-shit-done/workflows/verify-phase.md +243 -0
  180. package/get-shit-done/workflows/verify-work.md +584 -0
  181. package/package.json +55 -0
  182. package/scripts/build-hooks.js +43 -0
  183. package/scripts/run-tests.cjs +29 -0
@@ -0,0 +1,205 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * GSD Assistant Adapters — Unified interface for AI coding assistants
5
+ *
6
+ * Adapters for: Claude Code, OpenCode, Gemini CLI, Codex
7
+ *
8
+ * Usage:
9
+ * const { createAdapter } = require('./assistant-adapter.cjs');
10
+ * const adapter = createAdapter('claude-code');
11
+ * await adapter.spawnAgent('planner', { prompt: '...' });
12
+ */
13
+
14
+ const Logger = require('./logger.cjs');
15
+ const logger = new Logger();
16
+
17
+ /**
18
+ * Base adapter interface
19
+ */
20
+ class AssistantAdapter {
21
+ constructor(name) {
22
+ if (new.target === AssistantAdapter) {
23
+ throw new Error('AssistantAdapter is abstract - use a concrete subclass');
24
+ }
25
+ this.name = name;
26
+ }
27
+
28
+ /**
29
+ * Spawn a subagent
30
+ * @param {string} type - Agent type
31
+ * @param {Object} options - Agent options
32
+ * @returns {Promise<Object>} - Agent result
33
+ */
34
+ async spawnAgent(type, options) {
35
+ throw new Error('spawnAgent must be implemented');
36
+ }
37
+
38
+ /**
39
+ * Call a tool/function
40
+ * @param {string} tool - Tool name
41
+ * @param {Object} params - Tool parameters
42
+ * @returns {Promise<any>} - Tool result
43
+ */
44
+ async callTool(tool, params) {
45
+ throw new Error('callTool must be implemented');
46
+ }
47
+
48
+ /**
49
+ * Select model for task
50
+ * @param {string} taskType - Task type (planning, execution, verification)
51
+ * @returns {string} - Model name
52
+ */
53
+ selectModel(taskType) {
54
+ throw new Error('selectModel must be implemented');
55
+ }
56
+
57
+ /**
58
+ * Get adapter info
59
+ * @returns {Object} - Adapter information
60
+ */
61
+ getInfo() {
62
+ return {
63
+ name: this.name,
64
+ type: this.constructor.name
65
+ };
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Claude Code adapter
71
+ */
72
+ class ClaudeCodeAdapter extends AssistantAdapter {
73
+ constructor() {
74
+ super('claude-code');
75
+ }
76
+
77
+ async spawnAgent(type, options) {
78
+ logger.info('Claude Code: spawning agent', { type });
79
+ // Would use Claude Code's Task tool in production
80
+ return { type, status: 'completed', result: '[Claude Code agent result]' };
81
+ }
82
+
83
+ async callTool(tool, params) {
84
+ logger.info('Claude Code: calling tool', { tool });
85
+ // Would use Claude Code's tool system
86
+ return { tool, status: 'success' };
87
+ }
88
+
89
+ selectModel(taskType) {
90
+ const models = {
91
+ planning: 'claude-3-opus',
92
+ execution: 'claude-3-sonnet',
93
+ verification: 'claude-3-sonnet'
94
+ };
95
+ return models[taskType] || models.execution;
96
+ }
97
+ }
98
+
99
+ /**
100
+ * OpenCode adapter
101
+ */
102
+ class OpenCodeAdapter extends AssistantAdapter {
103
+ constructor() {
104
+ super('opencode');
105
+ }
106
+
107
+ async spawnAgent(type, options) {
108
+ logger.info('OpenCode: spawning agent', { type });
109
+ return { type, status: 'completed', result: '[OpenCode agent result]' };
110
+ }
111
+
112
+ async callTool(tool, params) {
113
+ logger.info('OpenCode: calling tool', { tool });
114
+ return { tool, status: 'success' };
115
+ }
116
+
117
+ selectModel(taskType) {
118
+ return 'gpt-4-turbo';
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Gemini CLI adapter
124
+ */
125
+ class GeminiAdapter extends AssistantAdapter {
126
+ constructor() {
127
+ super('gemini');
128
+ }
129
+
130
+ async spawnAgent(type, options) {
131
+ logger.info('Gemini: spawning agent', { type });
132
+ return { type, status: 'completed', result: '[Gemini agent result]' };
133
+ }
134
+
135
+ async callTool(tool, params) {
136
+ logger.info('Gemini: calling tool', { tool });
137
+ return { tool, status: 'success' };
138
+ }
139
+
140
+ selectModel(taskType) {
141
+ return 'gemini-pro';
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Codex adapter
147
+ */
148
+ class CodexAdapter extends AssistantAdapter {
149
+ constructor() {
150
+ super('codex');
151
+ }
152
+
153
+ async spawnAgent(type, options) {
154
+ logger.info('Codex: spawning agent', { type });
155
+ return { type, status: 'completed', result: '[Codex agent result]' };
156
+ }
157
+
158
+ async callTool(tool, params) {
159
+ logger.info('Codex: calling tool', { tool });
160
+ return { tool, status: 'success' };
161
+ }
162
+
163
+ selectModel(taskType) {
164
+ return 'codex-latest';
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Factory function to create adapter
170
+ * @param {string} type - Adapter type
171
+ * @returns {AssistantAdapter} - Adapter instance
172
+ */
173
+ function createAdapter(type) {
174
+ const adapters = {
175
+ 'claude-code': ClaudeCodeAdapter,
176
+ 'opencode': OpenCodeAdapter,
177
+ 'gemini': GeminiAdapter,
178
+ 'codex': CodexAdapter
179
+ };
180
+
181
+ const AdapterClass = adapters[type];
182
+ if (!AdapterClass) {
183
+ throw new Error(`Unknown adapter type: ${type}. Available: ${Object.keys(adapters).join(', ')}`);
184
+ }
185
+
186
+ return new AdapterClass();
187
+ }
188
+
189
+ /**
190
+ * Get available adapters
191
+ * @returns {string[]} - List of adapter names
192
+ */
193
+ function getAvailableAdapters() {
194
+ return ['claude-code', 'opencode', 'gemini', 'codex'];
195
+ }
196
+
197
+ module.exports = {
198
+ AssistantAdapter,
199
+ ClaudeCodeAdapter,
200
+ OpenCodeAdapter,
201
+ GeminiAdapter,
202
+ CodexAdapter,
203
+ createAdapter,
204
+ getAvailableAdapters
205
+ };
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * GSD Audit Exec — Command execution with full audit logging
5
+ *
6
+ * Logs all command executions to audit file for security review:
7
+ * - Timestamp, command, arguments, context
8
+ * - Duration and result status
9
+ * - Error details if failed
10
+ *
11
+ * Usage:
12
+ * const { auditExec } = require('./audit-exec.cjs');
13
+ * const result = await auditExec('git', ['status'], { context: 'my-module' });
14
+ */
15
+
16
+ const { execFile } = require('child_process');
17
+ const { promisify } = require('util');
18
+ const { appendFileSync, existsSync, mkdirSync } = require('fs');
19
+ const { join } = require('path');
20
+ const Logger = require('./logger.cjs');
21
+ const logger = new Logger();
22
+
23
+ const execFileAsync = promisify(execFile);
24
+
25
+ // Audit log file path
26
+ const AUDIT_DIR = join('.planning', 'logs');
27
+ const AUDIT_FILE = join(AUDIT_DIR, `audit-${new Date().toISOString().split('T')[0]}.jsonl`);
28
+
29
+ // Ensure audit directory exists
30
+ if (!existsSync(AUDIT_DIR)) {
31
+ mkdirSync(AUDIT_DIR, { recursive: true });
32
+ }
33
+
34
+ /**
35
+ * Write audit log entry
36
+ * @param {Object} entry - Audit entry
37
+ */
38
+ function writeAudit(entry) {
39
+ const line = JSON.stringify(entry) + '\n';
40
+ appendFileSync(AUDIT_FILE, line, 'utf-8');
41
+ }
42
+
43
+ /**
44
+ * Execute command with full audit logging
45
+ * @param {string} cmd - Command to execute
46
+ * @param {string[]} args - Command arguments
47
+ * @param {Object} options - Execution options
48
+ * @param {string} options.context - Calling context (which module/function)
49
+ * @param {string} options.user - User identifier
50
+ * @returns {Promise<string>} - Command stdout
51
+ */
52
+ async function auditExec(cmd, args = [], options = {}) {
53
+ const { context = 'unknown', user = 'system', timeout = 30000 } = options;
54
+
55
+ const entry = {
56
+ timestamp: new Date().toISOString(),
57
+ cmd,
58
+ args,
59
+ context,
60
+ user,
61
+ status: 'started'
62
+ };
63
+
64
+ // Log start
65
+ writeAudit(entry);
66
+ logger.info('Audit: command started', { cmd, args: args.join(' '), context });
67
+
68
+ const startTime = Date.now();
69
+
70
+ try {
71
+ const result = await execFileAsync(cmd, args, {
72
+ timeout,
73
+ maxBuffer: 10 * 1024 * 1024
74
+ });
75
+
76
+ const duration = Date.now() - startTime;
77
+
78
+ // Log success
79
+ const successEntry = {
80
+ ...entry,
81
+ status: 'success',
82
+ duration,
83
+ stdout_length: result.stdout?.length || 0
84
+ };
85
+ writeAudit(successEntry);
86
+
87
+ logger.debug('Audit: command completed', { cmd, duration, context });
88
+
89
+ return result.stdout.trim();
90
+ } catch (err) {
91
+ const duration = Date.now() - startTime;
92
+
93
+ // Log failure
94
+ const errorEntry = {
95
+ ...entry,
96
+ status: 'error',
97
+ duration,
98
+ error: err.message,
99
+ code: err.code,
100
+ signal: err.signal
101
+ };
102
+ writeAudit(errorEntry);
103
+
104
+ logger.error('Audit: command failed', { cmd, error: err.message, duration, context });
105
+
106
+ throw err;
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Get today's audit log path
112
+ * @returns {string} - Audit file path
113
+ */
114
+ function getAuditFilePath() {
115
+ return AUDIT_FILE;
116
+ }
117
+
118
+ /**
119
+ * Read audit log entries for a specific date
120
+ * @param {string} date - Date string (YYYY-MM-DD)
121
+ * @returns {Object[]} - Array of audit entries
122
+ */
123
+ function readAuditLog(date = new Date().toISOString().split('T')[0]) {
124
+ const filePath = join(AUDIT_DIR, `audit-${date}.jsonl`);
125
+
126
+ if (!existsSync(filePath)) {
127
+ return [];
128
+ }
129
+
130
+ const content = require('fs').readFileSync(filePath, 'utf-8');
131
+ return content.trim().split('\n').map(line => JSON.parse(line));
132
+ }
133
+
134
+ /**
135
+ * Search audit log for specific command
136
+ * @param {string} cmdFilter - Command to filter by
137
+ * @param {string} date - Date string
138
+ * @returns {Object[]} - Matching entries
139
+ */
140
+ function searchAuditLog(cmdFilter, date) {
141
+ const entries = readAuditLog(date);
142
+ return entries.filter(e => e.cmd === cmdFilter);
143
+ }
144
+
145
+ module.exports = {
146
+ auditExec,
147
+ getAuditFilePath,
148
+ readAuditLog,
149
+ searchAuditLog
150
+ };
@@ -0,0 +1,175 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * GSD Auth — Secure credential storage using system keychain
5
+ *
6
+ * Stores API keys securely using:
7
+ * - keytar for system keychain (Windows Credential Manager, macOS Keychain, libsecret)
8
+ * - Fallback to encrypted file storage if keytar unavailable
9
+ *
10
+ * Usage:
11
+ * const { saveCredential, loadCredential } = require('./auth.cjs');
12
+ * await saveCredential('anthropic', 'sk-...');
13
+ */
14
+
15
+ const Logger = require('./logger.cjs');
16
+ const logger = new Logger();
17
+ const path = require('path');
18
+ const fs = require('fs');
19
+ const os = require('os');
20
+
21
+ const SERVICE_NAME = 'gsd-multi-model';
22
+
23
+ // Provider account names
24
+ const PROVIDERS = {
25
+ ANTHROPIC: 'anthropic',
26
+ MOONSHOT: 'moonshot',
27
+ ALIBABA: 'alibaba',
28
+ OPENAI: 'openai'
29
+ };
30
+
31
+ // Fallback storage path
32
+ const FALLBACK_FILE = path.join(os.homedir(), '.gsd-credentials.json');
33
+
34
+ // Try to load keytar
35
+ let keytar = null;
36
+ try {
37
+ keytar = require('keytar');
38
+ logger.info('keytar loaded — using system keychain');
39
+ } catch (err) {
40
+ logger.warn('keytar not available — using fallback storage');
41
+ }
42
+
43
+ /**
44
+ * Save credential securely
45
+ * @param {string} provider - Provider name (anthropic, moonshot, etc.)
46
+ * @param {string} secret - API key or secret
47
+ * @returns {Promise<boolean>} - Success status
48
+ */
49
+ async function saveCredential(provider, secret) {
50
+ try {
51
+ if (keytar) {
52
+ await keytar.setPassword(SERVICE_NAME, provider, secret);
53
+ logger.info('Credential saved to system keychain', { provider });
54
+ return true;
55
+ } else {
56
+ // Fallback: save to file
57
+ let credentials = {};
58
+ if (fs.existsSync(FALLBACK_FILE)) {
59
+ const content = fs.readFileSync(FALLBACK_FILE, 'utf-8');
60
+ try {
61
+ credentials = JSON.parse(content);
62
+ } catch (e) {
63
+ credentials = {};
64
+ }
65
+ }
66
+ credentials[provider] = secret;
67
+ fs.writeFileSync(FALLBACK_FILE, JSON.stringify(credentials, null, 2), 'utf-8');
68
+ logger.warn('Credential saved to fallback file', { provider, path: FALLBACK_FILE });
69
+ return true;
70
+ }
71
+ } catch (err) {
72
+ logger.error('Failed to save credential', { provider, error: err.message });
73
+ return false;
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Load credential securely
79
+ * @param {string} provider - Provider name
80
+ * @returns {Promise<string|null>} - API key or null if not found
81
+ */
82
+ async function loadCredential(provider) {
83
+ try {
84
+ if (keytar) {
85
+ const secret = await keytar.getPassword(SERVICE_NAME, provider);
86
+ if (secret) {
87
+ logger.debug('Credential loaded from keychain', { provider });
88
+ }
89
+ return secret || null;
90
+ } else {
91
+ // Fallback: load from file
92
+ if (fs.existsSync(FALLBACK_FILE)) {
93
+ const content = fs.readFileSync(FALLBACK_FILE, 'utf-8');
94
+ const credentials = JSON.parse(content);
95
+ return credentials[provider] || null;
96
+ }
97
+ return null;
98
+ }
99
+ } catch (err) {
100
+ logger.error('Failed to load credential', { provider, error: err.message });
101
+ return null;
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Delete credential
107
+ * @param {string} provider - Provider name
108
+ * @returns {Promise<boolean>} - Success status
109
+ */
110
+ async function deleteCredential(provider) {
111
+ try {
112
+ if (keytar) {
113
+ await keytar.deletePassword(SERVICE_NAME, provider);
114
+ logger.info('Credential deleted from keychain', { provider });
115
+ return true;
116
+ } else {
117
+ // Fallback: remove from file
118
+ if (fs.existsSync(FALLBACK_FILE)) {
119
+ const content = fs.readFileSync(FALLBACK_FILE, 'utf-8');
120
+ const credentials = JSON.parse(content);
121
+ if (credentials[provider]) {
122
+ delete credentials[provider];
123
+ fs.writeFileSync(FALLBACK_FILE, JSON.stringify(credentials, null, 2), 'utf-8');
124
+ logger.info('Credential deleted from fallback file', { provider });
125
+ return true;
126
+ }
127
+ }
128
+ return false;
129
+ }
130
+ } catch (err) {
131
+ logger.error('Failed to delete credential', { provider, error: err.message });
132
+ return false;
133
+ }
134
+ }
135
+
136
+ /**
137
+ * List all stored providers
138
+ * @returns {Promise<string[]>} - Array of provider names
139
+ */
140
+ async function listProviders() {
141
+ const stored = [];
142
+
143
+ if (keytar) {
144
+ for (const [, name] of Object.entries(PROVIDERS)) {
145
+ const cred = await keytar.getPassword(SERVICE_NAME, name);
146
+ if (cred) stored.push(name);
147
+ }
148
+ } else {
149
+ if (fs.existsSync(FALLBACK_FILE)) {
150
+ const content = fs.readFileSync(FALLBACK_FILE, 'utf-8');
151
+ const credentials = JSON.parse(content);
152
+ return Object.keys(credentials);
153
+ }
154
+ }
155
+
156
+ return stored;
157
+ }
158
+
159
+ /**
160
+ * Check if keytar is available
161
+ * @returns {boolean} - True if using system keychain
162
+ */
163
+ function isKeychainAvailable() {
164
+ return keytar !== null;
165
+ }
166
+
167
+ module.exports = {
168
+ saveCredential,
169
+ loadCredential,
170
+ deleteCredential,
171
+ listProviders,
172
+ isKeychainAvailable,
173
+ PROVIDERS,
174
+ SERVICE_NAME
175
+ };
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * GSD Circuit Breaker — Prevent cascading failures
5
+ *
6
+ * Implements circuit breaker pattern:
7
+ * - CLOSED: Normal operation
8
+ * - OPEN: Failing, reject requests
9
+ * - HALF_OPEN: Testing if service recovered
10
+ *
11
+ * Usage:
12
+ * const CircuitBreaker = require('./circuit-breaker.cjs');
13
+ * const breaker = new CircuitBreaker({ failureThreshold: 5 });
14
+ * const result = await breaker.execute(() => riskyOperation());
15
+ */
16
+
17
+ const Logger = require('./logger.cjs');
18
+ const logger = new Logger();
19
+
20
+ class CircuitBreaker {
21
+ /**
22
+ * Create circuit breaker
23
+ * @param {Object} options - Configuration
24
+ */
25
+ constructor(options = {}) {
26
+ this.failureThreshold = options.failureThreshold || 5;
27
+ this.resetTimeout = options.resetTimeout || 60000;
28
+ this.state = 'CLOSED';
29
+ this.failures = 0;
30
+ this.lastFailureTime = null;
31
+ this.successes = 0;
32
+ }
33
+
34
+ /**
35
+ * Execute operation with circuit breaker protection
36
+ * @param {Function} operation - Async function to execute
37
+ * @returns {Promise<any>} - Result of operation
38
+ */
39
+ async execute(operation) {
40
+ // Check if circuit is OPEN
41
+ if (this.state === 'OPEN') {
42
+ const timeSinceFailure = Date.now() - this.lastFailureTime;
43
+
44
+ if (timeSinceFailure > this.resetTimeout) {
45
+ // Try to recover
46
+ this.state = 'HALF_OPEN';
47
+ this.failures = 0;
48
+ logger.info('Circuit breaker HALF_OPEN - testing recovery');
49
+ } else {
50
+ const waitTime = Math.round((this.resetTimeout - timeSinceFailure) / 1000);
51
+ logger.warn('Circuit breaker OPEN - rejecting request', { waitTime });
52
+ throw new Error(`Circuit breaker is OPEN. Try again in ${waitTime}s`);
53
+ }
54
+ }
55
+
56
+ try {
57
+ const result = await operation();
58
+
59
+ // Success - reset counters if in HALF_OPEN
60
+ if (this.state === 'HALF_OPEN') {
61
+ this.state = 'CLOSED';
62
+ this.failures = 0;
63
+ logger.info('Circuit breaker CLOSED - service recovered');
64
+ }
65
+
66
+ this.successes++;
67
+ return result;
68
+ } catch (err) {
69
+ this.failures++;
70
+ this.lastFailureTime = Date.now();
71
+
72
+ // Open circuit if threshold reached
73
+ if (this.failures >= this.failureThreshold) {
74
+ this.state = 'OPEN';
75
+ logger.error('Circuit breaker OPEN - failure threshold reached', {
76
+ failures: this.failures
77
+ });
78
+ }
79
+
80
+ throw err;
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Get current state
86
+ * @returns {string} - CLOSED, OPEN, or HALF_OPEN
87
+ */
88
+ getState() {
89
+ return this.state;
90
+ }
91
+
92
+ /**
93
+ * Get stats
94
+ * @returns {Object} - Statistics
95
+ */
96
+ getStats() {
97
+ return {
98
+ state: this.state,
99
+ failures: this.failures,
100
+ successes: this.successes,
101
+ failureThreshold: this.failureThreshold,
102
+ lastFailureTime: this.lastFailureTime
103
+ };
104
+ }
105
+
106
+ /**
107
+ * Reset circuit breaker
108
+ */
109
+ reset() {
110
+ this.state = 'CLOSED';
111
+ this.failures = 0;
112
+ this.successes = 0;
113
+ this.lastFailureTime = null;
114
+ logger.info('Circuit breaker reset');
115
+ }
116
+ }
117
+
118
+ module.exports = CircuitBreaker;