@hunterzhu/pulse-cli 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -1
- package/dist/bin.js +126 -15
- package/dist/commands/interactive.js +9 -1
- package/dist/commands/resume.js +2 -2
- package/dist/commands/run.js +5 -6
- package/dist/components/App.js +36 -3
- package/dist/components/HelpView.js +1 -0
- package/dist/components/InputArea.d.ts +4 -1
- package/dist/components/InputArea.js +32 -3
- package/dist/config.d.ts +1 -1
- package/dist/presentation.d.ts +11 -0
- package/dist/presentation.js +64 -0
- package/dist/release-highlights.generated.d.ts +4 -0
- package/dist/release-highlights.generated.js +139 -0
- package/dist/utils/slashCompletion.d.ts +6 -0
- package/dist/utils/slashCompletion.js +11 -0
- package/package.json +4 -3
- package/scripts/postinstall.mjs +6 -49
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# @hunterzhu/pulse-cli
|
|
2
2
|
|
|
3
|
+
[简体中文](https://github.com/zhuhengtan/Pulse/blob/main/packages/cli/README.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
[Changelog](https://github.com/zhuhengtan/Pulse/blob/main/CHANGELOG.en.md)
|
|
6
|
+
|
|
3
7
|
**A neural-signal like AI assistant for coding, research and file tasks.**
|
|
4
8
|
|
|
5
9
|
Pulse runs in your project folder, keeps conversations on your machine, and can resume unfinished work. Requires Node.js 22+.
|
|
@@ -9,7 +13,7 @@ Pulse runs in your project folder, keeps conversations on your machine, and can
|
|
|
9
13
|
Install the CLI globally so the `pulse` command is on your `PATH`:
|
|
10
14
|
|
|
11
15
|
```bash
|
|
12
|
-
npm install -g @hunterzhu/pulse-cli
|
|
16
|
+
npm install -g @hunterzhu/pulse-cli --foreground-scripts
|
|
13
17
|
pulse --version
|
|
14
18
|
```
|
|
15
19
|
|
|
@@ -49,6 +53,10 @@ pulse --read-only
|
|
|
49
53
|
|
|
50
54
|
See `pulse --help` for all options.
|
|
51
55
|
|
|
56
|
+
## License
|
|
57
|
+
|
|
58
|
+
This package is licensed under [PolyForm Noncommercial 1.0.0](./LICENSE). Commercial use requires separate permission. This is a source-available noncommercial license, not an OSI-approved open-source license.
|
|
59
|
+
|
|
52
60
|
## Links
|
|
53
61
|
|
|
54
62
|
- [Full CLI configuration guide](https://github.com/zhuhengtan/Pulse/blob/main/docs/cli-config.md)
|
package/dist/bin.js
CHANGED
|
@@ -4,7 +4,7 @@ import { createRequire } from 'node:module';
|
|
|
4
4
|
import { isAbsolute, resolve } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { createMcpCapabilityPack, createPdfCapabilityPack, createSkillCapabilityPack, createSpreadsheetCapabilityPack } from '@hunterzhu/pulse-server';
|
|
7
|
-
import { ensurePulseUserConfig, expandHome, loadPulseConfig } from './config.js';
|
|
7
|
+
import { defaultPulseConfig, ensurePulseUserConfig, expandHome, loadPulseConfig } from './config.js';
|
|
8
8
|
import { runInteractive } from './commands/interactive.js';
|
|
9
9
|
import { runOneShot } from './commands/run.js';
|
|
10
10
|
import { runDoctor } from './commands/doctor.js';
|
|
@@ -16,6 +16,7 @@ import { runServiceCommand } from './commands/service.js';
|
|
|
16
16
|
import { runTemplateCommand } from './commands/template.js';
|
|
17
17
|
import { runMcpDoctor } from './commands/mcp.js';
|
|
18
18
|
import { isSameModulePath } from './utils/is-main-module.js';
|
|
19
|
+
import { formatVersionOutput, shouldUseColor } from './presentation.js';
|
|
19
20
|
const packageManifest = createRequire(import.meta.url)('../package.json');
|
|
20
21
|
export const version = packageManifest.version;
|
|
21
22
|
process.stdout.on('error', (error) => {
|
|
@@ -59,6 +60,12 @@ Options:
|
|
|
59
60
|
--system-prompt-file <path> load custom system instructions from file
|
|
60
61
|
--mock-response <text> deterministic response for local debugging
|
|
61
62
|
--mock-task-assessment <json> deterministic verifier result for local debugging
|
|
63
|
+
--mock-stage-assessment <json> deterministic stage verifier results for local debugging
|
|
64
|
+
--mock-parallel-plan <json> deterministic task or read-only split plan for local debugging
|
|
65
|
+
--mock-tool-calls <json> deterministic tool calls for local debugging
|
|
66
|
+
--mock-after-tool-response <text> deterministic response after tool execution
|
|
67
|
+
--task-controller force the multi-stage task controller
|
|
68
|
+
--no-task-controller keep a single ReAct loop
|
|
62
69
|
--live doctor: make one real provider request
|
|
63
70
|
--no-color disable terminal styling
|
|
64
71
|
--help, -h show this help
|
|
@@ -122,17 +129,37 @@ export async function hostOptions(parsed) {
|
|
|
122
129
|
if (explicitConfig === undefined && process.env.PULSE_CONFIG === undefined) {
|
|
123
130
|
await ensurePulseUserConfig();
|
|
124
131
|
}
|
|
125
|
-
const
|
|
126
|
-
|
|
132
|
+
const loaded = (await loadPulseConfig(requestedCwd ?? process.cwd(), explicitConfig, parsed.options['trust-workspace'] === true)).value;
|
|
133
|
+
// A missing user config still has to answer usage errors and list sessions.
|
|
134
|
+
const config = loaded.activeModel || loaded.models || loaded.providers ? loaded : { ...defaultPulseConfig, ...loaded };
|
|
135
|
+
const mockResponse = option(parsed.options, 'mock-response');
|
|
136
|
+
const mockTaskAssessment = option(parsed.options, 'mock-task-assessment');
|
|
137
|
+
const mockStageAssessment = option(parsed.options, 'mock-stage-assessment');
|
|
138
|
+
const mockToolCallsOption = option(parsed.options, 'mock-tool-calls');
|
|
139
|
+
const mockAfterToolResponse = option(parsed.options, 'mock-after-tool-response');
|
|
140
|
+
const mockParallelPlanOption = option(parsed.options, 'mock-parallel-plan');
|
|
141
|
+
const hasMockDemand = mockResponse !== undefined || mockTaskAssessment !== undefined || mockStageAssessment !== undefined || mockToolCallsOption !== undefined || mockAfterToolResponse !== undefined || mockParallelPlanOption !== undefined;
|
|
142
|
+
const cliModel = option(parsed.options, 'model');
|
|
143
|
+
const requestedModel = cliModel ?? (hasMockDemand ? 'mock' : process.env.PULSE_MODEL ?? config.activeModel);
|
|
144
|
+
const effectiveModels = { ...(config.models ?? {}) };
|
|
145
|
+
const effectiveProviders = { ...(config.providers ?? {}) };
|
|
146
|
+
if (requestedModel === 'mock' || (hasMockDemand && cliModel === undefined)) {
|
|
147
|
+
if (!effectiveModels.mock) {
|
|
148
|
+
effectiveModels.mock = { displayName: 'mock', provider: 'mock', modelCode: 'mock', maxContextTokens: 32_000, maxOutputTokens: 4_096, reasoningEffort: 'medium' };
|
|
149
|
+
}
|
|
150
|
+
if (!effectiveProviders.mock) {
|
|
151
|
+
effectiveProviders.mock = { provider: 'mock', name: 'Mock' };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
127
154
|
const configuredModelEntry = requestedModel === undefined
|
|
128
155
|
? undefined
|
|
129
|
-
: Object.entries(
|
|
156
|
+
: Object.entries(effectiveModels).find(([name, item]) => name === requestedModel || item.displayName === requestedModel);
|
|
130
157
|
if (!configuredModelEntry)
|
|
131
158
|
throw new Error(`UNKNOWN_MODEL_DISPLAY_NAME:${requestedModel ?? '(missing)'}`);
|
|
132
159
|
const [, modelSelection] = configuredModelEntry;
|
|
133
160
|
const activeModelName = modelSelection.displayName;
|
|
134
161
|
const providerName = modelSelection.provider;
|
|
135
|
-
const profile =
|
|
162
|
+
const profile = effectiveProviders[providerName];
|
|
136
163
|
if (!profile)
|
|
137
164
|
throw new Error(`MODEL_PROVIDER_NOT_FOUND:${providerName}`);
|
|
138
165
|
const model = modelSelection.modelCode;
|
|
@@ -168,7 +195,7 @@ export async function hostOptions(parsed) {
|
|
|
168
195
|
...(configuredReasoningEffort === undefined ? {} : { reasoningEffort: configuredReasoningEffort }),
|
|
169
196
|
...(configuredToolChoice === undefined ? {} : { toolChoice: configuredToolChoice }),
|
|
170
197
|
};
|
|
171
|
-
const providerProfiles = Object.fromEntries(Object.entries(
|
|
198
|
+
const providerProfiles = Object.fromEntries(Object.entries(effectiveProviders).flatMap(([name, item]) => {
|
|
172
199
|
if (!item.provider)
|
|
173
200
|
throw new Error(`PROVIDER_PROTOCOL_REQUIRED:${name}`);
|
|
174
201
|
const adapterProvider = item.provider;
|
|
@@ -185,8 +212,8 @@ export async function hostOptions(parsed) {
|
|
|
185
212
|
}]];
|
|
186
213
|
}));
|
|
187
214
|
const modelDisplayNames = new Set();
|
|
188
|
-
const providerModels = Object.fromEntries(Object.entries(
|
|
189
|
-
if (!
|
|
215
|
+
const providerModels = Object.fromEntries(Object.entries(effectiveModels).flatMap(([name, item]) => {
|
|
216
|
+
if (!effectiveProviders[item.provider])
|
|
190
217
|
throw new Error(`MODEL_PROVIDER_NOT_FOUND:${item.provider}`);
|
|
191
218
|
const displayName = item.displayName.trim();
|
|
192
219
|
if (!displayName)
|
|
@@ -212,8 +239,6 @@ export async function hostOptions(parsed) {
|
|
|
212
239
|
}
|
|
213
240
|
const cwd = requestedCwd ?? expandHome(config.cwd);
|
|
214
241
|
const dataDir = expandHome(option(parsed.options, 'data-dir') ?? process.env.PULSE_DATA_DIR ?? config.dataDir);
|
|
215
|
-
const mockResponse = option(parsed.options, 'mock-response');
|
|
216
|
-
const mockTaskAssessment = option(parsed.options, 'mock-task-assessment');
|
|
217
242
|
let mockTaskAssessments;
|
|
218
243
|
if (mockTaskAssessment !== undefined) {
|
|
219
244
|
try {
|
|
@@ -226,6 +251,45 @@ export async function hostOptions(parsed) {
|
|
|
226
251
|
throw new Error('INVALID_MOCK_TASK_ASSESSMENT: expected a non-empty JSON array of assessment objects');
|
|
227
252
|
}
|
|
228
253
|
}
|
|
254
|
+
else if (hasMockDemand) {
|
|
255
|
+
mockTaskAssessments = [{ status: 'accepted', criteria: [{ criterionId: 'criterion-1', status: 'passed', evidenceRefs: ['AUTO'], rationale: 'Accepted by mock verifier.' }] }];
|
|
256
|
+
}
|
|
257
|
+
let mockStageAssessments;
|
|
258
|
+
if (mockStageAssessment !== undefined) {
|
|
259
|
+
try {
|
|
260
|
+
const value = JSON.parse(mockStageAssessment);
|
|
261
|
+
if (!Array.isArray(value) || value.length === 0 || value.some((item) => !item || typeof item !== 'object' || Array.isArray(item)))
|
|
262
|
+
throw new Error('INVALID');
|
|
263
|
+
mockStageAssessments = value;
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
throw new Error('INVALID_MOCK_STAGE_ASSESSMENT: expected a non-empty JSON array of stage assessment objects');
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
let mockToolCalls;
|
|
270
|
+
if (mockToolCallsOption !== undefined) {
|
|
271
|
+
try {
|
|
272
|
+
const value = JSON.parse(mockToolCallsOption);
|
|
273
|
+
if (!Array.isArray(value) || value.some((item) => !item || typeof item !== 'object' || Array.isArray(item)))
|
|
274
|
+
throw new Error('INVALID');
|
|
275
|
+
mockToolCalls = value;
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
throw new Error('INVALID_MOCK_TOOL_CALLS: expected a JSON array of tool call objects');
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
let mockParallelPlan;
|
|
282
|
+
if (mockParallelPlanOption !== undefined) {
|
|
283
|
+
try {
|
|
284
|
+
const value = JSON.parse(mockParallelPlanOption);
|
|
285
|
+
if (!value || typeof value !== 'object')
|
|
286
|
+
throw new Error('INVALID');
|
|
287
|
+
mockParallelPlan = value;
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
throw new Error('INVALID_MOCK_PARALLEL_PLAN: expected a JSON object');
|
|
291
|
+
}
|
|
292
|
+
}
|
|
229
293
|
const requestedApprovalMode = option(parsed.options, 'approval-mode') ?? process.env.PULSE_APPROVAL_MODE;
|
|
230
294
|
const approvalMode = requestedApprovalMode === 'read-only' || parsed.options['read-only'] === true
|
|
231
295
|
? 'read-only'
|
|
@@ -290,7 +354,9 @@ export async function hostOptions(parsed) {
|
|
|
290
354
|
}
|
|
291
355
|
}
|
|
292
356
|
}
|
|
357
|
+
const explicitTaskController = parsed.options['task-controller'] === true ? true : parsed.options['no-task-controller'] === true ? false : mockParallelPlanOption !== undefined ? true : undefined;
|
|
293
358
|
return {
|
|
359
|
+
...(explicitTaskController === undefined ? {} : { taskController: explicitTaskController }),
|
|
294
360
|
...(cwd === undefined ? {} : { cwd }),
|
|
295
361
|
...(dataDir === undefined ? {} : { dataDir }),
|
|
296
362
|
...(systemPrompt && systemPrompt.trim().length > 0 ? { systemPrompt: systemPrompt.trim() } : {}),
|
|
@@ -301,11 +367,15 @@ export async function hostOptions(parsed) {
|
|
|
301
367
|
...(taskRouting === undefined ? {} : { taskRouting }),
|
|
302
368
|
capabilityPacks,
|
|
303
369
|
enabledCapabilityPacks,
|
|
304
|
-
capabilityConfig: { skills: skillNames },
|
|
370
|
+
capabilityConfig: capabilitySettings.skills === undefined ? {} : { skills: skillNames },
|
|
305
371
|
activeProviderCode: providerName,
|
|
306
372
|
activeModel: activeModelName,
|
|
307
373
|
...(mockResponse === undefined ? {} : { mockResponse }),
|
|
308
374
|
...(mockTaskAssessments === undefined ? {} : { mockTaskAssessments }),
|
|
375
|
+
...(mockStageAssessments === undefined ? {} : { mockStageAssessments }),
|
|
376
|
+
...(mockToolCalls === undefined ? {} : { mockToolCalls }),
|
|
377
|
+
...(mockAfterToolResponse === undefined ? {} : { mockAfterToolResponse }),
|
|
378
|
+
...(mockParallelPlan === undefined ? {} : { mockParallelPlan }),
|
|
309
379
|
...(approvalMode === undefined ? {} : { approvalMode }),
|
|
310
380
|
...(configuredMaxTurns === undefined ? {} : { maxTurns: configuredMaxTurns }),
|
|
311
381
|
...(configuredAutoCompactPercent === undefined ? {} : { autoCompactPercent: Math.min(90, configuredAutoCompactPercent) }),
|
|
@@ -320,7 +390,9 @@ export async function main() {
|
|
|
320
390
|
return 0;
|
|
321
391
|
}
|
|
322
392
|
if (parsed.options.version || parsed.options.v) {
|
|
323
|
-
process.stdout.write(
|
|
393
|
+
process.stdout.write(formatVersionOutput(version, {
|
|
394
|
+
color: parsed.options['no-color'] !== true && shouldUseColor(process.env, process.stdout.isTTY === true),
|
|
395
|
+
}));
|
|
324
396
|
return 0;
|
|
325
397
|
}
|
|
326
398
|
if (parsed.command === 'setup') {
|
|
@@ -342,21 +414,60 @@ export async function main() {
|
|
|
342
414
|
return runMcpDoctor(options, parsed.positionals.slice(1));
|
|
343
415
|
if (parsed.command === 'template')
|
|
344
416
|
return runTemplateCommand(options, parsed.positionals, option(parsed.options, 'format') ?? 'text', version);
|
|
417
|
+
async function readPipedStdin() {
|
|
418
|
+
if (process.stdin.isTTY)
|
|
419
|
+
return '';
|
|
420
|
+
return new Promise((resolve) => {
|
|
421
|
+
let data = '';
|
|
422
|
+
let settled = false;
|
|
423
|
+
const finish = () => {
|
|
424
|
+
if (settled)
|
|
425
|
+
return;
|
|
426
|
+
settled = true;
|
|
427
|
+
clearTimeout(idle);
|
|
428
|
+
process.stdin.pause();
|
|
429
|
+
resolve(data.trim());
|
|
430
|
+
};
|
|
431
|
+
// An open stdin with no bytes must not block a command that already has a task.
|
|
432
|
+
const idle = setTimeout(finish, 250);
|
|
433
|
+
process.stdin.setEncoding('utf8');
|
|
434
|
+
process.stdin.on('data', (chunk) => {
|
|
435
|
+
data += chunk;
|
|
436
|
+
clearTimeout(idle);
|
|
437
|
+
});
|
|
438
|
+
process.stdin.on('end', finish);
|
|
439
|
+
process.stdin.on('error', finish);
|
|
440
|
+
if (process.stdin.readableEnded)
|
|
441
|
+
finish();
|
|
442
|
+
});
|
|
443
|
+
}
|
|
345
444
|
if (parsed.command === 'run') {
|
|
346
|
-
|
|
445
|
+
let task = parsed.positionals.join(' ').trim();
|
|
446
|
+
if (!task && !process.stdin.isTTY) {
|
|
447
|
+
task = await readPipedStdin();
|
|
448
|
+
}
|
|
449
|
+
else if (task && !process.stdin.isTTY) {
|
|
450
|
+
const piped = await readPipedStdin();
|
|
451
|
+
if (piped)
|
|
452
|
+
task = `${task}\n\n${piped}`;
|
|
453
|
+
}
|
|
347
454
|
if (!task)
|
|
348
455
|
throw new Error('TASK_REQUIRED');
|
|
349
456
|
return runOneShot(options, task, option(parsed.options, 'format') ?? 'text', version);
|
|
350
457
|
}
|
|
351
458
|
if (parsed.command === 'resume') {
|
|
352
459
|
const id = parsed.positionals.shift();
|
|
353
|
-
|
|
460
|
+
let task = parsed.positionals.join(' ').trim();
|
|
461
|
+
if (!task && !process.stdin.isTTY) {
|
|
462
|
+
task = await readPipedStdin();
|
|
463
|
+
}
|
|
354
464
|
if (!id)
|
|
355
465
|
throw new Error('RESUME_REQUIRES_ID');
|
|
356
466
|
return runResume(options, id, task || undefined, option(parsed.options, 'format') ?? 'text', version);
|
|
357
467
|
}
|
|
358
468
|
// 默认启动交互式 Ink 界面
|
|
359
|
-
|
|
469
|
+
const initialTask = parsed.positionals.join(' ').trim() || undefined;
|
|
470
|
+
return runInteractive(options, undefined, initialTask, version, parsed.options.resume === true);
|
|
360
471
|
}
|
|
361
472
|
if (process.argv[1] && isSameModulePath(process.argv[1], fileURLToPath(import.meta.url))) {
|
|
362
473
|
main()
|
|
@@ -3,6 +3,7 @@ import { render } from 'ink';
|
|
|
3
3
|
import { App } from '../components/App.js';
|
|
4
4
|
import { createLocalHost } from '@hunterzhu/pulse-server';
|
|
5
5
|
import { closePendingHosts } from '../hooks/useHost.js';
|
|
6
|
+
import { runOneShot } from './run.js';
|
|
6
7
|
export async function findResumeConversation(options) {
|
|
7
8
|
const host = createLocalHost(options);
|
|
8
9
|
await host.init();
|
|
@@ -14,7 +15,14 @@ export async function findResumeConversation(options) {
|
|
|
14
15
|
await host.close();
|
|
15
16
|
}
|
|
16
17
|
}
|
|
17
|
-
export async function runInteractive(options, conversationId, initialTask, version = '0.1
|
|
18
|
+
export async function runInteractive(options, conversationId, initialTask, version = '0.4.1', resumeLatest = false) {
|
|
19
|
+
if (!process.stdin.isTTY) {
|
|
20
|
+
if (initialTask) {
|
|
21
|
+
return runOneShot(options, initialTask, 'text', version);
|
|
22
|
+
}
|
|
23
|
+
process.stderr.write('交互模式需要 TTY 终端。非交互执行请使用 "pulse run <task>"。\n');
|
|
24
|
+
return 1;
|
|
25
|
+
}
|
|
18
26
|
const selectedConversationId = resumeLatest
|
|
19
27
|
? await findResumeConversation(options)
|
|
20
28
|
: conversationId;
|
package/dist/commands/resume.js
CHANGED
|
@@ -20,8 +20,8 @@ function writeEvent(event, format) {
|
|
|
20
20
|
process.stderr.write(`\n[错误] ${String(event.data ?? '')}\n`);
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
|
-
export async function runResume(options, conversationId, task, format = 'text', version = '0.1
|
|
24
|
-
//
|
|
23
|
+
export async function runResume(options, conversationId, task, format = 'text', version = '0.4.1') {
|
|
24
|
+
// 交互式终端进入 Ink 界面;jsonl 保持非交互输出
|
|
25
25
|
if (process.stdout.isTTY && format !== 'jsonl') {
|
|
26
26
|
return runInteractive(options, conversationId, task, version);
|
|
27
27
|
}
|
package/dist/commands/run.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { createLocalHost } from '@hunterzhu/pulse-server';
|
|
2
|
-
import { runInteractive } from './interactive.js';
|
|
3
2
|
import { bindRunSignals } from './signals.js';
|
|
4
3
|
function writeEvent(event, format) {
|
|
5
4
|
if (format === 'jsonl') {
|
|
@@ -20,11 +19,7 @@ function writeEvent(event, format) {
|
|
|
20
19
|
process.stderr.write(`\n[错误] ${String(event.data ?? '')}\n`);
|
|
21
20
|
}
|
|
22
21
|
}
|
|
23
|
-
export async function runOneShot(options, task, format = 'text', version = '0.1
|
|
24
|
-
// 如果在交互式终端且没有指定 jsonl,启动交互式 Ink 界面直接执行任务
|
|
25
|
-
if (process.stdout.isTTY && format !== 'jsonl') {
|
|
26
|
-
return runInteractive(options, undefined, task, version);
|
|
27
|
-
}
|
|
22
|
+
export async function runOneShot(options, task, format = 'text', version = '0.4.1') {
|
|
28
23
|
const host = createLocalHost(options);
|
|
29
24
|
let activeRun;
|
|
30
25
|
const unbindSignals = bindRunSignals(host, () => activeRun);
|
|
@@ -76,6 +71,10 @@ export async function runOneShot(options, task, format = 'text', version = '0.1.
|
|
|
76
71
|
return 3;
|
|
77
72
|
return outcome.status === 'succeeded' && (!taskOutcome || taskOutcome.status === 'accepted') ? 0 : 1;
|
|
78
73
|
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
process.stderr.write(`\n[错误] ${error instanceof Error ? error.message : String(error)}\n`);
|
|
76
|
+
return 1;
|
|
77
|
+
}
|
|
79
78
|
finally {
|
|
80
79
|
unbindSignals();
|
|
81
80
|
await host.close();
|
package/dist/components/App.js
CHANGED
|
@@ -18,11 +18,14 @@ import { useTokenStats } from '../hooks/useTokenStats.js';
|
|
|
18
18
|
import { useSlashCommands } from '../hooks/useSlashCommands.js';
|
|
19
19
|
import { copyToClipboard, isCopyShortcut } from '../utils/clipboard.js';
|
|
20
20
|
import { emptyInputHistory, navigateInputHistory, rememberInput } from '../utils/inputHistory.js';
|
|
21
|
+
import { skillSuggestions } from '../utils/slashCompletion.js';
|
|
21
22
|
export function App({ hostOptions, conversationId: initialConversationId, initialTask, resumeOnStart = false, version, }) {
|
|
22
23
|
const { exit } = useApp();
|
|
23
24
|
const { stdout } = useStdout();
|
|
24
25
|
const [terminalRows, setTerminalRows] = useState(stdout.rows || 24);
|
|
25
26
|
useEffect(() => { const resize = () => setTerminalRows(stdout.rows || 24); stdout.on('resize', resize); return () => { stdout.off('resize', resize); }; }, [stdout]);
|
|
27
|
+
const [skillNames, setSkillNames] = useState([]);
|
|
28
|
+
const [skillError, setSkillError] = useState(null);
|
|
26
29
|
const [mode, setMode] = useState('chat');
|
|
27
30
|
const [mouseEnabled, setMouseEnabled] = useState(true);
|
|
28
31
|
const [selectedText, setSelectedText] = useState('');
|
|
@@ -38,6 +41,11 @@ export function App({ hostOptions, conversationId: initialConversationId, initia
|
|
|
38
41
|
const startedTask = useRef(false);
|
|
39
42
|
const startedResume = useRef(false);
|
|
40
43
|
const { host, error: hostError, ready: hostReady } = useHost(hostOptions);
|
|
44
|
+
const refreshSkills = useCallback(() => {
|
|
45
|
+
if (host)
|
|
46
|
+
void host.listSkills().then(names => { setSkillNames(names); setSkillError(null); }, error => setSkillError(String(error)));
|
|
47
|
+
}, [host]);
|
|
48
|
+
useEffect(refreshSkills, [refreshSkills]);
|
|
41
49
|
const { conversation, messages, addUserMessage, addAssistantMessage, clearMessages, switchConversation, newConversation, createConversation, error: conversationError, } = useConversation({ host, conversationId: initialConversationId });
|
|
42
50
|
const { isRunning, error: runError, approvalRequest, askRequest, lanes, approvalSubmitting, sendMessage, resumeActive, approveAction, replyAsk, cancelRun, } = useRun({
|
|
43
51
|
host,
|
|
@@ -127,7 +135,7 @@ export function App({ hostOptions, conversationId: initialConversationId, initia
|
|
|
127
135
|
await sendMessage(initialTask, target.id);
|
|
128
136
|
})();
|
|
129
137
|
}, [hostReady, conversation, initialTask, isRunning, addUserMessage, addAssistantMessage, createConversation, sendMessage]);
|
|
130
|
-
const { executeCommand, isSlashCommand } = useSlashCommands({
|
|
138
|
+
const { commands, executeCommand, isSlashCommand } = useSlashCommands({
|
|
131
139
|
onMouse: (arg) => setMouseEnabled((current) => arg === 'off' ? false : arg === 'on' ? true : !current),
|
|
132
140
|
onCopy: async () => {
|
|
133
141
|
if (selectedText) {
|
|
@@ -455,6 +463,31 @@ export function App({ hostOptions, conversationId: initialConversationId, initia
|
|
|
455
463
|
if (isSlashCommand(text)) {
|
|
456
464
|
const handled = await executeCommand(text);
|
|
457
465
|
if (!handled) {
|
|
466
|
+
const token = text.split(/\s+/)[0].slice(1).replace(/^skill:/, '');
|
|
467
|
+
let currentSkills = skillNames;
|
|
468
|
+
try {
|
|
469
|
+
if (host) {
|
|
470
|
+
currentSkills = await host.listSkills();
|
|
471
|
+
setSkillNames(currentSkills);
|
|
472
|
+
setSkillError(null);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
catch (error) {
|
|
476
|
+
setSkillError(String(error));
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
if (currentSkills.includes(token)) {
|
|
480
|
+
if (isRunning) {
|
|
481
|
+
addAssistantMessage({ id: `skill-busy-${Date.now()}`, role: 'system', text: '请等当前任务完成,或使用 /cancel 后再调用技能。', createdAt: new Date().toISOString() });
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
const target = conversation ?? await createConversation();
|
|
485
|
+
if (!target)
|
|
486
|
+
return;
|
|
487
|
+
addUserMessage(text);
|
|
488
|
+
await sendMessage(text, target.id);
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
458
491
|
addAssistantMessage({
|
|
459
492
|
id: `command-${Date.now()}`,
|
|
460
493
|
role: 'system',
|
|
@@ -470,7 +503,7 @@ export function App({ hostOptions, conversationId: initialConversationId, initia
|
|
|
470
503
|
addUserMessage(text);
|
|
471
504
|
await sendMessage(text, target.id);
|
|
472
505
|
}
|
|
473
|
-
}, [isSlashCommand, executeCommand, conversation, createConversation, addUserMessage, sendMessage]);
|
|
506
|
+
}, [isSlashCommand, executeCommand, conversation, createConversation, addUserMessage, sendMessage, skillNames, isRunning, addAssistantMessage, host]);
|
|
474
507
|
useInput((input, key) => {
|
|
475
508
|
if (isCopyShortcut(input, key)) {
|
|
476
509
|
if (selectedText)
|
|
@@ -541,6 +574,6 @@ export function App({ hostOptions, conversationId: initialConversationId, initia
|
|
|
541
574
|
}, onInput: () => {
|
|
542
575
|
setApprovalChoiceFocused(false);
|
|
543
576
|
setApprovalInputMode(true);
|
|
544
|
-
} })) : askRequest ? (_jsx(AskPrompt, { request: askRequest, disabled: approvalSubmitting, onReply: (value) => void replyAsk(askRequest.effectId, value) }, askRequest.effectId)) : (
|
|
577
|
+
} })) : askRequest ? (_jsx(AskPrompt, { request: askRequest, disabled: approvalSubmitting, onReply: (value) => void replyAsk(askRequest.effectId, value) }, askRequest.effectId)) : (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [skillError && _jsxs(Text, { color: "red", children: ["\u6280\u80FD\u7D22\u5F15\u5931\u8D25\uFF1A", skillError] }), _jsx(InputArea, { suggestions: [...commands, ...skillSuggestions(skillNames, commands.flatMap(command => [command, ...(command.aliases ?? []).map(name => ({ name, description: command.description }))]))], onSearchSkills: refreshSkills, onSubmit: (txt) => void handleSubmit(txt), onRememberInput: rememberUserInput, onNavigateHistory: navigateUserInputHistory, disabled: approvalSubmitting, focus: !approvalSubmitting, placeholder: isRunning ? '运行中也可以输入;/cancel 可取消当前运行...' : '输入消息或 /help...' })] }))] }));
|
|
545
578
|
}
|
|
546
579
|
export default App;
|
|
@@ -38,6 +38,7 @@ const COMMANDS = [
|
|
|
38
38
|
{ name: '/model [name]', desc: '切换模型' },
|
|
39
39
|
{ name: '/thinking [level]', desc: '设置模型思考深度 (low/medium/high/off)' },
|
|
40
40
|
{ name: '/compact', desc: '手动压缩历史;默认在上下文 90% 时自动压缩' },
|
|
41
|
+
{ name: '/skill:名称 任务', desc: '按需加载技能;输入 / 搜索,↑↓ 选择,Tab 补全' },
|
|
41
42
|
{ name: '/config', desc: '查看当前运行时配置' },
|
|
42
43
|
],
|
|
43
44
|
},
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { type SlashSuggestion } from '../utils/slashCompletion.js';
|
|
1
2
|
interface Props {
|
|
3
|
+
suggestions?: SlashSuggestion[];
|
|
4
|
+
onSearchSkills?: () => void;
|
|
2
5
|
onSubmit: (text: string) => void;
|
|
3
6
|
onRememberInput: (text: string) => void;
|
|
4
7
|
onNavigateHistory: (direction: 'up' | 'down', currentValue: string) => string | undefined;
|
|
@@ -6,5 +9,5 @@ interface Props {
|
|
|
6
9
|
focus?: boolean;
|
|
7
10
|
placeholder?: string;
|
|
8
11
|
}
|
|
9
|
-
export declare function InputArea({ onSubmit, onRememberInput, onNavigateHistory, disabled, focus, placeholder }: Props): import("react").JSX.Element;
|
|
12
|
+
export declare function InputArea({ onSubmit, onRememberInput, onNavigateHistory, disabled, focus, placeholder, suggestions, onSearchSkills }: Props): import("react").JSX.Element;
|
|
10
13
|
export {};
|
|
@@ -2,15 +2,44 @@ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { Box, Text, useInput } from 'ink';
|
|
3
3
|
import TextInput from './TextInput.js';
|
|
4
4
|
import { useState } from 'react';
|
|
5
|
+
import { searchSlashSuggestions } from '../utils/slashCompletion.js';
|
|
5
6
|
import { theme } from '../theme.js';
|
|
6
|
-
export function InputArea({ onSubmit, onRememberInput, onNavigateHistory, disabled, focus = true, placeholder }) {
|
|
7
|
+
export function InputArea({ onSubmit, onRememberInput, onNavigateHistory, disabled, focus = true, placeholder, suggestions = [], onSearchSkills }) {
|
|
7
8
|
const [value, setValue] = useState('');
|
|
9
|
+
const [completionRevision, setCompletionRevision] = useState(0);
|
|
10
|
+
const [selected, setSelected] = useState(0);
|
|
11
|
+
const [dismissed, setDismissed] = useState(false);
|
|
12
|
+
const matches = dismissed ? [] : searchSlashSuggestions(value, suggestions);
|
|
13
|
+
const chosen = Math.min(selected, Math.max(0, matches.length - 1));
|
|
14
|
+
const changeValue = (next) => {
|
|
15
|
+
if (next.startsWith('/') && !value.startsWith('/'))
|
|
16
|
+
onSearchSkills?.();
|
|
17
|
+
setSelected(0);
|
|
18
|
+
setDismissed(false);
|
|
19
|
+
setValue(next);
|
|
20
|
+
};
|
|
8
21
|
const [accumulatedLines, setAccumulatedLines] = useState([]);
|
|
9
22
|
useInput((input, key) => {
|
|
10
23
|
if (disabled || !focus)
|
|
11
24
|
return;
|
|
12
25
|
if (key.ctrl || key.meta || key.pageUp || key.pageDown)
|
|
13
26
|
return;
|
|
27
|
+
if (matches.length && !accumulatedLines.length) {
|
|
28
|
+
if (key.escape) {
|
|
29
|
+
setDismissed(true);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (key.upArrow || key.downArrow) {
|
|
33
|
+
setSelected(Math.max(0, Math.min(matches.length - 1, chosen + (key.upArrow ? -1 : 1))));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (key.tab) {
|
|
37
|
+
setValue(`${matches[chosen].name} `);
|
|
38
|
+
setCompletionRevision(revision => revision + 1);
|
|
39
|
+
setSelected(0);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
14
43
|
if (key.return) {
|
|
15
44
|
if (key.shift) {
|
|
16
45
|
setAccumulatedLines((current) => [...current, value]);
|
|
@@ -35,7 +64,7 @@ export function InputArea({ onSubmit, onRememberInput, onNavigateHistory, disabl
|
|
|
35
64
|
const restoreInput = (text) => {
|
|
36
65
|
const lines = text.split('\n');
|
|
37
66
|
setAccumulatedLines(lines.slice(0, -1));
|
|
38
|
-
|
|
67
|
+
changeValue(lines.at(-1) ?? '');
|
|
39
68
|
};
|
|
40
69
|
const handleSubmit = (text) => {
|
|
41
70
|
if (text.endsWith('\\')) {
|
|
@@ -56,5 +85,5 @@ export function InputArea({ onSubmit, onRememberInput, onNavigateHistory, disabl
|
|
|
56
85
|
};
|
|
57
86
|
const promptChar = accumulatedLines.length > 0 ? '…' : '›';
|
|
58
87
|
const promptColor = disabled ? theme.dim : theme.primary;
|
|
59
|
-
return (_jsxs(Box, { flexDirection: "column", children: [accumulatedLines.map((line, idx) => (_jsxs(Box, { children: [_jsxs(Text, { color: promptColor, children: [promptChar, " "] }), _jsx(Text, { children: line })] }, idx))), _jsxs(Box, { children: [_jsxs(Text, { color: promptColor, children: [promptChar, " "] }), disabled ? (_jsx(Text, { color: theme.dim, children: placeholder || '处理中...' })) : (_jsx(TextInput, { focus: focus, value: value, onChange:
|
|
88
|
+
return (_jsxs(Box, { flexDirection: "column", children: [matches.length > 0 && accumulatedLines.length === 0 && (_jsxs(Box, { flexDirection: "column", children: [matches.slice(Math.max(0, chosen - 5), Math.max(0, chosen - 5) + 6).map(item => (_jsxs(Text, { color: item === matches[chosen] ? theme.primary : theme.dim, children: [item === matches[chosen] ? '› ' : ' ', item.name, " ", item.description] }, item.name))), _jsxs(Text, { dimColor: true, children: ["\u2191\u2193 \u9009\u62E9 \u00B7 Tab \u8865\u5168 \u00B7 \u8F93\u5165\u4EFB\u52A1\u540E Enter \u53D1\u9001 \u00B7 Esc \u6536\u8D77\uFF08", matches.length, " \u9879\uFF09"] })] })), accumulatedLines.map((line, idx) => (_jsxs(Box, { children: [_jsxs(Text, { color: promptColor, children: [promptChar, " "] }), _jsx(Text, { children: line })] }, idx))), _jsxs(Box, { children: [_jsxs(Text, { color: promptColor, children: [promptChar, " "] }), disabled ? (_jsx(Text, { color: theme.dim, children: placeholder || '处理中...' })) : (_jsx(TextInput, { focus: focus, value: value, onChange: changeValue, placeholder: placeholder || '' }, completionRevision))] })] }));
|
|
60
89
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -47,7 +47,7 @@ export interface PulseCliMcpServer {
|
|
|
47
47
|
export interface PulseCliCapabilities {
|
|
48
48
|
/** IDs must be explicitly enabled; workspace config cannot enable host extensions. */
|
|
49
49
|
enabled?: string[];
|
|
50
|
-
/**
|
|
50
|
+
/** Optional discovery allow-list of skill directory names; omitted discovers all. Bodies load only on invocation. */
|
|
51
51
|
skills?: string[];
|
|
52
52
|
/** Additional absolute roots explicitly trusted by the user for installed skills. */
|
|
53
53
|
trustedSkillRoots?: string[];
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface PresentationOptions {
|
|
2
|
+
color?: boolean;
|
|
3
|
+
}
|
|
4
|
+
export declare function shouldUseColor(env: NodeJS.ProcessEnv, isTTY: boolean): boolean;
|
|
5
|
+
export declare function formatVersionOutput(version: string, options?: PresentationOptions): string;
|
|
6
|
+
export interface InstallWelcomeOptions extends PresentationOptions {
|
|
7
|
+
version: string;
|
|
8
|
+
configPath?: string;
|
|
9
|
+
error?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function formatInstallWelcome(options: InstallWelcomeOptions): string;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { releaseHighlights } from './release-highlights.generated.js';
|
|
2
|
+
const bannerLines = [
|
|
3
|
+
'██████╗ ██╗ ██╗██╗ ███████╗███████╗',
|
|
4
|
+
'██╔══██╗██║ ██║██║ ██╔════╝██╔════╝',
|
|
5
|
+
'██████╔╝██║ ██║██║ ███████╗█████╗ ',
|
|
6
|
+
'██╔═══╝ ██║ ██║██║ ╚════██║██╔══╝ ',
|
|
7
|
+
'██║ ╚██████╔╝███████╗███████║███████╗',
|
|
8
|
+
'╚═╝ ╚═════╝ ╚══════╝╚══════╝╚══════╝',
|
|
9
|
+
];
|
|
10
|
+
function paint(code, enabled, value) {
|
|
11
|
+
return enabled ? `\u001b[${code}m${value}\u001b[0m` : value;
|
|
12
|
+
}
|
|
13
|
+
export function shouldUseColor(env, isTTY) {
|
|
14
|
+
return env.NO_COLOR === undefined && env.FORCE_COLOR !== '0' && (isTTY || env.FORCE_COLOR !== undefined);
|
|
15
|
+
}
|
|
16
|
+
function banner(color) {
|
|
17
|
+
return bannerLines.map((line) => paint('36', color, line)).join('\n');
|
|
18
|
+
}
|
|
19
|
+
export function formatVersionOutput(version, options = {}) {
|
|
20
|
+
const color = options.color === true;
|
|
21
|
+
const highlights = releaseHighlights[version];
|
|
22
|
+
const lines = [
|
|
23
|
+
banner(color),
|
|
24
|
+
'',
|
|
25
|
+
` ${paint('1', color, 'Pulse')} ${paint('2', color, `v${version}`)}`,
|
|
26
|
+
'',
|
|
27
|
+
` ${paint('1', color, '本版本亮点 / What’s new in this version')}`,
|
|
28
|
+
];
|
|
29
|
+
if (highlights?.length) {
|
|
30
|
+
for (const item of highlights)
|
|
31
|
+
lines.push(` • ${item.zh}`, ` ${item.en}`);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
lines.push(' 暂无此版本的更新亮点记录。', ' No release highlights are available for this version yet.');
|
|
35
|
+
}
|
|
36
|
+
lines.push('');
|
|
37
|
+
return `${lines.join('\n')}\n`;
|
|
38
|
+
}
|
|
39
|
+
export function formatInstallWelcome(options) {
|
|
40
|
+
const { color = false, version, configPath, error } = options;
|
|
41
|
+
const cyan = (value) => paint('36', color, value);
|
|
42
|
+
const dim = (value) => paint('2', color, value);
|
|
43
|
+
const lines = [
|
|
44
|
+
'',
|
|
45
|
+
banner(color),
|
|
46
|
+
'',
|
|
47
|
+
` ${paint('1', color, 'Pulse')} v${version} — 面向编程、研究与文件工作的 AI 助手`,
|
|
48
|
+
` ${paint('1', color, 'Pulse')} — an AI assistant for coding, research, and file tasks`,
|
|
49
|
+
'',
|
|
50
|
+
` ${paint('1', color, '开始使用 / Get started:')}`,
|
|
51
|
+
` ${cyan('cd project_dir')}`,
|
|
52
|
+
` ${cyan('pulse')}`,
|
|
53
|
+
'',
|
|
54
|
+
];
|
|
55
|
+
if (configPath) {
|
|
56
|
+
lines.push(` ${dim('配置已就绪 / Configuration ready at:')}`, ` ${configPath}`);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
lines.push(` ${dim('配置创建失败 / Configuration could not be created:')} ${error ?? 'Unknown error'}`);
|
|
60
|
+
lines.push(` ${dim('稍后运行 "pulse setup" 创建配置。 / Run "pulse setup" later to create it.')}`);
|
|
61
|
+
}
|
|
62
|
+
lines.push('');
|
|
63
|
+
return `${lines.join('\n')}\n`;
|
|
64
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// Generated from CHANGELOG.md and CHANGELOG.en.md by scripts/release/sync-cli-highlights.mjs. Do not edit manually.
|
|
2
|
+
export const releaseHighlights = {
|
|
3
|
+
"0.4.2": [
|
|
4
|
+
{
|
|
5
|
+
"zh": "阶段验收要求每条引用都有效;涉及文件、命令或测试的阶段必须有成功的工具证据,纯写作阶段仍可凭候选结果通过。",
|
|
6
|
+
"en": "Stage acceptance requires every cited reference to be valid. Stages that touch files, commands, or tests need successful tool evidence, while pure writing stages can still pass on the candidate result."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"zh": "DeepSeek 不再把未命名 JSON 当成文件写入或命令执行;回复后的附加文本保持为正文,损坏的 DSML 参数按截断处理。",
|
|
10
|
+
"en": "DeepSeek recovery no longer treats unnamed JSON as a file write or shell command. Trailing text stays text, and invalid DSML parameters are treated as truncation."
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"zh": "暂存写入和提交会拒绝 `.pulse` 路径;列出不存在的目录会返回 ENOENT,已存在的空目录仍返回空列表。",
|
|
14
|
+
"en": "Staged writes and commits reject `.pulse` paths. Listing a missing directory returns ENOENT, while an existing empty directory still returns an empty list."
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"zh": "命令拆分能识别引号;通过 `sh -c`、`bash -c` 等启动器执行的命令会被拒绝。",
|
|
18
|
+
"en": "Command splitting respects quotes. Commands launched through shells such as `sh -c` or `bash -c` are rejected."
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"zh": "`pulse resume` 在终端重新进入交互界面;已有任务的非交互运行不再因空闲标准输入一直等待,显式 mock 参数可以覆盖 `PULSE_MODEL`。",
|
|
22
|
+
"en": "`pulse resume` returns to the interactive screen on a terminal. A non-interactive run that already has a task no longer waits on idle stdin, and explicit mock flags can override `PULSE_MODEL`."
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"0.4.1": [
|
|
26
|
+
{
|
|
27
|
+
"zh": "新增按需发现与调用 Skill 的能力,减少无关指令进入任务上下文。",
|
|
28
|
+
"en": "Added on-demand Skill discovery and invocation to keep unrelated instructions out of task context."
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"zh": "美化安装欢迎界面和版本页,提供中英双语介绍及版本亮点。",
|
|
32
|
+
"en": "Polished the installation welcome screen and version page with bilingual product information and release highlights."
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"zh": "提供中英文分离的更新日志和双语用户文档,并校验版本亮点来源。",
|
|
36
|
+
"en": "Added separate Chinese and English changelogs and bilingual user documentation, with validation for version highlights."
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"zh": "为全部五个 npm 包采用 PolyForm Noncommercial 1.0.0,并随包提供许可证说明。",
|
|
40
|
+
"en": "Licensed all five npm packages under PolyForm Noncommercial 1.0.0 and included package-level license notices."
|
|
41
|
+
}
|
|
42
|
+
],
|
|
43
|
+
"0.4.0": [
|
|
44
|
+
{
|
|
45
|
+
"zh": "默认使用异步并行 event loop,让模型请求、工具调用等副作用并发推进。",
|
|
46
|
+
"en": "Uses an asynchronous, parallel event loop by default so model requests, tool calls, and other effects can progress concurrently."
|
|
47
|
+
}
|
|
48
|
+
],
|
|
49
|
+
"0.3.0": [
|
|
50
|
+
{
|
|
51
|
+
"zh": "增强 CLI 交互体验,支持内容选择与复制、推理用量归因、任务进度流式展示和可恢复会话。",
|
|
52
|
+
"en": "Improved CLI interaction with text selection and copying, reasoning-usage attribution, streamed task progress, and resumable sessions."
|
|
53
|
+
}
|
|
54
|
+
],
|
|
55
|
+
"0.2.1": [
|
|
56
|
+
{
|
|
57
|
+
"zh": "增加安装后的欢迎界面,并完善跨平台本地与云端 CI 验证入口。",
|
|
58
|
+
"en": "Added the post-install welcome screen and improved the shared cross-platform local and cloud CI entry point."
|
|
59
|
+
}
|
|
60
|
+
],
|
|
61
|
+
"0.2.0": [
|
|
62
|
+
{
|
|
63
|
+
"zh": "扩展 CLI 与 Host 能力,完善会话恢复、人机协作、工具调用和 Windows 沙箱及发布验证。",
|
|
64
|
+
"en": "Expanded CLI and Host capabilities with resumable sessions, human interaction, tool calls, and improved Windows sandbox and release validation."
|
|
65
|
+
}
|
|
66
|
+
],
|
|
67
|
+
"0.1.11": [
|
|
68
|
+
{
|
|
69
|
+
"zh": "修复 Windows 下打包 CLI 入口加载问题,并改进跨平台安装与发布验证。",
|
|
70
|
+
"en": "Fixed loading the packaged CLI entry point on Windows and improved cross-platform installation and release validation."
|
|
71
|
+
}
|
|
72
|
+
],
|
|
73
|
+
"0.1.10": [
|
|
74
|
+
{
|
|
75
|
+
"zh": "维护性版本发布;未单独记录用户可见更新。",
|
|
76
|
+
"en": "Maintenance release; no separate user-facing changes were recorded."
|
|
77
|
+
}
|
|
78
|
+
],
|
|
79
|
+
"0.1.9": [
|
|
80
|
+
{
|
|
81
|
+
"zh": "维护性版本发布;未单独记录用户可见更新。",
|
|
82
|
+
"en": "Maintenance release; no separate user-facing changes were recorded."
|
|
83
|
+
}
|
|
84
|
+
],
|
|
85
|
+
"0.1.8": [
|
|
86
|
+
{
|
|
87
|
+
"zh": "增加 npm 安装后的首次配置初始化流程。",
|
|
88
|
+
"en": "Added first-run configuration initialization after npm installation."
|
|
89
|
+
}
|
|
90
|
+
],
|
|
91
|
+
"0.1.7": [
|
|
92
|
+
{
|
|
93
|
+
"zh": "增加完整屏幕 CLI 工作区、模型配置与会话交互能力。",
|
|
94
|
+
"en": "Added a full-screen CLI workspace, model configuration, and interactive session capabilities."
|
|
95
|
+
}
|
|
96
|
+
],
|
|
97
|
+
"0.1.6": [
|
|
98
|
+
{
|
|
99
|
+
"zh": "增加人类输入仲裁和主动 CLI 交互,并强化上下文、持久化与流式工具调用。",
|
|
100
|
+
"en": "Added human-input arbitration and active CLI interaction, while strengthening context, persistence, and streamed tool calls."
|
|
101
|
+
}
|
|
102
|
+
],
|
|
103
|
+
"0.1.5": [
|
|
104
|
+
{
|
|
105
|
+
"zh": "改进 Provider 工具名称兼容性。",
|
|
106
|
+
"en": "Improved compatibility of tool names across providers."
|
|
107
|
+
}
|
|
108
|
+
],
|
|
109
|
+
"0.1.4": [
|
|
110
|
+
{
|
|
111
|
+
"zh": "修复 Provider 安全工具名称的适配问题。",
|
|
112
|
+
"en": "Fixed adapter handling for provider-safe tool names."
|
|
113
|
+
}
|
|
114
|
+
],
|
|
115
|
+
"0.1.3": [
|
|
116
|
+
{
|
|
117
|
+
"zh": "完善 CLI 工具暴露、用户文件目录和 Provider 错误反馈。",
|
|
118
|
+
"en": "Improved CLI tool exposure, user-file locations, and provider error reporting."
|
|
119
|
+
}
|
|
120
|
+
],
|
|
121
|
+
"0.1.2": [
|
|
122
|
+
{
|
|
123
|
+
"zh": "修复 CLI 显示已安装包版本的问题。",
|
|
124
|
+
"en": "Fixed CLI reporting of the installed package version."
|
|
125
|
+
}
|
|
126
|
+
],
|
|
127
|
+
"0.1.1": [
|
|
128
|
+
{
|
|
129
|
+
"zh": "完善首次运行配置创建及基础 npm 工作区使用体验。",
|
|
130
|
+
"en": "Improved first-run configuration setup and basic npm workspace usage."
|
|
131
|
+
}
|
|
132
|
+
],
|
|
133
|
+
"0.1.0": [
|
|
134
|
+
{
|
|
135
|
+
"zh": "建立 Pulse CLI 初始 npm 发布包和仓库发布元数据。",
|
|
136
|
+
"en": "Established the initial Pulse CLI npm package and repository release metadata."
|
|
137
|
+
}
|
|
138
|
+
]
|
|
139
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export interface SlashSuggestion {
|
|
2
|
+
name: string;
|
|
3
|
+
description: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function skillSuggestions(names: readonly string[], commands: readonly SlashSuggestion[]): SlashSuggestion[];
|
|
6
|
+
export declare function searchSlashSuggestions(value: string, items: readonly SlashSuggestion[]): SlashSuggestion[];
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function skillSuggestions(names, commands) {
|
|
2
|
+
const reserved = new Set(commands.map(item => item.name.toLowerCase()));
|
|
3
|
+
return names.map(name => ({ name: reserved.has(`/${name}`.toLowerCase()) ? `/skill:${name}` : `/${name}`, description: '技能 · 提交时加载' }));
|
|
4
|
+
}
|
|
5
|
+
export function searchSlashSuggestions(value, items) {
|
|
6
|
+
if (!/^\/\S*$/.test(value))
|
|
7
|
+
return [];
|
|
8
|
+
const query = value.slice(1).toLowerCase();
|
|
9
|
+
return items.filter(item => item.name.slice(1).toLowerCase().includes(query))
|
|
10
|
+
.sort((a, b) => Number(b.name.slice(1).toLowerCase().startsWith(query)) - Number(a.name.slice(1).toLowerCase().startsWith(query)) || a.name.localeCompare(b.name));
|
|
11
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hunterzhu/pulse-cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "A local-first AI task assistant for coding, research, and file workflows.",
|
|
5
|
+
"license": "PolyForm-Noncommercial-1.0.0",
|
|
5
6
|
"keywords": [
|
|
6
7
|
"ai",
|
|
7
8
|
"agent",
|
|
@@ -27,7 +28,7 @@
|
|
|
27
28
|
"scripts"
|
|
28
29
|
],
|
|
29
30
|
"scripts": {
|
|
30
|
-
"build": "tsc -p tsconfig.json",
|
|
31
|
+
"build": "pnpm --dir ../.. changelog:sync && tsc -p tsconfig.json",
|
|
31
32
|
"postinstall": "node ./scripts/postinstall.mjs"
|
|
32
33
|
},
|
|
33
34
|
"publishConfig": {
|
|
@@ -35,7 +36,7 @@
|
|
|
35
36
|
"registry": "https://registry.npmjs.org"
|
|
36
37
|
},
|
|
37
38
|
"dependencies": {
|
|
38
|
-
"@hunterzhu/pulse-server": "0.4.
|
|
39
|
+
"@hunterzhu/pulse-server": "0.4.2",
|
|
39
40
|
"chalk": "^5.4.1",
|
|
40
41
|
"highlight.js": "^11.10.0",
|
|
41
42
|
"ink": "^7.1.1",
|
package/scripts/postinstall.mjs
CHANGED
|
@@ -1,58 +1,15 @@
|
|
|
1
|
+
import { createRequire } from 'node:module'
|
|
1
2
|
import { ensurePulseUserConfig, defaultPulseConfigPath } from '../dist/config.js'
|
|
3
|
+
import { formatInstallWelcome, shouldUseColor } from '../dist/presentation.js'
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
}
|
|
5
|
+
const { version } = createRequire(import.meta.url)('../package.json')
|
|
6
|
+
const color = shouldUseColor(process.env, process.stdout.isTTY === true)
|
|
50
7
|
|
|
51
8
|
try {
|
|
52
9
|
const configPath = defaultPulseConfigPath()
|
|
53
10
|
await ensurePulseUserConfig(configPath)
|
|
54
|
-
|
|
11
|
+
process.stdout.write(formatInstallWelcome({ version, configPath, color }))
|
|
55
12
|
} catch (error) {
|
|
56
13
|
const message = error instanceof Error ? error.message : String(error)
|
|
57
|
-
|
|
14
|
+
process.stdout.write(formatInstallWelcome({ version, error: message, color }))
|
|
58
15
|
}
|