@hunterzhu/pulse-cli 0.1.3 → 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
package/dist/bin.d.ts CHANGED
@@ -1,2 +1,11 @@
1
1
  #!/usr/bin/env node
2
- export {};
2
+ import type { LocalHostOptions } from '@hunterzhu/pulse-server';
3
+ export declare const version: string;
4
+ export interface Parsed {
5
+ command: string;
6
+ positionals: string[];
7
+ options: Record<string, string | boolean>;
8
+ }
9
+ export declare function parse(argv: string[]): Parsed;
10
+ export declare function option(options: Parsed['options'], key: string): string | undefined;
11
+ export declare function hostOptions(parsed: Parsed): Promise<LocalHostOptions>;
package/dist/bin.js CHANGED
@@ -1,12 +1,18 @@
1
1
  #!/usr/bin/env node
2
- import { createInterface } from 'node:readline';
3
- import { access, chmod, mkdir, writeFile } from 'node:fs/promises';
4
2
  import { createRequire } from 'node:module';
5
- import { dirname } from 'node:path';
6
- import { createLocalHost } from '@hunterzhu/pulse-server';
7
- import { defaultPulseConfig, defaultPulseConfigPath, ensurePulseUserConfig, expandHome, loadPulseConfig } from './config.js';
3
+ import { ensurePulseUserConfig, expandHome, loadPulseConfig } from './config.js';
4
+ import { runInteractive } from './commands/interactive.js';
5
+ import { runOneShot } from './commands/run.js';
6
+ import { runDoctor } from './commands/doctor.js';
7
+ import { runSessions } from './commands/sessions.js';
8
+ import { runResume } from './commands/resume.js';
9
+ import { runSetup } from './commands/setup.js';
8
10
  const packageManifest = createRequire(import.meta.url)('../package.json');
9
- const version = packageManifest.version;
11
+ export const version = packageManifest.version;
12
+ process.stdout.on('error', (error) => {
13
+ if (error.code !== 'EPIPE')
14
+ process.exitCode = 1;
15
+ });
10
16
  const help = `Pulse ${version}
11
17
 
12
18
  Usage:
@@ -35,7 +41,7 @@ Options:
35
41
  --version, -v show the version
36
42
  setup --force write a user config template (also created on first run)
37
43
  `;
38
- function parse(argv) {
44
+ export function parse(argv) {
39
45
  const options = {};
40
46
  const positionals = [];
41
47
  let command = '';
@@ -63,8 +69,9 @@ function parse(argv) {
63
69
  options[key] = next;
64
70
  index++;
65
71
  }
66
- else
72
+ else {
67
73
  options[key] = true;
74
+ }
68
75
  continue;
69
76
  }
70
77
  if (arg.startsWith('-') && arg.length === 2) {
@@ -72,134 +79,56 @@ function parse(argv) {
72
79
  options[key] = true;
73
80
  continue;
74
81
  }
75
- if (!command && ['run', 'sessions', 'resume', 'doctor', 'setup'].includes(arg))
82
+ if (!command && ['run', 'sessions', 'resume', 'doctor', 'setup'].includes(arg)) {
76
83
  command = arg;
77
- else
84
+ }
85
+ else {
78
86
  positionals.push(arg);
87
+ }
79
88
  }
80
89
  return { command, positionals, options };
81
90
  }
82
- function option(options, key) { const value = options[key]; return typeof value === 'string' ? value : undefined; }
83
- async function hostOptions(parsed) { const requestedCwd = expandHome(option(parsed.options, 'cwd')); const explicitConfig = expandHome(option(parsed.options, 'config')); if (explicitConfig === undefined && process.env.PULSE_CONFIG === undefined)
84
- await ensurePulseUserConfig(); const config = (await loadPulseConfig(requestedCwd ?? process.cwd(), explicitConfig, parsed.options['trust-workspace'] === true)).value; const providerName = option(parsed.options, 'provider') ?? process.env.PULSE_PROVIDER ?? config.provider?.provider; const model = option(parsed.options, 'model') ?? process.env.PULSE_MODEL ?? config.provider?.model; const baseURL = option(parsed.options, 'base-url') ?? process.env.PULSE_BASE_URL ?? config.provider?.baseURL; const apiKeyEnv = config.provider?.apiKeyEnv ?? (providerName === 'anthropic' ? 'ANTHROPIC_API_KEY' : 'OPENAI_API_KEY'); const apiKey = process.env[apiKeyEnv]; const provider = providerName ? { provider: providerName, ...(model === undefined ? {} : { defaultModel: model }), ...(baseURL === undefined ? {} : { baseURL }), ...(apiKey === undefined ? {} : { apiKey }) } : undefined; const cwd = requestedCwd ?? expandHome(config.cwd); const dataDir = expandHome(option(parsed.options, 'data-dir') ?? process.env.PULSE_DATA_DIR ?? config.dataDir); const mockResponse = option(parsed.options, 'mock-response'); const approvalMode = parsed.options['read-only'] === true ? 'read-only' : parsed.options['auto-approve'] === true || process.env.PULSE_AUTO_APPROVE === '1' ? 'auto' : config.approvalMode; const allowNetwork = parsed.options['allow-network'] === true || process.env.PULSE_ALLOW_NETWORK === '1' ? true : config.allowNetwork; return { ...(cwd === undefined ? {} : { cwd }), ...(dataDir === undefined ? {} : { dataDir }), ...(provider === undefined ? {} : { provider }), ...(mockResponse === undefined ? {} : { mockResponse }), ...(approvalMode === undefined ? {} : { approvalMode }), ...(allowNetwork === undefined ? {} : { allowNetwork }) }; }
85
- async function setupConfig(force, explicitPath) { const path = explicitPath ?? process.env.PULSE_CONFIG ?? defaultPulseConfigPath(); try {
86
- await access(path);
87
- if (!force)
88
- throw new Error(`CONFIG_EXISTS:${path}`);
91
+ export function option(options, key) {
92
+ const value = options[key];
93
+ return typeof value === 'string' ? value : undefined;
89
94
  }
90
- catch (error) {
91
- if (error.code !== 'ENOENT' && error instanceof Error && error.message.startsWith('CONFIG_EXISTS:'))
92
- throw error;
93
- } await mkdir(dirname(path), { recursive: true }); await writeFile(path, `${JSON.stringify(defaultPulseConfig, null, 2)}\n`, { mode: 0o600 }); await chmod(path, 0o600); process.stdout.write(`Wrote ${path}\n`); }
94
- function writeEvent(event, format) { if (format === 'jsonl') {
95
- process.stdout.write(`${JSON.stringify(event)}\n`);
96
- return;
97
- } if (event.type === 'text')
98
- process.stdout.write(String(event.data ?? ''));
99
- else if (event.type === 'waiting')
100
- process.stdout.write(`\n[需要输入] ${JSON.stringify(event.data ?? '')}\n`);
101
- else if (event.type === 'error')
102
- process.stderr.write(`\n[错误] ${String(event.data ?? '')}\n`); }
103
- async function consume(run, format, approvalInput) { let streamedText = false; let ownedApprovalInput = false; let input = approvalInput; try {
104
- for await (const event of run.events) {
105
- if (event.type === 'text')
106
- streamedText = true;
107
- writeEvent(event, format);
108
- if (event.type === 'waiting') {
109
- const payload = event.data && typeof event.data === 'object' && !Array.isArray(event.data) ? event.data : {};
110
- const effectId = typeof payload.effectId === 'string' ? payload.effectId : undefined;
111
- const request = payload.input && typeof payload.input === 'object' && !Array.isArray(payload.input) ? payload.input : {};
112
- if (!effectId) {
113
- await run.cancel('INVALID_APPROVAL_REQUEST');
114
- continue;
115
- }
116
- if (!process.stdin.isTTY) {
117
- await run.cancel('INTERACTION_REQUIRED');
118
- continue;
119
- }
120
- if (!input) {
121
- input = createInterface({ input: process.stdin, output: process.stderr, terminal: true });
122
- ownedApprovalInput = true;
123
- }
124
- const prompt = typeof request.prompt === 'string' ? request.prompt : 'Approve this operation?';
125
- const answer = await new Promise((resolve) => input.question(`${prompt}\nApprove? [y/N] `, resolve));
126
- const approved = ['y', 'yes', '是', '确认'].includes(answer.trim().toLocaleLowerCase());
127
- await run.reply(effectId, { approved, ...(approved ? {} : { reason: answer.trim() || 'User denied the operation.' }) });
128
- }
95
+ export async function hostOptions(parsed) {
96
+ const requestedCwd = expandHome(option(parsed.options, 'cwd'));
97
+ const explicitConfig = expandHome(option(parsed.options, 'config'));
98
+ if (explicitConfig === undefined && process.env.PULSE_CONFIG === undefined) {
99
+ await ensurePulseUserConfig();
129
100
  }
130
- const outcome = await run.outcome();
131
- if (format === 'jsonl') {
132
- process.stdout.write(`${JSON.stringify({ schemaVersion: 1, type: 'result', runId: run.id, status: outcome.status, text: outcome.text ?? null, error: outcome.error ?? null })}\n`);
133
- }
134
- else if (outcome.status === 'failed' && outcome.error)
135
- process.stderr.write(`\n[错误] ${outcome.error.code}: ${outcome.error.message}\n`);
136
- else if (!streamedText && outcome.text)
137
- process.stdout.write(`${outcome.text}\n`);
138
- else
139
- process.stdout.write(`\n[${outcome.status}]\n`);
140
- return outcome;
141
- }
142
- finally {
143
- if (ownedApprovalInput)
144
- input?.close();
145
- } }
146
- async function consumeManaged(run, format, approvalInput, setActive) { setActive(run); try {
147
- return await consume(run, format, approvalInput);
148
- }
149
- finally {
150
- setActive(undefined);
151
- } }
152
- async function interactive(host, conversationId, setActive) {
153
- const conversation = conversationId ? await host.getConversation(conversationId) : await host.createConversation();
154
- process.stderr.write(`Pulse · ${conversation.summary.cwd}\nType /help for commands.\n`);
155
- const input = createInterface({ input: process.stdin, output: process.stderr, terminal: process.stdin.isTTY });
156
- input.setPrompt('› ');
157
- const prompt = () => { if (input.terminal)
158
- input.prompt(); };
159
- prompt();
160
- for await (const line of input) {
161
- const text = line.trim();
162
- if (!text) {
163
- prompt();
164
- continue;
165
- }
166
- if (text === '/exit' || text === '/quit')
167
- break;
168
- if (text === '/help') {
169
- process.stderr.write('Commands: /help /status /tools /artifacts /exit\n');
170
- prompt();
171
- continue;
172
- }
173
- if (text === '/status') {
174
- process.stderr.write(`${JSON.stringify(conversation.summary)}\n`);
175
- prompt();
176
- continue;
177
- }
178
- if (text === '/tools') {
179
- process.stderr.write(`${(await host.doctor()).tools.join(', ')}\n`);
180
- prompt();
181
- continue;
182
- }
183
- if (text === '/artifacts') {
184
- process.stderr.write(`${JSON.stringify(await host.listArtifacts(conversation.id), null, 2)}\n`);
185
- prompt();
186
- continue;
187
- }
188
- if (text === '/new') {
189
- process.stderr.write('Start another `npx @hunterzhu/pulse-cli` process for a new conversation.\n');
190
- prompt();
191
- continue;
192
- }
193
- try {
194
- const run = await host.sendMessage(conversation.id, { text });
195
- await consumeManaged(run, 'text', input, setActive);
196
- }
197
- catch (error) {
198
- process.stderr.write(`[错误] ${error instanceof Error ? error.message : String(error)}\n`);
199
- }
200
- prompt();
201
- }
202
- input.close();
101
+ const config = (await loadPulseConfig(requestedCwd ?? process.cwd(), explicitConfig, parsed.options['trust-workspace'] === true)).value;
102
+ const providerName = option(parsed.options, 'provider') ?? process.env.PULSE_PROVIDER ?? config.provider?.provider;
103
+ const model = option(parsed.options, 'model') ?? process.env.PULSE_MODEL ?? config.provider?.model;
104
+ const baseURL = option(parsed.options, 'base-url') ?? process.env.PULSE_BASE_URL ?? config.provider?.baseURL;
105
+ const apiKeyEnv = config.provider?.apiKeyEnv ?? (providerName === 'anthropic' ? 'ANTHROPIC_API_KEY' : 'OPENAI_API_KEY');
106
+ const apiKey = process.env[apiKeyEnv];
107
+ const provider = providerName
108
+ ? {
109
+ provider: providerName,
110
+ ...(model === undefined ? {} : { defaultModel: model }),
111
+ ...(baseURL === undefined ? {} : { baseURL }),
112
+ ...(apiKey === undefined ? {} : { apiKey }),
113
+ }
114
+ : undefined;
115
+ const cwd = requestedCwd ?? expandHome(config.cwd);
116
+ const dataDir = expandHome(option(parsed.options, 'data-dir') ?? process.env.PULSE_DATA_DIR ?? config.dataDir);
117
+ const mockResponse = option(parsed.options, 'mock-response');
118
+ const approvalMode = parsed.options['read-only'] === true
119
+ ? 'read-only'
120
+ : parsed.options['auto-approve'] === true || process.env.PULSE_AUTO_APPROVE === '1'
121
+ ? 'auto'
122
+ : config.approvalMode;
123
+ const allowNetwork = parsed.options['allow-network'] === true || process.env.PULSE_ALLOW_NETWORK === '1' ? true : config.allowNetwork;
124
+ return {
125
+ ...(cwd === undefined ? {} : { cwd }),
126
+ ...(dataDir === undefined ? {} : { dataDir }),
127
+ ...(provider === undefined ? {} : { provider }),
128
+ ...(mockResponse === undefined ? {} : { mockResponse }),
129
+ ...(approvalMode === undefined ? {} : { approvalMode }),
130
+ ...(allowNetwork === undefined ? {} : { allowNetwork }),
131
+ };
203
132
  }
204
133
  async function main() {
205
134
  const parsed = parse(process.argv.slice(2));
@@ -212,56 +141,36 @@ async function main() {
212
141
  return 0;
213
142
  }
214
143
  if (parsed.command === 'setup') {
215
- await setupConfig(parsed.options.force === true, expandHome(option(parsed.options, 'config')));
216
- process.stdout.write('Configure the provider in that file, then run `pulse doctor`.\n');
217
- return 0;
144
+ return runSetup(parsed.options.force === true, expandHome(option(parsed.options, 'config')));
218
145
  }
219
- const host = createLocalHost(await hostOptions(parsed));
220
- await host.init();
221
- let activeRun;
222
- const onInterrupt = () => { if (activeRun) {
223
- void activeRun.cancel('USER_INTERRUPT');
146
+ const options = await hostOptions(parsed);
147
+ if (parsed.command === 'doctor') {
148
+ return runDoctor(options, parsed.options.live === true, option(parsed.options, 'format') ?? 'text');
224
149
  }
225
- else
226
- process.exitCode = 130; };
227
- process.once('SIGINT', onInterrupt);
228
- process.once('SIGTERM', onInterrupt);
229
- try {
230
- if (parsed.command === 'doctor') {
231
- const result = await host.doctor({ live: parsed.options.live === true });
232
- process.stdout.write(parsed.options.format === 'jsonl' ? `${JSON.stringify(result)}\n` : `${result.ok ? 'ok' : 'error'}\nworkspace: ${result.cwd}\ndata: ${result.dataDir}\nnode: ${result.node}\nprovider: ${result.provider}\ntools: ${result.tools.join(', ')}\n${result.live ? `live: ${result.live.ok ? 'ok' : 'error'} (${result.live.message})\n` : ''}${result.errors.map((item) => `error: ${item}`).join('\n')}`.trim() + '\n');
233
- return result.ok ? 0 : 1;
234
- }
235
- if (parsed.command === 'sessions') {
236
- const sessions = await host.listConversations();
237
- process.stdout.write(parsed.options.format === 'jsonl' ? sessions.map((item) => `${JSON.stringify(item)}\n`).join('') : (sessions.length ? sessions.map((item) => `${item.id}\t${item.updatedAt}\t${item.title}\t${item.cwd}`).join('\n') + '\n' : 'No conversations.\n'));
238
- return 0;
239
- }
240
- if (parsed.command === 'run') {
241
- const task = parsed.positionals.join(' ').trim();
242
- if (!task)
243
- throw new Error('TASK_REQUIRED');
244
- const cwd = option(parsed.options, 'cwd');
245
- const conversation = await host.createConversation(cwd === undefined ? {} : { cwd });
246
- const outcome = await consumeManaged(await host.sendMessage(conversation.id, { text: task, format: option(parsed.options, 'format') === 'jsonl' ? 'jsonl' : 'text' }), option(parsed.options, 'format') ?? 'text', undefined, (run) => { activeRun = run; });
247
- return outcome.status === 'succeeded' ? 0 : outcome.status === 'cancelled' ? 3 : 1;
248
- }
249
- if (parsed.command === 'resume') {
250
- const id = parsed.positionals.shift();
251
- const task = parsed.positionals.join(' ').trim();
252
- if (!id)
253
- throw new Error('RESUME_REQUIRES_ID');
254
- const run = task ? await host.sendMessage(id, { text: task }) : await host.resumeRun(id);
255
- const outcome = await consumeManaged(run, option(parsed.options, 'format') ?? 'text', undefined, (current) => { activeRun = current; });
256
- return outcome.status === 'succeeded' ? 0 : outcome.status === 'cancelled' ? 3 : 1;
257
- }
258
- await interactive(host, undefined, (run) => { activeRun = run; });
259
- return 0;
150
+ if (parsed.command === 'sessions') {
151
+ return runSessions(options, option(parsed.options, 'format') ?? 'text', version);
152
+ }
153
+ if (parsed.command === 'run') {
154
+ const task = parsed.positionals.join(' ').trim();
155
+ if (!task)
156
+ throw new Error('TASK_REQUIRED');
157
+ return runOneShot(options, task, option(parsed.options, 'format') ?? 'text', version);
260
158
  }
261
- finally {
262
- process.removeListener('SIGINT', onInterrupt);
263
- process.removeListener('SIGTERM', onInterrupt);
264
- await host.close();
159
+ if (parsed.command === 'resume') {
160
+ const id = parsed.positionals.shift();
161
+ const task = parsed.positionals.join(' ').trim();
162
+ if (!id)
163
+ throw new Error('RESUME_REQUIRES_ID');
164
+ return runResume(options, id, task || undefined, option(parsed.options, 'format') ?? 'text', version);
265
165
  }
166
+ // 默认启动交互式 Ink 界面
167
+ return runInteractive(options, undefined, undefined, version);
266
168
  }
267
- main().then((code) => { process.exitCode = code; }).catch((error) => { process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); process.exitCode = 1; });
169
+ main()
170
+ .then((code) => {
171
+ process.exitCode = code;
172
+ })
173
+ .catch((error) => {
174
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
175
+ process.exitCode = 1;
176
+ });
@@ -0,0 +1,2 @@
1
+ import { type LocalHostOptions } from '@hunterzhu/pulse-server';
2
+ export declare function runDoctor(options: LocalHostOptions, live?: boolean, format?: string): Promise<number>;
@@ -0,0 +1,36 @@
1
+ import { createLocalHost } from '@hunterzhu/pulse-server';
2
+ import chalk from 'chalk';
3
+ import { theme } from '../theme.js';
4
+ export async function runDoctor(options, live = false, format = 'text') {
5
+ const host = createLocalHost(options);
6
+ try {
7
+ const result = await host.doctor({ live });
8
+ if (format === 'json' || format === 'jsonl') {
9
+ process.stdout.write(`${JSON.stringify(result)}\n`);
10
+ return result.ok ? 0 : 1;
11
+ }
12
+ process.stdout.write(`${result.ok ? chalk.hex(theme.success)('● 诊断通过') : chalk.hex(theme.error)('● 发现问题')}\n`);
13
+ process.stdout.write(`工作区: ${chalk.hex(theme.accent)(result.cwd)}\n`);
14
+ process.stdout.write(`数据目录: ${chalk.hex(theme.dim)(result.dataDir)}\n`);
15
+ process.stdout.write(`Node 版本: ${result.node}\n`);
16
+ process.stdout.write(`Provider: ${chalk.hex(theme.primary)(result.provider)}\n`);
17
+ process.stdout.write(`可用工具: ${result.tools.map((t) => chalk.hex(theme.tool)(t)).join(', ')}\n`);
18
+ if (result.live) {
19
+ process.stdout.write(`实时连接: ${result.live.ok ? chalk.hex(theme.success)('✓ 成功') : chalk.hex(theme.error)(`✗ 失败 (${result.live.message})`)}\n`);
20
+ }
21
+ if (result.errors.length > 0) {
22
+ process.stdout.write(`\n${chalk.hex(theme.error)('错误详情:')}\n`);
23
+ for (const err of result.errors) {
24
+ process.stdout.write(` ✗ ${err}\n`);
25
+ }
26
+ }
27
+ return result.ok ? 0 : 1;
28
+ }
29
+ catch (error) {
30
+ process.stderr.write(`${chalk.hex(theme.error)('诊断执行失败:')} ${error instanceof Error ? error.message : String(error)}\n`);
31
+ return 1;
32
+ }
33
+ finally {
34
+ await host.close();
35
+ }
36
+ }
@@ -0,0 +1,2 @@
1
+ import type { LocalHostOptions } from '@hunterzhu/pulse-server';
2
+ export declare function runInteractive(options: LocalHostOptions, conversationId?: string | undefined, initialTask?: string | undefined, version?: string): Promise<number>;
@@ -0,0 +1,27 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { render } from 'ink';
3
+ import { App } from '../components/App.js';
4
+ import { closePendingHosts } from '../hooks/useHost.js';
5
+ export async function runInteractive(options, conversationId, initialTask, version = '0.1.4') {
6
+ const { waitUntilExit, unmount } = render(_jsx(App, { hostOptions: options, conversationId: conversationId, initialTask: initialTask, version: version }));
7
+ let interrupted = false;
8
+ const onSignal = () => {
9
+ interrupted = true;
10
+ unmount();
11
+ };
12
+ process.on('SIGINT', onSignal);
13
+ process.on('SIGTERM', onSignal);
14
+ try {
15
+ await waitUntilExit();
16
+ return interrupted ? 130 : 0;
17
+ }
18
+ catch (error) {
19
+ console.error(error);
20
+ return 1;
21
+ }
22
+ finally {
23
+ process.off('SIGINT', onSignal);
24
+ process.off('SIGTERM', onSignal);
25
+ await closePendingHosts();
26
+ }
27
+ }
@@ -0,0 +1,2 @@
1
+ import { type LocalHostOptions } from '@hunterzhu/pulse-server';
2
+ export declare function runResume(options: LocalHostOptions, conversationId: string, task?: string | undefined, format?: string, version?: string): Promise<number>;
@@ -0,0 +1,75 @@
1
+ import { createLocalHost } from '@hunterzhu/pulse-server';
2
+ import { runInteractive } from './interactive.js';
3
+ import { bindRunSignals } from './signals.js';
4
+ function writeEvent(event, format) {
5
+ if (format === 'jsonl') {
6
+ process.stdout.write(`${JSON.stringify(event)}\n`);
7
+ return;
8
+ }
9
+ if (event.type === 'text') {
10
+ process.stdout.write(String(event.data ?? ''));
11
+ }
12
+ else if (event.type === 'waiting') {
13
+ process.stdout.write(`\n[需要输入] ${JSON.stringify(event.data ?? '')}\n`);
14
+ }
15
+ else if (event.type === 'error') {
16
+ process.stderr.write(`\n[错误] ${String(event.data ?? '')}\n`);
17
+ }
18
+ }
19
+ export async function runResume(options, conversationId, task, format = 'text', version = '0.1.4') {
20
+ // 如果在交互式终端且没有指定 jsonl,启动交互式 Ink 界面直接继续会话
21
+ if (process.stdout.isTTY && format !== 'jsonl') {
22
+ return runInteractive(options, conversationId, task, version);
23
+ }
24
+ const host = createLocalHost(options);
25
+ let activeRun;
26
+ const unbindSignals = bindRunSignals(host, () => activeRun);
27
+ try {
28
+ await host.init();
29
+ const run = task
30
+ ? await host.sendMessage(conversationId, { text: task, format: format === 'jsonl' ? 'jsonl' : 'text' })
31
+ : await host.resumeRun(conversationId);
32
+ activeRun = run;
33
+ let streamedText = false;
34
+ let approvalCancelled = false;
35
+ for await (const event of run.events) {
36
+ if (event.type === 'text')
37
+ streamedText = true;
38
+ writeEvent(event, format);
39
+ if (event.type === 'waiting' && !approvalCancelled) {
40
+ approvalCancelled = true;
41
+ process.stderr.write('\n[错误] 此运行处于非交互模式,无法请求工具审批;运行已取消。\n');
42
+ await run.cancel('INTERACTION_REQUIRED');
43
+ }
44
+ }
45
+ const outcome = await run.outcome();
46
+ if (format === 'jsonl') {
47
+ process.stdout.write(`${JSON.stringify({
48
+ schemaVersion: 1,
49
+ type: 'result',
50
+ runId: run.id,
51
+ status: outcome.status,
52
+ text: outcome.text ?? null,
53
+ error: outcome.error ?? null,
54
+ })}\n`);
55
+ }
56
+ else if (outcome.status === 'failed' && outcome.error) {
57
+ process.stderr.write(`\n[错误] ${outcome.error.code}: ${outcome.error.message}\n`);
58
+ }
59
+ else if (!streamedText && outcome.text) {
60
+ process.stdout.write(`${outcome.text}\n`);
61
+ }
62
+ else {
63
+ process.stdout.write(`\n[${outcome.status}]\n`);
64
+ }
65
+ return outcome.status === 'succeeded' ? 0 : outcome.status === 'cancelled' ? 3 : 1;
66
+ }
67
+ catch (error) {
68
+ process.stderr.write(`[错误] ${error instanceof Error ? error.message : String(error)}\n`);
69
+ return 1;
70
+ }
71
+ finally {
72
+ unbindSignals();
73
+ await host.close();
74
+ }
75
+ }
@@ -0,0 +1,2 @@
1
+ import { type LocalHostOptions } from '@hunterzhu/pulse-server';
2
+ export declare function runOneShot(options: LocalHostOptions, task: string, format?: string, version?: string): Promise<number>;
@@ -0,0 +1,73 @@
1
+ import { createLocalHost } from '@hunterzhu/pulse-server';
2
+ import { runInteractive } from './interactive.js';
3
+ import { bindRunSignals } from './signals.js';
4
+ function writeEvent(event, format) {
5
+ if (format === 'jsonl') {
6
+ process.stdout.write(`${JSON.stringify(event)}\n`);
7
+ return;
8
+ }
9
+ if (event.type === 'text') {
10
+ process.stdout.write(String(event.data ?? ''));
11
+ }
12
+ else if (event.type === 'waiting') {
13
+ process.stdout.write(`\n[需要输入] ${JSON.stringify(event.data ?? '')}\n`);
14
+ }
15
+ else if (event.type === 'error') {
16
+ process.stderr.write(`\n[错误] ${String(event.data ?? '')}\n`);
17
+ }
18
+ }
19
+ export async function runOneShot(options, task, format = 'text', version = '0.1.4') {
20
+ // 如果在交互式终端且没有指定 jsonl,启动交互式 Ink 界面直接执行任务
21
+ if (process.stdout.isTTY && format !== 'jsonl') {
22
+ return runInteractive(options, undefined, task, version);
23
+ }
24
+ const host = createLocalHost(options);
25
+ let activeRun;
26
+ const unbindSignals = bindRunSignals(host, () => activeRun);
27
+ try {
28
+ await host.init();
29
+ const conversation = await host.createConversation(options.cwd ? { cwd: options.cwd } : {});
30
+ const run = await host.sendMessage(conversation.id, {
31
+ text: task,
32
+ format: format === 'jsonl' ? 'jsonl' : 'text',
33
+ });
34
+ activeRun = run;
35
+ let streamedText = false;
36
+ let approvalCancelled = false;
37
+ for await (const event of run.events) {
38
+ if (event.type === 'text')
39
+ streamedText = true;
40
+ writeEvent(event, format);
41
+ if (event.type === 'waiting' && !approvalCancelled) {
42
+ approvalCancelled = true;
43
+ process.stderr.write('\n[错误] 此运行处于非交互模式,无法请求工具审批;运行已取消。\n');
44
+ await run.cancel('INTERACTION_REQUIRED');
45
+ }
46
+ }
47
+ const outcome = await run.outcome();
48
+ if (format === 'jsonl') {
49
+ process.stdout.write(`${JSON.stringify({
50
+ schemaVersion: 1,
51
+ type: 'result',
52
+ runId: run.id,
53
+ status: outcome.status,
54
+ text: outcome.text ?? null,
55
+ error: outcome.error ?? null,
56
+ })}\n`);
57
+ }
58
+ else if (outcome.status === 'failed' && outcome.error) {
59
+ process.stderr.write(`\n[错误] ${outcome.error.code}: ${outcome.error.message}\n`);
60
+ }
61
+ else if (!streamedText && outcome.text) {
62
+ process.stdout.write(`${outcome.text}\n`);
63
+ }
64
+ else {
65
+ process.stdout.write(`\n[${outcome.status}]\n`);
66
+ }
67
+ return outcome.status === 'succeeded' ? 0 : outcome.status === 'cancelled' ? 3 : 1;
68
+ }
69
+ finally {
70
+ unbindSignals();
71
+ await host.close();
72
+ }
73
+ }
@@ -0,0 +1,2 @@
1
+ import { type LocalHostOptions } from '@hunterzhu/pulse-server';
2
+ export declare function runSessions(options: LocalHostOptions, format?: string, version?: string): Promise<number>;
@@ -0,0 +1,66 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { render } from 'ink';
3
+ import { useState } from 'react';
4
+ import { createLocalHost } from '@hunterzhu/pulse-server';
5
+ import { SessionList } from '../components/SessionList.js';
6
+ import { runInteractive } from './interactive.js';
7
+ export async function runSessions(options, format = 'text', version = '0.1.4') {
8
+ const host = createLocalHost(options);
9
+ try {
10
+ await host.init();
11
+ const sessions = await host.listConversations();
12
+ if (format === 'jsonl') {
13
+ for (const s of sessions) {
14
+ process.stdout.write(`${JSON.stringify(s)}\n`);
15
+ }
16
+ return 0;
17
+ }
18
+ if (!process.stdout.isTTY) {
19
+ if (sessions.length === 0) {
20
+ process.stdout.write('暂无历史会话。\n');
21
+ return 0;
22
+ }
23
+ for (const s of sessions) {
24
+ process.stdout.write(`${s.id}\t${s.updatedAt}\t${s.title}\t${s.cwd}\n`);
25
+ }
26
+ return 0;
27
+ }
28
+ // 交互式终端:使用 SessionList
29
+ let selectedId;
30
+ let closeMenu = () => undefined;
31
+ const AppContainer = () => {
32
+ const [items, setItems] = useState(sessions);
33
+ const [notice, setNotice] = useState(null);
34
+ return (_jsx(SessionList, { sessions: items, notice: notice, onSelect: (id) => {
35
+ selectedId = id;
36
+ closeMenu();
37
+ }, onDelete: async (id) => {
38
+ try {
39
+ await host.deleteConversation(id);
40
+ setItems((current) => current.filter((session) => session.id !== id));
41
+ setNotice(null);
42
+ }
43
+ catch (error) {
44
+ setNotice(error instanceof Error ? error.message : String(error));
45
+ }
46
+ }, onBack: () => {
47
+ closeMenu();
48
+ } }));
49
+ };
50
+ const rendered = render(_jsx(AppContainer, {}));
51
+ closeMenu = rendered.unmount;
52
+ const { waitUntilExit } = rendered;
53
+ await waitUntilExit();
54
+ if (selectedId) {
55
+ return runInteractive(options, selectedId, undefined, version);
56
+ }
57
+ return 0;
58
+ }
59
+ catch (error) {
60
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
61
+ return 1;
62
+ }
63
+ finally {
64
+ await host.close();
65
+ }
66
+ }
@@ -0,0 +1 @@
1
+ export declare function runSetup(force: boolean, explicitPath?: string): Promise<number>;