@hunterzhu/pulse-cli 0.1.4 → 0.1.6
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/dist/bin.d.ts +10 -1
- package/dist/bin.js +86 -177
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +36 -0
- package/dist/commands/interactive.d.ts +2 -0
- package/dist/commands/interactive.js +27 -0
- package/dist/commands/resume.d.ts +2 -0
- package/dist/commands/resume.js +75 -0
- package/dist/commands/run.d.ts +2 -0
- package/dist/commands/run.js +73 -0
- package/dist/commands/sessions.d.ts +2 -0
- package/dist/commands/sessions.js +66 -0
- package/dist/commands/setup.d.ts +1 -0
- package/dist/commands/setup.js +30 -0
- package/dist/commands/signals.d.ts +2 -0
- package/dist/commands/signals.js +20 -0
- package/dist/components/App.d.ts +10 -0
- package/dist/components/App.js +364 -0
- package/dist/components/ApprovalPrompt.d.ts +8 -0
- package/dist/components/ApprovalPrompt.js +21 -0
- package/dist/components/AssistantMessage.d.ts +10 -0
- package/dist/components/AssistantMessage.js +10 -0
- package/dist/components/Header.d.ts +8 -0
- package/dist/components/Header.js +8 -0
- package/dist/components/HelpView.d.ts +1 -0
- package/dist/components/HelpView.js +44 -0
- package/dist/components/InputArea.d.ts +7 -0
- package/dist/components/InputArea.js +55 -0
- package/dist/components/MessageList.d.ts +8 -0
- package/dist/components/MessageList.js +12 -0
- package/dist/components/SessionList.d.ts +15 -0
- package/dist/components/SessionList.js +33 -0
- package/dist/components/Spinner.d.ts +4 -0
- package/dist/components/Spinner.js +7 -0
- package/dist/components/ThinkingBlock.d.ts +5 -0
- package/dist/components/ThinkingBlock.js +8 -0
- package/dist/components/TokenStats.d.ts +7 -0
- package/dist/components/TokenStats.js +15 -0
- package/dist/components/ToolCallCard.d.ts +8 -0
- package/dist/components/ToolCallCard.js +31 -0
- package/dist/components/UserMessage.d.ts +5 -0
- package/dist/components/UserMessage.js +7 -0
- package/dist/components/Welcome.d.ts +6 -0
- package/dist/components/Welcome.js +6 -0
- package/dist/hooks/useConversation.d.ts +15 -0
- package/dist/hooks/useConversation.js +115 -0
- package/dist/hooks/useHost.d.ts +7 -0
- package/dist/hooks/useHost.js +44 -0
- package/dist/hooks/useMultilineInput.d.ts +11 -0
- package/dist/hooks/useMultilineInput.js +58 -0
- package/dist/hooks/useRun.d.ts +17 -0
- package/dist/hooks/useRun.js +213 -0
- package/dist/hooks/useSlashCommands.d.ts +26 -0
- package/dist/hooks/useSlashCommands.js +45 -0
- package/dist/hooks/useTokenStats.d.ts +7 -0
- package/dist/hooks/useTokenStats.js +38 -0
- package/dist/theme.d.ts +27 -0
- package/dist/theme.js +27 -0
- package/dist/types.d.ts +65 -0
- package/dist/types.js +1 -0
- package/dist/utils/ansi.d.ts +5 -0
- package/dist/utils/ansi.js +20 -0
- package/dist/utils/approval.d.ts +10 -0
- package/dist/utils/approval.js +50 -0
- package/dist/utils/format.d.ts +6 -0
- package/dist/utils/format.js +51 -0
- package/dist/utils/highlight.d.ts +1 -0
- package/dist/utils/highlight.js +80 -0
- package/dist/utils/markdown.d.ts +1 -0
- package/dist/utils/markdown.js +138 -0
- package/package.json +13 -2
package/dist/bin.d.ts
CHANGED
|
@@ -1,2 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
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 {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
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
|
-
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
78
86
|
positionals.push(arg);
|
|
87
|
+
}
|
|
79
88
|
}
|
|
80
89
|
return { command, positionals, options };
|
|
81
90
|
}
|
|
82
|
-
function option(options, key) {
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
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
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if (
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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()
|
|
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,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,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,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,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,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>;
|