@bahulam/code 2.6.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 +80 -0
- package/package.json +49 -0
- package/pulse/app/activity/page.tsx +190 -0
- package/pulse/app/api/activity/route.ts +138 -0
- package/pulse/app/api/benchmark/route.ts +113 -0
- package/pulse/app/api/benchmarks/route.ts +195 -0
- package/pulse/app/api/costs/route.ts +88 -0
- package/pulse/app/api/export/route.ts +77 -0
- package/pulse/app/api/history/route.ts +11 -0
- package/pulse/app/api/import/route.ts +31 -0
- package/pulse/app/api/memory/route.ts +50 -0
- package/pulse/app/api/plans/route.ts +9 -0
- package/pulse/app/api/projects/[slug]/route.ts +96 -0
- package/pulse/app/api/projects/route.ts +121 -0
- package/pulse/app/api/sessions/[id]/replay/route.ts +20 -0
- package/pulse/app/api/sessions/[id]/route.ts +31 -0
- package/pulse/app/api/sessions/route.ts +112 -0
- package/pulse/app/api/settings/route.ts +14 -0
- package/pulse/app/api/stats/route.ts +143 -0
- package/pulse/app/api/todos/route.ts +9 -0
- package/pulse/app/api/tools/route.ts +160 -0
- package/pulse/app/benchmarks/page.tsx +224 -0
- package/pulse/app/costs/page.tsx +179 -0
- package/pulse/app/export/page.tsx +465 -0
- package/pulse/app/favicon.ico +0 -0
- package/pulse/app/globals.css +263 -0
- package/pulse/app/help/page.tsx +143 -0
- package/pulse/app/history/page.tsx +157 -0
- package/pulse/app/layout.tsx +46 -0
- package/pulse/app/memory/page.tsx +365 -0
- package/pulse/app/overview-client.tsx +393 -0
- package/pulse/app/page.tsx +14 -0
- package/pulse/app/plans/page.tsx +308 -0
- package/pulse/app/projects/[slug]/page.tsx +390 -0
- package/pulse/app/projects/page.tsx +110 -0
- package/pulse/app/sessions/[id]/page.tsx +243 -0
- package/pulse/app/sessions/page.tsx +39 -0
- package/pulse/app/settings/page.tsx +188 -0
- package/pulse/app/todos/page.tsx +211 -0
- package/pulse/app/tools/page.tsx +249 -0
- package/pulse/cli.js +164 -0
- package/pulse/components/activity/day-of-week-chart.tsx +35 -0
- package/pulse/components/activity/streak-card.tsx +36 -0
- package/pulse/components/costs/cache-efficiency-panel.tsx +76 -0
- package/pulse/components/costs/cost-by-project-chart.tsx +48 -0
- package/pulse/components/costs/cost-over-time-chart.tsx +95 -0
- package/pulse/components/costs/model-token-table.tsx +60 -0
- package/pulse/components/global-search.tsx +193 -0
- package/pulse/components/keyboard-nav-provider.tsx +23 -0
- package/pulse/components/layout/bottom-nav.tsx +53 -0
- package/pulse/components/layout/client-layout.tsx +31 -0
- package/pulse/components/layout/sidebar-context.tsx +50 -0
- package/pulse/components/layout/sidebar.tsx +183 -0
- package/pulse/components/layout/top-bar.tsx +121 -0
- package/pulse/components/overview/activity-heatmap.tsx +107 -0
- package/pulse/components/overview/conversation-table.tsx +148 -0
- package/pulse/components/overview/model-breakdown-donut.tsx +95 -0
- package/pulse/components/overview/peak-hours-chart.tsx +87 -0
- package/pulse/components/overview/project-activity-donut.tsx +96 -0
- package/pulse/components/overview/stat-card.tsx +102 -0
- package/pulse/components/overview/usage-over-time-chart.tsx +166 -0
- package/pulse/components/projects/project-card.tsx +175 -0
- package/pulse/components/sessions/replay/assistant-markdown.tsx +94 -0
- package/pulse/components/sessions/replay/compaction-card.tsx +25 -0
- package/pulse/components/sessions/replay/session-sidebar.tsx +231 -0
- package/pulse/components/sessions/replay/token-accumulation-chart.tsx +98 -0
- package/pulse/components/sessions/replay/tool-call-badge.tsx +127 -0
- package/pulse/components/sessions/replay/turn-cards.tsx +220 -0
- package/pulse/components/sessions/replay/user-tool-result.tsx +158 -0
- package/pulse/components/sessions/session-badges.tsx +49 -0
- package/pulse/components/sessions/session-table.tsx +299 -0
- package/pulse/components/theme-provider.tsx +44 -0
- package/pulse/components/tools/feature-adoption-table.tsx +58 -0
- package/pulse/components/tools/mcp-server-panel.tsx +45 -0
- package/pulse/components/tools/tool-ranking-chart.tsx +57 -0
- package/pulse/components/tools/version-history-table.tsx +32 -0
- package/pulse/components/ui/alert.tsx +66 -0
- package/pulse/components/ui/badge.tsx +48 -0
- package/pulse/components/ui/breadcrumb.tsx +109 -0
- package/pulse/components/ui/button.tsx +64 -0
- package/pulse/components/ui/calendar.tsx +220 -0
- package/pulse/components/ui/card.tsx +92 -0
- package/pulse/components/ui/command.tsx +158 -0
- package/pulse/components/ui/dialog.tsx +158 -0
- package/pulse/components/ui/input.tsx +21 -0
- package/pulse/components/ui/popover.tsx +89 -0
- package/pulse/components/ui/progress.tsx +31 -0
- package/pulse/components/ui/select.tsx +190 -0
- package/pulse/components/ui/separator.tsx +28 -0
- package/pulse/components/ui/sheet.tsx +143 -0
- package/pulse/components/ui/skeleton.tsx +13 -0
- package/pulse/components/ui/table.tsx +116 -0
- package/pulse/components/ui/tabs.tsx +91 -0
- package/pulse/components/ui/tooltip.tsx +57 -0
- package/pulse/components/use-global-keyboard-nav.ts +79 -0
- package/pulse/components.json +23 -0
- package/pulse/eslint.config.mjs +18 -0
- package/pulse/lib/bahulam-paths.ts +23 -0
- package/pulse/lib/claude-reader.ts +592 -0
- package/pulse/lib/decode.ts +129 -0
- package/pulse/lib/pricing.ts +102 -0
- package/pulse/lib/replay-parser.ts +165 -0
- package/pulse/lib/tool-categories.ts +127 -0
- package/pulse/lib/utils.ts +6 -0
- package/pulse/next-env.d.ts +6 -0
- package/pulse/next.config.ts +16 -0
- package/pulse/package.json +45 -0
- package/pulse/postcss.config.mjs +7 -0
- package/pulse/public/activity.png +0 -0
- package/pulse/public/cc-lens.png +0 -0
- package/pulse/public/command-k.png +0 -0
- package/pulse/public/costs.png +0 -0
- package/pulse/public/dashboard-dark.png +0 -0
- package/pulse/public/dashboard-white.png +0 -0
- package/pulse/public/export.png +0 -0
- package/pulse/public/file.svg +1 -0
- package/pulse/public/globe.svg +1 -0
- package/pulse/public/next.svg +1 -0
- package/pulse/public/projects.png +0 -0
- package/pulse/public/session-chat.png +0 -0
- package/pulse/public/todos.png +0 -0
- package/pulse/public/tools.png +0 -0
- package/pulse/public/vercel.svg +1 -0
- package/pulse/public/window.svg +1 -0
- package/pulse/tsconfig.json +34 -0
- package/pulse/types/claude.ts +294 -0
- package/src/agents/loader.mjs +94 -0
- package/src/agents/multi_workflow_loader.mjs +330 -0
- package/src/agents/parser.mjs +205 -0
- package/src/agents/scaffold.mjs +222 -0
- package/src/agents/teams.mjs +123 -0
- package/src/agents/workflow_loader.mjs +122 -0
- package/src/agents/workflow_scaffold.mjs +249 -0
- package/src/auth/oauth.mjs +220 -0
- package/src/auth/tarang-auth.mjs +306 -0
- package/src/commands/agent.mjs +220 -0
- package/src/commands/workflow.mjs +581 -0
- package/src/config/cli-args.mjs +200 -0
- package/src/config/env.mjs +263 -0
- package/src/config/hook-runner.mjs +100 -0
- package/src/config/memory-loader.mjs +32 -0
- package/src/config/settings-loader.mjs +45 -0
- package/src/config/settings.mjs +132 -0
- package/src/context/ast-parser.mjs +298 -0
- package/src/context/bm25.mjs +85 -0
- package/src/context/retriever.mjs +308 -0
- package/src/context/skeleton.mjs +134 -0
- package/src/context/symbol-indexer.mjs +375 -0
- package/src/core/agent-history.mjs +111 -0
- package/src/core/agent-loop.mjs +486 -0
- package/src/core/approval-log.mjs +104 -0
- package/src/core/approval.mjs +476 -0
- package/src/core/attachments.mjs +380 -0
- package/src/core/backend-url.mjs +55 -0
- package/src/core/cache-control.mjs +92 -0
- package/src/core/cache.mjs +105 -0
- package/src/core/callback-client.mjs +180 -0
- package/src/core/checkpoints.mjs +142 -0
- package/src/core/compact-history.mjs +127 -0
- package/src/core/context-envelope.mjs +54 -0
- package/src/core/context-manager.mjs +198 -0
- package/src/core/error-guidance.mjs +311 -0
- package/src/core/file-diff.mjs +217 -0
- package/src/core/headless.mjs +448 -0
- package/src/core/hooks-manager.mjs +87 -0
- package/src/core/jsonl-writer.mjs +449 -0
- package/src/core/local-agent.mjs +537 -0
- package/src/core/local-store.mjs +836 -0
- package/src/core/mode-selector.mjs +51 -0
- package/src/core/output-filter.mjs +177 -0
- package/src/core/paths.mjs +190 -0
- package/src/core/policy-resolver.mjs +156 -0
- package/src/core/pricing.mjs +336 -0
- package/src/core/project-artifacts.mjs +39 -0
- package/src/core/project-context-loader.mjs +139 -0
- package/src/core/providers.mjs +219 -0
- package/src/core/rate-limit-display.mjs +121 -0
- package/src/core/rate-limiter.mjs +119 -0
- package/src/core/resume-mode.mjs +192 -0
- package/src/core/risk-tier.mjs +337 -0
- package/src/core/safety.mjs +203 -0
- package/src/core/scheduler.mjs +173 -0
- package/src/core/session-manager.mjs +360 -0
- package/src/core/session.mjs +143 -0
- package/src/core/settings-sync.mjs +85 -0
- package/src/core/stagnation.mjs +57 -0
- package/src/core/stream-client.mjs +829 -0
- package/src/core/streaming.mjs +182 -0
- package/src/core/system-prompt.mjs +140 -0
- package/src/core/tasks.mjs +196 -0
- package/src/core/tool-executor.mjs +1950 -0
- package/src/core/trust.mjs +158 -0
- package/src/core/work-scope.mjs +248 -0
- package/src/hooks/engine.mjs +162 -0
- package/src/index.mjs +426 -0
- package/src/mcp/client.mjs +253 -0
- package/src/mcp/transport-shttp.mjs +130 -0
- package/src/mcp/transport-sse.mjs +131 -0
- package/src/mcp/transport-ws.mjs +134 -0
- package/src/onboarding/preflight.mjs +360 -0
- package/src/permissions/checker.mjs +57 -0
- package/src/permissions/command-classifier.mjs +652 -0
- package/src/permissions/injection-check.mjs +60 -0
- package/src/permissions/path-check.mjs +102 -0
- package/src/permissions/prompt.mjs +73 -0
- package/src/permissions/sandbox.mjs +112 -0
- package/src/plugins/loader.mjs +138 -0
- package/src/skills/installer.mjs +188 -0
- package/src/skills/loader.mjs +252 -0
- package/src/skills/runner.mjs +55 -0
- package/src/state/orbit.mjs +263 -0
- package/src/state/verbosity.mjs +99 -0
- package/src/telemetry/index.mjs +96 -0
- package/src/terminal/agents.mjs +177 -0
- package/src/terminal/analytics.mjs +292 -0
- package/src/terminal/ansi.mjs +695 -0
- package/src/terminal/init.mjs +145 -0
- package/src/terminal/main.mjs +269 -0
- package/src/terminal/repl-explore.mjs +35 -0
- package/src/terminal/repl-format.mjs +257 -0
- package/src/terminal/repl-render.mjs +561 -0
- package/src/terminal/repl-resume.mjs +625 -0
- package/src/terminal/repl-state.mjs +103 -0
- package/src/terminal/repl-utils.mjs +34 -0
- package/src/terminal/repl.mjs +3832 -0
- package/src/terminal/skills.mjs +54 -0
- package/src/terminal/tool-display.mjs +240 -0
- package/src/tools/agent.mjs +137 -0
- package/src/tools/ask-user.mjs +61 -0
- package/src/tools/bash.mjs +231 -0
- package/src/tools/cron-create.mjs +120 -0
- package/src/tools/cron-delete.mjs +49 -0
- package/src/tools/cron-list.mjs +37 -0
- package/src/tools/edit.mjs +82 -0
- package/src/tools/enter-worktree.mjs +69 -0
- package/src/tools/exit-worktree.mjs +57 -0
- package/src/tools/glob.mjs +117 -0
- package/src/tools/grep.mjs +129 -0
- package/src/tools/lint.mjs +71 -0
- package/src/tools/ls.mjs +58 -0
- package/src/tools/lsp.mjs +115 -0
- package/src/tools/multi-edit.mjs +94 -0
- package/src/tools/notebook-edit.mjs +96 -0
- package/src/tools/project-overview.mjs +641 -0
- package/src/tools/read-mcp-resource.mjs +57 -0
- package/src/tools/read.mjs +138 -0
- package/src/tools/registry.mjs +116 -0
- package/src/tools/remote-trigger.mjs +84 -0
- package/src/tools/send-message.mjs +64 -0
- package/src/tools/skill.mjs +52 -0
- package/src/tools/test-runner.mjs +49 -0
- package/src/tools/todo-write.mjs +68 -0
- package/src/tools/tool-search.mjs +77 -0
- package/src/tools/web-fetch.mjs +65 -0
- package/src/tools/web-search.mjs +89 -0
- package/src/tools/write.mjs +55 -0
- package/src/ui/approval.mjs +263 -0
- package/src/ui/banner.mjs +235 -0
- package/src/ui/commands.mjs +537 -0
- package/src/ui/formatter.mjs +409 -0
- package/src/ui/icons.mjs +164 -0
- package/src/ui/input-dock.mjs +444 -0
- package/src/ui/markdown.mjs +278 -0
- package/src/ui/mission-report.mjs +296 -0
- package/src/ui/palette.mjs +189 -0
- package/src/ui/slash-commands.mjs +245 -0
- package/src/ui/spinner.mjs +116 -0
- package/src/ui/sub-agent.mjs +152 -0
- package/src/ui/term.mjs +159 -0
- package/src/ui/text-layout.mjs +127 -0
- package/src/ui/tool-card.mjs +463 -0
- package/src/ui/tool-details.mjs +312 -0
- package/src/ui/transcript-block.mjs +21 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal Markdown Renderer — parse markdown to ANSI-styled text.
|
|
3
|
+
*
|
|
4
|
+
* Supports:
|
|
5
|
+
* - **bold** -> bold text
|
|
6
|
+
* - *italic* -> italic text
|
|
7
|
+
* - `code` -> inverse/cyan
|
|
8
|
+
* - ```code block``` -> bordered box
|
|
9
|
+
* - - list item -> bullet
|
|
10
|
+
* - # heading -> bold + underline
|
|
11
|
+
* - [link](url) -> blue underline
|
|
12
|
+
* - | table | -> formatted table
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const ANSI = {
|
|
16
|
+
reset: '\x1b[0m',
|
|
17
|
+
bold: '\x1b[1m',
|
|
18
|
+
dim: '\x1b[2m',
|
|
19
|
+
italic: '\x1b[3m',
|
|
20
|
+
underline: '\x1b[4m',
|
|
21
|
+
inverse: '\x1b[7m',
|
|
22
|
+
red: '\x1b[31m',
|
|
23
|
+
green: '\x1b[32m',
|
|
24
|
+
yellow: '\x1b[33m',
|
|
25
|
+
blue: '\x1b[34m',
|
|
26
|
+
magenta: '\x1b[35m',
|
|
27
|
+
cyan: '\x1b[36m',
|
|
28
|
+
white: '\x1b[37m',
|
|
29
|
+
gray: '\x1b[90m',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const noColor = process.env.NO_COLOR === '1';
|
|
33
|
+
|
|
34
|
+
function a(codes, text) {
|
|
35
|
+
if (noColor) return text;
|
|
36
|
+
const prefix = Array.isArray(codes) ? codes.join('') : codes;
|
|
37
|
+
return `${prefix}${text}${ANSI.reset}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Render inline markdown formatting within a single line.
|
|
42
|
+
* @param {string} line
|
|
43
|
+
* @returns {string}
|
|
44
|
+
*/
|
|
45
|
+
export function renderInline(line) {
|
|
46
|
+
if (noColor) return line;
|
|
47
|
+
let result = line;
|
|
48
|
+
|
|
49
|
+
// Bold: **text**
|
|
50
|
+
result = result.replace(/\*\*(.+?)\*\*/g, (_, t) => a(ANSI.bold, t));
|
|
51
|
+
|
|
52
|
+
// Italic: *text* (not preceded/followed by *)
|
|
53
|
+
result = result.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, (_, t) => a(ANSI.italic, t));
|
|
54
|
+
|
|
55
|
+
// Inline code: `text`
|
|
56
|
+
result = result.replace(/`([^`]+)`/g, (_, t) => a(ANSI.cyan, t));
|
|
57
|
+
|
|
58
|
+
// Links: [text](url)
|
|
59
|
+
result = result.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, text, url) => {
|
|
60
|
+
return `${a([ANSI.blue, ANSI.underline], text)} ${a(ANSI.dim, `(${url})`)}`;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Highlight syntax within a code line (basic keyword/string/number/comment coloring).
|
|
68
|
+
* @param {string} line
|
|
69
|
+
* @param {string} lang
|
|
70
|
+
* @returns {string}
|
|
71
|
+
*/
|
|
72
|
+
export function highlightSyntax(line, lang) {
|
|
73
|
+
if (noColor) return line;
|
|
74
|
+
let result = line;
|
|
75
|
+
|
|
76
|
+
// Strings
|
|
77
|
+
result = result.replace(/(["'`])(.*?)\1/g, (m, q, s) => a(ANSI.green, `${q}${s}${q}`));
|
|
78
|
+
|
|
79
|
+
// Keywords
|
|
80
|
+
const kw = /\b(const|let|var|function|return|if|else|for|while|class|import|export|from|async|await|try|catch|throw|new|this|def|fn|pub|use|mod|struct|enum|impl|match|type|interface)\b/g;
|
|
81
|
+
result = result.replace(kw, (m) => a(ANSI.magenta, m));
|
|
82
|
+
|
|
83
|
+
// Numbers
|
|
84
|
+
result = result.replace(/\b(\d+\.?\d*)\b/g, (m) => a(ANSI.yellow, m));
|
|
85
|
+
|
|
86
|
+
// Comments
|
|
87
|
+
result = result.replace(/(\/\/.*|#.*)$/, (m) => a(ANSI.gray, m));
|
|
88
|
+
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Render a full markdown string to ANSI terminal output.
|
|
94
|
+
* Handles block-level elements (headings, code blocks, lists, tables)
|
|
95
|
+
* and inline formatting.
|
|
96
|
+
*
|
|
97
|
+
* @param {string} text - raw markdown
|
|
98
|
+
* @returns {string} - ANSI-formatted string
|
|
99
|
+
*/
|
|
100
|
+
export function renderMarkdown(text) {
|
|
101
|
+
if (!text) return '';
|
|
102
|
+
const lines = text.split('\n');
|
|
103
|
+
const output = [];
|
|
104
|
+
let inCodeBlock = false;
|
|
105
|
+
let codeLang = '';
|
|
106
|
+
let codeLines = [];
|
|
107
|
+
let inTable = false;
|
|
108
|
+
let tableRows = [];
|
|
109
|
+
|
|
110
|
+
for (let i = 0; i < lines.length; i++) {
|
|
111
|
+
const line = lines[i];
|
|
112
|
+
|
|
113
|
+
// Code block start/end
|
|
114
|
+
if (line.trimStart().startsWith('```')) {
|
|
115
|
+
if (inCodeBlock) {
|
|
116
|
+
// End code block - render it
|
|
117
|
+
output.push(formatCodeBlock(codeLines, codeLang));
|
|
118
|
+
inCodeBlock = false;
|
|
119
|
+
codeLines = [];
|
|
120
|
+
codeLang = '';
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
// Start code block
|
|
124
|
+
inCodeBlock = true;
|
|
125
|
+
codeLang = line.trimStart().slice(3).trim();
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (inCodeBlock) {
|
|
130
|
+
codeLines.push(line);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Table detection
|
|
135
|
+
if (line.trim().startsWith('|') && line.trim().endsWith('|')) {
|
|
136
|
+
// Check if separator row
|
|
137
|
+
if (/^\|[\s\-:|]+\|$/.test(line.trim())) {
|
|
138
|
+
// Table separator, skip
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
tableRows.push(line);
|
|
142
|
+
// Check if next line is NOT a table row
|
|
143
|
+
const nextLine = lines[i + 1];
|
|
144
|
+
if (!nextLine || (!nextLine.trim().startsWith('|') || !nextLine.trim().endsWith('|'))) {
|
|
145
|
+
// Flush table
|
|
146
|
+
if (tableRows.length > 0) {
|
|
147
|
+
output.push(formatTable(tableRows));
|
|
148
|
+
tableRows = [];
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Flush any pending table rows
|
|
155
|
+
if (tableRows.length > 0) {
|
|
156
|
+
output.push(formatTable(tableRows));
|
|
157
|
+
tableRows = [];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Headings
|
|
161
|
+
const headingMatch = line.match(/^(#{1,6})\s+(.+)/);
|
|
162
|
+
if (headingMatch) {
|
|
163
|
+
const level = headingMatch[1].length;
|
|
164
|
+
const text = headingMatch[2];
|
|
165
|
+
if (level === 1) {
|
|
166
|
+
output.push(a([ANSI.bold, ANSI.underline], text));
|
|
167
|
+
} else if (level === 2) {
|
|
168
|
+
output.push(a(ANSI.bold, text));
|
|
169
|
+
} else {
|
|
170
|
+
output.push(a(ANSI.bold, text));
|
|
171
|
+
}
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Unordered list
|
|
176
|
+
const listMatch = line.match(/^(\s*)([-*+])\s+(.*)/);
|
|
177
|
+
if (listMatch) {
|
|
178
|
+
const indent = listMatch[1];
|
|
179
|
+
const content = renderInline(listMatch[3]);
|
|
180
|
+
output.push(`${indent} * ${content}`);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Ordered list
|
|
185
|
+
const olMatch = line.match(/^(\s*)(\d+)\.\s+(.*)/);
|
|
186
|
+
if (olMatch) {
|
|
187
|
+
const indent = olMatch[1];
|
|
188
|
+
const num = olMatch[2];
|
|
189
|
+
const content = renderInline(olMatch[3]);
|
|
190
|
+
output.push(`${indent} ${num}. ${content}`);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Horizontal rule
|
|
195
|
+
if (/^[-*_]{3,}\s*$/.test(line)) {
|
|
196
|
+
const cols = process.stdout.columns || 80;
|
|
197
|
+
output.push(a(ANSI.dim, '\u2500'.repeat(Math.min(cols, 60))));
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Blockquote
|
|
202
|
+
const bqMatch = line.match(/^>\s?(.*)/);
|
|
203
|
+
if (bqMatch) {
|
|
204
|
+
output.push(a(ANSI.dim, ` | ${renderInline(bqMatch[1])}`));
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Normal line with inline formatting
|
|
209
|
+
output.push(renderInline(line));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Flush remaining
|
|
213
|
+
if (inCodeBlock && codeLines.length > 0) {
|
|
214
|
+
output.push(formatCodeBlock(codeLines, codeLang));
|
|
215
|
+
}
|
|
216
|
+
if (tableRows.length > 0) {
|
|
217
|
+
output.push(formatTable(tableRows));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return output.join('\n');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Format a code block with a border.
|
|
225
|
+
* @param {string[]} lines
|
|
226
|
+
* @param {string} lang
|
|
227
|
+
* @returns {string}
|
|
228
|
+
*/
|
|
229
|
+
function formatCodeBlock(lines, lang) {
|
|
230
|
+
if (noColor) {
|
|
231
|
+
return lines.map(l => ` ${l}`).join('\n');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const maxLen = Math.max(...lines.map(l => l.length), 20);
|
|
235
|
+
const width = Math.min(maxLen + 4, (process.stdout.columns || 80) - 4);
|
|
236
|
+
const top = a(ANSI.gray, ` \u250C${'─'.repeat(width)}\u2510${lang ? ` ${lang}` : ''}`);
|
|
237
|
+
const bot = a(ANSI.gray, ` \u2514${'─'.repeat(width)}\u2518`);
|
|
238
|
+
const body = lines.map(l => {
|
|
239
|
+
const highlighted = highlightSyntax(l, lang);
|
|
240
|
+
return ` ${a(ANSI.gray, '\u2502')} ${highlighted}`;
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
return [top, ...body, bot].join('\n');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Format a markdown table.
|
|
248
|
+
* @param {string[]} rows
|
|
249
|
+
* @returns {string}
|
|
250
|
+
*/
|
|
251
|
+
function formatTable(rows) {
|
|
252
|
+
const parsed = rows.map(r =>
|
|
253
|
+
r.split('|').slice(1, -1).map(c => c.trim())
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
if (parsed.length === 0) return '';
|
|
257
|
+
|
|
258
|
+
// Calculate column widths
|
|
259
|
+
const colCount = parsed[0].length;
|
|
260
|
+
const widths = [];
|
|
261
|
+
for (let c = 0; c < colCount; c++) {
|
|
262
|
+
widths.push(Math.max(...parsed.map(r => (r[c] || '').length)));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const formatted = parsed.map((row, ri) => {
|
|
266
|
+
const cells = row.map((cell, ci) => cell.padEnd(widths[ci] || 0));
|
|
267
|
+
const line = ` ${cells.join(' ')}`;
|
|
268
|
+
return ri === 0 ? a(ANSI.bold, line) : line;
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// Add separator after header
|
|
272
|
+
if (formatted.length > 1) {
|
|
273
|
+
const sep = widths.map(w => '─'.repeat(w)).join('──');
|
|
274
|
+
formatted.splice(1, 0, a(ANSI.dim, ` ${sep}`));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return formatted.join('\n');
|
|
278
|
+
}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mission report — Mission Control (PRD-055 §11).
|
|
3
|
+
*
|
|
4
|
+
* Replaces the trailing "Done" message at the end of a session with a
|
|
5
|
+
* structured summary:
|
|
6
|
+
*
|
|
7
|
+
* ───────────────────────────────────────────────────
|
|
8
|
+
* ✓ done
|
|
9
|
+
* ───────────────────────────────────────────────────
|
|
10
|
+
* 📂 Files auth.py, tests/test_auth.py
|
|
11
|
+
* 🛠️ Tools read(4) edit(2) shell(1) test(1) · ⏱ Time 2m 18s
|
|
12
|
+
* 🛰️ Sub-agents explore(1) plan(1) · saved ≈ $0.08
|
|
13
|
+
* ✅ Health 24/24 tests pass
|
|
14
|
+
* ───────────────────────────────────────────────────
|
|
15
|
+
*
|
|
16
|
+
*
|
|
17
|
+
* Failure variant uses "held" and lists blockers.
|
|
18
|
+
*
|
|
19
|
+
* `renderMissionReport(state)` returns the ANSI block; `toMarkdown(state)`
|
|
20
|
+
* returns the plain-markdown version saved by `/report`.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
import fs from 'node:fs';
|
|
25
|
+
import { execFileSync } from 'node:child_process';
|
|
26
|
+
import { paint, width as visibleWidth } from './palette.mjs';
|
|
27
|
+
import { icons } from './icons.mjs';
|
|
28
|
+
import { toolFamily } from './icons.mjs';
|
|
29
|
+
|
|
30
|
+
const WIDTH = 56;
|
|
31
|
+
|
|
32
|
+
// ── Public API ─────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Render the ANSI mission-report block.
|
|
36
|
+
*
|
|
37
|
+
* @param {object} state
|
|
38
|
+
* task — string (the user's prompt for this session)
|
|
39
|
+
* success — boolean (overall outcome)
|
|
40
|
+
* filesChanged — string[]
|
|
41
|
+
* filesRead — string[]
|
|
42
|
+
* toolCounts — { [tool]: count } or array of {tool}
|
|
43
|
+
* subAgents — array of { type, costUsd?, tokens? } or { explore:1, plan:1 }
|
|
44
|
+
* costUsd — number
|
|
45
|
+
* durationS — number
|
|
46
|
+
* testsPass — { passed: number, total: number } | null
|
|
47
|
+
* blockers — string[] (for failure variant)
|
|
48
|
+
* nextActions — string[] (slash-command hints)
|
|
49
|
+
* cwd — string (used to derive git repo + author metadata)
|
|
50
|
+
*/
|
|
51
|
+
export function renderMissionReport(state) {
|
|
52
|
+
const success = state.success !== false;
|
|
53
|
+
const lines = [];
|
|
54
|
+
const rule = paint.text.dim('─'.repeat(WIDTH));
|
|
55
|
+
|
|
56
|
+
const statusIcon = success ? paint.state.success('✓') : paint.state.danger('✗');
|
|
57
|
+
const statusText = success ? paint.state.success('done') : paint.state.danger('held');
|
|
58
|
+
const headerTask = state.task ? paint.text.dim(' · ') + paint.text.primary(truncate(state.task, 60)) : '';
|
|
59
|
+
|
|
60
|
+
lines.push('');
|
|
61
|
+
lines.push(rule);
|
|
62
|
+
lines.push(`${statusIcon} ${statusText}${headerTask}`);
|
|
63
|
+
lines.push(rule);
|
|
64
|
+
|
|
65
|
+
if (Array.isArray(state.filesChanged) && state.filesChanged.length) {
|
|
66
|
+
lines.push(row('📂', 'Files', formatFiles(state.filesChanged)));
|
|
67
|
+
}
|
|
68
|
+
if (Array.isArray(state.filesRead) && state.filesRead.length) {
|
|
69
|
+
lines.push(row('📖', 'Read', formatFiles(state.filesRead)));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const toolSummary = formatToolCounts(state.toolCounts);
|
|
73
|
+
const time = state.durationS != null ? paint.brand.data(formatDuration(state.durationS)) : '';
|
|
74
|
+
const metricSegments = [];
|
|
75
|
+
if (toolSummary) metricSegments.push(`${icons.write} ${paint.text.dim('Tools')} ${toolSummary}`);
|
|
76
|
+
if (time) metricSegments.push(`${paint.text.dim('⏱ Time')} ${time}`);
|
|
77
|
+
if (metricSegments.length) lines.push(' ' + metricSegments.join(paint.text.dim(' · ')));
|
|
78
|
+
|
|
79
|
+
if (state.subAgents) {
|
|
80
|
+
const subSummary = formatSubAgents(state.subAgents);
|
|
81
|
+
if (subSummary) lines.push(row(icons.subAgent, 'Sub-agents', subSummary));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Test health.
|
|
85
|
+
if (state.testsPass && typeof state.testsPass.total === 'number' && state.testsPass.total > 0) {
|
|
86
|
+
const { passed = 0, total = 0 } = state.testsPass;
|
|
87
|
+
const allGreen = passed === total;
|
|
88
|
+
const icon = allGreen ? paint.state.success('✅') : paint.state.danger('❌');
|
|
89
|
+
const text = allGreen
|
|
90
|
+
? `${passed}/${total} tests pass`
|
|
91
|
+
: `${passed}/${total} tests pass · ${paint.state.danger((total - passed) + ' failing')}`;
|
|
92
|
+
lines.push(row(icon, allGreen ? 'Health' : 'Tests', text, /*alreadyIcon*/ true));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
lines.push(rule);
|
|
96
|
+
|
|
97
|
+
if (!success && Array.isArray(state.blockers) && state.blockers.length) {
|
|
98
|
+
lines.push('');
|
|
99
|
+
lines.push(' ' + paint.bold(paint.state.danger('Blocked by:')));
|
|
100
|
+
for (const b of state.blockers.slice(0, 6)) {
|
|
101
|
+
lines.push(' ' + paint.text.dim('•') + ' ' + paint.text.primary(truncate(b, WIDTH * 2)));
|
|
102
|
+
}
|
|
103
|
+
if (state.blockers.length > 6) {
|
|
104
|
+
lines.push(' ' + paint.text.dim(`… ${state.blockers.length - 6} more`));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (Array.isArray(state.nextActions) && state.nextActions.length) {
|
|
109
|
+
lines.push('');
|
|
110
|
+
const next = state.nextActions.map(a => paint.brand.data(a)).join(paint.text.dim(' '));
|
|
111
|
+
lines.push(' ' + paint.text.dim('Next: ') + next);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
lines.push('');
|
|
115
|
+
return lines.join('\n');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Same content as renderMissionReport, but as plain markdown so callers
|
|
120
|
+
* can persist it under `.bahulam/reports/`.
|
|
121
|
+
*/
|
|
122
|
+
export function toMarkdown(state) {
|
|
123
|
+
const success = state.success !== false;
|
|
124
|
+
const meta = resolveReportMeta(state);
|
|
125
|
+
const out = [];
|
|
126
|
+
out.push(`# ${success ? 'Done' : 'Held'}${state.task ? ' — ' + state.task : ''}`);
|
|
127
|
+
out.push('');
|
|
128
|
+
out.push('**Repo**: ' + meta.repo);
|
|
129
|
+
out.push('**Author**: ' + meta.author);
|
|
130
|
+
if (Array.isArray(state.filesChanged) && state.filesChanged.length) {
|
|
131
|
+
out.push('**Files**: ' + state.filesChanged.join(', '));
|
|
132
|
+
}
|
|
133
|
+
if (Array.isArray(state.filesRead) && state.filesRead.length) {
|
|
134
|
+
out.push('**Read**: ' + state.filesRead.join(', '));
|
|
135
|
+
}
|
|
136
|
+
const toolSummary = stripAnsi(formatToolCounts(state.toolCounts) || '');
|
|
137
|
+
if (toolSummary) out.push('**Tools**: ' + toolSummary);
|
|
138
|
+
if (state.subAgents) {
|
|
139
|
+
const sub = stripAnsi(formatSubAgents(state.subAgents) || '');
|
|
140
|
+
if (sub) out.push('**Sub-agents**: ' + sub);
|
|
141
|
+
}
|
|
142
|
+
if (state.costUsd != null) out.push('**Cost**: ' + stripAnsi(formatCost(state.costUsd)));
|
|
143
|
+
if (state.durationS != null) out.push('**Time**: ' + formatDuration(state.durationS));
|
|
144
|
+
if (state.testsPass) {
|
|
145
|
+
const { passed = 0, total = 0 } = state.testsPass;
|
|
146
|
+
out.push(`**Tests**: ${passed}/${total} ${passed === total ? 'pass' : 'pass · ' + (total - passed) + ' failing'}`);
|
|
147
|
+
}
|
|
148
|
+
if (!success && Array.isArray(state.blockers) && state.blockers.length) {
|
|
149
|
+
out.push('');
|
|
150
|
+
out.push('## Blocked by');
|
|
151
|
+
for (const b of state.blockers) out.push('- ' + b);
|
|
152
|
+
}
|
|
153
|
+
if (Array.isArray(state.nextActions) && state.nextActions.length) {
|
|
154
|
+
out.push('');
|
|
155
|
+
out.push('**Next**: ' + state.nextActions.join(' '));
|
|
156
|
+
}
|
|
157
|
+
out.push('');
|
|
158
|
+
return out.join('\n');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Save a markdown copy of the report to `.bahulam/reports/<timestamp>.md`
|
|
163
|
+
* inside the working directory. Returns the absolute path.
|
|
164
|
+
*/
|
|
165
|
+
export function saveReport(state, { cwd = process.cwd(), timestamp } = {}) {
|
|
166
|
+
const dir = path.join(cwd, '.bahulam', 'reports');
|
|
167
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
168
|
+
const stamp = timestamp || new Date().toISOString().replace(/[:.]/g, '-');
|
|
169
|
+
const out = path.join(dir, `${stamp}.md`);
|
|
170
|
+
fs.writeFileSync(out, toMarkdown(state));
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ── Helpers ────────────────────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
function row(icon, label, value, alreadyIcon = false) {
|
|
177
|
+
const i = alreadyIcon ? icon : icon;
|
|
178
|
+
const labelText = paint.text.dim(label.padEnd(11));
|
|
179
|
+
return ` ${i} ${labelText} ${value}`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function resolveReportMeta(state = {}) {
|
|
183
|
+
const cwd = state.cwd || process.cwd();
|
|
184
|
+
return {
|
|
185
|
+
repo: state.repo || gitValue(cwd, ['remote', 'get-url', 'origin']) ||
|
|
186
|
+
gitValue(cwd, ['rev-parse', '--show-toplevel'], value => path.basename(value)) ||
|
|
187
|
+
path.basename(cwd),
|
|
188
|
+
author: state.author || gitAuthor(cwd) || 'unknown',
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function gitAuthor(cwd) {
|
|
193
|
+
const name = gitValue(cwd, ['config', '--get', 'user.name']);
|
|
194
|
+
const email = gitValue(cwd, ['config', '--get', 'user.email']);
|
|
195
|
+
if (name && email) return `${name} <${email}>`;
|
|
196
|
+
return name || email || '';
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function gitValue(cwd, args, transform = value => value) {
|
|
200
|
+
try {
|
|
201
|
+
const value = execFileSync('git', args, {
|
|
202
|
+
cwd,
|
|
203
|
+
encoding: 'utf-8',
|
|
204
|
+
timeout: 1500,
|
|
205
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
206
|
+
}).trim();
|
|
207
|
+
return value ? transform(value) : '';
|
|
208
|
+
} catch {
|
|
209
|
+
return '';
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function formatFiles(files) {
|
|
214
|
+
const shortened = files.map(f => paint.text.primary(path.basename(f)));
|
|
215
|
+
if (shortened.length <= 4) return shortened.join(paint.text.dim(', '));
|
|
216
|
+
return shortened.slice(0, 4).join(paint.text.dim(', ')) + paint.text.dim(`, +${files.length - 4} more`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Render `read(4) edit(2) shell(1) test(1)` from a counts object/array.
|
|
221
|
+
* Buckets by tool family so the line stays compact.
|
|
222
|
+
*/
|
|
223
|
+
function formatToolCounts(counts) {
|
|
224
|
+
if (!counts) return '';
|
|
225
|
+
const entries = Array.isArray(counts)
|
|
226
|
+
? counts
|
|
227
|
+
: Object.entries(counts).map(([tool, n]) => ({ tool, count: n }));
|
|
228
|
+
if (!entries.length) return '';
|
|
229
|
+
|
|
230
|
+
const buckets = { read: 0, edit: 0, shell: 0, test: 0, other: 0 };
|
|
231
|
+
for (const { tool, count } of entries) {
|
|
232
|
+
const c = Number(count) || 0;
|
|
233
|
+
if (!c) continue;
|
|
234
|
+
const fam = toolFamily(tool);
|
|
235
|
+
if (tool === 'run_tests' || tool === 'validate_build') buckets.test += c;
|
|
236
|
+
else if (fam === 'write') buckets.edit += c;
|
|
237
|
+
else if (fam === 'shell') buckets.shell += c;
|
|
238
|
+
else if (fam === 'search') buckets.read += c;
|
|
239
|
+
else buckets.other += c;
|
|
240
|
+
}
|
|
241
|
+
const parts = [];
|
|
242
|
+
if (buckets.read) parts.push(`${paint.brand.data('read')}(${buckets.read})`);
|
|
243
|
+
if (buckets.edit) parts.push(`${paint.brand.primary('edit')}(${buckets.edit})`);
|
|
244
|
+
if (buckets.shell) parts.push(`${paint.state.warn('shell')}(${buckets.shell})`);
|
|
245
|
+
if (buckets.test) parts.push(`${paint.state.success('test')}(${buckets.test})`);
|
|
246
|
+
if (buckets.other) parts.push(`${paint.text.muted('tool')}(${buckets.other})`);
|
|
247
|
+
return parts.join(paint.text.dim(' '));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function formatSubAgents(subAgents) {
|
|
251
|
+
// Accepts either a flat counts object { explore: 1, plan: 1, savedUsd: 0.08 }
|
|
252
|
+
// or an array of { type, costUsd, tokens }.
|
|
253
|
+
if (!subAgents) return '';
|
|
254
|
+
let counts = {};
|
|
255
|
+
let savedUsd = 0;
|
|
256
|
+
if (Array.isArray(subAgents)) {
|
|
257
|
+
for (const s of subAgents) {
|
|
258
|
+
counts[s.type] = (counts[s.type] || 0) + 1;
|
|
259
|
+
if (typeof s.savedUsd === 'number') savedUsd += s.savedUsd;
|
|
260
|
+
}
|
|
261
|
+
} else {
|
|
262
|
+
counts = { ...subAgents };
|
|
263
|
+
savedUsd = subAgents.savedUsd || 0;
|
|
264
|
+
delete counts.savedUsd;
|
|
265
|
+
}
|
|
266
|
+
const entries = Object.entries(counts).filter(([, n]) => Number(n) > 0);
|
|
267
|
+
if (!entries.length) return '';
|
|
268
|
+
const list = entries.map(([type, n]) => `${paint.brand.data(type)}(${n})`).join(paint.text.dim(' '));
|
|
269
|
+
if (savedUsd > 0) {
|
|
270
|
+
return list + paint.text.dim(` · saved ≈ ${formatCost(savedUsd)}`);
|
|
271
|
+
}
|
|
272
|
+
return list;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function formatCost(usd) {
|
|
276
|
+
if (typeof usd !== 'number' || !Number.isFinite(usd)) return '$0.00';
|
|
277
|
+
if (usd < 0.01) return `$${usd.toFixed(4)}`;
|
|
278
|
+
return `$${usd.toFixed(2)}`;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function formatDuration(s) {
|
|
282
|
+
if (typeof s !== 'number' || !Number.isFinite(s)) return '0s';
|
|
283
|
+
if (s < 60) return `${s.toFixed(1)}s`;
|
|
284
|
+
const m = Math.floor(s / 60);
|
|
285
|
+
const rem = Math.round(s - m * 60);
|
|
286
|
+
return `${m}m ${rem}s`;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function truncate(s, n) {
|
|
290
|
+
const str = String(s || '');
|
|
291
|
+
return str.length <= n ? str : str.slice(0, n - 1) + '…';
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function stripAnsi(s) {
|
|
295
|
+
return String(s || '').replace(/\x1b\[[0-9;]*m/g, '');
|
|
296
|
+
}
|