@axplusb/kepler 0.0.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -0
- package/package.json +36 -4
- package/pulse/app/activity/page.tsx +190 -0
- package/pulse/app/api/activity/route.ts +138 -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 +52 -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/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 +142 -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 +159 -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 +52 -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 +182 -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/claude-reader.ts +594 -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 +89 -0
- package/src/agents/parser.mjs +98 -0
- package/src/agents/teams.mjs +123 -0
- package/src/auth/oauth.mjs +220 -0
- package/src/auth/tarang-auth.mjs +277 -0
- package/src/config/cli-args.mjs +173 -0
- package/src/config/env.mjs +263 -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 +270 -0
- package/src/context/skeleton.mjs +134 -0
- package/src/core/agent-loop.mjs +480 -0
- package/src/core/approval.mjs +273 -0
- package/src/core/backend-url.mjs +57 -0
- package/src/core/cache.mjs +105 -0
- package/src/core/callback-client.mjs +149 -0
- package/src/core/checkpoints.mjs +142 -0
- package/src/core/context-manager.mjs +198 -0
- package/src/core/headless.mjs +168 -0
- package/src/core/hooks-manager.mjs +87 -0
- package/src/core/jsonl-writer.mjs +351 -0
- package/src/core/local-agent.mjs +429 -0
- package/src/core/local-store.mjs +325 -0
- package/src/core/mode-selector.mjs +51 -0
- package/src/core/output-filter.mjs +177 -0
- package/src/core/paths.mjs +98 -0
- package/src/core/pricing.mjs +314 -0
- package/src/core/providers.mjs +219 -0
- package/src/core/rate-limiter.mjs +119 -0
- package/src/core/safety.mjs +200 -0
- package/src/core/scheduler.mjs +173 -0
- package/src/core/session-manager.mjs +317 -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 +367 -0
- package/src/core/streaming.mjs +182 -0
- package/src/core/system-prompt.mjs +135 -0
- package/src/core/tool-executor.mjs +725 -0
- package/src/hooks/engine.mjs +162 -0
- package/src/index.mjs +370 -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/permissions/checker.mjs +57 -0
- package/src/permissions/command-classifier.mjs +573 -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/loader.mjs +147 -0
- package/src/skills/runner.mjs +55 -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 +421 -0
- package/src/terminal/main.mjs +150 -0
- package/src/terminal/repl.mjs +1484 -0
- package/src/terminal/tool-display.mjs +58 -0
- package/src/tools/agent.mjs +137 -0
- package/src/tools/ask-user.mjs +61 -0
- package/src/tools/bash.mjs +148 -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/read-mcp-resource.mjs +57 -0
- package/src/tools/read.mjs +138 -0
- package/src/tools/registry.mjs +132 -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/banner.mjs +237 -0
- package/src/ui/commands.mjs +499 -0
- package/src/ui/formatter.mjs +379 -0
- package/src/ui/markdown.mjs +278 -0
- package/src/ui/slash-commands.mjs +258 -0
- package/index.js +0 -1
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formatter — Clean terminal output for Tarang events.
|
|
3
|
+
*
|
|
4
|
+
* Style reference: Claude Code screenshot
|
|
5
|
+
* ◐ Cyan spinner + cyan text for working/thinking
|
|
6
|
+
* ✓ Green checkmark + green path for file changes
|
|
7
|
+
* ✗ Red for errors
|
|
8
|
+
* White for content and summaries
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const RESET = '\x1b[0m';
|
|
12
|
+
const BOLD = '\x1b[1m';
|
|
13
|
+
const DIM = '\x1b[2m';
|
|
14
|
+
const RED = '\x1b[31m';
|
|
15
|
+
const GREEN = '\x1b[32m';
|
|
16
|
+
const YELLOW = '\x1b[33m';
|
|
17
|
+
const CYAN = '\x1b[36m';
|
|
18
|
+
|
|
19
|
+
// Spinner frames
|
|
20
|
+
const SPINNER = ['◐', '◓', '◑', '◒'];
|
|
21
|
+
|
|
22
|
+
export class EventFormatter {
|
|
23
|
+
constructor({ verbose = false } = {}) {
|
|
24
|
+
this.verbose = verbose;
|
|
25
|
+
this.toolCount = 0;
|
|
26
|
+
this.toolCalls = [];
|
|
27
|
+
this.changes = [];
|
|
28
|
+
this.phases = new Map();
|
|
29
|
+
this.sessionInfo = null;
|
|
30
|
+
this.tokenCount = { input: 0, output: 0 };
|
|
31
|
+
this._spinnerFrame = 0;
|
|
32
|
+
this._hasContent = false;
|
|
33
|
+
this._lastContent = '';
|
|
34
|
+
this._completed = false;
|
|
35
|
+
this._seenCallIds = new Set();
|
|
36
|
+
this._lastThinking = '';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
render(event) {
|
|
40
|
+
const { type, data } = event;
|
|
41
|
+
switch (type) {
|
|
42
|
+
case 'session_info':
|
|
43
|
+
this.sessionInfo = data;
|
|
44
|
+
if (this.verbose) {
|
|
45
|
+
process.stderr.write(`${DIM} [session] ${data.session_id || ''}${RESET}\n`);
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
case 'status':
|
|
49
|
+
this._status(data);
|
|
50
|
+
return true;
|
|
51
|
+
case 'thinking':
|
|
52
|
+
this._thinking(data);
|
|
53
|
+
return true;
|
|
54
|
+
case 'content':
|
|
55
|
+
case 'content_partial':
|
|
56
|
+
this._content(data);
|
|
57
|
+
return true;
|
|
58
|
+
case 'tool_call':
|
|
59
|
+
case 'tool_request':
|
|
60
|
+
this._toolCall(data);
|
|
61
|
+
return true;
|
|
62
|
+
case 'tool_done':
|
|
63
|
+
this._toolDone(data);
|
|
64
|
+
return true;
|
|
65
|
+
case 'complete':
|
|
66
|
+
this._complete(data);
|
|
67
|
+
return true;
|
|
68
|
+
case 'error':
|
|
69
|
+
this._error(data);
|
|
70
|
+
return true;
|
|
71
|
+
case 'plan':
|
|
72
|
+
this._plan(data);
|
|
73
|
+
return true;
|
|
74
|
+
case 'phase_start':
|
|
75
|
+
this._phaseStart(data);
|
|
76
|
+
return true;
|
|
77
|
+
case 'phase_update':
|
|
78
|
+
this._phaseUpdate(data);
|
|
79
|
+
return true;
|
|
80
|
+
case 'phase_summary':
|
|
81
|
+
this._phaseSummary(data);
|
|
82
|
+
return true;
|
|
83
|
+
case 'change':
|
|
84
|
+
this._change(data);
|
|
85
|
+
return true;
|
|
86
|
+
case 'worker_update':
|
|
87
|
+
case 'worker_start':
|
|
88
|
+
case 'worker_done':
|
|
89
|
+
this._workerEvent(type, data);
|
|
90
|
+
return true;
|
|
91
|
+
case 'delegation':
|
|
92
|
+
this._delegation(data);
|
|
93
|
+
return true;
|
|
94
|
+
case 'cancelled':
|
|
95
|
+
process.stderr.write(`\n${YELLOW} Cancelled${data?.reason ? ': ' + data.reason : ''}${RESET}\n`);
|
|
96
|
+
return true;
|
|
97
|
+
case 'paused':
|
|
98
|
+
process.stderr.write(`${CYAN} Paused${RESET}\n`);
|
|
99
|
+
return true;
|
|
100
|
+
case 'resumed':
|
|
101
|
+
process.stderr.write(`${GREEN} Resumed${RESET}\n`);
|
|
102
|
+
return true;
|
|
103
|
+
case 'pause_instruction':
|
|
104
|
+
this._pauseInstruction(data);
|
|
105
|
+
return true;
|
|
106
|
+
default:
|
|
107
|
+
if (this.verbose) {
|
|
108
|
+
process.stderr.write(`${DIM} [${type}] ${JSON.stringify(data).slice(0, 100)}${RESET}\n`);
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
_spinner() {
|
|
115
|
+
const frame = SPINNER[this._spinnerFrame % SPINNER.length];
|
|
116
|
+
this._spinnerFrame++;
|
|
117
|
+
return `${CYAN}${frame}${RESET}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
_status(data) {
|
|
121
|
+
const msg = data?.message || '';
|
|
122
|
+
// Skip noisy statuses
|
|
123
|
+
if (!msg || msg === 'Agent started') return;
|
|
124
|
+
if (msg.startsWith('Creating agent') || msg.startsWith('Task type:')) {
|
|
125
|
+
process.stderr.write(`${DIM} ${msg}${RESET}\n`);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
process.stderr.write(` ${this._spinner()} ${CYAN}${msg}${RESET}\n`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
_thinking(data) {
|
|
132
|
+
if (!this.verbose) return;
|
|
133
|
+
|
|
134
|
+
const text = data?.message || data?.text || '';
|
|
135
|
+
if (!text || text === this._lastThinking) return;
|
|
136
|
+
this._lastThinking = text;
|
|
137
|
+
|
|
138
|
+
// Skip generic "Processing (iteration N)..." — too noisy
|
|
139
|
+
if (text.startsWith('Processing')) return;
|
|
140
|
+
|
|
141
|
+
process.stderr.write(` ${this._spinner()} ${CYAN}${text.slice(0, 200)}${RESET}\n`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
_content(data) {
|
|
145
|
+
const text = data?.text || '';
|
|
146
|
+
if (!text) return;
|
|
147
|
+
|
|
148
|
+
// Deduplicate exact same content (CONTENT event may repeat)
|
|
149
|
+
if (text === this._lastContent) return;
|
|
150
|
+
this._lastContent = text;
|
|
151
|
+
|
|
152
|
+
// Add newline separator before content block
|
|
153
|
+
if (this.toolCount > 0 || this._hasContent) process.stdout.write('\n');
|
|
154
|
+
this._hasContent = true;
|
|
155
|
+
|
|
156
|
+
// Render content with 2-space indent
|
|
157
|
+
const lines = text.split('\n');
|
|
158
|
+
for (const line of lines) {
|
|
159
|
+
process.stdout.write(` ${line}\n`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
_toolCall(data) {
|
|
164
|
+
const callId = data?.call_id;
|
|
165
|
+
const tool = data?.tool || 'unknown';
|
|
166
|
+
const args = data?.args || {};
|
|
167
|
+
|
|
168
|
+
// Deduplicate: agent event + bridge event both fire
|
|
169
|
+
if (callId) {
|
|
170
|
+
if (this._seenCallIds.has(callId)) return;
|
|
171
|
+
this._seenCallIds.add(callId);
|
|
172
|
+
} else {
|
|
173
|
+
// Agent event (no call_id) — skip if bridge event follows
|
|
174
|
+
// Use tool+args as dedup key
|
|
175
|
+
const key = `${tool}:${JSON.stringify(args)}`;
|
|
176
|
+
if (this._seenCallIds.has(key)) return;
|
|
177
|
+
this._seenCallIds.add(key);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
this.toolCount++;
|
|
181
|
+
|
|
182
|
+
// Build human-readable description
|
|
183
|
+
const desc = this._toolDescription(tool, args);
|
|
184
|
+
process.stderr.write(` ${this._spinner()} [${this.toolCount}] ${CYAN}${tool}: ${desc}${RESET}\n`);
|
|
185
|
+
|
|
186
|
+
this.toolCalls.push({ name: tool, callId, startTime: Date.now() });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
_toolDescription(tool, args) {
|
|
190
|
+
switch (tool) {
|
|
191
|
+
case 'read_file':
|
|
192
|
+
return `Reading ${args.file_path || 'file'}...`;
|
|
193
|
+
case 'read_files': {
|
|
194
|
+
const paths = args.file_paths || [];
|
|
195
|
+
return `Reading ${paths.length} files...`;
|
|
196
|
+
}
|
|
197
|
+
case 'write_file':
|
|
198
|
+
return `Writing ${args.file_path || 'file'}...`;
|
|
199
|
+
case 'write_project': {
|
|
200
|
+
const files = args.files || [];
|
|
201
|
+
return `Writing ${files.length} files...`;
|
|
202
|
+
}
|
|
203
|
+
case 'edit_file':
|
|
204
|
+
return `Editing ${args.file_path || 'file'}...`;
|
|
205
|
+
case 'list_files':
|
|
206
|
+
return `Listing files${args.pattern ? ' (' + args.pattern + ')' : ''}...`;
|
|
207
|
+
case 'search_files':
|
|
208
|
+
return `Searching for "${args.pattern || ''}"...`;
|
|
209
|
+
case 'search_code':
|
|
210
|
+
return `Searching code: ${args.query || ''}...`;
|
|
211
|
+
case 'shell': {
|
|
212
|
+
const cmd = (args.command || '').slice(0, 60);
|
|
213
|
+
return `Running: ${cmd}${(args.command || '').length > 60 ? '...' : ''}`;
|
|
214
|
+
}
|
|
215
|
+
case 'validate_build':
|
|
216
|
+
return `Running build validation...`;
|
|
217
|
+
case 'validate_file':
|
|
218
|
+
return `Validating ${args.file_path || 'file'}...`;
|
|
219
|
+
case 'lint_check':
|
|
220
|
+
return `Linting ${args.file_path || 'file'}...`;
|
|
221
|
+
default:
|
|
222
|
+
return `${tool}...`;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
_toolDone(data) {
|
|
227
|
+
const tool = data?.tool || '';
|
|
228
|
+
const success = data?.success !== false;
|
|
229
|
+
const durationMs = data?.duration_ms;
|
|
230
|
+
|
|
231
|
+
if (this.verbose) {
|
|
232
|
+
const dur = durationMs ? ` (${durationMs}ms)` : '';
|
|
233
|
+
process.stderr.write(` ${GREEN}✓${RESET} ${tool} done${dur}\n`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Show file modifications as green checkmarks
|
|
237
|
+
if (tool === 'write_file' || tool === 'edit_file' || tool === 'write_project') {
|
|
238
|
+
const path = data?.result?.file_path || data?.args?.file_path || '';
|
|
239
|
+
if (path) {
|
|
240
|
+
const action = tool === 'edit_file' ? 'Modified' : tool === 'write_project' ? 'Created' : 'Written';
|
|
241
|
+
process.stderr.write(` ${GREEN}✓ ${action} ${path}${RESET}\n`);
|
|
242
|
+
this.changes.push({ path, action });
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Show validation results
|
|
247
|
+
if (tool === 'validate_build' || tool === 'lint_check' || tool === 'validate_file') {
|
|
248
|
+
if (success) {
|
|
249
|
+
const label = tool === 'validate_build' ? 'Build passed' :
|
|
250
|
+
tool === 'lint_check' ? 'Lint check passed' :
|
|
251
|
+
'File validated';
|
|
252
|
+
process.stderr.write(` ${GREEN}✓ ${label}${RESET}\n`);
|
|
253
|
+
} else {
|
|
254
|
+
const msg = data?.result?.error || data?.result?.stderr || 'Failed';
|
|
255
|
+
process.stderr.write(` ${RED}✗ ${tool.replace('_', ' ')} failed: ${msg.slice(0, 100)}${RESET}\n`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Show shell command results (if verbose or if failed)
|
|
260
|
+
if (tool === 'shell' && !success) {
|
|
261
|
+
const stderr = data?.result?.stderr || '';
|
|
262
|
+
if (stderr) {
|
|
263
|
+
process.stderr.write(` ${RED}✗ Command failed: ${stderr.slice(0, 100)}${RESET}\n`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
_plan(data) {
|
|
269
|
+
const milestones = data?.milestones || [];
|
|
270
|
+
if (milestones.length === 0) return;
|
|
271
|
+
process.stderr.write(`\n ${BOLD}Plan${RESET}\n`);
|
|
272
|
+
for (const m of milestones) {
|
|
273
|
+
const icon = m.status === 'completed' ? `${GREEN}✓${RESET}` :
|
|
274
|
+
m.status === 'started' ? `${CYAN}◐${RESET}` :
|
|
275
|
+
`${DIM}○${RESET}`;
|
|
276
|
+
process.stderr.write(` ${icon} ${m.name}\n`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
_phaseStart(data) {
|
|
281
|
+
const phase = data?.phase || data?.stage_name || '';
|
|
282
|
+
if (phase && phase !== 'undefined') {
|
|
283
|
+
process.stderr.write(`\n ${this._spinner()} ${CYAN}${BOLD}${phase}${RESET}\n`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
_phaseUpdate(data) {
|
|
288
|
+
const phase = data?.phase || data?.stage_name || '';
|
|
289
|
+
const status = data?.status || '';
|
|
290
|
+
if (phase && phase !== 'undefined') {
|
|
291
|
+
this.phases.set(phase, status);
|
|
292
|
+
process.stderr.write(`\n ${this._spinner()} ${CYAN}${BOLD}${phase}${RESET}\n`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
_phaseSummary(data) {
|
|
297
|
+
if (data?.summary) {
|
|
298
|
+
process.stderr.write(` ${GREEN}✓${RESET} ${data.summary.slice(0, 200)}\n`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
_workerEvent(type, data) {
|
|
303
|
+
const worker = data?.worker || data?.name || '';
|
|
304
|
+
const status = data?.status || '';
|
|
305
|
+
if (type === 'worker_start') {
|
|
306
|
+
process.stderr.write(` ${this._spinner()} ${CYAN}${worker} starting${RESET}\n`);
|
|
307
|
+
} else if (type === 'worker_done') {
|
|
308
|
+
process.stderr.write(` ${GREEN}✓${RESET} ${worker} done\n`);
|
|
309
|
+
} else {
|
|
310
|
+
process.stderr.write(` ${this._spinner()} ${CYAN}${worker}: ${status}${RESET}\n`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
_delegation(data) {
|
|
315
|
+
const from = data?.from || '';
|
|
316
|
+
const to = data?.to || '';
|
|
317
|
+
const instruction = data?.instruction || '';
|
|
318
|
+
process.stderr.write(` ${CYAN}${from} → ${to}${RESET}${instruction ? ': ' + instruction : ''}\n`);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
_pauseInstruction(data) {
|
|
322
|
+
const instruction = data?.instruction || '';
|
|
323
|
+
if (instruction) {
|
|
324
|
+
process.stderr.write(` ${YELLOW}Pause instruction: ${instruction}${RESET}\n`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
_change(data) {
|
|
329
|
+
const icon = data?.type === 'create' ? `${GREEN}+${RESET}` : `${GREEN}~${RESET}`;
|
|
330
|
+
process.stderr.write(` ${icon} ${GREEN}${data?.path || ''}${RESET}\n`);
|
|
331
|
+
this.changes.push(data);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
_error(data) {
|
|
335
|
+
const msg = data?.message || 'Unknown error';
|
|
336
|
+
process.stderr.write(`\n ${RED}✗ ${msg}${RESET}\n`);
|
|
337
|
+
|
|
338
|
+
// Helpful suggestions for common errors
|
|
339
|
+
if (msg.includes('Authentication') || msg.includes('token')) {
|
|
340
|
+
process.stderr.write(` ${DIM}Run /login to re-authenticate${RESET}\n`);
|
|
341
|
+
} else if (msg.includes('API key') || msg.includes('OpenRouter')) {
|
|
342
|
+
process.stderr.write(` ${DIM}Run /config to set up your provider${RESET}\n`);
|
|
343
|
+
} else if (msg.includes('Backend') || msg.includes('Network')) {
|
|
344
|
+
process.stderr.write(` ${DIM}Check if the backend is running at ${this.sessionInfo?.backend || 'localhost:8000'}${RESET}\n`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
_complete(data) {
|
|
349
|
+
// Only show once
|
|
350
|
+
if (this._completed) return;
|
|
351
|
+
this._completed = true;
|
|
352
|
+
|
|
353
|
+
const summary = data?.summary || '';
|
|
354
|
+
const duration = data?.duration_s ? `${Number(data.duration_s).toFixed(1)}s` : '';
|
|
355
|
+
const tools = this.toolCount || data?.tool_calls || 0;
|
|
356
|
+
const changeCount = data?.changes || this.changes.length || 0;
|
|
357
|
+
|
|
358
|
+
// Summary line
|
|
359
|
+
const parts = [];
|
|
360
|
+
if (duration) parts.push(duration);
|
|
361
|
+
if (tools > 0) parts.push(`${tools} tool calls`);
|
|
362
|
+
if (changeCount > 0) parts.push(`${changeCount} changes`);
|
|
363
|
+
|
|
364
|
+
const stats = parts.length > 0 ? ` (${parts.join(', ')})` : '';
|
|
365
|
+
if (summary) {
|
|
366
|
+
process.stderr.write(`\n ${GREEN}✓ ${summary}${stats}${RESET}\n`);
|
|
367
|
+
} else {
|
|
368
|
+
process.stderr.write(`\n ${GREEN}✓ Done${stats}${RESET}\n`);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Token usage if available
|
|
372
|
+
const usage = data?.usage;
|
|
373
|
+
if (usage && (usage.input_tokens || usage.total_tokens)) {
|
|
374
|
+
const inp = usage.input_tokens || 0;
|
|
375
|
+
const out = usage.output_tokens || 0;
|
|
376
|
+
process.stderr.write(` ${DIM}Tokens: ${inp.toLocaleString()} in / ${out.toLocaleString()} out${RESET}\n`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
@@ -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
|
+
}
|