claude-cli-advanced-starter-pack 1.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.
- package/LICENSE +21 -0
- package/OVERVIEW.md +597 -0
- package/README.md +439 -0
- package/bin/gtask.js +282 -0
- package/bin/postinstall.js +53 -0
- package/package.json +69 -0
- package/src/agents/phase-dev-templates.js +1011 -0
- package/src/agents/templates.js +668 -0
- package/src/analysis/checklist-parser.js +414 -0
- package/src/analysis/codebase.js +481 -0
- package/src/cli/menu.js +958 -0
- package/src/commands/claude-audit.js +1482 -0
- package/src/commands/claude-settings.js +2243 -0
- package/src/commands/create-agent.js +681 -0
- package/src/commands/create-command.js +337 -0
- package/src/commands/create-hook.js +262 -0
- package/src/commands/create-phase-dev/codebase-analyzer.js +813 -0
- package/src/commands/create-phase-dev/documentation-generator.js +352 -0
- package/src/commands/create-phase-dev/post-completion.js +404 -0
- package/src/commands/create-phase-dev/scale-calculator.js +344 -0
- package/src/commands/create-phase-dev/wizard.js +492 -0
- package/src/commands/create-phase-dev.js +481 -0
- package/src/commands/create-skill.js +313 -0
- package/src/commands/create.js +446 -0
- package/src/commands/decompose.js +392 -0
- package/src/commands/detect-tech-stack.js +768 -0
- package/src/commands/explore-mcp/claude-md-updater.js +252 -0
- package/src/commands/explore-mcp/mcp-installer.js +346 -0
- package/src/commands/explore-mcp/mcp-registry.js +438 -0
- package/src/commands/explore-mcp.js +638 -0
- package/src/commands/gtask-init.js +641 -0
- package/src/commands/help.js +128 -0
- package/src/commands/init.js +1890 -0
- package/src/commands/install.js +250 -0
- package/src/commands/list.js +116 -0
- package/src/commands/roadmap.js +750 -0
- package/src/commands/setup-wizard.js +482 -0
- package/src/commands/setup.js +351 -0
- package/src/commands/sync.js +534 -0
- package/src/commands/test-run.js +456 -0
- package/src/commands/test-setup.js +456 -0
- package/src/commands/validate.js +67 -0
- package/src/config/tech-stack.defaults.json +182 -0
- package/src/config/tech-stack.schema.json +502 -0
- package/src/github/client.js +359 -0
- package/src/index.js +84 -0
- package/src/templates/claude-command.js +244 -0
- package/src/templates/issue-body.js +284 -0
- package/src/testing/config.js +411 -0
- package/src/utils/template-engine.js +398 -0
- package/src/utils/validate-templates.js +223 -0
- package/src/utils.js +396 -0
- package/templates/commands/ccasp-setup.template.md +113 -0
- package/templates/commands/context-audit.template.md +97 -0
- package/templates/commands/create-task-list.template.md +382 -0
- package/templates/commands/deploy-full.template.md +261 -0
- package/templates/commands/github-task-start.template.md +99 -0
- package/templates/commands/github-update.template.md +69 -0
- package/templates/commands/happy-start.template.md +117 -0
- package/templates/commands/phase-track.template.md +142 -0
- package/templates/commands/tunnel-start.template.md +127 -0
- package/templates/commands/tunnel-stop.template.md +106 -0
- package/templates/hooks/context-guardian.template.js +173 -0
- package/templates/hooks/deployment-orchestrator.template.js +219 -0
- package/templates/hooks/github-progress-hook.template.js +197 -0
- package/templates/hooks/happy-checkpoint-manager.template.js +222 -0
- package/templates/hooks/phase-dev-enforcer.template.js +183 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Guardian Hook
|
|
3
|
+
*
|
|
4
|
+
* Monitors token usage and triggers appropriate actions at configured thresholds.
|
|
5
|
+
* Configured via tech-stack.json tokenManagement settings.
|
|
6
|
+
*
|
|
7
|
+
* Event: PostToolUse (monitors after each tool execution)
|
|
8
|
+
* Priority: {{hooks.priorities.lifecycle}}
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
// Configuration from tech-stack.json
|
|
15
|
+
const CONFIG = {
|
|
16
|
+
enabled: {{tokenManagement.enabled}},
|
|
17
|
+
dailyBudget: {{tokenManagement.dailyBudget}},
|
|
18
|
+
thresholds: {
|
|
19
|
+
compact: {{tokenManagement.thresholds.compact}},
|
|
20
|
+
archive: {{tokenManagement.thresholds.archive}},
|
|
21
|
+
respawn: {{tokenManagement.thresholds.respawn}},
|
|
22
|
+
},
|
|
23
|
+
trackingFile: '{{tokenManagement.trackingFile}}',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Load current token usage data
|
|
28
|
+
*/
|
|
29
|
+
function loadUsageData() {
|
|
30
|
+
const trackingPath = path.join(process.cwd(), CONFIG.trackingFile);
|
|
31
|
+
|
|
32
|
+
if (fs.existsSync(trackingPath)) {
|
|
33
|
+
try {
|
|
34
|
+
return JSON.parse(fs.readFileSync(trackingPath, 'utf8'));
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.warn('[context-guardian] Could not parse tracking file:', error.message);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Return default usage data
|
|
41
|
+
return {
|
|
42
|
+
date: new Date().toISOString().split('T')[0],
|
|
43
|
+
sessions: [],
|
|
44
|
+
totalTokens: 0,
|
|
45
|
+
currentSession: {
|
|
46
|
+
id: generateSessionId(),
|
|
47
|
+
startTime: new Date().toISOString(),
|
|
48
|
+
tokens: 0,
|
|
49
|
+
toolCalls: 0,
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Save usage data
|
|
56
|
+
*/
|
|
57
|
+
function saveUsageData(data) {
|
|
58
|
+
const trackingPath = path.join(process.cwd(), CONFIG.trackingFile);
|
|
59
|
+
const trackingDir = path.dirname(trackingPath);
|
|
60
|
+
|
|
61
|
+
if (!fs.existsSync(trackingDir)) {
|
|
62
|
+
fs.mkdirSync(trackingDir, { recursive: true });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
fs.writeFileSync(trackingPath, JSON.stringify(data, null, 2), 'utf8');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Generate a simple session ID
|
|
70
|
+
*/
|
|
71
|
+
function generateSessionId() {
|
|
72
|
+
return `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Estimate tokens from tool output
|
|
77
|
+
* Rough estimation: ~4 chars per token
|
|
78
|
+
*/
|
|
79
|
+
function estimateTokens(text) {
|
|
80
|
+
if (!text) return 0;
|
|
81
|
+
const str = typeof text === 'string' ? text : JSON.stringify(text);
|
|
82
|
+
return Math.ceil(str.length / 4);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get usage percentage
|
|
87
|
+
*/
|
|
88
|
+
function getUsagePercentage(usedTokens) {
|
|
89
|
+
return usedTokens / CONFIG.dailyBudget;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Main hook handler
|
|
94
|
+
*/
|
|
95
|
+
module.exports = async function contextGuardian(context) {
|
|
96
|
+
// Skip if disabled
|
|
97
|
+
if (!CONFIG.enabled) {
|
|
98
|
+
return { continue: true };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const { tool, output, error } = context;
|
|
102
|
+
|
|
103
|
+
// Load current usage
|
|
104
|
+
const usage = loadUsageData();
|
|
105
|
+
|
|
106
|
+
// Check if we need to reset for a new day
|
|
107
|
+
const today = new Date().toISOString().split('T')[0];
|
|
108
|
+
if (usage.date !== today) {
|
|
109
|
+
// Archive previous day
|
|
110
|
+
usage.sessions.push(usage.currentSession);
|
|
111
|
+
usage.date = today;
|
|
112
|
+
usage.totalTokens = 0;
|
|
113
|
+
usage.currentSession = {
|
|
114
|
+
id: generateSessionId(),
|
|
115
|
+
startTime: new Date().toISOString(),
|
|
116
|
+
tokens: 0,
|
|
117
|
+
toolCalls: 0,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Estimate tokens from this tool call
|
|
122
|
+
const outputTokens = estimateTokens(output);
|
|
123
|
+
const errorTokens = estimateTokens(error);
|
|
124
|
+
const callTokens = outputTokens + errorTokens;
|
|
125
|
+
|
|
126
|
+
// Update usage
|
|
127
|
+
usage.currentSession.tokens += callTokens;
|
|
128
|
+
usage.currentSession.toolCalls += 1;
|
|
129
|
+
usage.totalTokens += callTokens;
|
|
130
|
+
|
|
131
|
+
// Save updated usage
|
|
132
|
+
saveUsageData(usage);
|
|
133
|
+
|
|
134
|
+
// Calculate usage percentage
|
|
135
|
+
const usagePercent = getUsagePercentage(usage.totalTokens);
|
|
136
|
+
|
|
137
|
+
// Check thresholds and add warnings
|
|
138
|
+
let message = null;
|
|
139
|
+
|
|
140
|
+
if (usagePercent >= CONFIG.thresholds.respawn) {
|
|
141
|
+
message = `⚠️ CRITICAL: Token usage at ${(usagePercent * 100).toFixed(1)}% of daily budget!
|
|
142
|
+
Consider respawning session to preserve budget.
|
|
143
|
+
Run /context-audit for details.`;
|
|
144
|
+
} else if (usagePercent >= CONFIG.thresholds.archive) {
|
|
145
|
+
message = `⚠️ WARNING: Token usage at ${(usagePercent * 100).toFixed(1)}% of daily budget.
|
|
146
|
+
Consider archiving this session.
|
|
147
|
+
Run /context-audit for recommendations.`;
|
|
148
|
+
} else if (usagePercent >= CONFIG.thresholds.compact) {
|
|
149
|
+
message = `ℹ️ Token usage at ${(usagePercent * 100).toFixed(1)}% of daily budget.
|
|
150
|
+
Consider compacting context soon.`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
continue: true,
|
|
155
|
+
message: message,
|
|
156
|
+
// Add metadata for other hooks/tools to use
|
|
157
|
+
metadata: {
|
|
158
|
+
tokenUsage: {
|
|
159
|
+
session: usage.currentSession.tokens,
|
|
160
|
+
daily: usage.totalTokens,
|
|
161
|
+
budget: CONFIG.dailyBudget,
|
|
162
|
+
percentUsed: usagePercent,
|
|
163
|
+
status: usagePercent >= CONFIG.thresholds.respawn
|
|
164
|
+
? 'critical'
|
|
165
|
+
: usagePercent >= CONFIG.thresholds.archive
|
|
166
|
+
? 'warning'
|
|
167
|
+
: usagePercent >= CONFIG.thresholds.compact
|
|
168
|
+
? 'attention'
|
|
169
|
+
: 'ok',
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
};
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deployment Orchestrator Hook
|
|
3
|
+
*
|
|
4
|
+
* Monitors deployment-related commands and provides coordination.
|
|
5
|
+
* Prevents conflicting deployments and tracks deployment state.
|
|
6
|
+
*
|
|
7
|
+
* Event: PreToolUse
|
|
8
|
+
* Priority: {{hooks.priorities.tools}}
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const { execSync } = require('child_process');
|
|
14
|
+
|
|
15
|
+
// Configuration from tech-stack.json
|
|
16
|
+
const CONFIG = {
|
|
17
|
+
backend: {
|
|
18
|
+
platform: '{{deployment.backend.platform}}',
|
|
19
|
+
projectId: '{{deployment.backend.projectId}}',
|
|
20
|
+
serviceId: '{{deployment.backend.serviceId}}',
|
|
21
|
+
},
|
|
22
|
+
frontend: {
|
|
23
|
+
platform: '{{deployment.frontend.platform}}',
|
|
24
|
+
projectName: '{{deployment.frontend.projectName}}',
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const DEPLOYMENT_STATE_FILE = '.claude/hooks/cache/deployment-state.json';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Load deployment state
|
|
32
|
+
*/
|
|
33
|
+
function loadDeploymentState() {
|
|
34
|
+
const statePath = path.join(process.cwd(), DEPLOYMENT_STATE_FILE);
|
|
35
|
+
|
|
36
|
+
if (fs.existsSync(statePath)) {
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
39
|
+
} catch (error) {
|
|
40
|
+
// Ignore parse errors
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
backend: { status: 'idle', lastDeployment: null },
|
|
46
|
+
frontend: { status: 'idle', lastDeployment: null },
|
|
47
|
+
inProgress: false,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Save deployment state
|
|
53
|
+
*/
|
|
54
|
+
function saveDeploymentState(state) {
|
|
55
|
+
const statePath = path.join(process.cwd(), DEPLOYMENT_STATE_FILE);
|
|
56
|
+
const stateDir = path.dirname(statePath);
|
|
57
|
+
|
|
58
|
+
if (!fs.existsSync(stateDir)) {
|
|
59
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
state.lastUpdated = new Date().toISOString();
|
|
63
|
+
fs.writeFileSync(statePath, JSON.stringify(state, null, 2), 'utf8');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Check if a deployment is in progress
|
|
68
|
+
*/
|
|
69
|
+
function checkDeploymentInProgress(state) {
|
|
70
|
+
// Check if state indicates deployment in progress
|
|
71
|
+
if (state.inProgress) {
|
|
72
|
+
// Check if it's been more than 10 minutes (likely stale)
|
|
73
|
+
if (state.lastUpdated) {
|
|
74
|
+
const lastUpdate = new Date(state.lastUpdated);
|
|
75
|
+
const now = new Date();
|
|
76
|
+
const minutes = (now - lastUpdate) / 1000 / 60;
|
|
77
|
+
|
|
78
|
+
if (minutes > 10) {
|
|
79
|
+
// Stale state, reset
|
|
80
|
+
state.inProgress = false;
|
|
81
|
+
state.backend.status = 'idle';
|
|
82
|
+
state.frontend.status = 'idle';
|
|
83
|
+
saveDeploymentState(state);
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Detect deployment-related commands
|
|
94
|
+
*/
|
|
95
|
+
function isDeploymentCommand(tool, input) {
|
|
96
|
+
// MCP Railway deployment
|
|
97
|
+
if (tool.includes('railway') && tool.includes('deployment')) {
|
|
98
|
+
return { type: 'backend', platform: 'railway' };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Bash commands
|
|
102
|
+
if (tool === 'Bash' && input && input.command) {
|
|
103
|
+
const cmd = input.command.toLowerCase();
|
|
104
|
+
|
|
105
|
+
// Cloudflare/Vercel/Netlify deploys
|
|
106
|
+
if (cmd.includes('wrangler pages deploy') || cmd.includes('wrangler deploy')) {
|
|
107
|
+
return { type: 'frontend', platform: 'cloudflare' };
|
|
108
|
+
}
|
|
109
|
+
if (cmd.includes('vercel') && (cmd.includes('--prod') || cmd.includes('deploy'))) {
|
|
110
|
+
return { type: 'frontend', platform: 'vercel' };
|
|
111
|
+
}
|
|
112
|
+
if (cmd.includes('netlify deploy')) {
|
|
113
|
+
return { type: 'frontend', platform: 'netlify' };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Heroku
|
|
117
|
+
if (cmd.includes('git push heroku')) {
|
|
118
|
+
return { type: 'backend', platform: 'heroku' };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Fly.io
|
|
122
|
+
if (cmd.includes('fly deploy')) {
|
|
123
|
+
return { type: 'backend', platform: 'fly' };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Main hook handler
|
|
132
|
+
*/
|
|
133
|
+
module.exports = async function deploymentOrchestrator(context) {
|
|
134
|
+
const { tool, input } = context;
|
|
135
|
+
|
|
136
|
+
// Check if this is a deployment command
|
|
137
|
+
const deployment = isDeploymentCommand(tool, input);
|
|
138
|
+
|
|
139
|
+
if (!deployment) {
|
|
140
|
+
return { continue: true };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Load current deployment state
|
|
144
|
+
const state = loadDeploymentState();
|
|
145
|
+
|
|
146
|
+
// Check if another deployment is in progress
|
|
147
|
+
if (checkDeploymentInProgress(state)) {
|
|
148
|
+
const inProgressType = state.backend.status === 'deploying' ? 'backend' : 'frontend';
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
continue: false, // Block the deployment
|
|
152
|
+
message: `⚠️ Deployment blocked: ${inProgressType} deployment already in progress.
|
|
153
|
+
|
|
154
|
+
Current state:
|
|
155
|
+
- Backend: ${state.backend.status}
|
|
156
|
+
- Frontend: ${state.frontend.status}
|
|
157
|
+
|
|
158
|
+
Wait for the current deployment to complete, or clear the state if it's stuck:
|
|
159
|
+
- Check deployment status in your platform dashboard
|
|
160
|
+
- Clear state by deleting ${DEPLOYMENT_STATE_FILE}`,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Mark deployment as in progress
|
|
165
|
+
state.inProgress = true;
|
|
166
|
+
state[deployment.type].status = 'deploying';
|
|
167
|
+
state[deployment.type].startTime = new Date().toISOString();
|
|
168
|
+
state[deployment.type].platform = deployment.platform;
|
|
169
|
+
saveDeploymentState(state);
|
|
170
|
+
|
|
171
|
+
// Allow the deployment to proceed
|
|
172
|
+
return {
|
|
173
|
+
continue: true,
|
|
174
|
+
message: `🚀 Starting ${deployment.type} deployment to ${deployment.platform}...
|
|
175
|
+
|
|
176
|
+
Pre-flight checks:
|
|
177
|
+
${CONFIG[deployment.type].platform === deployment.platform ? '✅' : '⚠️'} Platform matches config
|
|
178
|
+
✅ No conflicting deployments
|
|
179
|
+
|
|
180
|
+
Deployment state saved. Run /deploy-full to see full deployment status.`,
|
|
181
|
+
};
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Post-deployment cleanup (called as PostToolUse)
|
|
186
|
+
*/
|
|
187
|
+
module.exports.postDeployment = async function postDeploymentOrchestrator(context) {
|
|
188
|
+
const { tool, input, output, error } = context;
|
|
189
|
+
|
|
190
|
+
// Check if this was a deployment command
|
|
191
|
+
const deployment = isDeploymentCommand(tool, input);
|
|
192
|
+
|
|
193
|
+
if (!deployment) {
|
|
194
|
+
return { continue: true };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Load and update state
|
|
198
|
+
const state = loadDeploymentState();
|
|
199
|
+
|
|
200
|
+
state.inProgress = false;
|
|
201
|
+
state[deployment.type].status = error ? 'failed' : 'completed';
|
|
202
|
+
state[deployment.type].lastDeployment = new Date().toISOString();
|
|
203
|
+
state[deployment.type].duration = state[deployment.type].startTime
|
|
204
|
+
? Math.round((Date.now() - new Date(state[deployment.type].startTime)) / 1000)
|
|
205
|
+
: null;
|
|
206
|
+
|
|
207
|
+
if (error) {
|
|
208
|
+
state[deployment.type].lastError = error.message || String(error);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
saveDeploymentState(state);
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
continue: true,
|
|
215
|
+
message: error
|
|
216
|
+
? `❌ ${deployment.type} deployment failed after ${state[deployment.type].duration}s`
|
|
217
|
+
: `✅ ${deployment.type} deployment completed in ${state[deployment.type].duration}s`,
|
|
218
|
+
};
|
|
219
|
+
};
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Progress Hook
|
|
3
|
+
*
|
|
4
|
+
* Automatically updates GitHub issues as tasks are completed.
|
|
5
|
+
* Monitors TodoWrite/TaskUpdate calls and syncs progress to linked GitHub issues.
|
|
6
|
+
*
|
|
7
|
+
* Event: PostToolUse
|
|
8
|
+
* Priority: {{hooks.priorities.automation}}
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { execSync } = require('child_process');
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
// Configuration from tech-stack.json
|
|
16
|
+
const CONFIG = {
|
|
17
|
+
owner: '{{versionControl.owner}}',
|
|
18
|
+
repo: '{{versionControl.repo}}',
|
|
19
|
+
projectNumber: {{versionControl.projectBoard.number}},
|
|
20
|
+
enabled: '{{versionControl.projectBoard.type}}' === 'github-projects',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const PROGRESS_FILE = '.claude/hooks/cache/github-progress.json';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Load progress tracking data
|
|
27
|
+
*/
|
|
28
|
+
function loadProgress() {
|
|
29
|
+
const progressPath = path.join(process.cwd(), PROGRESS_FILE);
|
|
30
|
+
|
|
31
|
+
if (fs.existsSync(progressPath)) {
|
|
32
|
+
try {
|
|
33
|
+
return JSON.parse(fs.readFileSync(progressPath, 'utf8'));
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.warn('[github-progress] Could not parse progress file');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
linkedIssue: null,
|
|
41
|
+
tasks: [],
|
|
42
|
+
completedTasks: [],
|
|
43
|
+
lastUpdate: null,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Save progress tracking data
|
|
49
|
+
*/
|
|
50
|
+
function saveProgress(progress) {
|
|
51
|
+
const progressPath = path.join(process.cwd(), PROGRESS_FILE);
|
|
52
|
+
const progressDir = path.dirname(progressPath);
|
|
53
|
+
|
|
54
|
+
if (!fs.existsSync(progressDir)) {
|
|
55
|
+
fs.mkdirSync(progressDir, { recursive: true });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
progress.lastUpdate = new Date().toISOString();
|
|
59
|
+
fs.writeFileSync(progressPath, JSON.stringify(progress, null, 2), 'utf8');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Check if gh CLI is available
|
|
64
|
+
*/
|
|
65
|
+
function hasGhCli() {
|
|
66
|
+
try {
|
|
67
|
+
execSync('gh --version', { stdio: 'ignore' });
|
|
68
|
+
return true;
|
|
69
|
+
} catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Update GitHub issue with progress
|
|
76
|
+
*/
|
|
77
|
+
function updateGitHubIssue(issueNumber, completedTasks, totalTasks, latestTask) {
|
|
78
|
+
if (!hasGhCli()) {
|
|
79
|
+
console.warn('[github-progress] gh CLI not available');
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
// Create progress comment
|
|
85
|
+
const percentage = Math.round((completedTasks / totalTasks) * 100);
|
|
86
|
+
const progressBar = '█'.repeat(Math.floor(percentage / 10)) + '░'.repeat(10 - Math.floor(percentage / 10));
|
|
87
|
+
|
|
88
|
+
const comment = `### Progress Update
|
|
89
|
+
|
|
90
|
+
${progressBar} ${percentage}% (${completedTasks}/${totalTasks} tasks)
|
|
91
|
+
|
|
92
|
+
**Latest completed:** ${latestTask || 'N/A'}
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
*Auto-updated by Claude Code github-progress-hook*`;
|
|
96
|
+
|
|
97
|
+
// Add comment to issue
|
|
98
|
+
execSync(
|
|
99
|
+
`gh issue comment ${issueNumber} --repo ${CONFIG.owner}/${CONFIG.repo} --body "${comment.replace(/"/g, '\\"')}"`,
|
|
100
|
+
{ stdio: 'ignore' }
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
return true;
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.warn('[github-progress] Failed to update GitHub:', error.message);
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Extract linked issue from task metadata or context
|
|
112
|
+
*/
|
|
113
|
+
function findLinkedIssue(input, progress) {
|
|
114
|
+
// Check if issue is already linked
|
|
115
|
+
if (progress.linkedIssue) {
|
|
116
|
+
return progress.linkedIssue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Try to extract from task description
|
|
120
|
+
if (input && input.description) {
|
|
121
|
+
const issueMatch = input.description.match(/#(\d+)/);
|
|
122
|
+
if (issueMatch) {
|
|
123
|
+
return parseInt(issueMatch[1], 10);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Try to extract from subject
|
|
128
|
+
if (input && input.subject) {
|
|
129
|
+
const issueMatch = input.subject.match(/#(\d+)/);
|
|
130
|
+
if (issueMatch) {
|
|
131
|
+
return parseInt(issueMatch[1], 10);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Main hook handler
|
|
140
|
+
*/
|
|
141
|
+
module.exports = async function githubProgressHook(context) {
|
|
142
|
+
// Skip if not configured
|
|
143
|
+
if (!CONFIG.enabled || !CONFIG.owner || !CONFIG.repo) {
|
|
144
|
+
return { continue: true };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const { tool, input } = context;
|
|
148
|
+
|
|
149
|
+
// Only process task-related tools
|
|
150
|
+
if (!['TodoWrite', 'TaskUpdate', 'TaskCreate'].includes(tool)) {
|
|
151
|
+
return { continue: true };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Load progress tracking
|
|
155
|
+
const progress = loadProgress();
|
|
156
|
+
|
|
157
|
+
// Check for linked issue
|
|
158
|
+
const issueNumber = findLinkedIssue(input, progress);
|
|
159
|
+
|
|
160
|
+
if (issueNumber && !progress.linkedIssue) {
|
|
161
|
+
progress.linkedIssue = issueNumber;
|
|
162
|
+
console.log(`[github-progress] Linked to issue #${issueNumber}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Track task completion
|
|
166
|
+
if (tool === 'TaskUpdate' && input && input.status === 'completed') {
|
|
167
|
+
const taskId = input.taskId;
|
|
168
|
+
if (taskId && !progress.completedTasks.includes(taskId)) {
|
|
169
|
+
progress.completedTasks.push(taskId);
|
|
170
|
+
|
|
171
|
+
// Update GitHub if we have a linked issue
|
|
172
|
+
if (progress.linkedIssue) {
|
|
173
|
+
const totalTasks = progress.tasks.length || progress.completedTasks.length;
|
|
174
|
+
updateGitHubIssue(
|
|
175
|
+
progress.linkedIssue,
|
|
176
|
+
progress.completedTasks.length,
|
|
177
|
+
totalTasks,
|
|
178
|
+
input.subject || `Task ${taskId}`
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Track new tasks
|
|
185
|
+
if (tool === 'TaskCreate' && input && input.subject) {
|
|
186
|
+
progress.tasks.push({
|
|
187
|
+
id: Date.now().toString(),
|
|
188
|
+
subject: input.subject,
|
|
189
|
+
created: new Date().toISOString(),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Save updated progress
|
|
194
|
+
saveProgress(progress);
|
|
195
|
+
|
|
196
|
+
return { continue: true };
|
|
197
|
+
};
|