codehere 0.8.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/README.md +37 -184
  2. package/dist/application/use-cases/check-claimed-tests.js +81 -0
  3. package/dist/application/use-cases/detect-test-weakening.js +237 -0
  4. package/dist/application/use-cases/rerun-tests.js +329 -0
  5. package/dist/index.js +74 -1004
  6. package/dist/infrastructure/orchestration/artifacts-extractor.js +2 -265
  7. package/dist/presentation/cli/commands/check-command.js +251 -0
  8. package/dist/presentation/cli/commands/help-command.js +22 -286
  9. package/dist/ui.js +19 -438
  10. package/dist/utils/version.js +0 -12
  11. package/package.json +18 -27
  12. package/dist/application/use-cases/domain-trust.js +0 -110
  13. package/dist/application/use-cases/generate-final-report.js +0 -134
  14. package/dist/application/use-cases/generate-narration.js +0 -128
  15. package/dist/application/use-cases/generate-thread-title.js +0 -178
  16. package/dist/application/use-cases/narrator-prompt-template.js +0 -158
  17. package/dist/application/use-cases/thread-export.js +0 -155
  18. package/dist/application/use-cases/verify-claim.js +0 -274
  19. package/dist/application/use-cases/write-pass.js +0 -277
  20. package/dist/domain/entities/task.js +0 -61
  21. package/dist/domain/services/codebase-detector.js +0 -91
  22. package/dist/error-handler.js +0 -251
  23. package/dist/infrastructure/completion/bash-completion.js +0 -25
  24. package/dist/infrastructure/completion/fish-completion.js +0 -22
  25. package/dist/infrastructure/completion/zsh-completion.js +0 -30
  26. package/dist/infrastructure/config/config-manager.js +0 -173
  27. package/dist/infrastructure/config/product-context.js +0 -67
  28. package/dist/infrastructure/context/adapters/drawio-source.js +0 -157
  29. package/dist/infrastructure/context/adapters/figma-source.js +0 -163
  30. package/dist/infrastructure/context/adapters/github-issue-source.js +0 -159
  31. package/dist/infrastructure/context/adapters/linear-source.js +0 -190
  32. package/dist/infrastructure/context/context-resolver.js +0 -144
  33. package/dist/infrastructure/context/context-source.js +0 -114
  34. package/dist/infrastructure/context/oauth-flow.js +0 -207
  35. package/dist/infrastructure/context/oauth-store.js +0 -119
  36. package/dist/infrastructure/context/registry-bootstrap.js +0 -99
  37. package/dist/infrastructure/context/write-pass-resolver.js +0 -106
  38. package/dist/infrastructure/cost/budget-config.js +0 -113
  39. package/dist/infrastructure/cost/cost-router.js +0 -104
  40. package/dist/infrastructure/cost/spend-aggregator.js +0 -147
  41. package/dist/infrastructure/hooks/claude-sdk-hook.js +0 -888
  42. package/dist/infrastructure/hooks/hook-mode.js +0 -71
  43. package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +0 -43
  44. package/dist/infrastructure/hooks/pre-prompt-scan.js +0 -122
  45. package/dist/infrastructure/hooks/session-analyzer.js +0 -177
  46. package/dist/infrastructure/observability/execution-tracer.js +0 -227
  47. package/dist/infrastructure/orchestration/agent-adapter.js +0 -13
  48. package/dist/infrastructure/orchestration/agent-installer.js +0 -101
  49. package/dist/infrastructure/orchestration/agents-config-seed.js +0 -56
  50. package/dist/infrastructure/orchestration/aider-adapter.js +0 -447
  51. package/dist/infrastructure/orchestration/cassette-recorder.js +0 -120
  52. package/dist/infrastructure/orchestration/claude-code-adapter.js +0 -478
  53. package/dist/infrastructure/orchestration/codex-adapter.js +0 -538
  54. package/dist/infrastructure/orchestration/conductor-adapter.js +0 -377
  55. package/dist/infrastructure/orchestration/context-handoff.js +0 -144
  56. package/dist/infrastructure/orchestration/control/codex-control-surface.js +0 -99
  57. package/dist/infrastructure/orchestration/control/control-surface.js +0 -15
  58. package/dist/infrastructure/orchestration/failover-classifier.js +0 -151
  59. package/dist/infrastructure/orchestration/failover-config.js +0 -77
  60. package/dist/infrastructure/orchestration/failover-policy.js +0 -106
  61. package/dist/infrastructure/orchestration/gemini-adapter.js +0 -583
  62. package/dist/infrastructure/orchestration/generic-cli-adapter.js +0 -558
  63. package/dist/infrastructure/orchestration/graphify-context.js +0 -254
  64. package/dist/infrastructure/orchestration/memory-injector.js +0 -380
  65. package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +0 -233
  66. package/dist/infrastructure/orchestration/mocks/mock-codex-app-server.js +0 -73
  67. package/dist/infrastructure/orchestration/mocks/mock-synthesis.js +0 -83
  68. package/dist/infrastructure/orchestration/mocks/resolve-agent-spawn.js +0 -37
  69. package/dist/infrastructure/orchestration/opencode-adapter.js +0 -510
  70. package/dist/infrastructure/orchestration/progress-aggregator.js +0 -689
  71. package/dist/infrastructure/orchestration/readiness.js +0 -103
  72. package/dist/infrastructure/orchestration/session-manager.js +0 -1091
  73. package/dist/infrastructure/orchestration/session-persistence.js +0 -258
  74. package/dist/infrastructure/orchestration/thread-key-store.js +0 -127
  75. package/dist/infrastructure/orchestration/write-pass-store.js +0 -46
  76. package/dist/infrastructure/protocols/cloud-sync.js +0 -207
  77. package/dist/infrastructure/protocols/mcp-helpers.js +0 -25
  78. package/dist/infrastructure/protocols/mcp-server-impl.js +0 -2853
  79. package/dist/infrastructure/protocols/probe-port.js +0 -36
  80. package/dist/infrastructure/protocols/thread-phase.js +0 -105
  81. package/dist/infrastructure/protocols/ui-page-v2.js +0 -4418
  82. package/dist/infrastructure/protocols/ui-page.js +0 -2377
  83. package/dist/infrastructure/protocols/verify-page.js +0 -983
  84. package/dist/infrastructure/security/file-collection.js +0 -118
  85. package/dist/infrastructure/security/input-validator.js +0 -152
  86. package/dist/infrastructure/security/permissions-config.js +0 -234
  87. package/dist/infrastructure/security/trust-scorer.js +0 -657
  88. package/dist/infrastructure/storage/cross-tool-agents.js +0 -268
  89. package/dist/infrastructure/storage/embeddings-path.js +0 -37
  90. package/dist/infrastructure/storage/project-thread-migration.js +0 -237
  91. package/dist/infrastructure/storage/project-thread-mirror.js +0 -524
  92. package/dist/infrastructure/storage/project-thread-reader.js +0 -172
  93. package/dist/infrastructure/storage/project-thread-title-backfill.js +0 -214
  94. package/dist/infrastructure/storage/task-repository.js +0 -206
  95. package/dist/monitoring.js +0 -212
  96. package/dist/presentation/cli/commands/audit-command.js +0 -126
  97. package/dist/presentation/cli/commands/budget-command.js +0 -79
  98. package/dist/presentation/cli/commands/config-command.js +0 -125
  99. package/dist/presentation/cli/commands/context-source-command.js +0 -198
  100. package/dist/presentation/cli/commands/hook-command.js +0 -265
  101. package/dist/presentation/cli/commands/init-command.js +0 -578
  102. package/dist/presentation/cli/commands/memory-command.js +0 -1031
  103. package/dist/presentation/cli/commands/narrator-command.js +0 -101
  104. package/dist/presentation/cli/commands/permission-command.js +0 -116
  105. package/dist/presentation/cli/commands/protocols-builder.js +0 -254
  106. package/dist/presentation/cli/commands/protocols-command.js +0 -84
  107. package/dist/presentation/cli/commands/repo-map-builder.js +0 -247
  108. package/dist/presentation/cli/commands/report-command.js +0 -387
  109. package/dist/presentation/cli/commands/scan-tool-command.js +0 -94
  110. package/dist/presentation/cli/commands/setup-command.js +0 -307
  111. package/dist/presentation/cli/commands/status-command.js +0 -661
  112. package/dist/presentation/cli/commands/thread-command.js +0 -288
  113. package/dist/presentation/cli/commands/trust-command.js +0 -306
  114. package/dist/presentation/cli/commands/verify-command.js +0 -233
  115. package/dist/presentation/cli/error-display.js +0 -336
  116. package/dist/presentation/cli/repl.js +0 -797
  117. package/dist/presentation/cli/task-deliverable.js +0 -127
  118. package/dist/testing/cassette.js +0 -125
  119. package/dist/testing/replay-adapter.js +0 -166
@@ -1,661 +0,0 @@
1
- /**
2
- * CLI Command: status
3
- *
4
- * Growth/audit surface — personal security dashboard.
5
- * Shows what Claude Code has been doing, what got blocked, and how your project is doing.
6
- *
7
- * Usage:
8
- * codehere status # full dashboard
9
- * codehere status --json # machine-readable output
10
- * codehere status --trust # include a quick trust scan
11
- */
12
- import { readFileSync, existsSync } from 'fs';
13
- import { join } from 'path';
14
- import { homedir } from 'os';
15
- import { colors, newline, sectionHeader } from '../../../ui.js';
16
- import { getVersion } from '../../../utils/version.js';
17
- import { loadProductContext } from '../../../infrastructure/config/product-context.js';
18
- import { AUDIT_FILE } from '../../../infrastructure/hooks/claude-sdk-hook.js';
19
- import { analyzeSessions, detectAmplificationAlerts, formatDuration, } from '../../../infrastructure/hooks/session-analyzer.js';
20
- function readAuditLog(maxLines = 5000) {
21
- if (!existsSync(AUDIT_FILE))
22
- return [];
23
- try {
24
- const lines = readFileSync(AUDIT_FILE, 'utf-8').split('\n').filter(Boolean);
25
- const recent = lines.slice(-maxLines);
26
- return recent
27
- .map((l) => {
28
- try {
29
- return JSON.parse(l);
30
- }
31
- catch {
32
- return null;
33
- }
34
- })
35
- .filter(Boolean);
36
- }
37
- catch {
38
- return [];
39
- }
40
- }
41
- function withinHours(timestamp, hours) {
42
- return Date.now() - new Date(timestamp).getTime() < hours * 3600 * 1000;
43
- }
44
- function timeAgo(timestamp) {
45
- const ms = Date.now() - new Date(timestamp).getTime();
46
- const mins = Math.floor(ms / 60000);
47
- if (mins < 60)
48
- return `${mins}m ago`;
49
- const hours = Math.floor(mins / 60);
50
- if (hours < 24)
51
- return `${hours}h ago`;
52
- return `${Math.floor(hours / 24)}d ago`;
53
- }
54
- // ── helpers ───────────────────────────────────────────────────────────────────
55
- function extractBlockReason(entry) {
56
- try {
57
- const input = JSON.parse(entry.toolInput);
58
- // Bash: show command description or first 50 chars of command
59
- if (input.description)
60
- return String(input.description).substring(0, 50);
61
- if (input.command)
62
- return String(input.command).replace(/\n/g, ' ').substring(0, 50);
63
- // Edit/Write: show filename
64
- if (input.file_path) {
65
- const parts = String(input.file_path).split('/');
66
- return parts.slice(-2).join('/');
67
- }
68
- // MultiEdit: show first filename
69
- if (Array.isArray(input.edits) && input.edits[0]?.file_path) {
70
- const parts = String(input.edits[0].file_path).split('/');
71
- return parts.slice(-2).join('/');
72
- }
73
- }
74
- catch {
75
- /* parse failed — truncated JSON, try regex */
76
- }
77
- // Fallback: extract file_path or command from truncated JSON via regex
78
- if (!entry.toolInput)
79
- return 'tool call';
80
- const fpMatch = entry.toolInput.match(/"file_path"\s*:\s*"([^"]+)"/);
81
- if (fpMatch) {
82
- const parts = fpMatch[1].split('/');
83
- return parts.slice(-2).join('/');
84
- }
85
- const cmdMatch = entry.toolInput.match(/"command"\s*:\s*"([^"]{0,60})"/);
86
- if (cmdMatch)
87
- return cmdMatch[1].replace(/\\n/g, ' ').substring(0, 50);
88
- return 'tool call';
89
- }
90
- // ── main command ──────────────────────────────────────────────────────────────
91
- // ── session drill-down ────────────────────────────────────────────────────────
92
- function renderSessionDrillDown(sessionId, entries) {
93
- const sessionEntries = entries.filter((e) => e.sessionId === sessionId || e.sessionId?.startsWith(sessionId));
94
- if (sessionEntries.length === 0) {
95
- console.log(colors.yellow(`No entries found for session: ${sessionId}`));
96
- console.log(colors.dim(' Tip: use the first 8+ characters of a session ID from `codehere status --sessions`'));
97
- return;
98
- }
99
- const sorted = [...sessionEntries].sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
100
- const blocked = sorted.filter((e) => e.blocked);
101
- const critical = sorted.filter((e) => e.criticalFindings > 0);
102
- const durationMs = new Date(sorted[sorted.length - 1].timestamp).getTime() - new Date(sorted[0].timestamp).getTime();
103
- const toolCounts = {};
104
- for (const e of sorted) {
105
- toolCounts[e.toolName] = (toolCounts[e.toolName] ?? 0) + 1;
106
- }
107
- sectionHeader(`Session: ${sessionId.substring(0, 16)}…`);
108
- newline();
109
- console.log(` ${sorted.length} tool calls · ${blocked.length} blocked · ${critical.length} critical`);
110
- console.log(` Duration: ${Math.round(durationMs / 1000)}s · Started ${timeAgo(sorted[0].timestamp)}`);
111
- newline();
112
- // Tool breakdown
113
- console.log(colors.bold('Tool breakdown:'));
114
- const sorted_tools = Object.entries(toolCounts).sort((a, b) => b[1] - a[1]);
115
- for (const [tool, count] of sorted_tools) {
116
- const bar = '█'.repeat(Math.min(count, 20));
117
- const pct = Math.round((count / sorted.length) * 100);
118
- console.log(` ${tool.padEnd(12)} ${String(count).padStart(3)} ${colors.dim(bar)} ${colors.dim(pct + '%')}`);
119
- }
120
- newline();
121
- // Timeline of blocked calls
122
- if (blocked.length > 0) {
123
- console.log(colors.bold('Blocked calls:'));
124
- for (const entry of blocked) {
125
- const time = colors.dim(new Date(entry.timestamp).toLocaleTimeString());
126
- const tool = colors.cyan(entry.toolName.padEnd(12));
127
- const reason = extractBlockReason(entry);
128
- const risk = entry.riskScore > 70 ? colors.red(`risk:${entry.riskScore}`) : colors.yellow(`risk:${entry.riskScore}`);
129
- console.log(` ${colors.red('✗')} ${time} ${tool} ${risk} ${colors.dim(reason)}`);
130
- }
131
- newline();
132
- }
133
- else {
134
- console.log(` ${colors.green('✓')} No blocked calls in this session.`);
135
- newline();
136
- }
137
- // Full timeline (last 20)
138
- const toShow = sorted.slice(-20);
139
- if (sorted.length > 20) {
140
- console.log(colors.dim(` (showing last 20 of ${sorted.length} calls)`));
141
- }
142
- console.log(colors.bold('Timeline:'));
143
- for (const entry of toShow) {
144
- const time = colors.dim(new Date(entry.timestamp).toLocaleTimeString());
145
- const tool = (entry.blocked ? colors.red : colors.cyan)(entry.toolName.padEnd(12));
146
- const icon = entry.blocked ? colors.red('✗') : entry.criticalFindings > 0 ? colors.yellow('!') : colors.green('✓');
147
- const reason = extractBlockReason(entry);
148
- console.log(` ${icon} ${time} ${tool} ${colors.dim(reason)}`);
149
- }
150
- newline();
151
- // Verdict — rc.17: tighten "injection" claim to actual high block ratio
152
- // (UX_OBSERVATIONS.md P1; long attended sessions shouldn't get accused).
153
- if (blocked.length > 2 && sorted.length > 20 && blocked.length / sorted.length > 0.05) {
154
- const pct = ((blocked.length / sorted.length) * 100).toFixed(0);
155
- console.log(colors.red(` ⚠ ${pct}% block rate (${blocked.length}/${sorted.length}) — review for injection patterns.`));
156
- }
157
- else if (blocked.length > 0 && sorted.length > 20) {
158
- console.log(colors.yellow(` ${blocked.length} blocked / ${sorted.length} total — long session, review if unattended.`));
159
- }
160
- else if (blocked.length > 0) {
161
- console.log(colors.yellow(` ${blocked.length} blocked call${blocked.length !== 1 ? 's' : ''} — review the blocked entries above.`));
162
- }
163
- else if (sorted.length > 50) {
164
- console.log(colors.yellow(` ${sorted.length} calls in one session — check for amplification.`));
165
- }
166
- else {
167
- console.log(colors.green(' Session looks clean.'));
168
- }
169
- newline();
170
- }
171
- function extractFilePath(entry) {
172
- if (!entry.toolInput)
173
- return null;
174
- try {
175
- const input = JSON.parse(entry.toolInput);
176
- if (input.file_path)
177
- return String(input.file_path);
178
- if (Array.isArray(input.edits) && input.edits[0]?.file_path)
179
- return String(input.edits[0].file_path);
180
- }
181
- catch {
182
- const m = entry.toolInput.match(/"file_path"\s*:\s*"([^"]+)"/);
183
- if (m)
184
- return m[1];
185
- }
186
- return null;
187
- }
188
- const BLOCK_CATEGORY_LABEL = {
189
- 'self-test': "codehere's own canary tests",
190
- 'cache-cleanup': 'routine cache/tmp deletion',
191
- 'content-match': 'editing files that mention dangerous patterns (docs/tests)',
192
- secret: 'potential credential in content',
193
- destructive: 'destructive command (rm -rf, force push)',
194
- injection: 'code/command injection pattern',
195
- other: 'uncategorized',
196
- };
197
- function classifyBlock(entry) {
198
- const raw = entry.toolInput.toLowerCase();
199
- // Codehere's own test canaries (seeing these means someone is running our tests)
200
- if (raw.includes('rm -rf /home/user') || raw.includes('evil.com')) {
201
- return 'self-test';
202
- }
203
- // rm -rf against caches/tmp/build artifacts — real cleanup, not a threat
204
- if (raw.includes('rm -rf') &&
205
- (raw.includes('/.cache/') ||
206
- raw.includes('library/caches/') ||
207
- raw.includes('/tmp/') ||
208
- raw.includes('node_modules') ||
209
- raw.includes('/dist/') ||
210
- raw.includes('/.next/') ||
211
- raw.includes('/build/'))) {
212
- return 'cache-cleanup';
213
- }
214
- // Edit/Write into docs/source that merely mentions dangerous patterns — content match FP
215
- if (entry.toolName === 'Edit' || entry.toolName === 'MultiEdit' || entry.toolName === 'Write') {
216
- const fp = extractFilePath(entry) || '';
217
- if (/\.(md|tsx?|jsx?|html|rst|txt|ya?ml|json)$/i.test(fp)) {
218
- return 'content-match';
219
- }
220
- }
221
- // Destructive (not caught as cleanup above — so targeting user data)
222
- if (raw.includes('rm -rf') || raw.includes('--force') || raw.includes('force push')) {
223
- return 'destructive';
224
- }
225
- // Secret-in-content
226
- if (/api_key|apikey|auth_token|bearer\s|password\s*[:=]|secret\s*[:=]|sk-[a-z0-9]{20,}/i.test(raw)) {
227
- return 'secret';
228
- }
229
- // Code/command injection
230
- if (raw.includes('eval(') || raw.includes('child_process.exec') || raw.includes('innerhtml')) {
231
- return 'injection';
232
- }
233
- return 'other';
234
- }
235
- function formatDateRange(fromMs, toMs) {
236
- const f = new Date(fromMs);
237
- const t = new Date(toMs);
238
- const opt = { month: 'short', day: 'numeric' };
239
- return `${f.toLocaleDateString('en-US', opt)} – ${t.toLocaleDateString('en-US', opt)}`;
240
- }
241
- function renderWeeklyRecap(entries, jsonOut) {
242
- const weekMs = 7 * 24 * 3600 * 1000;
243
- const now = Date.now();
244
- const windowStart = now - weekMs;
245
- const week = entries.filter((e) => new Date(e.timestamp).getTime() >= windowStart);
246
- // Aggregates
247
- const sessions = new Set(week.map((e) => e.sessionId).filter(Boolean));
248
- const writes = week.filter((e) => e.toolName === 'Write');
249
- const edits = week.filter((e) => e.toolName === 'Edit' || e.toolName === 'MultiEdit');
250
- const bash = week.filter((e) => e.toolName === 'Bash');
251
- const blocks = week.filter((e) => e.blocked);
252
- const fileStats = {};
253
- for (const e of [...writes, ...edits]) {
254
- const fp = extractFilePath(e);
255
- if (!fp)
256
- continue;
257
- if (!fileStats[fp])
258
- fileStats[fp] = { path: fp, edits: 0, writes: 0 };
259
- if (e.toolName === 'Write')
260
- fileStats[fp].writes++;
261
- else
262
- fileStats[fp].edits++;
263
- }
264
- const topFiles = Object.values(fileStats)
265
- .sort((a, b) => b.edits + b.writes - (a.edits + a.writes))
266
- .slice(0, 5);
267
- const uniqueFiles = Object.keys(fileStats).length;
268
- // Classify blocks — most of the 18-blocks headline is noise, users deserve to see which kind
269
- const blockCategories = {
270
- 'self-test': 0,
271
- 'cache-cleanup': 0,
272
- 'content-match': 0,
273
- secret: 0,
274
- destructive: 0,
275
- injection: 0,
276
- other: 0,
277
- };
278
- for (const b of blocks) {
279
- blockCategories[classifyBlock(b)]++;
280
- }
281
- const blockBreakdown = Object.entries(blockCategories)
282
- .filter(([, n]) => n > 0)
283
- .sort((a, b) => b[1] - a[1]);
284
- const shortPath = (p) => {
285
- const parts = p.split('/').filter(Boolean);
286
- return parts.slice(-2).join('/');
287
- };
288
- if (jsonOut) {
289
- console.log(JSON.stringify({
290
- range: { from: new Date(windowStart).toISOString(), to: new Date(now).toISOString() },
291
- sessions: sessions.size,
292
- toolCalls: week.length,
293
- writes: writes.length,
294
- edits: edits.length,
295
- bashCommands: bash.length,
296
- uniqueFiles,
297
- blocks: blocks.length,
298
- topFiles: topFiles.map((f) => ({ file: shortPath(f.path), edits: f.edits, writes: f.writes })),
299
- blockBreakdown: blockBreakdown.map(([category, count]) => ({
300
- category,
301
- count,
302
- label: BLOCK_CATEGORY_LABEL[category],
303
- })),
304
- }, null, 2));
305
- return;
306
- }
307
- const pad = (s, n) => (s.length >= n ? s : s + ' '.repeat(n - s.length));
308
- const num = (n) => colors.bold(String(n).padStart(6));
309
- sectionHeader(`codehere weekly · ${formatDateRange(windowStart, now)}`);
310
- newline();
311
- if (week.length === 0) {
312
- console.log(colors.dim(' No activity in the last 7 days.'));
313
- console.log(colors.dim(' Run Claude Code with the codehere hook installed to start tracking.'));
314
- newline();
315
- return;
316
- }
317
- // Headline numbers
318
- console.log(` ${colors.bold(String(sessions.size))} session${sessions.size === 1 ? '' : 's'} · ${colors.bold(week.length.toLocaleString())} tool call${week.length === 1 ? '' : 's'} scanned · ${blocks.length > 0 ? colors.yellow(String(blocks.length)) : colors.dim('0')} block${blocks.length === 1 ? '' : 's'}`);
319
- newline();
320
- // Activity breakdown
321
- console.log(colors.dim(' ACTIVITY'));
322
- console.log(` ${num(writes.length)} new files written`);
323
- console.log(` ${num(edits.length)} edits to existing files`);
324
- console.log(` ${num(bash.length)} shell commands`);
325
- console.log(` ${num(uniqueFiles)} unique files touched`);
326
- newline();
327
- // Top files
328
- if (topFiles.length > 0) {
329
- console.log(colors.dim(' TOP FILES'));
330
- for (const f of topFiles) {
331
- const count = f.edits + f.writes;
332
- const kind = f.writes > 0 && f.edits === 0 ? 'write' : f.edits > 0 && f.writes === 0 ? 'edit' : 'touch';
333
- const plural = count === 1 ? '' : kind === 'touch' ? 'es' : 's';
334
- const countStr = String(count).padStart(3);
335
- const kindStr = `${kind}${plural}`.padEnd(7);
336
- console.log(` ${colors.dim(countStr)} ${colors.dim(kindStr)} ${shortPath(f.path)}`);
337
- }
338
- newline();
339
- }
340
- // Blocks — categorized so users see signal vs noise at a glance
341
- if (blocks.length > 0) {
342
- console.log(colors.dim(` BLOCKS ${blocks.length} total`));
343
- for (const [category, count] of blockBreakdown) {
344
- const categoryColor = category === 'other' || category === 'destructive' || category === 'secret' || category === 'injection'
345
- ? colors.yellow
346
- : colors.dim;
347
- const countStr = String(count).padStart(3);
348
- console.log(` ${colors.dim(countStr)} ${categoryColor(pad(category, 15))} ${colors.dim(BLOCK_CATEGORY_LABEL[category])}`);
349
- }
350
- newline();
351
- console.log(colors.dim(' Drill in: codehere status --sessions'));
352
- newline();
353
- }
354
- else {
355
- console.log(` ${colors.green('✓')} Nothing blocked this week.`);
356
- newline();
357
- }
358
- console.log(colors.dim(' ─────'));
359
- console.log(colors.dim(' codehere status --weekly --json Export for sharing'));
360
- newline();
361
- }
362
- // ── audit export ─────────────────────────────────────────────────────────────
363
- function parseDurationToHours(s) {
364
- const m = /^(\d+)([hdw])$/.exec(s.trim());
365
- if (!m)
366
- return null;
367
- const n = parseInt(m[1], 10);
368
- switch (m[2]) {
369
- case 'h':
370
- return n;
371
- case 'd':
372
- return n * 24;
373
- case 'w':
374
- return n * 24 * 7;
375
- }
376
- return null;
377
- }
378
- function exportAudit(entries, format, sinceHours) {
379
- const filtered = sinceHours != null ? entries.filter((e) => withinHours(e.timestamp, sinceHours)) : entries;
380
- if (format === 'jsonl') {
381
- for (const e of filtered)
382
- console.log(JSON.stringify(e));
383
- return;
384
- }
385
- if (format === 'csv') {
386
- console.log('timestamp,sessionId,toolName,riskScore,blocked,findings,criticalFindings');
387
- for (const e of filtered) {
388
- const row = [e.timestamp, e.sessionId, e.toolName, e.riskScore, e.blocked, e.findings, e.criticalFindings].join(',');
389
- console.log(row);
390
- }
391
- return;
392
- }
393
- if (format === 'json') {
394
- console.log(JSON.stringify(filtered, null, 2));
395
- return;
396
- }
397
- if (format === 'md') {
398
- console.log(`# codehere audit log · ${filtered.length} events`);
399
- console.log('');
400
- if (sinceHours != null)
401
- console.log(`*Window: last ${sinceHours} hour${sinceHours === 1 ? '' : 's'}*\n`);
402
- console.log('| Timestamp | Session | Tool | Risk | Blocked | Findings |');
403
- console.log('|---|---|---|---:|:---:|---:|');
404
- for (const e of filtered) {
405
- const sid = (e.sessionId || '').substring(0, 8);
406
- const blocked = e.blocked ? '✗' : '✓';
407
- console.log(`| \`${e.timestamp}\` | \`${sid}\` | ${e.toolName} | ${e.riskScore} | ${blocked} | ${e.findings} |`);
408
- }
409
- return;
410
- }
411
- throw new Error(`Unsupported export format: ${format}. Use jsonl, json, csv, or md.`);
412
- }
413
- export async function executeStatusCommand(options = {}) {
414
- const cwd = process.cwd();
415
- const version = getVersion();
416
- const entries = readAuditLog();
417
- // Validate --since up front — applies to ALL modes, not just --export.
418
- // (rc.15 fix: previously the validation ran only inside the --export branch,
419
- // so `codehere status --since bogus` silently succeeded with exit 0.)
420
- if (options.since && parseDurationToHours(options.since) == null) {
421
- console.error(`Invalid --since value: "${options.since}". Use e.g. 24h, 7d, 4w.`);
422
- process.exit(1);
423
- }
424
- // Session drill-down mode
425
- if (options.session) {
426
- renderSessionDrillDown(options.session, entries);
427
- return;
428
- }
429
- // Audit export mode — dump raw events for archival/compliance/sharing
430
- if (options.export) {
431
- const sinceHours = options.since ? parseDurationToHours(options.since) : null;
432
- try {
433
- exportAudit(entries, options.export, sinceHours);
434
- }
435
- catch (e) {
436
- console.error(e.message);
437
- process.exit(1);
438
- }
439
- return;
440
- }
441
- // Weekly recap mode — shareable agent-activity summary
442
- if (options.weekly) {
443
- renderWeeklyRecap(entries, options.json);
444
- return;
445
- }
446
- const last24h = entries.filter((e) => withinHours(e.timestamp, 24));
447
- const last7d = entries.filter((e) => withinHours(e.timestamp, 24 * 7));
448
- const stats = (subset) => ({
449
- total: subset.length,
450
- blocked: subset.filter((e) => e.blocked).length,
451
- critical: subset.filter((e) => e.criticalFindings > 0).length,
452
- });
453
- const s24 = stats(last24h);
454
- const s7d = stats(last7d);
455
- const sAll = stats(entries);
456
- const productCtx = loadProductContext(cwd);
457
- const hookRunning = existsSync(join(homedir(), '.claude', 'settings.json'));
458
- const hasSkills = existsSync(join(homedir(), '.claude', 'commands', 'codehere'));
459
- // ── JSON mode ──────────────────────────────────────────────────────────────
460
- if (options.json) {
461
- const result = {
462
- version,
463
- status: 'ok',
464
- hooks: {
465
- running: hookRunning,
466
- stats: { last24h: s24, last7d: s7d, allTime: sAll },
467
- },
468
- skills: { installed: hasSkills },
469
- product: productCtx,
470
- };
471
- if (options.tasks) {
472
- try {
473
- const { getTaskRepository } = await import('../../../infrastructure/storage/task-repository.js');
474
- const taskRepo = getTaskRepository();
475
- const tasks = await taskRepo.getAll();
476
- result.tasks = tasks.slice(0, 20).map((t) => ({
477
- id: t.id,
478
- mode: t.mode,
479
- status: t.status,
480
- riskTier: t.riskTier,
481
- environment: t.environment,
482
- goal: t.goal,
483
- createdAt: t.createdAt,
484
- }));
485
- result.taskCount = tasks.length;
486
- }
487
- catch (e) {
488
- // rc.25: JSON consumers reading only stdout were still being fed a
489
- // lie ([] tasks, 0 count) on real failure (Codex final review:
490
- // "degraded behavior being presented as success"). Now we surface a
491
- // structured taskLoadError + status:'degraded' so automation can
492
- // distinguish "no tasks" from "load failed."
493
- console.error('[status --tasks --json] task repository failed to load:', e);
494
- result.tasks = [];
495
- result.taskCount = 0;
496
- result.taskLoadError = e instanceof Error ? e.message : String(e);
497
- result.status = 'degraded';
498
- }
499
- }
500
- console.log(JSON.stringify(result, null, 2));
501
- return;
502
- }
503
- // ── Human dashboard ────────────────────────────────────────────────────────
504
- sectionHeader('Codehere Status');
505
- newline();
506
- // Version + hooks
507
- const hookStatus = hookRunning ? colors.green('✓') + ' Hooks running' : colors.yellow('⚠') + ' Hooks not installed';
508
- console.log(` Version: ${version} · Status: Running · ${hookStatus}`);
509
- newline();
510
- // Product brain
511
- if (productCtx) {
512
- const riskColor = productCtx.riskLevel === 'critical'
513
- ? colors.red
514
- : productCtx.riskLevel === 'high'
515
- ? colors.yellow
516
- : productCtx.riskLevel === 'medium'
517
- ? colors.cyan
518
- : colors.green;
519
- const parts = [
520
- colors.bold(productCtx.name),
521
- `risk: ${riskColor(productCtx.riskLevel)}`,
522
- productCtx.productionApp ? colors.yellow('production') : '',
523
- ].filter(Boolean);
524
- console.log(' ' + parts.join(colors.dim(' · ')));
525
- newline();
526
- }
527
- if (entries.length === 0) {
528
- console.log(colors.dim(' No activity yet. Run Claude Code with hooks installed to start tracking.'));
529
- newline();
530
- }
531
- else {
532
- // Activity table
533
- // Pad number BEFORE applying color, so ANSI codes don't break alignment
534
- const fmtNum = (n, color) => {
535
- const padded = String(n).padStart(10);
536
- return color && n > 0 ? color(padded) : colors.dim(padded);
537
- };
538
- const labelW = 12;
539
- const label = (s) => s.padEnd(labelW);
540
- const header = ` ${label('')}${'Last 24h'.padStart(10)}${'This week'.padStart(10)}${'All time'.padStart(10)}`;
541
- console.log(colors.dim(header));
542
- console.log(colors.dim(` ${'-'.repeat(labelW + 30)}`));
543
- console.log(` ${label('Scans')}${fmtNum(s24.total)}${fmtNum(s7d.total)}${fmtNum(sAll.total)}`);
544
- console.log(` ${label('Blocked')}${fmtNum(s24.blocked, colors.red)}${fmtNum(s7d.blocked, colors.red)}${fmtNum(sAll.blocked, colors.red)}`);
545
- if (sAll.critical > 0) {
546
- console.log(` ${label('Critical')}${fmtNum(s24.critical, colors.red)}${fmtNum(s7d.critical, colors.red)}${fmtNum(sAll.critical, colors.red)}`);
547
- }
548
- newline();
549
- // Recent blocks
550
- const recentBlocked = [...entries]
551
- .reverse()
552
- .filter((e) => e.blocked)
553
- .slice(0, 5);
554
- if (recentBlocked.length > 0) {
555
- console.log(colors.dim(' RECENT BLOCKS'));
556
- for (const entry of recentBlocked) {
557
- const tool = colors.cyan(entry.toolName.padEnd(10));
558
- const time = colors.dim(timeAgo(entry.timestamp).padEnd(8));
559
- const reason = extractBlockReason(entry);
560
- console.log(` ${colors.red('✗')} ${time} ${tool} ${colors.dim(reason)}`);
561
- }
562
- newline();
563
- }
564
- // If nothing was ever blocked, celebrate
565
- if (sAll.blocked === 0) {
566
- console.log(` ${colors.green('✓')} Nothing blocked yet — all scans clean.`);
567
- newline();
568
- }
569
- }
570
- // Skills status
571
- console.log(colors.dim(' ─────'));
572
- if (hasSkills) {
573
- console.log(colors.dim(' /codehere:trust /codehere:report /codehere:audit /codehere:hook-status'));
574
- }
575
- else {
576
- console.log(colors.dim(' Run codehere hook install to add slash commands'));
577
- }
578
- newline();
579
- // Multi-agent session analysis
580
- if (options.sessions && entries.length > 0) {
581
- const allSessions = analyzeSessions(entries);
582
- const sessions = allSessions.filter((s) => s.sessionId !== 'unknown');
583
- const alerts = detectAmplificationAlerts(sessions);
584
- console.log(colors.bold('Sessions (last 10):'));
585
- for (const s of sessions.slice(0, 10)) {
586
- const sid = s.sessionId.substring(0, 8);
587
- const risk = s.amplificationRisk === 'high'
588
- ? colors.red('high')
589
- : s.amplificationRisk === 'medium'
590
- ? colors.yellow('med')
591
- : colors.green('low');
592
- const blocked = s.blockedCalls > 0 ? colors.red(` ${s.blockedCalls} blocked`) : '';
593
- const dur = formatDuration(s.durationMs);
594
- console.log(` ${colors.dim(sid)} ${String(s.totalCalls).padStart(4)} calls ${dur.padEnd(6)} risk: ${risk}${blocked}`);
595
- }
596
- newline();
597
- if (alerts.length > 0) {
598
- console.log(colors.bold('Amplification alerts:'));
599
- for (const alert of alerts) {
600
- const icon = alert.risk === 'high' ? colors.red('!') : colors.yellow('!');
601
- console.log(` ${icon} ${colors.dim(alert.sessionId.substring(0, 8))} ${alert.message}`);
602
- }
603
- newline();
604
- }
605
- else {
606
- console.log(colors.dim(' No amplification patterns detected.'));
607
- newline();
608
- }
609
- }
610
- // Removed: Cost router jargon — not meaningful to users
611
- // Tasks table (--tasks flag)
612
- if (options.tasks) {
613
- newline();
614
- console.log(colors.bold('Recent tasks (experimental)'));
615
- console.log(colors.dim('Task storage: .codehere/tasks.jsonl'));
616
- newline();
617
- try {
618
- const { getTaskRepository } = await import('../../../infrastructure/storage/task-repository.js');
619
- const taskRepo = getTaskRepository();
620
- const tasks = await taskRepo.getAll();
621
- if (tasks.length === 0) {
622
- console.log(colors.dim('No tasks found. Run "codehere start" to open the task intake, or "codehere task <prompt>" from the CLI.'));
623
- }
624
- else {
625
- const pad = (s, n) => (s.length >= n ? s.substring(0, n) : s + ' '.repeat(n - s.length));
626
- const trunc = (s, n) => (s.length <= n ? s : s.substring(0, n - 1) + '…');
627
- console.log(colors.dim([pad('ID', 10), pad('Mode', 8), pad('Status', 12), pad('Risk', 4), pad('Env', 6), 'Goal'].join(' ')));
628
- console.log(colors.dim(['-'.repeat(10), '-'.repeat(8), '-'.repeat(12), '-'.repeat(4), '-'.repeat(6), '-'.repeat(30)].join(' ')));
629
- tasks.slice(0, 10).forEach((task) => {
630
- console.log([
631
- pad(task.id.substring(0, 8) + '..', 10),
632
- pad(task.mode, 8),
633
- pad(task.status, 12),
634
- pad(task.riskTier.toUpperCase(), 4),
635
- pad(task.environment.substring(0, 6), 6),
636
- trunc(task.goal, 30),
637
- ].join(' '));
638
- });
639
- if (tasks.length > 10) {
640
- newline();
641
- console.log(colors.dim(`... and ${tasks.length - 10} more tasks`));
642
- }
643
- }
644
- }
645
- catch (e) {
646
- // rc.25: don't print "No tasks found" — that's a lie when load failed
647
- // (Codex final review: "degraded behavior presented as success"). Show
648
- // a clear error line; stderr already has the cause for the operator.
649
- console.error('[status] task repository failed to load:', e);
650
- const msg = e instanceof Error ? e.message : String(e);
651
- console.log(colors.red(` ✗ Could not load task repository: ${msg}`));
652
- console.log(colors.dim(' See logs above for details. Try `codehere start` to (re)init the task intake.'));
653
- }
654
- }
655
- // Quick trust scan (opt-in)
656
- if (options.trust) {
657
- const { executeTrustCommand } = await import('./trust-command.js');
658
- await executeTrustCommand(undefined, {});
659
- }
660
- }
661
- //# sourceMappingURL=status-command.js.map