@hunterzhu/pulse-cli 0.2.1 → 0.4.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.
@@ -6,25 +6,58 @@ import { renderMarkdownToAnsi } from './markdown.js';
6
6
  export function transcriptLines(messages, width, verbosity = 'normal') {
7
7
  const blocks = messages.map((message) => {
8
8
  const text = stripTerminalControls(message.text);
9
- if (message.role === 'user')
10
- return chalk.cyan(`你 › ${text}`);
9
+ const contentWidth = Math.max(8, width - 2);
11
10
  if (message.role === 'system')
12
- return chalk.dim(text);
11
+ return { id: message.id, role: message.role, lines: wrapAnsi(chalk.dim(`· ${text}`), Math.max(8, width), { hard: true, trim: true }).split('\n') };
12
+ if (message.role === 'user') {
13
+ const body = wrapAnsi(text, contentWidth, { hard: true, trim: false }).split('\n');
14
+ return { id: message.id, role: message.role, lines: [chalk.blue.bold('❯ 你发来'), ...body.map((line) => ` ${line}`)] };
15
+ }
13
16
  const parts = [];
14
17
  const tools = message.toolCalls ?? [];
15
18
  if (verbosity !== 'quiet' && tools.length) {
16
19
  const failed = tools.filter((tool) => tool.status === 'failed' || tool.status === 'cancelled');
17
20
  parts.push(chalk.dim(`工具 · ${tools.filter((tool) => tool.status === 'succeeded').length}/${tools.length} 已完成${failed.length ? ` · ${failed.length} 未成功` : ''}`));
18
- for (const tool of verbosity === 'verbose' ? tools : failed) {
19
- const detail = `${tool.name} · ${tool.status}${verbosity === 'verbose' ? `\n${JSON.stringify(tool.arguments ?? {}, null, 2)}` : ''}${tool.result === undefined ? '' : `\n${typeof tool.result === 'string' ? tool.result : JSON.stringify(tool.result, null, 2)}`}`;
20
- parts.push(chalk.dim(stripTerminalControls(detail)));
21
+ const statusLabel = { running: '正在调用', succeeded: '已完成', failed: '失败', cancelled: '已取消' };
22
+ if (verbosity === 'verbose') {
23
+ for (const tool of tools) {
24
+ const detail = `${tool.name} · ${statusLabel[tool.status]}\n${JSON.stringify(tool.arguments ?? {}, null, 2)}${tool.result === undefined ? '' : `\n${typeof tool.result === 'string' ? tool.result : JSON.stringify(tool.result, null, 2)}`}`;
25
+ parts.push(chalk.dim(stripTerminalControls(detail)));
26
+ }
27
+ }
28
+ else {
29
+ const groups = new Map();
30
+ for (const tool of tools) {
31
+ const result = tool.result && typeof tool.result === 'object' && !Array.isArray(tool.result) ? tool.result : undefined;
32
+ const error = tool.status === 'failed' || tool.status === 'cancelled'
33
+ ? [result?.code, result?.message].filter((value) => typeof value === 'string').at(-1)
34
+ : undefined;
35
+ const key = `${tool.name}\0${tool.status}\0${error ?? ''}`;
36
+ const group = groups.get(key);
37
+ if (group)
38
+ group.count++;
39
+ else
40
+ groups.set(key, { name: tool.name, status: tool.status, count: 1, ...(error ? { error } : {}) });
41
+ }
42
+ for (const group of groups.values()) {
43
+ parts.push(chalk.dim(`${group.name}${group.count > 1 ? ` × ${group.count}` : ''} · ${statusLabel[group.status]}${group.error ? ` · ${group.error}` : ''}`));
44
+ }
21
45
  }
22
46
  }
23
47
  if (text)
24
- parts.push(chalk.cyan.bold('Pulse'), renderMarkdownToAnsi(text));
25
- return parts.join('\n');
26
- }).filter(Boolean);
27
- return wrapAnsi(blocks.join('\n\n'), Math.max(8, width), { hard: true, trim: false }).split('\n');
48
+ parts.push(renderMarkdownToAnsi(text));
49
+ const body = wrapAnsi(parts.join('\n'), contentWidth, { hard: true, trim: false }).split('\n');
50
+ const streamLabel = message.streamStatus === 'streaming' ? chalk.yellow(' · 正在生成') : message.streamStatus === 'incomplete' ? chalk.red(' · 未完成') : '';
51
+ return { id: message.id, role: message.role, lines: [chalk.cyan.bold('Pulse') + streamLabel, ...body.map((line) => ` ${line}`)] };
52
+ }).filter((block) => block.lines.length > 0);
53
+ return blocks.flatMap((block, index) => {
54
+ if (index === blocks.length - 1)
55
+ return block.lines;
56
+ const next = blocks[index + 1];
57
+ const isLiveActivity = (item) => item.role === 'system' && item.id.startsWith('run-activity-');
58
+ const separator = isLiveActivity(block) || isLiveActivity(next) ? [] : [''];
59
+ return [...block.lines, ...separator];
60
+ });
28
61
  }
29
62
  export function scrollAction(input, key) {
30
63
  if (key.pageUp || (key.ctrl && input === 'p'))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hunterzhu/pulse-cli",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "description": "A local-first AI task assistant for coding, research, and file workflows.",
5
5
  "keywords": [
6
6
  "ai",
@@ -35,7 +35,7 @@
35
35
  "registry": "https://registry.npmjs.org"
36
36
  },
37
37
  "dependencies": {
38
- "@hunterzhu/pulse-server": "0.2.1",
38
+ "@hunterzhu/pulse-server": "0.4.0",
39
39
  "chalk": "^5.4.1",
40
40
  "highlight.js": "^11.10.0",
41
41
  "ink": "^7.1.1",
@@ -1,11 +1,58 @@
1
1
  import { ensurePulseUserConfig, defaultPulseConfigPath } from '../dist/config.js'
2
2
 
3
+ // Install-time welcome screen. Colors stay off for piped or captured install
4
+ // logs and can be forced with FORCE_COLOR for debugging.
5
+ const colorEnabled =
6
+ process.env.NO_COLOR === undefined &&
7
+ process.env.FORCE_COLOR !== '0' &&
8
+ (process.stdout.isTTY === true || process.env.FORCE_COLOR !== undefined)
9
+
10
+ const paint = (code) => (text) => (colorEnabled ? `\u001b[${code}m${text}\u001b[0m` : text)
11
+ const cyan = paint('36')
12
+ const bold = paint('1')
13
+ const dim = paint('2')
14
+
15
+ const banner = [
16
+ '██████╗ ██╗ ██╗██╗ ███████╗███████╗',
17
+ '██╔══██╗██║ ██║██║ ██╔════╝██╔════╝',
18
+ '██████╔╝██║ ██║██║ ███████╗█████╗ ',
19
+ '██╔═══╝ ██║ ██║██║ ╚════██║██╔══╝ ',
20
+ '██║ ╚██████╔╝███████╗███████║███████╗',
21
+ '╚═╝ ╚═════╝ ╚══════╝╚══════╝╚══════╝',
22
+ ].map((line) => cyan(line)).join('\n')
23
+
24
+ function printWelcome({ configPath, error } = {}) {
25
+ const lines = [
26
+ '',
27
+ banner,
28
+ '',
29
+ ` ${bold('Pulse')} - a neural-signal like AI assistant for coding, research and file tasks.`,
30
+ '',
31
+ ` ${dim('Get started:')}`,
32
+ ` ${cyan('cd project_dir')}`,
33
+ ` ${cyan('pulse')}`,
34
+ '',
35
+ ]
36
+
37
+ if (configPath) {
38
+ // Print the fully resolved absolute path on its own, uncolored line so it
39
+ // can be copied straight out of the terminal without ANSI escape codes.
40
+ lines.push(` ${dim('Config ready at')}`)
41
+ lines.push(` ${configPath}`)
42
+ } else {
43
+ lines.push(` ${dim('Config not created:')} ${error}`)
44
+ lines.push(` ${dim('Run "pulse setup" later to create it.')}`)
45
+ }
46
+
47
+ lines.push('')
48
+ console.log(lines.join('\n'))
49
+ }
50
+
3
51
  try {
4
52
  const configPath = defaultPulseConfigPath()
5
53
  await ensurePulseUserConfig(configPath)
6
- console.log(`Pulse config is ready at ${configPath}`)
54
+ printWelcome({ configPath })
7
55
  } catch (error) {
8
56
  const message = error instanceof Error ? error.message : String(error)
9
- console.warn(`Pulse could not create its default config: ${message}`)
10
- console.warn('Run "pulse setup" later to create it.')
57
+ printWelcome({ error: message })
11
58
  }