@haposoft/cafekit 0.3.11 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -28
- package/bin/install.js +125 -1
- package/package.json +5 -3
- package/src/claude/hooks/agent.cjs +203 -0
- package/src/claude/hooks/lib/color.cjs +95 -0
- package/src/claude/hooks/lib/config.cjs +831 -0
- package/src/claude/hooks/lib/context.cjs +616 -0
- package/src/claude/hooks/lib/counter.cjs +103 -0
- package/src/claude/hooks/lib/detect.cjs +474 -0
- package/src/claude/hooks/lib/git.cjs +143 -0
- package/src/claude/hooks/lib/parser.cjs +182 -0
- package/src/claude/hooks/session.cjs +360 -0
- package/src/claude/hooks/usage.cjs +179 -0
- package/src/claude/migration-manifest.json +27 -2
- package/src/claude/settings/status.settings.json +54 -0
- package/src/claude/status.cjs +539 -0
- package/src/common/skills/code/SKILL.md +55 -0
- package/src/common/skills/code/references/execution-loop.md +21 -0
- package/src/common/skills/impact-analysis/references/change-detection.md +16 -16
- package/src/common/skills/impact-analysis/references/dependency-scouting.md +8 -8
- package/src/common/skills/impact-analysis/references/edge-case-identification.md +11 -11
- package/src/common/skills/impact-analysis/references/industry-techniques.md +36 -36
- package/src/common/skills/impact-analysis/references/practical-techniques-guide.md +16 -16
- package/src/common/skills/impact-analysis/references/project-detection.md +1 -1
- package/src/common/skills/impact-analysis/references/report-template.md +2 -2
- package/src/common/skills/impact-analysis/scripts/README.md +3 -3
- package/src/common/skills/review/SKILL.md +46 -0
- package/src/common/skills/review/references/review-focus.md +28 -0
- package/src/common/skills/spec-design/SKILL.md +66 -0
- package/src/common/skills/spec-design/references/design-discovery.md +46 -0
- package/src/common/skills/spec-init/SKILL.md +61 -0
- package/src/common/skills/spec-requirements/SKILL.md +59 -0
- package/src/common/skills/spec-requirements/references/requirements-workflow.md +36 -0
- package/src/common/skills/spec-tasks/SKILL.md +60 -0
- package/src/common/skills/spec-tasks/references/task-sizing.md +36 -0
- package/src/common/skills/test/SKILL.md +40 -0
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Custom Claude Code statusline for Node.js - Multi-line Edition
|
|
6
|
+
* Cross-platform support: Windows, macOS, Linux
|
|
7
|
+
* Features: ANSI colors, tool/agent/todo tracking, context window, session timer
|
|
8
|
+
* No external dependencies - uses only Node.js built-in modules
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { stdin, env } = require('process');
|
|
12
|
+
const os = require('os');
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
|
|
16
|
+
// Import modular components
|
|
17
|
+
const { green, yellow, red, cyan, magenta, dim, coloredBar, RESET, shouldUseColor } = require('./hooks/lib/color.cjs');
|
|
18
|
+
const { parseTranscript } = require('./hooks/lib/parser.cjs');
|
|
19
|
+
const { countConfigs } = require('./hooks/lib/counter.cjs');
|
|
20
|
+
const { loadConfig } = require('./hooks/lib/config.cjs');
|
|
21
|
+
const { getGitInfo } = require('./hooks/lib/git.cjs');
|
|
22
|
+
|
|
23
|
+
// Buffer constant matching /context output (22.5% of 200k)
|
|
24
|
+
const AUTOCOMPACT_BUFFER = 45000;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Expand home directory to ~
|
|
28
|
+
*/
|
|
29
|
+
function expandHome(filePath) {
|
|
30
|
+
const homeDir = os.homedir();
|
|
31
|
+
return filePath.startsWith(homeDir) ? filePath.replace(homeDir, '~') : filePath;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get terminal width with fallback chain
|
|
36
|
+
* Piped context (statusline) needs alternative detection
|
|
37
|
+
*/
|
|
38
|
+
function getTerminalWidth() {
|
|
39
|
+
// Try multiple sources - stderr might still be TTY even when stdout is piped
|
|
40
|
+
if (process.stderr.columns) return process.stderr.columns;
|
|
41
|
+
if (env.COLUMNS) {
|
|
42
|
+
const parsed = parseInt(env.COLUMNS, 10);
|
|
43
|
+
if (!isNaN(parsed) && parsed > 0) return parsed;
|
|
44
|
+
}
|
|
45
|
+
return 120; // Safe default
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Calculate visible string length (strip ANSI codes, account for emoji width)
|
|
50
|
+
* Emojis typically render as 2 columns in terminals
|
|
51
|
+
*/
|
|
52
|
+
function visibleLength(str) {
|
|
53
|
+
if (!str || typeof str !== 'string') return 0;
|
|
54
|
+
// Strip ANSI escape codes
|
|
55
|
+
const noAnsi = str.replace(/\x1b\[[0-9;]*m/g, '');
|
|
56
|
+
// Count emojis (they render as ~2 cols) - common emoji ranges
|
|
57
|
+
const emojiMatches = noAnsi.match(/[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/gu) || [];
|
|
58
|
+
return noAnsi.length + emojiMatches.length; // +1 per emoji (base length + 1 = 2 cols)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Format elapsed time from start to end (or now)
|
|
63
|
+
*/
|
|
64
|
+
function formatElapsed(startTime, endTime) {
|
|
65
|
+
if (!startTime) return '0s';
|
|
66
|
+
const start = startTime instanceof Date ? startTime.getTime() : new Date(startTime).getTime();
|
|
67
|
+
if (isNaN(start)) return '0s';
|
|
68
|
+
const end = endTime ? (endTime instanceof Date ? endTime.getTime() : new Date(endTime).getTime()) : Date.now();
|
|
69
|
+
if (isNaN(end)) return '0s';
|
|
70
|
+
const ms = end - start;
|
|
71
|
+
if (ms < 0 || ms < 1000) return '<1s';
|
|
72
|
+
if (ms < 60000) return `${Math.round(ms / 1000)}s`;
|
|
73
|
+
const mins = Math.floor(ms / 60000);
|
|
74
|
+
const secs = Math.round((ms % 60000) / 1000);
|
|
75
|
+
return `${mins}m ${secs}s`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Read stdin asynchronously
|
|
80
|
+
*/
|
|
81
|
+
async function readStdin() {
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
const chunks = [];
|
|
84
|
+
stdin.setEncoding('utf8');
|
|
85
|
+
stdin.on('data', chunk => chunks.push(chunk));
|
|
86
|
+
stdin.on('end', () => resolve(chunks.join('')));
|
|
87
|
+
stdin.on('error', reject);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ============================================================================
|
|
92
|
+
// LINE RENDERERS
|
|
93
|
+
// ============================================================================
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Build usage time string with optional percentage
|
|
97
|
+
* @returns {string|null} Formatted usage string or null if unavailable
|
|
98
|
+
*/
|
|
99
|
+
function buildUsageString(ctx) {
|
|
100
|
+
if (!ctx.sessionText || ctx.sessionText === 'N/A') return null;
|
|
101
|
+
let str = ctx.sessionText.replace(' until reset', ' left');
|
|
102
|
+
if (ctx.usagePercent != null) str += ` (${Math.round(ctx.usagePercent)}%)`;
|
|
103
|
+
return str;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Render session lines with multi-level responsive wrapping
|
|
108
|
+
* Combines parts based on content length vs terminal width
|
|
109
|
+
* Tries to minimize lines while keeping content readable
|
|
110
|
+
*/
|
|
111
|
+
function renderSessionLines(ctx) {
|
|
112
|
+
const lines = [];
|
|
113
|
+
const termWidth = getTerminalWidth();
|
|
114
|
+
const threshold = Math.floor(termWidth * 0.85);
|
|
115
|
+
|
|
116
|
+
// Build all atomic parts for flexible composition (no colors on static text)
|
|
117
|
+
const dirPart = `📁 ${ctx.currentDir}`;
|
|
118
|
+
|
|
119
|
+
let branchPart = '';
|
|
120
|
+
if (ctx.gitBranch) {
|
|
121
|
+
branchPart = `🌿 ${ctx.gitBranch}`;
|
|
122
|
+
// Build git status indicators: (unstaged, +staged, ahead↑, behind↓)
|
|
123
|
+
const gitIndicators = [];
|
|
124
|
+
if (ctx.gitUnstaged > 0) gitIndicators.push(`${ctx.gitUnstaged}`);
|
|
125
|
+
if (ctx.gitStaged > 0) gitIndicators.push(`+${ctx.gitStaged}`);
|
|
126
|
+
if (ctx.gitAhead > 0) gitIndicators.push(`${ctx.gitAhead}↑`);
|
|
127
|
+
if (ctx.gitBehind > 0) gitIndicators.push(`${ctx.gitBehind}↓`);
|
|
128
|
+
if (gitIndicators.length > 0) {
|
|
129
|
+
branchPart += ` ${yellow(`(${gitIndicators.join(', ')})`)}`;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Active plan indicator (disabled for now - code preserved)
|
|
134
|
+
// const planPart = ctx.activePlan ? `📋 ${ctx.activePlan}` : '';
|
|
135
|
+
const planPart = '';
|
|
136
|
+
|
|
137
|
+
// Combined location (dir + branch + plan)
|
|
138
|
+
let locationPart = branchPart ? `${dirPart} ${branchPart}` : dirPart;
|
|
139
|
+
if (planPart) locationPart += ` ${planPart}`;
|
|
140
|
+
|
|
141
|
+
// Build session part: 🤖 model contextBar% ⌛ time left (usage%)
|
|
142
|
+
let sessionPart = `🤖 ${ctx.modelName}`;
|
|
143
|
+
if (ctx.contextPercent > 0) {
|
|
144
|
+
sessionPart += ` ${coloredBar(ctx.contextPercent, 12)} ${ctx.contextPercent}%`;
|
|
145
|
+
}
|
|
146
|
+
// Add usage/reset info to session part (stays on line 1 with model - Claude Code only reads line 1)
|
|
147
|
+
const usageStr = buildUsageString(ctx);
|
|
148
|
+
if (usageStr) {
|
|
149
|
+
sessionPart += ` ⌛ ${usageStr.replace(/\)$/, ' used)')}`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Build stats part (only lines changed now)
|
|
153
|
+
const statsItems = [];
|
|
154
|
+
// if (ctx.costText) statsItems.push(`💵 ${ctx.costText.replace(/(\.\d{2})\d+/, '$1')}`);
|
|
155
|
+
if (ctx.linesAdded > 0 || ctx.linesRemoved > 0) {
|
|
156
|
+
statsItems.push(`📝 ${green(`+${ctx.linesAdded}`)} ${red(`-${ctx.linesRemoved}`)}`);
|
|
157
|
+
}
|
|
158
|
+
const statsPart = statsItems.join(' ');
|
|
159
|
+
|
|
160
|
+
// Calculate lengths for layout decisions
|
|
161
|
+
const locationLen = visibleLength(locationPart);
|
|
162
|
+
const sessionLen = visibleLength(sessionPart);
|
|
163
|
+
const statsLen = visibleLength(statsPart);
|
|
164
|
+
|
|
165
|
+
// Layout priority: SESSION FIRST (Claude Code only reads line 1)
|
|
166
|
+
// Line 1: model + context + usage (most important for Claude Code)
|
|
167
|
+
// Line 2+: location, git, stats
|
|
168
|
+
const allOneLine = `${sessionPart} ${locationPart} ${statsPart}`;
|
|
169
|
+
const sessionLocation = `${sessionPart} ${locationPart}`;
|
|
170
|
+
const sessionStats = `${sessionPart} ${statsPart}`;
|
|
171
|
+
|
|
172
|
+
if (visibleLength(allOneLine) <= threshold && statsLen > 0) {
|
|
173
|
+
// Ultra-wide: everything on one line (session first)
|
|
174
|
+
lines.push(allOneLine);
|
|
175
|
+
} else if (visibleLength(sessionLocation) <= threshold) {
|
|
176
|
+
// Wide: session+location on line 1 | stats on line 2
|
|
177
|
+
lines.push(sessionLocation);
|
|
178
|
+
if (statsLen > 0) lines.push(statsPart);
|
|
179
|
+
} else if (sessionLen <= threshold) {
|
|
180
|
+
// Medium: session on line 1 | location on line 2 | stats on line 3
|
|
181
|
+
lines.push(sessionPart);
|
|
182
|
+
lines.push(locationPart);
|
|
183
|
+
if (statsLen > 0) lines.push(statsPart);
|
|
184
|
+
} else {
|
|
185
|
+
// Narrow: session | dir | branch | stats (each on own line)
|
|
186
|
+
lines.push(sessionPart);
|
|
187
|
+
lines.push(dirPart);
|
|
188
|
+
if (branchPart) lines.push(branchPart);
|
|
189
|
+
if (planPart) lines.push(planPart);
|
|
190
|
+
if (statsLen > 0) lines.push(statsPart);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return lines;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Safe date parsing - returns epoch ms or 0 for invalid dates
|
|
198
|
+
*/
|
|
199
|
+
function safeGetTime(dateValue) {
|
|
200
|
+
if (!dateValue) return 0;
|
|
201
|
+
const time = new Date(dateValue).getTime();
|
|
202
|
+
return isNaN(time) ? 0 : time;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Render agents lines as compact chronological flow with duplicate collapsing
|
|
207
|
+
* Format: ○ type ×N → ● type (N done)
|
|
208
|
+
* ▸ description (elapsed)
|
|
209
|
+
* @returns {string[]} Array of lines (flow line + optional task line)
|
|
210
|
+
*/
|
|
211
|
+
function renderAgentsLines(transcript) {
|
|
212
|
+
const { agents } = transcript;
|
|
213
|
+
if (!agents || agents.length === 0) return [];
|
|
214
|
+
|
|
215
|
+
const running = agents.filter(a => a.status === 'running');
|
|
216
|
+
const completed = agents.filter(a => a.status === 'completed');
|
|
217
|
+
|
|
218
|
+
// Sort all by startTime (safe NaN handling)
|
|
219
|
+
const allAgents = [...running, ...completed];
|
|
220
|
+
allAgents.sort((a, b) => safeGetTime(a.startTime) - safeGetTime(b.startTime));
|
|
221
|
+
|
|
222
|
+
if (allAgents.length === 0) return [];
|
|
223
|
+
|
|
224
|
+
// Collapse consecutive duplicate types FIRST (before slicing)
|
|
225
|
+
const collapsed = [];
|
|
226
|
+
for (const agent of allAgents) {
|
|
227
|
+
const type = agent.type || 'agent'; // fallback for missing type
|
|
228
|
+
const last = collapsed[collapsed.length - 1];
|
|
229
|
+
if (last && last.type === type && last.status === agent.status) {
|
|
230
|
+
last.count++;
|
|
231
|
+
last.agents.push(agent);
|
|
232
|
+
} else {
|
|
233
|
+
collapsed.push({ type, status: agent.status, count: 1, agents: [agent] });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// THEN slice to show last 4 collapsed groups
|
|
238
|
+
const toShow = collapsed.slice(-4);
|
|
239
|
+
|
|
240
|
+
// Build compact flow line with dots and ×N for duplicates
|
|
241
|
+
const flowParts = toShow.map(group => {
|
|
242
|
+
const icon = group.status === 'running' ? yellow('●') : dim('○');
|
|
243
|
+
const suffix = group.count > 1 ? ` ×${group.count}` : '';
|
|
244
|
+
return `${icon} ${group.type}${suffix}`;
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const lines = [];
|
|
248
|
+
const completedCount = agents.filter(a => a.status === 'completed').length;
|
|
249
|
+
const flowSuffix = completedCount > 2 ? ` ${dim(`(${completedCount} done)`)}` : '';
|
|
250
|
+
lines.push(flowParts.join(' → ') + flowSuffix);
|
|
251
|
+
|
|
252
|
+
// Add indented task description for running agent, or last completed if none running
|
|
253
|
+
const runningAgent = running[0];
|
|
254
|
+
const lastCompleted = completed[completed.length - 1];
|
|
255
|
+
const detailAgent = runningAgent || lastCompleted;
|
|
256
|
+
|
|
257
|
+
if (detailAgent && detailAgent.description) {
|
|
258
|
+
const desc = detailAgent.description.length > 50
|
|
259
|
+
? detailAgent.description.slice(0, 47) + '...'
|
|
260
|
+
: detailAgent.description;
|
|
261
|
+
const elapsed = formatElapsed(detailAgent.startTime, detailAgent.endTime);
|
|
262
|
+
const icon = detailAgent.status === 'running' ? yellow('▸') : dim('▸');
|
|
263
|
+
lines.push(` ${icon} ${desc} ${dim(`(${elapsed})`)}`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return lines;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Render todos line (if todos exist)
|
|
271
|
+
* In-progress task with activeForm + detailed progress (done/pending)
|
|
272
|
+
*/
|
|
273
|
+
function renderTodosLine(transcript) {
|
|
274
|
+
const { todos } = transcript;
|
|
275
|
+
if (!todos || todos.length === 0) return null;
|
|
276
|
+
|
|
277
|
+
const inProgress = todos.find(t => t.status === 'in_progress');
|
|
278
|
+
const completedCount = todos.filter(t => t.status === 'completed').length;
|
|
279
|
+
const pendingCount = todos.filter(t => t.status === 'pending').length;
|
|
280
|
+
const total = todos.length;
|
|
281
|
+
|
|
282
|
+
if (!inProgress) {
|
|
283
|
+
if (completedCount === total && total > 0) {
|
|
284
|
+
return `${green('✓')} All ${total} todos complete`;
|
|
285
|
+
}
|
|
286
|
+
// Show pending if no in_progress
|
|
287
|
+
if (pendingCount > 0) {
|
|
288
|
+
const nextPending = todos.find(t => t.status === 'pending');
|
|
289
|
+
const nextTask = nextPending?.content || 'Next task';
|
|
290
|
+
const display = nextTask.length > 40 ? nextTask.slice(0, 37) + '...' : nextTask;
|
|
291
|
+
return `${dim('○')} Next: ${display} ${dim(`(${completedCount} done, ${pendingCount} pending)`)}`;
|
|
292
|
+
}
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Show activeForm (present continuous) if available, else content
|
|
297
|
+
const displayText = inProgress.activeForm || inProgress.content;
|
|
298
|
+
const display = displayText.length > 50 ? displayText.slice(0, 47) + '...' : displayText;
|
|
299
|
+
return `${yellow('▸')} ${display} ${dim(`(${completedCount} done, ${pendingCount} pending)`)}`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Render minimal mode - single line with emojis, no progress bar
|
|
304
|
+
* Format: "🤖 opus 4.5 🔋 50% ⏰ 2h 16m (38%) 🌿 branch 📁 ~/path"
|
|
305
|
+
*/
|
|
306
|
+
function renderMinimal(ctx) {
|
|
307
|
+
const parts = [`🤖 ${ctx.modelName}`];
|
|
308
|
+
if (ctx.contextPercent > 0) {
|
|
309
|
+
const batteryIcon = ctx.contextPercent > 70 ? red('🔋') : '🔋';
|
|
310
|
+
parts.push(`${batteryIcon} ${ctx.contextPercent}%`);
|
|
311
|
+
}
|
|
312
|
+
const usageStr = buildUsageString(ctx);
|
|
313
|
+
if (usageStr) parts.push(`⏰ ${usageStr}`);
|
|
314
|
+
if (ctx.gitBranch) parts.push(`🌿 ${ctx.gitBranch}`);
|
|
315
|
+
parts.push(`📁 ${ctx.currentDir}`);
|
|
316
|
+
console.log(parts.join(' '));
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Render compact mode - 2 lines: session info + location (branch + dir)
|
|
321
|
+
*/
|
|
322
|
+
function renderCompact(ctx) {
|
|
323
|
+
// Line 1: Session info (model + context + usage)
|
|
324
|
+
let line1 = `🤖 ${ctx.modelName}`;
|
|
325
|
+
if (ctx.contextPercent > 0) {
|
|
326
|
+
line1 += ` ${coloredBar(ctx.contextPercent, 12)} ${ctx.contextPercent}%`;
|
|
327
|
+
}
|
|
328
|
+
const usageStr = buildUsageString(ctx);
|
|
329
|
+
if (usageStr) line1 += ` ⌛ ${usageStr}`;
|
|
330
|
+
console.log(line1.replace(/ /g, '\u00A0'));
|
|
331
|
+
|
|
332
|
+
// Line 2: Location (branch + directory)
|
|
333
|
+
let line2 = `📁 ${ctx.currentDir}`;
|
|
334
|
+
if (ctx.gitBranch) line2 += ` 🌿 ${ctx.gitBranch}`;
|
|
335
|
+
console.log(line2.replace(/ /g, '\u00A0'));
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Main render function - outputs all lines
|
|
340
|
+
* Falls back to single line if multi-line fails
|
|
341
|
+
*/
|
|
342
|
+
function render(ctx, singleLineMode = false) {
|
|
343
|
+
const lines = [];
|
|
344
|
+
|
|
345
|
+
// Session lines (cleaner multi-line layout)
|
|
346
|
+
const sessionLines = renderSessionLines(ctx);
|
|
347
|
+
lines.push(...sessionLines);
|
|
348
|
+
|
|
349
|
+
if (!singleLineMode) {
|
|
350
|
+
// Agents lines (one per agent for clarity)
|
|
351
|
+
const agentsLines = renderAgentsLines(ctx.transcript);
|
|
352
|
+
lines.push(...agentsLines);
|
|
353
|
+
|
|
354
|
+
// Todos line (if exist)
|
|
355
|
+
const todosLine = renderTodosLine(ctx.transcript);
|
|
356
|
+
if (todosLine) lines.push(todosLine);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Output all lines with non-breaking spaces for alignment
|
|
360
|
+
for (const line of lines) {
|
|
361
|
+
const outputLine = shouldUseColor ? `${RESET}${line.replace(/ /g, '\u00A0')}` : line;
|
|
362
|
+
console.log(outputLine);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// ============================================================================
|
|
367
|
+
// MAIN
|
|
368
|
+
// ============================================================================
|
|
369
|
+
|
|
370
|
+
async function main() {
|
|
371
|
+
try {
|
|
372
|
+
const input = await readStdin();
|
|
373
|
+
if (!input.trim()) {
|
|
374
|
+
console.error('No input provided');
|
|
375
|
+
process.exit(1);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const data = JSON.parse(input);
|
|
379
|
+
|
|
380
|
+
// Extract basic information
|
|
381
|
+
let currentDir = data.workspace?.current_dir || data.cwd || 'unknown';
|
|
382
|
+
currentDir = expandHome(currentDir);
|
|
383
|
+
|
|
384
|
+
const modelName = data.model?.display_name || 'Claude';
|
|
385
|
+
|
|
386
|
+
// Git detection using batched cache
|
|
387
|
+
const rawDir = data.workspace?.current_dir || data.cwd || process.cwd();
|
|
388
|
+
const gitInfo = getGitInfo(rawDir);
|
|
389
|
+
const gitBranch = gitInfo?.branch || '';
|
|
390
|
+
const gitUnstaged = gitInfo?.unstaged || 0;
|
|
391
|
+
const gitStaged = gitInfo?.staged || 0;
|
|
392
|
+
const gitAhead = gitInfo?.ahead || 0;
|
|
393
|
+
const gitBehind = gitInfo?.behind || 0;
|
|
394
|
+
|
|
395
|
+
// Active plan detection - read from session temp file
|
|
396
|
+
let activePlan = '';
|
|
397
|
+
try {
|
|
398
|
+
const sessionId = data.session_id;
|
|
399
|
+
if (sessionId) {
|
|
400
|
+
const sessionPath = path.join(os.tmpdir(), `ck-session-${sessionId}.json`);
|
|
401
|
+
if (fs.existsSync(sessionPath)) {
|
|
402
|
+
const session = JSON.parse(fs.readFileSync(sessionPath, 'utf8'));
|
|
403
|
+
const planPath = session.activePlan?.trim();
|
|
404
|
+
if (planPath) {
|
|
405
|
+
// Extract slug from path like "plans/260106-1554-statusline-visual"
|
|
406
|
+
const match = planPath.match(/plans\/\d+-\d+-(.+?)(?:\/|$)/);
|
|
407
|
+
activePlan = match ? match[1] : planPath.split('/').pop();
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
} catch {}
|
|
412
|
+
|
|
413
|
+
// Context window - use current_usage fields with AUTOCOMPACT_BUFFER
|
|
414
|
+
const usage = data.context_window?.current_usage || {};
|
|
415
|
+
const contextSize = data.context_window?.context_window_size || 0;
|
|
416
|
+
let contextPercent = 0;
|
|
417
|
+
let totalTokens = 0;
|
|
418
|
+
|
|
419
|
+
if (contextSize > 0 && contextSize > AUTOCOMPACT_BUFFER) {
|
|
420
|
+
totalTokens = (usage.input_tokens ?? 0) +
|
|
421
|
+
(usage.cache_creation_input_tokens ?? 0) +
|
|
422
|
+
(usage.cache_read_input_tokens ?? 0);
|
|
423
|
+
|
|
424
|
+
// Add buffer to match /context calculation
|
|
425
|
+
contextPercent = Math.min(100, Math.round(((totalTokens + AUTOCOMPACT_BUFFER) / contextSize) * 100));
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Write context data to temp file for hooks to read
|
|
429
|
+
const sessionId = data.session_id;
|
|
430
|
+
if (sessionId && contextSize > 0) {
|
|
431
|
+
try {
|
|
432
|
+
const contextDataPath = path.join(os.tmpdir(), `ck-context-${sessionId}.json`);
|
|
433
|
+
fs.writeFileSync(contextDataPath, JSON.stringify({
|
|
434
|
+
percent: contextPercent,
|
|
435
|
+
tokens: totalTokens,
|
|
436
|
+
size: contextSize,
|
|
437
|
+
usage: usage,
|
|
438
|
+
timestamp: Date.now()
|
|
439
|
+
}));
|
|
440
|
+
} catch {}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Session timer - read actual reset time from usage limits cache
|
|
444
|
+
let sessionText = '';
|
|
445
|
+
const transcriptPath = data.transcript_path;
|
|
446
|
+
|
|
447
|
+
// Parse transcript for tools/agents/todos
|
|
448
|
+
const transcript = transcriptPath ? await parseTranscript(transcriptPath) : { tools: [], agents: [], todos: [], sessionStart: null };
|
|
449
|
+
|
|
450
|
+
// Read actual reset time and utilization from usage limits cache (written by usage-context-awareness hook)
|
|
451
|
+
let usagePercent = null;
|
|
452
|
+
try {
|
|
453
|
+
const usageCachePath = path.join(os.tmpdir(), 'ck-usage-limits-cache.json');
|
|
454
|
+
if (fs.existsSync(usageCachePath)) {
|
|
455
|
+
const cache = JSON.parse(fs.readFileSync(usageCachePath, 'utf8'));
|
|
456
|
+
|
|
457
|
+
// Check status flag for fallback (non-OAuth scenarios)
|
|
458
|
+
if (cache.status === 'unavailable') {
|
|
459
|
+
sessionText = 'N/A';
|
|
460
|
+
} else {
|
|
461
|
+
const fiveHour = cache.data?.five_hour;
|
|
462
|
+
usagePercent = fiveHour?.utilization ?? null;
|
|
463
|
+
const resetAt = fiveHour?.resets_at;
|
|
464
|
+
if (resetAt) {
|
|
465
|
+
const resetTime = new Date(resetAt);
|
|
466
|
+
const remaining = Math.floor(resetTime.getTime() / 1000) - Math.floor(Date.now() / 1000);
|
|
467
|
+
if (remaining > 0 && remaining < 18000) {
|
|
468
|
+
const rh = Math.floor(remaining / 3600);
|
|
469
|
+
const rm = Math.floor((remaining % 3600) / 60);
|
|
470
|
+
sessionText = `${rh}h ${rm}m until reset`;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
} catch {}
|
|
476
|
+
|
|
477
|
+
// Cost and lines changed
|
|
478
|
+
const billingMode = env.CLAUDE_BILLING_MODE || 'api';
|
|
479
|
+
const costUSD = data.cost?.total_cost_usd;
|
|
480
|
+
const costText = billingMode === 'api' && costUSD && /^\d+(\.\d+)?$/.test(String(costUSD))
|
|
481
|
+
? `$${parseFloat(costUSD).toFixed(4)}`
|
|
482
|
+
: null;
|
|
483
|
+
const linesAdded = data.cost?.total_lines_added || 0;
|
|
484
|
+
const linesRemoved = data.cost?.total_lines_removed || 0;
|
|
485
|
+
|
|
486
|
+
// Config counts
|
|
487
|
+
const configs = countConfigs(rawDir);
|
|
488
|
+
|
|
489
|
+
// Build render context
|
|
490
|
+
const ctx = {
|
|
491
|
+
modelName,
|
|
492
|
+
currentDir,
|
|
493
|
+
gitBranch,
|
|
494
|
+
gitUnstaged,
|
|
495
|
+
gitStaged,
|
|
496
|
+
gitAhead,
|
|
497
|
+
gitBehind,
|
|
498
|
+
activePlan,
|
|
499
|
+
contextPercent,
|
|
500
|
+
sessionText,
|
|
501
|
+
usagePercent,
|
|
502
|
+
costText,
|
|
503
|
+
linesAdded,
|
|
504
|
+
linesRemoved,
|
|
505
|
+
configs,
|
|
506
|
+
transcript
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
// Load config and get statusline mode
|
|
510
|
+
const config = loadConfig({ includeProject: false, includeAssertions: false, includeLocale: false });
|
|
511
|
+
const statuslineMode = config.statusline || 'full';
|
|
512
|
+
|
|
513
|
+
// Render based on mode
|
|
514
|
+
switch (statuslineMode) {
|
|
515
|
+
case 'none':
|
|
516
|
+
console.log('');
|
|
517
|
+
break;
|
|
518
|
+
case 'minimal':
|
|
519
|
+
renderMinimal(ctx);
|
|
520
|
+
break;
|
|
521
|
+
case 'compact':
|
|
522
|
+
renderCompact(ctx);
|
|
523
|
+
break;
|
|
524
|
+
case 'full':
|
|
525
|
+
default:
|
|
526
|
+
render(ctx, false);
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
} catch (err) {
|
|
531
|
+
// Fallback: output minimal single line on any error
|
|
532
|
+
console.log('📁 ' + (process.cwd() || 'unknown'));
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
main().catch(() => {
|
|
537
|
+
console.log('📁 error');
|
|
538
|
+
process.exit(1);
|
|
539
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: hapo:code
|
|
3
|
+
description: Implement the next approved spec task and then hand off to hapo:test and hapo:review.
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
argument-hint: <feature-name>
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Hapo Code
|
|
9
|
+
|
|
10
|
+
Implement the next pending task from an approved spec instead of coding from memory.
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
/hapo:code <feature-name>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Load First
|
|
19
|
+
|
|
20
|
+
- `references/execution-loop.md`
|
|
21
|
+
- `.specs/$ARGUMENTS/tasks.md`
|
|
22
|
+
- `.specs/$ARGUMENTS/design.md`
|
|
23
|
+
- `.specs/$ARGUMENTS/requirements.md`
|
|
24
|
+
|
|
25
|
+
## Execute
|
|
26
|
+
|
|
27
|
+
1. Read `.specs/$ARGUMENTS/tasks.md` and identify the next pending task.
|
|
28
|
+
2. Read the related context from `design.md` and `requirements.md`.
|
|
29
|
+
3. Implement only that task. Keep scope tight.
|
|
30
|
+
4. Follow the project standards already present in the repo.
|
|
31
|
+
5. Update task status in the spec artifact if the workflow supports it.
|
|
32
|
+
6. Hand off immediately to:
|
|
33
|
+
- `/hapo:test`
|
|
34
|
+
- `/hapo:review`
|
|
35
|
+
|
|
36
|
+
## Output
|
|
37
|
+
|
|
38
|
+
Return:
|
|
39
|
+
- implemented task name
|
|
40
|
+
- touched files
|
|
41
|
+
- blockers or follow-up items
|
|
42
|
+
- explicit handoff to `/hapo:test` and `/hapo:review`
|
|
43
|
+
|
|
44
|
+
## Rules
|
|
45
|
+
|
|
46
|
+
- Do not implement multiple major tasks in one pass.
|
|
47
|
+
- Stop if `tasks.md` is missing.
|
|
48
|
+
- Prefer existing patterns over new abstractions.
|
|
49
|
+
- Keep the work aligned to the spec.
|
|
50
|
+
|
|
51
|
+
## Related
|
|
52
|
+
|
|
53
|
+
- Command: `/code`
|
|
54
|
+
- Previous skill: `/hapo:spec-tasks`
|
|
55
|
+
- Next skills: `/hapo:test`, `/hapo:review`
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Execution Loop
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Implement one approved task at a time and immediately validate it.
|
|
6
|
+
|
|
7
|
+
## Loop
|
|
8
|
+
|
|
9
|
+
1. Read the next pending task in `tasks.md`.
|
|
10
|
+
2. Read the matching context in `design.md` and `requirements.md`.
|
|
11
|
+
3. Implement only that task.
|
|
12
|
+
4. Run `/hapo:test`.
|
|
13
|
+
5. Run `/hapo:review`.
|
|
14
|
+
6. Move to the next task only after the current pass is clear.
|
|
15
|
+
|
|
16
|
+
## Guardrails
|
|
17
|
+
|
|
18
|
+
- Keep scope tight.
|
|
19
|
+
- Prefer existing project patterns.
|
|
20
|
+
- Do not batch multiple major tasks into one pass.
|
|
21
|
+
- Stop and report blockers instead of guessing.
|
|
@@ -1,54 +1,54 @@
|
|
|
1
|
-
# Change Detection -
|
|
1
|
+
# Change Detection - Detecting Changes
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Methods for detecting and classifying code changes for impact analysis.
|
|
4
4
|
|
|
5
5
|
## Git Commands
|
|
6
6
|
|
|
7
7
|
### 1. Detect Recent Changes
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
#
|
|
10
|
+
# Uncommitted changes
|
|
11
11
|
git diff --name-only
|
|
12
12
|
|
|
13
|
-
#
|
|
13
|
+
# Staged changes
|
|
14
14
|
git diff --cached --name-only
|
|
15
15
|
|
|
16
|
-
# Changes
|
|
16
|
+
# Changes in last commit
|
|
17
17
|
git diff --name-only HEAD~1
|
|
18
18
|
|
|
19
|
-
# Changes
|
|
19
|
+
# Changes compared to another branch
|
|
20
20
|
git diff --name-only main...HEAD
|
|
21
21
|
|
|
22
|
-
#
|
|
22
|
+
# View detailed changes
|
|
23
23
|
git diff HEAD~1
|
|
24
24
|
|
|
25
|
-
#
|
|
25
|
+
# View statistics
|
|
26
26
|
git diff --stat HEAD~1
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
### 2. Get Change Details
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
|
-
#
|
|
32
|
+
# Lines changed
|
|
33
33
|
git diff --numstat HEAD~1
|
|
34
34
|
|
|
35
|
-
# Files
|
|
35
|
+
# Files and change count
|
|
36
36
|
git diff --shortstat HEAD~1
|
|
37
37
|
|
|
38
|
-
#
|
|
38
|
+
# Only view added/deleted files
|
|
39
39
|
git diff --diff-filter=AD --name-only HEAD~1
|
|
40
40
|
```
|
|
41
41
|
|
|
42
42
|
### 3. Historical Analysis
|
|
43
43
|
|
|
44
44
|
```bash
|
|
45
|
-
#
|
|
45
|
+
# View file history
|
|
46
46
|
git log --oneline -10 {file}
|
|
47
47
|
|
|
48
|
-
#
|
|
48
|
+
# See who modified which line
|
|
49
49
|
git blame {file}
|
|
50
50
|
|
|
51
|
-
#
|
|
51
|
+
# View changes in specific commit
|
|
52
52
|
git show {commit-hash}
|
|
53
53
|
```
|
|
54
54
|
|
|
@@ -57,7 +57,7 @@ git show {commit-hash}
|
|
|
57
57
|
### Backend Changes
|
|
58
58
|
|
|
59
59
|
**Indicators:**
|
|
60
|
-
- Files: `*.ts`, `*.js`, `*.py`, `*.go`
|
|
60
|
+
- Files: `*.ts`, `*.js`, `*.py`, `*.go` in `src/api/`, `src/services/`, `src/controllers/`
|
|
61
61
|
- Patterns: API routes, database queries, business logic
|
|
62
62
|
|
|
63
63
|
**Risk Level:**
|
|
@@ -79,7 +79,7 @@ grep -r "SELECT\|INSERT\|UPDATE\|DELETE" {changed-files}
|
|
|
79
79
|
### Frontend Changes
|
|
80
80
|
|
|
81
81
|
**Indicators:**
|
|
82
|
-
- Files: `*.tsx`, `*.jsx`, `*.vue`, `*.svelte`
|
|
82
|
+
- Files: `*.tsx`, `*.jsx`, `*.vue`, `*.svelte` in `src/components/`, `src/pages/`
|
|
83
83
|
- Patterns: Components, hooks, state management
|
|
84
84
|
|
|
85
85
|
**Risk Level:**
|