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