@hunterzhu/pulse-cli 0.1.4 → 0.1.5

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 (71) hide show
  1. package/dist/bin.d.ts +10 -1
  2. package/dist/bin.js +86 -177
  3. package/dist/commands/doctor.d.ts +2 -0
  4. package/dist/commands/doctor.js +36 -0
  5. package/dist/commands/interactive.d.ts +2 -0
  6. package/dist/commands/interactive.js +27 -0
  7. package/dist/commands/resume.d.ts +2 -0
  8. package/dist/commands/resume.js +75 -0
  9. package/dist/commands/run.d.ts +2 -0
  10. package/dist/commands/run.js +73 -0
  11. package/dist/commands/sessions.d.ts +2 -0
  12. package/dist/commands/sessions.js +66 -0
  13. package/dist/commands/setup.d.ts +1 -0
  14. package/dist/commands/setup.js +30 -0
  15. package/dist/commands/signals.d.ts +2 -0
  16. package/dist/commands/signals.js +20 -0
  17. package/dist/components/App.d.ts +10 -0
  18. package/dist/components/App.js +352 -0
  19. package/dist/components/ApprovalPrompt.d.ts +8 -0
  20. package/dist/components/ApprovalPrompt.js +21 -0
  21. package/dist/components/AssistantMessage.d.ts +10 -0
  22. package/dist/components/AssistantMessage.js +10 -0
  23. package/dist/components/Header.d.ts +8 -0
  24. package/dist/components/Header.js +8 -0
  25. package/dist/components/HelpView.d.ts +1 -0
  26. package/dist/components/HelpView.js +43 -0
  27. package/dist/components/InputArea.d.ts +7 -0
  28. package/dist/components/InputArea.js +55 -0
  29. package/dist/components/MessageList.d.ts +8 -0
  30. package/dist/components/MessageList.js +12 -0
  31. package/dist/components/SessionList.d.ts +15 -0
  32. package/dist/components/SessionList.js +33 -0
  33. package/dist/components/Spinner.d.ts +4 -0
  34. package/dist/components/Spinner.js +7 -0
  35. package/dist/components/ThinkingBlock.d.ts +5 -0
  36. package/dist/components/ThinkingBlock.js +8 -0
  37. package/dist/components/TokenStats.d.ts +7 -0
  38. package/dist/components/TokenStats.js +15 -0
  39. package/dist/components/ToolCallCard.d.ts +8 -0
  40. package/dist/components/ToolCallCard.js +31 -0
  41. package/dist/components/UserMessage.d.ts +5 -0
  42. package/dist/components/UserMessage.js +7 -0
  43. package/dist/components/Welcome.d.ts +6 -0
  44. package/dist/components/Welcome.js +6 -0
  45. package/dist/hooks/useConversation.d.ts +15 -0
  46. package/dist/hooks/useConversation.js +115 -0
  47. package/dist/hooks/useHost.d.ts +7 -0
  48. package/dist/hooks/useHost.js +44 -0
  49. package/dist/hooks/useMultilineInput.d.ts +11 -0
  50. package/dist/hooks/useMultilineInput.js +58 -0
  51. package/dist/hooks/useRun.d.ts +16 -0
  52. package/dist/hooks/useRun.js +176 -0
  53. package/dist/hooks/useSlashCommands.d.ts +25 -0
  54. package/dist/hooks/useSlashCommands.js +44 -0
  55. package/dist/hooks/useTokenStats.d.ts +7 -0
  56. package/dist/hooks/useTokenStats.js +38 -0
  57. package/dist/theme.d.ts +27 -0
  58. package/dist/theme.js +27 -0
  59. package/dist/types.d.ts +65 -0
  60. package/dist/types.js +1 -0
  61. package/dist/utils/ansi.d.ts +5 -0
  62. package/dist/utils/ansi.js +20 -0
  63. package/dist/utils/approval.d.ts +10 -0
  64. package/dist/utils/approval.js +50 -0
  65. package/dist/utils/format.d.ts +6 -0
  66. package/dist/utils/format.js +51 -0
  67. package/dist/utils/highlight.d.ts +1 -0
  68. package/dist/utils/highlight.js +80 -0
  69. package/dist/utils/markdown.d.ts +1 -0
  70. package/dist/utils/markdown.js +138 -0
  71. package/package.json +13 -2
@@ -0,0 +1,44 @@
1
+ import { useMemo, useCallback } from 'react';
2
+ export function useSlashCommands(deps) {
3
+ const commands = useMemo(() => [
4
+ { name: '/help', aliases: ['/h'], description: '显示帮助信息', execute: async () => deps.onHelp?.() },
5
+ { name: '/status', description: '查看状态', execute: async () => deps.onStatus?.() },
6
+ { name: '/tools', description: '列出可用工具', execute: async () => deps.onTools?.() },
7
+ { name: '/artifacts', description: '查看当前产物', execute: async () => deps.onArtifacts?.() },
8
+ { name: '/exit', description: '退出程序', execute: async () => deps.onExit?.() },
9
+ { name: '/quit', aliases: ['/q'], description: '退出程序', execute: async () => deps.onQuit?.() },
10
+ { name: '/new', description: '开启新会话', execute: async () => deps.onNew?.() },
11
+ { name: '/sessions', description: '查看所有会话', execute: async () => deps.onSessions?.() },
12
+ { name: '/delete', description: '删除会话 [id]', execute: async (args) => deps.onDelete?.(args) },
13
+ { name: '/export', description: '导出会话 [markdown|json]', execute: async (args) => deps.onExport?.(args) },
14
+ { name: '/clear', description: '清空当前屏幕', execute: async () => deps.onClear?.() },
15
+ { name: '/config', description: '查看配置', execute: async () => deps.onConfig?.() },
16
+ { name: '/model', description: '切换模型 [name]', execute: async (args) => deps.onModel?.(args) },
17
+ { name: '/compact', description: '调用模型压缩对话历史上下文', execute: async () => deps.onCompact?.() },
18
+ { name: '/verbose', description: '开启详细输出模式', execute: async () => deps.onVerbose?.() },
19
+ { name: '/quiet', description: '开启精简输出模式', execute: async () => deps.onQuiet?.() },
20
+ { name: '/thinking', description: '设置模型思考深度 [low|medium|high|off]', execute: async (args) => deps.onThinking?.(args) },
21
+ ], [deps]);
22
+ const isSlashCommand = useCallback((input) => {
23
+ return input.trim().startsWith('/');
24
+ }, []);
25
+ const executeCommand = useCallback(async (input) => {
26
+ const trimmed = input.trim();
27
+ if (!trimmed.startsWith('/'))
28
+ return false;
29
+ const parts = trimmed.split(' ');
30
+ const cmdName = parts[0]?.toLowerCase();
31
+ const args = parts.slice(1).join(' ').trim();
32
+ const command = commands.find((c) => c.name === cmdName || c.aliases?.includes(cmdName));
33
+ if (command) {
34
+ await command.execute(args);
35
+ return true;
36
+ }
37
+ return false;
38
+ }, [commands]);
39
+ return {
40
+ commands,
41
+ isSlashCommand,
42
+ executeCommand,
43
+ };
44
+ }
@@ -0,0 +1,7 @@
1
+ import { TokenStatsData } from '../types.js';
2
+ export declare function useTokenStats(): {
3
+ current: TokenStatsData | null;
4
+ cumulative: TokenStatsData;
5
+ recordRun: (stats: TokenStatsData) => void;
6
+ reset: () => void;
7
+ };
@@ -0,0 +1,38 @@
1
+ import { useState, useCallback } from 'react';
2
+ export function useTokenStats() {
3
+ const [current, setCurrent] = useState(null);
4
+ const [cumulative, setCumulative] = useState({
5
+ inputTokens: 0,
6
+ outputTokens: 0,
7
+ totalTokens: 0,
8
+ durationMs: 0,
9
+ estimatedCost: 0
10
+ });
11
+ const recordRun = useCallback((stats) => {
12
+ setCurrent(stats);
13
+ // 累加本次 Run 的数据到总计中
14
+ setCumulative(prev => ({
15
+ inputTokens: prev.inputTokens + stats.inputTokens,
16
+ outputTokens: prev.outputTokens + stats.outputTokens,
17
+ totalTokens: prev.totalTokens + stats.totalTokens,
18
+ durationMs: prev.durationMs + stats.durationMs,
19
+ estimatedCost: (prev.estimatedCost || 0) + (stats.estimatedCost || 0)
20
+ }));
21
+ }, []);
22
+ const reset = useCallback(() => {
23
+ setCurrent(null);
24
+ setCumulative({
25
+ inputTokens: 0,
26
+ outputTokens: 0,
27
+ totalTokens: 0,
28
+ durationMs: 0,
29
+ estimatedCost: 0
30
+ });
31
+ }, []);
32
+ return {
33
+ current,
34
+ cumulative,
35
+ recordRun,
36
+ reset
37
+ };
38
+ }
@@ -0,0 +1,27 @@
1
+ /** Pulse CLI Cyan/Blue 主题色定义 */
2
+ export declare const theme: {
3
+ /** 主色调 - 标题、边框 */
4
+ readonly primary: "#06b6d4";
5
+ /** 强调色 - 关键操作 */
6
+ readonly accent: "#3b82f6";
7
+ /** 用户消息 */
8
+ readonly user: "#22d3ee";
9
+ /** 工具调用 */
10
+ readonly tool: "#a78bfa";
11
+ /** 思考过程 - 暗淡 */
12
+ readonly thinking: "#6b7280";
13
+ /** 成功 */
14
+ readonly success: "#22c55e";
15
+ /** 警告/审批 */
16
+ readonly warning: "#f59e0b";
17
+ /** 错误 */
18
+ readonly error: "#ef4444";
19
+ /** Token 统计 */
20
+ readonly stats: "#94a3b8";
21
+ /** 暗淡文本 */
22
+ readonly dim: "#64748b";
23
+ /** 边框 */
24
+ readonly border: "#334155";
25
+ /** 代码背景标签 */
26
+ readonly codeLang: "#475569";
27
+ };
package/dist/theme.js ADDED
@@ -0,0 +1,27 @@
1
+ /** Pulse CLI Cyan/Blue 主题色定义 */
2
+ export const theme = {
3
+ /** 主色调 - 标题、边框 */
4
+ primary: '#06b6d4',
5
+ /** 强调色 - 关键操作 */
6
+ accent: '#3b82f6',
7
+ /** 用户消息 */
8
+ user: '#22d3ee',
9
+ /** 工具调用 */
10
+ tool: '#a78bfa',
11
+ /** 思考过程 - 暗淡 */
12
+ thinking: '#6b7280',
13
+ /** 成功 */
14
+ success: '#22c55e',
15
+ /** 警告/审批 */
16
+ warning: '#f59e0b',
17
+ /** 错误 */
18
+ error: '#ef4444',
19
+ /** Token 统计 */
20
+ stats: '#94a3b8',
21
+ /** 暗淡文本 */
22
+ dim: '#64748b',
23
+ /** 边框 */
24
+ border: '#334155',
25
+ /** 代码背景标签 */
26
+ codeLang: '#475569',
27
+ };
@@ -0,0 +1,65 @@
1
+ import type { ConversationSummary, ArtifactSummary, AssistantEvent, RunHandle } from '@hunterzhu/pulse-server';
2
+ export type { ConversationSummary, ArtifactSummary, AssistantEvent, RunHandle };
3
+ /** 输出详细程度 */
4
+ export type Verbosity = 'verbose' | 'normal' | 'quiet';
5
+ /** Slash 命令定义 */
6
+ export interface SlashCommand {
7
+ name: string;
8
+ aliases?: string[] | undefined;
9
+ description: string;
10
+ execute: (args: string) => void | Promise<void>;
11
+ }
12
+ /** 消息角色 */
13
+ export type MessageRole = 'user' | 'assistant' | 'system';
14
+ /** 显示用消息 */
15
+ export interface DisplayMessage {
16
+ id: string;
17
+ role: MessageRole;
18
+ text: string;
19
+ runId?: string | undefined;
20
+ createdAt: string;
21
+ /** 工具调用事件 */
22
+ toolCalls?: ToolCallDisplay[] | undefined;
23
+ /** 思考过程 */
24
+ thinking?: string | undefined;
25
+ /** Token 统计 */
26
+ tokenStats?: TokenStatsData | undefined;
27
+ }
28
+ /** 工具调用展示数据 */
29
+ export interface ToolCallDisplay {
30
+ id: string;
31
+ name: string;
32
+ arguments?: Record<string, unknown> | undefined;
33
+ result?: unknown;
34
+ status: 'running' | 'succeeded' | 'failed';
35
+ durationMs?: number | undefined;
36
+ }
37
+ /** Token 统计数据 */
38
+ export interface TokenStatsData {
39
+ inputTokens: number;
40
+ outputTokens: number;
41
+ totalTokens: number;
42
+ durationMs: number;
43
+ estimatedCost?: number | undefined;
44
+ }
45
+ /** 审批请求数据 */
46
+ export interface ApprovalRequest {
47
+ effectId: string;
48
+ toolName: string;
49
+ toolArgs: Record<string, unknown>;
50
+ prompt: string;
51
+ digest?: string | undefined;
52
+ tools?: Array<{
53
+ name: string;
54
+ toolCallId?: string;
55
+ input: Record<string, unknown>;
56
+ }> | undefined;
57
+ }
58
+ /** 应用状态模式 */
59
+ export type AppMode = 'chat' | 'sessions' | 'help' | 'config';
60
+ /** 解析后的命令行参数 */
61
+ export interface ParsedArgs {
62
+ command: string;
63
+ positionals: string[];
64
+ options: Record<string, string | boolean>;
65
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,5 @@
1
+ export declare function stripAnsi(text: string): string;
2
+ /** Remove terminal control sequences from untrusted text before display. */
3
+ export declare function stripTerminalControls(text: string): string;
4
+ export declare function terminalWidth(): number;
5
+ export declare function horizontalLine(width?: number): string;
@@ -0,0 +1,20 @@
1
+ import process from 'node:process';
2
+ const completeOsc = /\u001B\][\s\S]*?(?:\u0007|\u001B\\)/g;
3
+ const incompleteOsc = /\u001B\][\s\S]*$/g;
4
+ const csi = /[\u001B\u009B][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g;
5
+ const otherEscape = /\u001B[@-_]/g;
6
+ const unsafeControls = /[\u0000-\u0008\u000B\u000C\u000E-\u001A\u001C-\u001F\u007F]/g;
7
+ export function stripAnsi(text) {
8
+ return text.replace(csi, '');
9
+ }
10
+ /** Remove terminal control sequences from untrusted text before display. */
11
+ export function stripTerminalControls(text) {
12
+ return text.replace(completeOsc, '').replace(incompleteOsc, '').replace(csi, '').replace(otherEscape, '').replace(unsafeControls, '');
13
+ }
14
+ export function terminalWidth() {
15
+ return process.stdout.columns || 80;
16
+ }
17
+ export function horizontalLine(width) {
18
+ const w = width || terminalWidth();
19
+ return '─'.repeat(Math.max(1, w));
20
+ }
@@ -0,0 +1,10 @@
1
+ export interface ApprovalToolInput {
2
+ name: string;
3
+ input: Record<string, unknown>;
4
+ }
5
+ export interface ApprovalToolPreview {
6
+ name: string;
7
+ body: string;
8
+ truncated: boolean;
9
+ }
10
+ export declare function describeApprovalTool(tool: ApprovalToolInput): ApprovalToolPreview;
@@ -0,0 +1,50 @@
1
+ import { stripTerminalControls } from './ansi.js';
2
+ const previewLimit = 1_000;
3
+ function clip(value) {
4
+ const clean = stripTerminalControls(value);
5
+ if (clean.length <= previewLimit)
6
+ return { text: clean, truncated: false };
7
+ const hidden = clean.length - previewLimit;
8
+ return {
9
+ truncated: true,
10
+ text: `${clean.slice(0, previewLimit)}\n… 还有 ${hidden} 个字符未显示。批准会执行完整内容(共 ${clean.length} 个字符)。`,
11
+ };
12
+ }
13
+ function textField(input, key) {
14
+ const value = input[key];
15
+ return typeof value === 'string' ? value : value === undefined ? '' : JSON.stringify(value);
16
+ }
17
+ export function describeApprovalTool(tool) {
18
+ if (tool.name === 'fs.write') {
19
+ const content = clip(textField(tool.input, 'content'));
20
+ const path = stripTerminalControls(textField(tool.input, 'path'));
21
+ return {
22
+ name: tool.name,
23
+ truncated: content.truncated,
24
+ body: `路径: ${path}\n内容(${textField(tool.input, 'content').length} 个字符):\n${content.text}`,
25
+ };
26
+ }
27
+ if (tool.name === 'fs.apply_patch') {
28
+ const find = clip(textField(tool.input, 'find'));
29
+ const replace = clip(textField(tool.input, 'replace'));
30
+ const path = stripTerminalControls(textField(tool.input, 'path'));
31
+ const all = tool.input.all === true ? '全部匹配' : '第一处匹配';
32
+ return {
33
+ name: tool.name,
34
+ truncated: find.truncated || replace.truncated,
35
+ body: `路径: ${path}\n范围: ${all}\n- ${find.text}\n+ ${replace.text}`,
36
+ };
37
+ }
38
+ if (tool.name === 'shell.exec') {
39
+ const command = stripTerminalControls(textField(tool.input, 'command'));
40
+ const args = Array.isArray(tool.input.args) ? tool.input.args.map((arg) => stripTerminalControls(String(arg))) : [];
41
+ const cwd = stripTerminalControls(textField(tool.input, 'cwd') || '.');
42
+ return {
43
+ name: tool.name,
44
+ truncated: false,
45
+ body: `命令: ${command}\n参数: ${args.join(' ') || '(无)'}\n目录: ${cwd}`,
46
+ };
47
+ }
48
+ const serialized = clip(JSON.stringify(tool.input, null, 2));
49
+ return { name: tool.name, truncated: serialized.truncated, body: serialized.text };
50
+ }
@@ -0,0 +1,6 @@
1
+ export declare function formatBytes(bytes: number): string;
2
+ export declare function formatDuration(ms: number): string;
3
+ export declare function formatRelativeTime(iso: string): string;
4
+ export declare function formatTokenCount(count: number): string;
5
+ export declare function formatCost(cost: number): string;
6
+ export declare function truncate(text: string, maxLen: number): string;
@@ -0,0 +1,51 @@
1
+ export function formatBytes(bytes) {
2
+ if (bytes === 0)
3
+ return '0 B';
4
+ const k = 1024;
5
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
6
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
7
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
8
+ }
9
+ export function formatDuration(ms) {
10
+ if (ms < 1000)
11
+ return `${ms}ms`;
12
+ const s = Math.floor(ms / 1000);
13
+ if (s < 60)
14
+ return `${(ms / 1000).toFixed(1)}s`;
15
+ const m = Math.floor(s / 60);
16
+ const remainingS = s % 60;
17
+ if (m < 60)
18
+ return `${m}m ${remainingS}s`;
19
+ const h = Math.floor(m / 60);
20
+ const remainingM = m % 60;
21
+ return `${h}h ${remainingM}m`;
22
+ }
23
+ export function formatRelativeTime(iso) {
24
+ const date = new Date(iso);
25
+ const now = new Date();
26
+ const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
27
+ if (diffInSeconds < 60)
28
+ return '刚刚';
29
+ if (diffInSeconds < 3600)
30
+ return `${Math.floor(diffInSeconds / 60)} 分钟前`;
31
+ if (diffInSeconds < 86400)
32
+ return `${Math.floor(diffInSeconds / 3600)} 小时前`;
33
+ if (diffInSeconds < 172800)
34
+ return '昨天';
35
+ return `${Math.floor(diffInSeconds / 86400)} 天前`;
36
+ }
37
+ export function formatTokenCount(count) {
38
+ return count.toLocaleString();
39
+ }
40
+ export function formatCost(cost) {
41
+ if (cost === 0)
42
+ return '$0.00';
43
+ if (cost < 0.0001)
44
+ return `<$0.0001`;
45
+ return `~$${cost.toFixed(4).replace(/0+$/, '').replace(/\.$/, '')}`;
46
+ }
47
+ export function truncate(text, maxLen) {
48
+ if (text.length <= maxLen)
49
+ return text;
50
+ return text.slice(0, maxLen - 3) + '...';
51
+ }
@@ -0,0 +1 @@
1
+ export declare function highlightCode(code: string, language?: string): string;
@@ -0,0 +1,80 @@
1
+ import hljs from 'highlight.js';
2
+ import chalk from 'chalk';
3
+ const themeColors = {
4
+ keyword: chalk.cyan,
5
+ string: chalk.green,
6
+ comment: chalk.gray,
7
+ number: chalk.yellow,
8
+ title: chalk.blue,
9
+ function: chalk.blue,
10
+ built_in: chalk.magenta,
11
+ literal: chalk.yellow,
12
+ meta: chalk.dim,
13
+ type: chalk.blueBright,
14
+ symbol: chalk.magentaBright,
15
+ regexp: chalk.red,
16
+ attr: chalk.cyanBright,
17
+ attribute: chalk.yellowBright,
18
+ addition: chalk.green,
19
+ deletion: chalk.red,
20
+ doctag: chalk.cyan,
21
+ name: chalk.blue,
22
+ selector: chalk.magenta,
23
+ quote: chalk.gray,
24
+ template_variable: chalk.redBright,
25
+ variable: chalk.redBright,
26
+ };
27
+ export function highlightCode(code, language) {
28
+ let highlighted;
29
+ try {
30
+ if (language && hljs.getLanguage(language)) {
31
+ highlighted = hljs.highlight(code, { language }).value;
32
+ }
33
+ else {
34
+ highlighted = hljs.highlightAuto(code).value;
35
+ }
36
+ }
37
+ catch {
38
+ return code;
39
+ }
40
+ const result = highlighted
41
+ .replace(/&amp;/g, '&')
42
+ .replace(/&lt;/g, '<')
43
+ .replace(/&gt;/g, '>')
44
+ .replace(/&quot;/g, '"')
45
+ .replace(/&#x27;/g, "'");
46
+ const stack = [];
47
+ const parts = [];
48
+ const regex = /(<span class="hljs-[^"]+">|<\/span>)/g;
49
+ let lastIndex = 0;
50
+ let match;
51
+ while ((match = regex.exec(result)) !== null) {
52
+ const textBefore = result.substring(lastIndex, match.index);
53
+ if (textBefore) {
54
+ parts.push(applyStackColor(textBefore, stack));
55
+ }
56
+ const tag = match[0];
57
+ if (tag === '</span>') {
58
+ stack.pop();
59
+ }
60
+ else {
61
+ const clsMatch = /hljs-([^"]+)/.exec(tag);
62
+ if (clsMatch && clsMatch[1]) {
63
+ stack.push(clsMatch[1]);
64
+ }
65
+ }
66
+ lastIndex = regex.lastIndex;
67
+ }
68
+ const textRemaining = result.substring(lastIndex);
69
+ if (textRemaining) {
70
+ parts.push(applyStackColor(textRemaining, stack));
71
+ }
72
+ return parts.join('');
73
+ }
74
+ function applyStackColor(text, stack) {
75
+ if (stack.length === 0)
76
+ return text;
77
+ const cls = stack[stack.length - 1];
78
+ const colorFn = themeColors[cls] || chalk.reset;
79
+ return colorFn(text);
80
+ }
@@ -0,0 +1 @@
1
+ export declare function renderMarkdownToAnsi(markdown: string): string;
@@ -0,0 +1,138 @@
1
+ import { marked } from 'marked';
2
+ import chalk from 'chalk';
3
+ import { theme } from '../theme.js';
4
+ import { highlightCode } from './highlight.js';
5
+ import { terminalWidth, horizontalLine, stripTerminalControls } from './ansi.js';
6
+ export function renderMarkdownToAnsi(markdown) {
7
+ const tokens = marked.lexer(stripTerminalControls(markdown));
8
+ return renderTokens(tokens).trim();
9
+ }
10
+ function renderTokens(tokens) {
11
+ return tokens.map(token => renderToken(token)).join('');
12
+ }
13
+ function renderToken(token) {
14
+ switch (token.type) {
15
+ case 'heading': {
16
+ const t = token;
17
+ const text = renderTokens(t.tokens);
18
+ const boldText = chalk.bold(text);
19
+ let coloredText = boldText;
20
+ switch (t.depth) {
21
+ case 1:
22
+ coloredText = chalk.hex(theme.primary)(boldText);
23
+ break;
24
+ case 2:
25
+ coloredText = chalk.hex(theme.accent)(boldText);
26
+ break;
27
+ case 3:
28
+ coloredText = chalk.hex(theme.user)(boldText);
29
+ break;
30
+ case 4:
31
+ coloredText = chalk.hex(theme.tool)(boldText);
32
+ break;
33
+ default:
34
+ coloredText = chalk.hex(theme.primary)(boldText);
35
+ break;
36
+ }
37
+ return `\n${coloredText}\n`;
38
+ }
39
+ case 'paragraph': {
40
+ const t = token;
41
+ return `${renderTokens(t.tokens)}\n\n`;
42
+ }
43
+ case 'code': {
44
+ const t = token;
45
+ const lang = t.lang || '';
46
+ const highlighted = highlightCode(t.text, lang);
47
+ const width = terminalWidth();
48
+ const topBorder = chalk.hex(theme.border)(`╭${'─'.repeat(Math.max(1, width - 2))}╮`);
49
+ const bottomBorder = chalk.hex(theme.border)(`╰${'─'.repeat(Math.max(1, width - 2))}╯`);
50
+ const lines = highlighted.split('\n');
51
+ const content = lines.map(line => `${chalk.hex(theme.border)('│')} ${line}`).join('\n');
52
+ let header = '';
53
+ if (lang) {
54
+ header = chalk.hex(theme.codeLang)(` ${lang} \n`);
55
+ }
56
+ return `\n${topBorder}\n${header}${content}\n${bottomBorder}\n\n`;
57
+ }
58
+ case 'codespan': {
59
+ const t = token;
60
+ return chalk.bgGray.white(` ${t.text} `);
61
+ }
62
+ case 'strong': {
63
+ const t = token;
64
+ return chalk.bold(renderTokens(t.tokens));
65
+ }
66
+ case 'em': {
67
+ const t = token;
68
+ return chalk.italic(renderTokens(t.tokens));
69
+ }
70
+ case 'del': {
71
+ const t = token;
72
+ return chalk.strikethrough(renderTokens(t.tokens));
73
+ }
74
+ case 'list': {
75
+ const t = token;
76
+ return t.items.map((item, index) => {
77
+ const startNum = typeof t.start === 'number' ? t.start : Number(t.start || 1);
78
+ const prefix = t.ordered ? `${startNum + index}. ` : '• ';
79
+ return ` ${chalk.hex(theme.primary)(prefix)}${renderTokens(item.tokens).trim()}\n`;
80
+ }).join('') + '\n';
81
+ }
82
+ case 'blockquote': {
83
+ const t = token;
84
+ const text = renderTokens(t.tokens).trim();
85
+ const lines = text.split('\n');
86
+ const quoted = lines.map(line => `${chalk.hex(theme.dim)('│')} ${chalk.hex(theme.dim)(line)}`).join('\n');
87
+ return `\n${quoted}\n\n`;
88
+ }
89
+ case 'hr': {
90
+ return `\n${chalk.hex(theme.border)(horizontalLine())}\n\n`;
91
+ }
92
+ case 'link': {
93
+ const t = token;
94
+ const text = renderTokens(t.tokens);
95
+ return chalk.blue.underline(text) + chalk.dim(` (${t.href})`);
96
+ }
97
+ case 'table': {
98
+ const t = token;
99
+ let tableOut = '\n';
100
+ const drawRow = (row) => {
101
+ return '| ' + row.map(cell => renderTokens(cell)).join(' | ') + ' |\n';
102
+ };
103
+ tableOut += drawRow(t.header.map(h => h.tokens));
104
+ tableOut += '|' + t.header.map(() => '---').join('|') + '|\n';
105
+ for (const row of t.rows) {
106
+ tableOut += drawRow(row.map(c => c.tokens));
107
+ }
108
+ return tableOut + '\n';
109
+ }
110
+ case 'space': {
111
+ return '';
112
+ }
113
+ case 'text': {
114
+ const t = token;
115
+ if (t.tokens && t.tokens.length > 0) {
116
+ return renderTokens(t.tokens);
117
+ }
118
+ return t.text;
119
+ }
120
+ case 'br': {
121
+ return '\n';
122
+ }
123
+ case 'escape': {
124
+ const t = token;
125
+ return t.text;
126
+ }
127
+ case 'image': {
128
+ const t = token;
129
+ return chalk.dim(`[图片: ${t.text}]`);
130
+ }
131
+ default: {
132
+ if ('raw' in token) {
133
+ return token.raw;
134
+ }
135
+ return '';
136
+ }
137
+ }
138
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hunterzhu/pulse-cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/zhuhengtan/Pulse"
@@ -19,6 +19,17 @@
19
19
  "build": "tsc -p tsconfig.json"
20
20
  },
21
21
  "dependencies": {
22
- "@hunterzhu/pulse-server": "0.1.4"
22
+ "@hunterzhu/pulse-server": "0.1.5",
23
+ "ink": "^7.1.1",
24
+ "react": "^19.0.0",
25
+ "ink-spinner": "^5.0.0",
26
+ "ink-select-input": "^6.2.0",
27
+ "ink-text-input": "^6.0.0",
28
+ "marked": "^14.0.0",
29
+ "highlight.js": "^11.10.0",
30
+ "chalk": "^5.4.1"
31
+ },
32
+ "devDependencies": {
33
+ "@types/react": "^19.0.0"
23
34
  }
24
35
  }