@hunterzhu/pulse-cli 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +120 -46
- package/dist/commands/interactive.d.ts +1 -1
- package/dist/commands/interactive.js +20 -5
- package/dist/commands/resume.js +2 -0
- package/dist/commands/run.d.ts +1 -1
- package/dist/commands/run.js +2 -1
- package/dist/components/App.d.ts +1 -1
- package/dist/components/App.js +30 -1
- package/dist/components/AskPrompt.d.ts +2 -1
- package/dist/components/AskPrompt.js +55 -13
- package/dist/config.d.ts +19 -8
- package/dist/config.js +23 -9
- package/dist/hooks/useSlashCommands.d.ts +1 -0
- package/dist/hooks/useSlashCommands.js +1 -0
- package/dist/release-highlights.generated.js +56 -0
- package/package.json +2 -2
package/dist/bin.js
CHANGED
|
@@ -3,7 +3,8 @@ import { readFile } from 'node:fs/promises';
|
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
4
|
import { isAbsolute, resolve } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import { assertJarvisApiUrl, createMcpCapabilityPack, createPdfCapabilityPack, createSkillCapabilityPack, createSpreadsheetCapabilityPack } from '@hunterzhu/pulse-server';
|
|
6
|
+
import { createLocalHost, automationDoctor, requestAutomationPermissions, assertJarvisApiUrl, createBuiltinSkillCapabilityPack, createMcpCapabilityPack, createPdfCapabilityPack, createSkillCapabilityPack, createSpreadsheetCapabilityPack } from '@hunterzhu/pulse-server';
|
|
7
|
+
import { modelSelectionId, normalizeModelConfig, resolveModelReference } from '@hunterzhu/pulse-server';
|
|
7
8
|
import { defaultPulseConfig, defaultPulseConfigPath, ensurePulseUserConfig, expandHome, loadPulseConfig } from './config.js';
|
|
8
9
|
import { runInteractive } from './commands/interactive.js';
|
|
9
10
|
import { runOneShot } from './commands/run.js';
|
|
@@ -32,13 +33,14 @@ Usage:
|
|
|
32
33
|
pulse run <task> [options] run one task
|
|
33
34
|
pulse sessions [options] list saved conversations
|
|
34
35
|
pulse resume <conversation-id> [task] continue or recover a conversation
|
|
35
|
-
pulse doctor [options] check local configuration
|
|
36
|
+
pulse drafts <conversation-id> <run-id> <draft-id> [--revision <hash> --confirm-owner]\n preview or explicitly migrate an ownerless draft\n pulse doctor [options] check local configuration
|
|
36
37
|
pulse h5 [start|qr|devices|revoke|install|status|stop|uninstall]
|
|
37
38
|
mobile web assistant and launchd daemon
|
|
38
39
|
pulse schedule <add|list|pause|resume|remove|daemon> [options]
|
|
39
40
|
manage recurring tasks (daemon requires read-only or auto approval)
|
|
40
41
|
pulse service <install|status|start|stop|uninstall>
|
|
41
42
|
pulse template <list|show|run> [name] [values...]
|
|
43
|
+
pulse automation doctor check browser and desktop dependencies
|
|
42
44
|
pulse mcp doctor [server-id]
|
|
43
45
|
|
|
44
46
|
Options:
|
|
@@ -50,8 +52,9 @@ Options:
|
|
|
50
52
|
--max-output-tokens <n> maximum generated tokens (default 4096)
|
|
51
53
|
--reasoning-effort <x> low, medium, or high
|
|
52
54
|
--max-turns <n> maximum ReAct model/tool turns (default 32)
|
|
55
|
+
--max-model-calls <n> maximum model attempts per work task (default 256)
|
|
53
56
|
--auto-compact-percent <n> compact automatically at this percent of the context window (1-90, default 90)
|
|
54
|
-
--format <text|jsonl> output format
|
|
57
|
+
--accept-build-migration <old:new> authorize one reviewed recovery build pair\n --requirements-doc <path> explicitly attach a workspace requirements document\n --format <text|jsonl> output format
|
|
55
58
|
--resume enter the latest saved session
|
|
56
59
|
--read-only disable write and shell tools
|
|
57
60
|
--auto-approve approve local writes and shell execution on your behalf
|
|
@@ -95,6 +98,7 @@ const knownBooleanFlags = new Set([
|
|
|
95
98
|
'help',
|
|
96
99
|
'version',
|
|
97
100
|
'force',
|
|
101
|
+
'confirm-owner',
|
|
98
102
|
'jarvis',
|
|
99
103
|
'no-jarvis',
|
|
100
104
|
'jarvis-allow-remote',
|
|
@@ -141,7 +145,7 @@ export function parse(argv) {
|
|
|
141
145
|
options[key] = true;
|
|
142
146
|
continue;
|
|
143
147
|
}
|
|
144
|
-
if (!command && ['run', 'sessions', 'resume', 'doctor', 'setup', 'schedule', 'service', 'template', 'mcp', 'h5'].includes(arg)) {
|
|
148
|
+
if (!command && ['run', 'sessions', 'resume', 'doctor', 'setup', 'schedule', 'service', 'template', 'mcp', 'h5', 'automation', 'drafts'].includes(arg)) {
|
|
145
149
|
command = arg;
|
|
146
150
|
}
|
|
147
151
|
else {
|
|
@@ -162,7 +166,7 @@ export async function hostOptions(parsed) {
|
|
|
162
166
|
}
|
|
163
167
|
const loaded = (await loadPulseConfig(requestedCwd ?? process.cwd(), explicitConfig, parsed.options['trust-workspace'] === true)).value;
|
|
164
168
|
// A missing user config still has to answer usage errors and list sessions.
|
|
165
|
-
const config = loaded.activeModel || loaded.models || loaded.providers ? loaded : { ...defaultPulseConfig, ...loaded };
|
|
169
|
+
const config = normalizeModelConfig(loaded.activeModel || loaded.models || loaded.providers ? loaded : { ...defaultPulseConfig, ...loaded });
|
|
166
170
|
const mockResponse = option(parsed.options, 'mock-response');
|
|
167
171
|
const mockTaskAssessment = option(parsed.options, 'mock-task-assessment');
|
|
168
172
|
const mockStageAssessment = option(parsed.options, 'mock-stage-assessment');
|
|
@@ -171,25 +175,33 @@ export async function hostOptions(parsed) {
|
|
|
171
175
|
const mockParallelPlanOption = option(parsed.options, 'mock-parallel-plan');
|
|
172
176
|
const hasMockDemand = mockResponse !== undefined || mockTaskAssessment !== undefined || mockStageAssessment !== undefined || mockToolCallsOption !== undefined || mockAfterToolResponse !== undefined || mockParallelPlanOption !== undefined;
|
|
173
177
|
const cliModel = option(parsed.options, 'model');
|
|
174
|
-
const requestedModel = cliModel ?? (hasMockDemand ? 'mock' : process.env.PULSE_MODEL ?? config.activeModel);
|
|
175
|
-
const effectiveModels = { ...(config.models ?? {}) };
|
|
178
|
+
const requestedModel = cliModel ?? (hasMockDemand ? 'mock/mock' : process.env.PULSE_MODEL ?? config.activeModel);
|
|
176
179
|
const effectiveProviders = { ...(config.providers ?? {}) };
|
|
177
|
-
if (requestedModel === 'mock' || (hasMockDemand && cliModel === undefined)) {
|
|
178
|
-
if (!effectiveModels.mock) {
|
|
179
|
-
effectiveModels.mock = { displayName: 'mock', provider: 'mock', modelCode: 'mock', maxContextTokens: 32_000, maxOutputTokens: 4_096, reasoningEffort: 'medium' };
|
|
180
|
-
}
|
|
180
|
+
if (requestedModel === 'mock/mock' || requestedModel === 'mock' || (hasMockDemand && cliModel === undefined)) {
|
|
181
181
|
if (!effectiveProviders.mock) {
|
|
182
182
|
effectiveProviders.mock = { provider: 'mock', name: 'Mock' };
|
|
183
183
|
}
|
|
184
|
+
effectiveProviders.mock.models ??= {};
|
|
185
|
+
effectiveProviders.mock.models.mock ??= { displayName: 'mock', modelCode: 'mock', maxContextTokens: 32_000, maxOutputTokens: 4_096, reasoningEffort: 'medium' };
|
|
186
|
+
}
|
|
187
|
+
const catalog = { ...config, providers: effectiveProviders };
|
|
188
|
+
const selections = Object.entries(effectiveProviders).flatMap(([providerId, profile]) => Object.entries(profile.models ?? {}).map(([modelId, model]) => ({ id: modelSelectionId(providerId, modelId), providerId, modelId, model })));
|
|
189
|
+
if (!requestedModel)
|
|
190
|
+
throw new Error('UNKNOWN_MODEL_DISPLAY_NAME:(missing)');
|
|
191
|
+
let activeModelName;
|
|
192
|
+
try {
|
|
193
|
+
activeModelName = resolveModelReference(catalog, requestedModel);
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
if (error instanceof Error && error.message.startsWith('AMBIGUOUS_MODEL_REFERENCE:'))
|
|
197
|
+
throw error;
|
|
198
|
+
throw new Error(`UNKNOWN_MODEL_DISPLAY_NAME:${requestedModel}`);
|
|
184
199
|
}
|
|
185
|
-
const configuredModelEntry =
|
|
186
|
-
? undefined
|
|
187
|
-
: Object.entries(effectiveModels).find(([name, item]) => name === requestedModel || item.displayName === requestedModel);
|
|
200
|
+
const configuredModelEntry = selections.find(item => item.id === activeModelName);
|
|
188
201
|
if (!configuredModelEntry)
|
|
189
|
-
throw new Error(`UNKNOWN_MODEL_DISPLAY_NAME:${requestedModel
|
|
190
|
-
const
|
|
191
|
-
const
|
|
192
|
-
const providerName = modelSelection.provider;
|
|
202
|
+
throw new Error(`UNKNOWN_MODEL_DISPLAY_NAME:${requestedModel}`);
|
|
203
|
+
const modelSelection = configuredModelEntry.model;
|
|
204
|
+
const providerName = configuredModelEntry.providerId;
|
|
193
205
|
const profile = effectiveProviders[providerName];
|
|
194
206
|
if (!profile)
|
|
195
207
|
throw new Error(`MODEL_PROVIDER_NOT_FOUND:${providerName}`);
|
|
@@ -201,6 +213,7 @@ export async function hostOptions(parsed) {
|
|
|
201
213
|
const maxOutputTokens = option(parsed.options, 'max-output-tokens') ?? process.env.PULSE_MAX_OUTPUT_TOKENS;
|
|
202
214
|
const reasoningEffort = option(parsed.options, 'reasoning-effort') ?? process.env.PULSE_REASONING_EFFORT;
|
|
203
215
|
const maxTurns = option(parsed.options, 'max-turns') ?? process.env.PULSE_MAX_TURNS;
|
|
216
|
+
const maxModelCalls = option(parsed.options, 'max-model-calls') ?? process.env.PULSE_MAX_MODEL_CALLS;
|
|
204
217
|
const maxRuntimeMs = option(parsed.options, 'max-runtime-ms') ?? process.env.PULSE_MAX_RUNTIME_MS;
|
|
205
218
|
const autoCompactPercent = option(parsed.options, 'auto-compact-percent') ?? process.env.PULSE_AUTO_COMPACT_PERCENT;
|
|
206
219
|
const apiKeyEnv = profile.apiKeyEnv ?? (profile.provider === 'anthropic' ? 'ANTHROPIC_API_KEY' : 'OPENAI_API_KEY');
|
|
@@ -216,6 +229,7 @@ export async function hostOptions(parsed) {
|
|
|
216
229
|
const configuredReasoningEffort = reasoningEffort === 'low' || reasoningEffort === 'medium' || reasoningEffort === 'high' ? reasoningEffort : modelSelection.reasoningEffort ?? profile.reasoningEffort;
|
|
217
230
|
const configuredToolChoice = profile.toolChoice;
|
|
218
231
|
const configuredMaxTurns = parsePositiveInteger(maxTurns) ?? config.maxTurns;
|
|
232
|
+
const configuredMaxModelCalls = parsePositiveInteger(maxModelCalls) ?? config.maxModelCalls ?? 256;
|
|
219
233
|
const configuredMaxRuntimeMs = parsePositiveInteger(maxRuntimeMs) ?? config.maxRuntimeMs;
|
|
220
234
|
const configuredAutoCompactPercent = parsePositiveInteger(autoCompactPercent) ?? config.autoCompactPercent;
|
|
221
235
|
const provider = {
|
|
@@ -244,28 +258,30 @@ export async function hostOptions(parsed) {
|
|
|
244
258
|
...(item.toolChoice === undefined ? {} : { toolChoice: item.toolChoice }),
|
|
245
259
|
}]];
|
|
246
260
|
}));
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
261
|
+
for (const selection of selections)
|
|
262
|
+
if (selection.model.imageInput !== undefined && !['supported', 'unsupported', 'unknown'].includes(selection.model.imageInput))
|
|
263
|
+
throw new Error(`INVALID_MODEL_IMAGE_INPUT:${selection.id}`);
|
|
264
|
+
const providerModels = Object.fromEntries(selections.map(({ id, providerId, model }) => [id, {
|
|
265
|
+
provider: providerId,
|
|
266
|
+
model: model.modelCode,
|
|
267
|
+
...(model.imageInput === undefined ? {} : { imageInput: model.imageInput }),
|
|
268
|
+
...(model.maxContextTokens === undefined ? {} : { maxContextTokens: model.maxContextTokens }),
|
|
269
|
+
...(model.maxOutputTokens === undefined ? {} : { maxOutputTokens: model.maxOutputTokens }),
|
|
270
|
+
...(model.reasoningEffort === undefined ? {} : { reasoningEffort: model.reasoningEffort }),
|
|
271
|
+
}]));
|
|
272
|
+
// Explicit CLI/env limits must reach the routed candidate too. Otherwise the
|
|
273
|
+
// model catalog silently overrides the adapter's smaller output/effort limits.
|
|
274
|
+
const activeCandidate = providerModels[activeModelName];
|
|
275
|
+
if (parsePositiveInteger(contextTokens) !== undefined)
|
|
276
|
+
activeCandidate.maxContextTokens = configuredContextTokens;
|
|
277
|
+
if (parsePositiveInteger(maxOutputTokens) !== undefined)
|
|
278
|
+
activeCandidate.maxOutputTokens = configuredMaxOutputTokens;
|
|
279
|
+
if (reasoningEffort === 'low' || reasoningEffort === 'medium' || reasoningEffort === 'high')
|
|
280
|
+
activeCandidate.reasoningEffort = reasoningEffort;
|
|
281
|
+
const assistantModelName = config.assistantModel ? resolveModelReference(catalog, config.assistantModel) : activeModelName;
|
|
266
282
|
if (!providerModels[assistantModelName])
|
|
267
283
|
throw new Error(`UNKNOWN_ASSISTANT_MODEL:${assistantModelName}`);
|
|
268
|
-
const taskRouting = config.taskRouting;
|
|
284
|
+
const taskRouting = config.taskRouting && Object.fromEntries(Object.entries(config.taskRouting).map(([task, route]) => [task, Array.isArray(route) ? route.map(item => resolveModelReference(catalog, item)) : route]));
|
|
269
285
|
for (const [task, models] of Object.entries(taskRouting ?? {})) {
|
|
270
286
|
if (!Array.isArray(models) || models.length === 0 || models.some((model) => typeof model !== 'string' || !providerModels[model])) {
|
|
271
287
|
throw new Error(`INVALID_TASK_MODEL_ROUTE:${task}`);
|
|
@@ -337,14 +353,18 @@ export async function hostOptions(parsed) {
|
|
|
337
353
|
const skillRoots = capabilitySettings.trustedSkillRoots ?? [];
|
|
338
354
|
if (!Array.isArray(skillRoots) || skillRoots.some((root) => typeof root !== 'string' || !isAbsolute(expandHome(root) ?? root)))
|
|
339
355
|
throw new Error('SKILL_TRUSTED_ROOT_MUST_BE_ABSOLUTE');
|
|
340
|
-
const
|
|
341
|
-
if (!Array.isArray(
|
|
356
|
+
const configuredCapabilityPacks = capabilitySettings.enabled ?? [];
|
|
357
|
+
if (!Array.isArray(configuredCapabilityPacks) || configuredCapabilityPacks.some((id) => typeof id !== 'string') || new Set(configuredCapabilityPacks).size !== configuredCapabilityPacks.length)
|
|
342
358
|
throw new Error('INVALID_ENABLED_CAPABILITY_LIST');
|
|
359
|
+
// Built-in skills are always enabled so they are discoverable with no external skills directory.
|
|
360
|
+
const enabledCapabilityPacks = ['builtin-skills', ...configuredCapabilityPacks.filter((id) => id !== 'builtin-skills')];
|
|
343
361
|
const skillNames = capabilitySettings.skills ?? [];
|
|
344
362
|
if (!Array.isArray(skillNames) || skillNames.some((name) => typeof name !== 'string'))
|
|
345
363
|
throw new Error('INVALID_SKILL_SELECTION');
|
|
346
|
-
const capabilityPacks = [createPdfCapabilityPack(), createSpreadsheetCapabilityPack(), createSkillCapabilityPack({ trustedRoots: skillRoots.map((root) => resolve(expandHome(root) ?? root)) })];
|
|
364
|
+
const capabilityPacks = [createBuiltinSkillCapabilityPack(), createPdfCapabilityPack(), createSpreadsheetCapabilityPack(), createSkillCapabilityPack({ trustedRoots: skillRoots.map((root) => resolve(expandHome(root) ?? root)) })];
|
|
347
365
|
for (const [id, server] of Object.entries(capabilitySettings.mcpServers ?? {})) {
|
|
366
|
+
if ((id === 'browser' && capabilitySettings.browser) || (id === 'computer' && capabilitySettings.computer))
|
|
367
|
+
throw new Error(`AUTOMATION_CAPABILITY_CONFLICT:${id}`);
|
|
348
368
|
if (!server || typeof server.command !== 'string' || server.command.trim().length === 0 || (server.args !== undefined && (!Array.isArray(server.args) || server.args.some((arg) => typeof arg !== 'string'))))
|
|
349
369
|
throw new Error(`INVALID_MCP_SERVER_CONFIG:${id}`);
|
|
350
370
|
if (server.toolPolicies && Object.entries(server.toolPolicies).some(([name, policy]) => !name || !['read', 'write', 'external'].includes(policy)))
|
|
@@ -368,7 +388,7 @@ export async function hostOptions(parsed) {
|
|
|
368
388
|
}));
|
|
369
389
|
}
|
|
370
390
|
for (const id of enabledCapabilityPacks) {
|
|
371
|
-
if (!capabilityPacks.some((pack) => pack.manifest.id === id))
|
|
391
|
+
if (!['browser', 'computer'].includes(id) && !capabilityPacks.some((pack) => pack.manifest.id === id))
|
|
372
392
|
throw new Error(`UNKNOWN_ENABLED_CAPABILITY:${id}`);
|
|
373
393
|
}
|
|
374
394
|
const rawSystemPrompt = option(parsed.options, 'system-prompt') ?? process.env.PULSE_SYSTEM_PROMPT ?? config.systemPrompt;
|
|
@@ -425,7 +445,8 @@ export async function hostOptions(parsed) {
|
|
|
425
445
|
provider,
|
|
426
446
|
providerProfiles,
|
|
427
447
|
providerModels,
|
|
428
|
-
|
|
448
|
+
automation: { ...(capabilitySettings.browser ? { browser: capabilitySettings.browser } : {}), ...(capabilitySettings.computer ? { computer: capabilitySettings.computer } : {}) },
|
|
449
|
+
modelPricing: Object.fromEntries(selections.flatMap(({ id, model }) => model.pricing ? [[id, model.pricing]] : [])),
|
|
429
450
|
...(taskRouting === undefined ? {} : { taskRouting }),
|
|
430
451
|
capabilityPacks,
|
|
431
452
|
enabledCapabilityPacks,
|
|
@@ -441,13 +462,32 @@ export async function hostOptions(parsed) {
|
|
|
441
462
|
...(mockParallelPlan === undefined ? {} : { mockParallelPlan }),
|
|
442
463
|
...(approvalMode === undefined ? {} : { approvalMode }),
|
|
443
464
|
...(configuredMaxTurns === undefined ? {} : { maxTurns: configuredMaxTurns }),
|
|
465
|
+
maxModelCalls: configuredMaxModelCalls,
|
|
466
|
+
...(option(parsed.options, 'accept-build-migration') ? { recoveryBuildMigration: (() => { const pair = option(parsed.options, 'accept-build-migration').split(':'); if (pair.length !== 2 || pair.some(hash => !/^[a-f0-9]{64}$/.test(hash)))
|
|
467
|
+
throw new Error('Invalid build migration pair; use the two hashes in the recovery diagnostic.'); return { from: pair[0], to: pair[1] }; })() } : {}),
|
|
468
|
+
...(config.modelBudgetPolicy === undefined ? {} : { modelBudgetPolicy: config.modelBudgetPolicy }),
|
|
469
|
+
...(config.persistenceBackend === undefined ? {} : { persistenceBackend: config.persistenceBackend }),
|
|
470
|
+
...(config.maxCostByCurrency === undefined ? {} : { maxCostByCurrency: config.maxCostByCurrency }),
|
|
444
471
|
...(configuredMaxRuntimeMs === undefined ? {} : { maxRuntimeMs: configuredMaxRuntimeMs }),
|
|
445
472
|
...(configuredAutoCompactPercent === undefined ? {} : { autoCompactPercent: Math.min(90, configuredAutoCompactPercent) }),
|
|
446
473
|
...(allowNetwork === undefined ? {} : { allowNetwork }),
|
|
447
474
|
...(config.networkHosts === undefined ? {} : { networkHosts: config.networkHosts }),
|
|
448
475
|
jarvis,
|
|
449
|
-
...(config.frontModel ? { frontModel:
|
|
450
|
-
|
|
476
|
+
...(config.frontModel ? { frontModel: (() => {
|
|
477
|
+
const selected = typeof config.frontModel?.model === 'string' ? resolveModelReference(catalog, config.frontModel.model) : undefined;
|
|
478
|
+
const entry = selections.find(item => item.id === selected);
|
|
479
|
+
const source = entry && effectiveProviders[entry.providerId];
|
|
480
|
+
const keyEnv = source?.apiKeyEnv ?? (source?.provider === 'anthropic' ? 'ANTHROPIC_API_KEY' : 'OPENAI_API_KEY');
|
|
481
|
+
return { ...config.frontModel, ...(entry && source ? { provider: source.provider, model: entry.model.modelCode, ...(source.baseURL ? { baseURL: source.baseURL } : {}), ...(process.env[keyEnv] ? { apiKey: process.env[keyEnv] } : {}) } : {}) };
|
|
482
|
+
})() } : {}),
|
|
483
|
+
...(config.approvalReview?.model ? { approvalReview: (() => {
|
|
484
|
+
const selected = resolveModelReference(catalog, config.approvalReview.model);
|
|
485
|
+
const entry = selections.find(item => item.id === selected);
|
|
486
|
+
const source = entry && effectiveProviders[entry.providerId];
|
|
487
|
+
if (!entry || !source || source.provider !== 'ollama')
|
|
488
|
+
throw new Error('APPROVAL_REVIEW_REQUIRES_OLLAMA');
|
|
489
|
+
return { provider: 'ollama', model: entry.model.modelCode, baseURL: source.baseURL ?? 'http://127.0.0.1:11434', timeoutMs: Math.max(30_000, Math.min(60_000, config.approvalReview.timeoutMs ?? 30_000)) };
|
|
490
|
+
})() } : {}),
|
|
451
491
|
};
|
|
452
492
|
}
|
|
453
493
|
export async function main() {
|
|
@@ -466,9 +506,43 @@ export async function main() {
|
|
|
466
506
|
return runSetup(parsed.options.force === true, expandHome(option(parsed.options, 'config')));
|
|
467
507
|
}
|
|
468
508
|
const options = await hostOptions(parsed);
|
|
509
|
+
if (parsed.command === 'automation') {
|
|
510
|
+
if (parsed.positionals[0] === 'permissions') {
|
|
511
|
+
process.stdout.write(JSON.stringify(await requestAutomationPermissions(options.automation), null, 2) + '\n');
|
|
512
|
+
return 0;
|
|
513
|
+
}
|
|
514
|
+
if (parsed.positionals[0] !== 'doctor')
|
|
515
|
+
throw new Error('Usage: pulse automation doctor|permissions');
|
|
516
|
+
if (parsed.options.live === true) {
|
|
517
|
+
const host = createLocalHost(options);
|
|
518
|
+
try {
|
|
519
|
+
process.stdout.write(JSON.stringify(await host.probeVisionModel(option(parsed.options, 'model') ?? options.taskRouting?.vision?.[0] ?? options.activeModel ?? ''), null, 2) + '\n');
|
|
520
|
+
return 0;
|
|
521
|
+
}
|
|
522
|
+
finally {
|
|
523
|
+
await host.close();
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
const report = await automationDoctor(options.automation);
|
|
527
|
+
process.stdout.write(JSON.stringify(report, null, 2) + '\n');
|
|
528
|
+
return report.desktop.error && options.enabledCapabilityPacks?.includes('computer') ? 1 : 0;
|
|
529
|
+
}
|
|
469
530
|
if (parsed.command === 'doctor') {
|
|
470
531
|
return runDoctor(options, parsed.options.live === true, option(parsed.options, 'format') ?? 'text');
|
|
471
532
|
}
|
|
533
|
+
if (parsed.command === 'drafts') {
|
|
534
|
+
const [conversationId, runId, draftId] = parsed.positionals;
|
|
535
|
+
if (!conversationId || !runId || !draftId)
|
|
536
|
+
throw new Error('Usage: pulse drafts <conversation-id> <run-id> <draft-id> [--revision <hash> --confirm-owner]');
|
|
537
|
+
const host = createLocalHost(options);
|
|
538
|
+
try {
|
|
539
|
+
process.stdout.write(JSON.stringify(await host.migrateDraft(conversationId, runId, draftId, option(parsed.options, 'revision'), parsed.options['confirm-owner'] === true), null, 2) + '\n');
|
|
540
|
+
return 0;
|
|
541
|
+
}
|
|
542
|
+
finally {
|
|
543
|
+
await host.close();
|
|
544
|
+
}
|
|
545
|
+
}
|
|
472
546
|
if (parsed.command === 'sessions') {
|
|
473
547
|
return runSessions(options, option(parsed.options, 'format') ?? 'text', version);
|
|
474
548
|
}
|
|
@@ -525,7 +599,7 @@ export async function main() {
|
|
|
525
599
|
}
|
|
526
600
|
if (!task)
|
|
527
601
|
throw new Error('TASK_REQUIRED');
|
|
528
|
-
return runOneShot(options, task, option(parsed.options, 'format') ?? 'text', version);
|
|
602
|
+
return runOneShot(options, task, option(parsed.options, 'format') ?? 'text', version, option(parsed.options, 'requirements-doc') ? [option(parsed.options, 'requirements-doc')] : undefined);
|
|
529
603
|
}
|
|
530
604
|
if (parsed.command === 'resume') {
|
|
531
605
|
const id = parsed.positionals.shift();
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { LocalHostOptions } from '@hunterzhu/pulse-server';
|
|
2
|
-
export declare function findResumeConversation(options: LocalHostOptions): Promise<string | undefined>;
|
|
2
|
+
export declare function findResumeConversation(options: LocalHostOptions, conversationId?: string | undefined): Promise<string | undefined>;
|
|
3
3
|
export declare function runInteractive(options: LocalHostOptions, conversationId?: string | undefined, initialTask?: string | undefined, version?: string, resumeLatest?: boolean): Promise<number>;
|
|
@@ -4,10 +4,24 @@ import { App } from '../components/App.js';
|
|
|
4
4
|
import { createLocalHost } from '@hunterzhu/pulse-server';
|
|
5
5
|
import { closePendingHosts } from '../hooks/useHost.js';
|
|
6
6
|
import { runOneShot } from './run.js';
|
|
7
|
-
export async function findResumeConversation(options) {
|
|
7
|
+
export async function findResumeConversation(options, conversationId) {
|
|
8
8
|
const host = createLocalHost(options);
|
|
9
9
|
await host.init();
|
|
10
10
|
try {
|
|
11
|
+
if (conversationId !== undefined) {
|
|
12
|
+
// Validate an explicit ID without creating a conversation. A missing ID
|
|
13
|
+
// must fail loudly instead of silently opening a fresh history.
|
|
14
|
+
try {
|
|
15
|
+
await host.getConversation(conversationId);
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
if (error.code === 'ENOENT') {
|
|
19
|
+
throw Object.assign(new Error(`未找到指定会话:${conversationId}`), { code: 'CONVERSATION_NOT_FOUND' });
|
|
20
|
+
}
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
return conversationId;
|
|
24
|
+
}
|
|
11
25
|
const sessions = await host.listConversations();
|
|
12
26
|
return sessions.find((session) => session.activeRunId)?.id ?? sessions[0]?.id;
|
|
13
27
|
}
|
|
@@ -23,10 +37,11 @@ export async function runInteractive(options, conversationId, initialTask, versi
|
|
|
23
37
|
process.stderr.write('交互模式需要 TTY 终端。非交互执行请使用 "pulse run <task>"。\n');
|
|
24
38
|
return 1;
|
|
25
39
|
}
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
40
|
+
const shouldResume = resumeLatest || conversationId !== undefined;
|
|
41
|
+
const selectedConversationId = shouldResume
|
|
42
|
+
? await findResumeConversation(options, conversationId)
|
|
43
|
+
: undefined;
|
|
44
|
+
const { waitUntilExit, unmount } = render(_jsx(App, { hostOptions: options, conversationId: selectedConversationId, initialTask: initialTask, resumeOnStart: shouldResume && selectedConversationId !== undefined, version: version }), {
|
|
30
45
|
// Keep Pulse in its own full-screen buffer so the shell's previous
|
|
31
46
|
// command history does not become part of the chat workspace.
|
|
32
47
|
alternateScreen: true,
|
package/dist/commands/resume.js
CHANGED
|
@@ -53,6 +53,7 @@ export async function runResume(options, conversationId, task, format = 'text',
|
|
|
53
53
|
}
|
|
54
54
|
const outcome = await run.outcome();
|
|
55
55
|
const taskOutcome = await run.taskOutcome();
|
|
56
|
+
const usage = await run.usage();
|
|
56
57
|
if (format === 'jsonl') {
|
|
57
58
|
process.stdout.write(`${JSON.stringify({
|
|
58
59
|
schemaVersion: 1,
|
|
@@ -60,6 +61,7 @@ export async function runResume(options, conversationId, task, format = 'text',
|
|
|
60
61
|
runId: run.id,
|
|
61
62
|
status: outcome.status,
|
|
62
63
|
taskOutcome: taskOutcome ?? null,
|
|
64
|
+
usage,
|
|
63
65
|
text: outcome.text ?? null,
|
|
64
66
|
error: outcome.error ?? null,
|
|
65
67
|
})}\n`);
|
package/dist/commands/run.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { type LocalHostOptions } from '@hunterzhu/pulse-server';
|
|
2
|
-
export declare function runOneShot(options: LocalHostOptions, task: string, format?: string, version?: string): Promise<number>;
|
|
2
|
+
export declare function runOneShot(options: LocalHostOptions, task: string, format?: string, version?: string, requirementPaths?: string[]): Promise<number>;
|
package/dist/commands/run.js
CHANGED
|
@@ -23,7 +23,7 @@ function writeEvent(event, format, printed) {
|
|
|
23
23
|
if (toolLines.length)
|
|
24
24
|
process.stderr.write(`${toolLines.join('\n')}\n`);
|
|
25
25
|
}
|
|
26
|
-
export async function runOneShot(options, task, format = 'text', version = '0.4.1') {
|
|
26
|
+
export async function runOneShot(options, task, format = 'text', version = '0.4.1', requirementPaths) {
|
|
27
27
|
const host = createLocalHost(options);
|
|
28
28
|
let activeRun;
|
|
29
29
|
const unbindSignals = bindRunSignals(host, () => activeRun);
|
|
@@ -32,6 +32,7 @@ export async function runOneShot(options, task, format = 'text', version = '0.4.
|
|
|
32
32
|
const conversation = await host.createConversation(options.cwd ? { cwd: options.cwd } : {});
|
|
33
33
|
const run = await host.sendMessage(conversation.id, {
|
|
34
34
|
text: task,
|
|
35
|
+
...(requirementPaths ? { requirementPaths } : {}),
|
|
35
36
|
format: format === 'jsonl' ? 'jsonl' : 'text',
|
|
36
37
|
});
|
|
37
38
|
activeRun = run;
|
package/dist/components/App.d.ts
CHANGED
package/dist/components/App.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
3
3
|
import { Box, Text, useApp, useInput, useStdout } from 'ink';
|
|
4
|
+
import { automationDoctor } from '@hunterzhu/pulse-server';
|
|
4
5
|
import { Header } from './Header.js';
|
|
5
6
|
import { Welcome } from './Welcome.js';
|
|
6
7
|
import { MessageList } from './MessageList.js';
|
|
@@ -136,6 +137,34 @@ export function App({ hostOptions, conversationId: initialConversationId, initia
|
|
|
136
137
|
})();
|
|
137
138
|
}, [hostReady, conversation, initialTask, isRunning, addUserMessage, addAssistantMessage, createConversation, sendMessage]);
|
|
138
139
|
const { commands, executeCommand, isSlashCommand } = useSlashCommands({
|
|
140
|
+
onAutomation: async (args) => {
|
|
141
|
+
if (!host)
|
|
142
|
+
return;
|
|
143
|
+
const report = (text) => addAssistantMessage({ id: `automation-${Date.now()}`, role: 'system', text, createdAt: new Date().toISOString() });
|
|
144
|
+
try {
|
|
145
|
+
const [action = 'status', id] = args.split(/\s+/).filter(Boolean);
|
|
146
|
+
if (action === 'doctor') {
|
|
147
|
+
await report(JSON.stringify(await automationDoctor(hostOptions.automation), null, 2));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const sessions = host.automation.list();
|
|
151
|
+
if (action === 'status') {
|
|
152
|
+
await report(JSON.stringify(sessions.map(session => ({ ...session, screenshotPath: session.state === 'closed' ? undefined : host.automation.screenshotPath(session.id) })), null, 2));
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (host.getAutomationOptions().approvalMode === 'read-only')
|
|
156
|
+
throw new Error('READ_ONLY');
|
|
157
|
+
const selected = id ?? sessions.find(session => session.state !== 'closed')?.id;
|
|
158
|
+
if (!selected || !['pause', 'takeover', 'resume', 'stop'].includes(action))
|
|
159
|
+
throw new Error('用法:/automation status|doctor|pause|takeover|resume|stop [id]');
|
|
160
|
+
const result = await host.automation.control(selected, action === 'stop' ? 'close' : action, 'local-cli');
|
|
161
|
+
const { lease: _lease, ...status } = result;
|
|
162
|
+
await report(JSON.stringify(status, null, 2) + (action === 'takeover' ? '\nAI 已暂停,可在本机手动操作;完成后 /automation resume。' : ''));
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
await report(String(error));
|
|
166
|
+
}
|
|
167
|
+
},
|
|
139
168
|
onMouse: (arg) => setMouseEnabled((current) => arg === 'off' ? false : arg === 'on' ? true : !current),
|
|
140
169
|
onCopy: async () => {
|
|
141
170
|
if (selectedText) {
|
|
@@ -574,6 +603,6 @@ export function App({ hostOptions, conversationId: initialConversationId, initia
|
|
|
574
603
|
}, onInput: () => {
|
|
575
604
|
setApprovalChoiceFocused(false);
|
|
576
605
|
setApprovalInputMode(true);
|
|
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...' })] }))] }));
|
|
606
|
+
} })) : askRequest ? (_jsx(AskPrompt, { request: askRequest, disabled: approvalSubmitting, onReply: (value) => void replyAsk(askRequest.effectId, value), onCancel: () => void cancelRun() }, 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...' })] }))] }));
|
|
578
607
|
}
|
|
579
608
|
export default App;
|
|
@@ -3,6 +3,7 @@ interface Props {
|
|
|
3
3
|
request: AskRequest;
|
|
4
4
|
disabled?: boolean;
|
|
5
5
|
onReply: (value: Record<string, unknown>) => void;
|
|
6
|
+
onCancel?: () => void;
|
|
6
7
|
}
|
|
7
|
-
export declare function AskPrompt({ request, disabled, onReply }: Props): import("react").JSX.Element;
|
|
8
|
+
export declare function AskPrompt({ request, disabled, onReply, onCancel }: Props): import("react").JSX.Element;
|
|
8
9
|
export {};
|
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
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
|
-
import { useState } from 'react';
|
|
4
|
+
import { useRef, useState } from 'react';
|
|
5
5
|
import { theme } from '../theme.js';
|
|
6
|
-
|
|
6
|
+
/** Typed text that cancels the pending question instead of choosing an option. */
|
|
7
|
+
const CANCEL_COMMAND = /^\/?\s*(?:cancel|quit|exit)\s*$/i;
|
|
8
|
+
export function AskPrompt({ request, disabled = false, onReply, onCancel }) {
|
|
7
9
|
const [value, setValue] = useState(request.defaultValue ?? '');
|
|
8
10
|
const [inputLines, setInputLines] = useState([]);
|
|
9
11
|
const [index, setIndex] = useState(0);
|
|
10
12
|
const [selected, setSelected] = useState([]);
|
|
13
|
+
const [typed, setTyped] = useState('');
|
|
14
|
+
// Mirror of `typed` that updates synchronously, so an Enter arriving in the
|
|
15
|
+
// same batch as a multi-character paste still sees the buffered text.
|
|
16
|
+
const typedRef = useRef('');
|
|
17
|
+
const [hint, setHint] = useState(null);
|
|
18
|
+
const updateTyped = (next) => {
|
|
19
|
+
const value = next(typedRef.current);
|
|
20
|
+
typedRef.current = value;
|
|
21
|
+
setTyped(value);
|
|
22
|
+
};
|
|
11
23
|
const options = request.options ?? [];
|
|
12
24
|
const clampedIndex = options.length === 0 ? 0 : Math.min(Math.max(0, index), options.length - 1);
|
|
25
|
+
const isList = request.type === 'choice' || request.type === 'multi';
|
|
13
26
|
useInput((input, key) => {
|
|
14
27
|
if (disabled)
|
|
15
28
|
return;
|
|
@@ -23,8 +36,11 @@ export function AskPrompt({ request, disabled = false, onReply }) {
|
|
|
23
36
|
}
|
|
24
37
|
return;
|
|
25
38
|
}
|
|
26
|
-
if (
|
|
39
|
+
if (!isList || options.length === 0)
|
|
27
40
|
return;
|
|
41
|
+
// Escape already cancels the whole run at the app level; here we add an
|
|
42
|
+
// explicit, text-level cancel so Enter never confirms the default option
|
|
43
|
+
// by accident after the user typed /cancel.
|
|
28
44
|
if (key.upArrow) {
|
|
29
45
|
setIndex((current) => Math.max(0, current - 1));
|
|
30
46
|
return;
|
|
@@ -33,25 +49,51 @@ export function AskPrompt({ request, disabled = false, onReply }) {
|
|
|
33
49
|
setIndex((current) => Math.min(options.length - 1, current + 1));
|
|
34
50
|
return;
|
|
35
51
|
}
|
|
36
|
-
if (
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
onReply({ value: option.value });
|
|
52
|
+
if (key.backspace || key.delete) {
|
|
53
|
+
updateTyped((current) => current.slice(0, -1));
|
|
54
|
+
setHint(null);
|
|
40
55
|
return;
|
|
41
56
|
}
|
|
42
|
-
if (request.type
|
|
43
|
-
|
|
44
|
-
if (input === ' ') {
|
|
45
|
-
const option = options[Math.min(Math.max(0, index), options.length - 1)];
|
|
57
|
+
if (request.type === 'multi' && input === ' ') {
|
|
58
|
+
const option = options[clampedIndex];
|
|
46
59
|
if (!option)
|
|
47
60
|
return;
|
|
48
61
|
setSelected((current) => current.includes(option.value) ? current.filter((item) => item !== option.value) : [...current, option.value]);
|
|
62
|
+
return;
|
|
49
63
|
}
|
|
50
|
-
|
|
64
|
+
if (key.return) {
|
|
65
|
+
const pending = typedRef.current.trim();
|
|
66
|
+
if (pending) {
|
|
67
|
+
if (CANCEL_COMMAND.test(pending)) {
|
|
68
|
+
onCancel?.();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
// A pending line of text must never turn Enter into a silent default
|
|
72
|
+
// selection: clear it and ask the user to choose deliberately.
|
|
73
|
+
updateTyped(() => '');
|
|
74
|
+
setHint('未提交选择:请用 ↑↓ 移动后按 Enter 确认;如需取消请输入 /cancel 或按 Esc。');
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (request.type === 'choice') {
|
|
78
|
+
const option = options[clampedIndex];
|
|
79
|
+
if (option)
|
|
80
|
+
onReply({ value: option.value });
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
51
83
|
if (selected.length < (request.min ?? 0) || (request.max !== undefined && selected.length > request.max))
|
|
52
84
|
return;
|
|
53
85
|
onReply({ values: selected });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
// Buffer the whole printable segment so pasted text such as /cancel is
|
|
89
|
+
// recognised without a dedicated box.
|
|
90
|
+
if (!key.ctrl && !key.meta) {
|
|
91
|
+
const printable = input.replace(/[\x00-\x1f\x7f]/g, '');
|
|
92
|
+
if (printable) {
|
|
93
|
+
updateTyped((current) => Array.from(current + printable).slice(-32).join(''));
|
|
94
|
+
setHint(null);
|
|
95
|
+
}
|
|
54
96
|
}
|
|
55
97
|
});
|
|
56
|
-
return (_jsxs(Box, { borderStyle: "double", borderColor: theme.primary, flexDirection: "column", paddingX: 1, marginTop: 1, children: [_jsxs(Text, { color: theme.primary, bold: true, children: ["\u2726 \u9700\u8981\u4F60\u7684\u56DE\u7B54 \u00B7 ", request.toolName] }), _jsx(Text, { color: "white", children: request.prompt }), request.type === 'choice' && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [options.map((option, optionIndex) => (_jsxs(Text, { ...(optionIndex === clampedIndex ? { color: theme.primary } : {}), children: [optionIndex === clampedIndex ? '› ' : ' ', option.label] }, `${optionIndex}:${option.value}`))), _jsx(Text, { dimColor: true, children: "\u2191\u2193 \u79FB\u52A8\uFF0CEnter \u786E\u8BA4" })] })), request.type === 'multi' && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [options.map((option, optionIndex) => (_jsxs(Text, { ...(optionIndex === clampedIndex ? { color: theme.primary } : {}), children: [optionIndex === clampedIndex ? '› ' : ' ', selected.includes(option.value) ? '[x] ' : '[ ] ', option.label] }, `${optionIndex}:${option.value}`))), _jsxs(Text, { dimColor: true, children: ["\u2191\u2193 \u79FB\u52A8\uFF0C\u7A7A\u683C\u9009\u62E9\uFF0CEnter \u786E\u8BA4", request.min ? `(至少 ${request.min} 项)` : ''] })] })), request.type === 'input' && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [inputLines.map((line, lineIndex) => (_jsxs(Box, { children: [_jsx(Text, { color: theme.primary, children: "\u203A " }), _jsx(Text, { children: line })] }, `${lineIndex}:${line}`))), _jsxs(Box, { children: [_jsx(Text, { color: theme.primary, children: "\u203A " }), _jsx(TextInput, { focus: !disabled, value: value, onChange: setValue, placeholder: request.placeholder ?? '' })] })] }))] }));
|
|
98
|
+
return (_jsxs(Box, { borderStyle: "double", borderColor: theme.primary, flexDirection: "column", paddingX: 1, marginTop: 1, children: [_jsxs(Text, { color: theme.primary, bold: true, children: ["\u2726 \u9700\u8981\u4F60\u7684\u56DE\u7B54 \u00B7 ", request.toolName] }), _jsx(Text, { color: "white", children: request.prompt }), hint ? _jsx(Text, { color: "yellow", children: hint }) : null, isList && typed ? _jsxs(Text, { color: "yellow", children: ["\u5DF2\u8F93\u5165\uFF1A", typed] }) : null, request.type === 'choice' && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [options.map((option, optionIndex) => (_jsxs(Text, { ...(optionIndex === clampedIndex ? { color: theme.primary } : {}), children: [optionIndex === clampedIndex ? '› ' : ' ', option.label] }, `${optionIndex}:${option.value}`))), _jsx(Text, { dimColor: true, children: "\u2191\u2193 \u79FB\u52A8\uFF0CEnter \u786E\u8BA4\uFF1BEsc \u6216\u8F93\u5165 /cancel \u53D6\u6D88" })] })), request.type === 'multi' && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [options.map((option, optionIndex) => (_jsxs(Text, { ...(optionIndex === clampedIndex ? { color: theme.primary } : {}), children: [optionIndex === clampedIndex ? '› ' : ' ', selected.includes(option.value) ? '[x] ' : '[ ] ', option.label] }, `${optionIndex}:${option.value}`))), _jsxs(Text, { dimColor: true, children: ["\u2191\u2193 \u79FB\u52A8\uFF0C\u7A7A\u683C\u9009\u62E9\uFF0CEnter \u786E\u8BA4", request.min ? `(至少 ${request.min} 项)` : '', "\uFF1BEsc \u6216\u8F93\u5165 /cancel \u53D6\u6D88"] })] })), request.type === 'input' && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [inputLines.map((line, lineIndex) => (_jsxs(Box, { children: [_jsx(Text, { color: theme.primary, children: "\u203A " }), _jsx(Text, { children: line })] }, `${lineIndex}:${line}`))), _jsxs(Box, { children: [_jsx(Text, { color: theme.primary, children: "\u203A " }), _jsx(TextInput, { focus: !disabled, value: value, onChange: setValue, placeholder: request.placeholder ?? '' })] })] }))] }));
|
|
57
99
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export interface PulseCliProviderProfile {
|
|
|
6
6
|
apiKeyEnv?: string;
|
|
7
7
|
maxContextTokens?: number;
|
|
8
8
|
maxOutputTokens?: number;
|
|
9
|
+
imageInput?: 'supported' | 'unsupported' | 'unknown';
|
|
9
10
|
reasoningEffort?: 'low' | 'medium' | 'high';
|
|
10
11
|
toolChoice?: 'auto' | 'required' | 'none' | {
|
|
11
12
|
type: 'function';
|
|
@@ -13,16 +14,19 @@ export interface PulseCliProviderProfile {
|
|
|
13
14
|
name: string;
|
|
14
15
|
};
|
|
15
16
|
};
|
|
17
|
+
/** Models served by this provider. The provider plus map key is the model identity. */
|
|
18
|
+
models?: Record<string, Omit<PulseCliModel, 'provider'>>;
|
|
16
19
|
}
|
|
17
20
|
export interface PulseCliModel {
|
|
18
|
-
/**
|
|
19
|
-
provider
|
|
21
|
+
/** Legacy flat-config field; nested models inherit their provider from the parent. */
|
|
22
|
+
provider?: string;
|
|
20
23
|
/** Exact model identifier sent to the provider. */
|
|
21
24
|
modelCode: string;
|
|
22
25
|
/** Globally unique name shown by the CLI and accepted by `/model`. */
|
|
23
|
-
displayName
|
|
26
|
+
displayName?: string;
|
|
24
27
|
maxContextTokens?: number;
|
|
25
28
|
maxOutputTokens?: number;
|
|
29
|
+
imageInput?: 'supported' | 'unsupported' | 'unknown';
|
|
26
30
|
reasoningEffort?: 'low' | 'medium' | 'high';
|
|
27
31
|
pricing?: {
|
|
28
32
|
currency: string;
|
|
@@ -31,7 +35,7 @@ export interface PulseCliModel {
|
|
|
31
35
|
version: string;
|
|
32
36
|
};
|
|
33
37
|
}
|
|
34
|
-
export type PulseModelTask = 'reason' | 'plan' | 'merge' | 'verify';
|
|
38
|
+
export type PulseModelTask = 'reason' | 'plan' | 'merge' | 'verify' | 'vision';
|
|
35
39
|
export interface PulseCliMcpServer {
|
|
36
40
|
command: string;
|
|
37
41
|
args?: string[];
|
|
@@ -45,6 +49,8 @@ export interface PulseCliMcpServer {
|
|
|
45
49
|
toolPolicies?: Record<string, 'read' | 'write' | 'external'>;
|
|
46
50
|
}
|
|
47
51
|
export interface PulseCliCapabilities {
|
|
52
|
+
browser?: import('@hunterzhu/pulse-server').AutomationConfig['browser'];
|
|
53
|
+
computer?: import('@hunterzhu/pulse-server').AutomationConfig['computer'];
|
|
48
54
|
/** IDs must be explicitly enabled; workspace config cannot enable host extensions. */
|
|
49
55
|
enabled?: string[];
|
|
50
56
|
/** Optional discovery allow-list of skill directory names; omitted discovers all. Bodies load only on invocation. */
|
|
@@ -61,18 +67,23 @@ export interface PulseCliConfig {
|
|
|
61
67
|
systemPromptFile?: string;
|
|
62
68
|
/** Named provider profiles. The map key is a stable provider code. */
|
|
63
69
|
providers?: Record<string, PulseCliProviderProfile>;
|
|
64
|
-
/**
|
|
70
|
+
/** @deprecated Legacy flat model catalog. New configs nest models under providers. */
|
|
65
71
|
models?: Record<string, PulseCliModel>;
|
|
66
|
-
/** Active Pulse model
|
|
72
|
+
/** Active Pulse model reference, preferably providerId/modelId. */
|
|
67
73
|
activeModel?: string;
|
|
68
74
|
/** Dedicated model for the persistent Pulse assistant chat; defaults to activeModel. */
|
|
69
75
|
assistantModel?: string;
|
|
70
|
-
/** Ordered
|
|
76
|
+
/** Ordered providerId/modelId references per task; later entries act as fallbacks. */
|
|
71
77
|
taskRouting?: Partial<Record<PulseModelTask, string[]>>;
|
|
72
78
|
capabilities?: PulseCliCapabilities;
|
|
73
79
|
approvalMode?: 'read-only' | 'ask' | 'auto';
|
|
74
80
|
maxTurns?: number;
|
|
75
|
-
/**
|
|
81
|
+
/** Provider attempts allowed across one controlled work task; defaults to 256. */
|
|
82
|
+
maxModelCalls?: number;
|
|
83
|
+
modelBudgetPolicy?: import('@hunterzhu/pulse-server').LocalHostOptions['modelBudgetPolicy'];
|
|
84
|
+
persistenceBackend?: 'json' | 'sqlite' | 'external-files';
|
|
85
|
+
maxCostByCurrency?: Record<string, number>;
|
|
86
|
+
/** Optional wall-clock limit for one run. Unset means no overall deadline. */
|
|
76
87
|
maxRuntimeMs?: number;
|
|
77
88
|
/** Percent of maxContextTokens that triggers automatic history compaction. Clamped to 1–90. */
|
|
78
89
|
autoCompactPercent?: number;
|
package/dist/config.js
CHANGED
|
@@ -3,28 +3,26 @@ import { homedir } from 'node:os';
|
|
|
3
3
|
import { dirname, join, resolve, sep } from 'node:path';
|
|
4
4
|
export const defaultPulseConfig = {
|
|
5
5
|
providers: {
|
|
6
|
-
mock: { provider: 'mock', name: 'Mock' },
|
|
6
|
+
mock: { provider: 'mock', name: 'Mock', models: { mock: { displayName: 'mock', modelCode: 'mock', maxContextTokens: 32_000, maxOutputTokens: 4_096, reasoningEffort: 'medium' } } },
|
|
7
7
|
openai: {
|
|
8
8
|
provider: 'openai-compatible',
|
|
9
9
|
name: 'OpenAI',
|
|
10
10
|
baseURL: 'https://api.openai.com/v1',
|
|
11
11
|
apiKeyEnv: 'OPENAI_API_KEY',
|
|
12
|
+
models: { 'gpt5.6-a': { displayName: 'gpt5.6-a', modelCode: 'gpt-5.6' } },
|
|
12
13
|
},
|
|
13
14
|
deepseek: {
|
|
14
15
|
provider: 'deepseek',
|
|
15
16
|
name: 'DeepSeek',
|
|
16
17
|
baseURL: 'https://api.deepseek.com',
|
|
17
18
|
apiKeyEnv: 'DEEPSEEK_API_KEY',
|
|
19
|
+
models: { 'deepseek-chat': { displayName: 'deepseek-chat', modelCode: 'deepseek-chat' } },
|
|
18
20
|
},
|
|
19
21
|
},
|
|
20
|
-
|
|
21
|
-
mock: { displayName: 'mock', provider: 'mock', modelCode: 'mock', maxContextTokens: 32_000, maxOutputTokens: 4_096, reasoningEffort: 'medium' },
|
|
22
|
-
'gpt5.6-a': { displayName: 'gpt5.6-a', provider: 'openai', modelCode: 'gpt-5.6' },
|
|
23
|
-
'deepseek-chat': { displayName: 'deepseek-chat', provider: 'deepseek', modelCode: 'deepseek-chat' },
|
|
24
|
-
},
|
|
25
|
-
activeModel: 'mock',
|
|
22
|
+
activeModel: 'mock/mock',
|
|
26
23
|
approvalMode: 'ask',
|
|
27
24
|
maxTurns: 32,
|
|
25
|
+
maxModelCalls: 256,
|
|
28
26
|
autoCompactPercent: 90,
|
|
29
27
|
allowNetwork: false,
|
|
30
28
|
jarvis: {
|
|
@@ -82,6 +80,13 @@ export function sanitizeWorkspaceConfig(value) {
|
|
|
82
80
|
const safe = {
|
|
83
81
|
provider: profile.provider,
|
|
84
82
|
...(profile.name === undefined ? {} : { name: profile.name }),
|
|
83
|
+
...(profile.models === undefined ? {} : { models: Object.fromEntries(Object.entries(profile.models).flatMap(([modelId, model]) => model.modelCode ? [[modelId, {
|
|
84
|
+
modelCode: model.modelCode,
|
|
85
|
+
...(model.displayName === undefined ? {} : { displayName: model.displayName }),
|
|
86
|
+
...(model.maxContextTokens === undefined ? {} : { maxContextTokens: model.maxContextTokens }),
|
|
87
|
+
...(model.maxOutputTokens === undefined ? {} : { maxOutputTokens: model.maxOutputTokens }),
|
|
88
|
+
...(model.reasoningEffort === undefined ? {} : { reasoningEffort: model.reasoningEffort }),
|
|
89
|
+
}]] : [])) }),
|
|
85
90
|
};
|
|
86
91
|
return Object.keys(safe).length ? [[name, safe]] : [];
|
|
87
92
|
}));
|
|
@@ -100,7 +105,7 @@ export function sanitizeWorkspaceConfig(value) {
|
|
|
100
105
|
return [[name, {
|
|
101
106
|
provider: model.provider,
|
|
102
107
|
modelCode: model.modelCode,
|
|
103
|
-
displayName: model.displayName,
|
|
108
|
+
...(model.displayName === undefined ? {} : { displayName: model.displayName }),
|
|
104
109
|
...(model.maxContextTokens === undefined ? {} : { maxContextTokens: model.maxContextTokens }),
|
|
105
110
|
...(model.maxOutputTokens === undefined ? {} : { maxOutputTokens: model.maxOutputTokens }),
|
|
106
111
|
...(model.reasoningEffort === undefined ? {} : { reasoningEffort: model.reasoningEffort }),
|
|
@@ -113,10 +118,19 @@ export function mergePulseConfigs(layers) {
|
|
|
113
118
|
let merged = {};
|
|
114
119
|
for (const layer of layers) {
|
|
115
120
|
const value = layer.trust === 'workspace' ? sanitizeWorkspaceConfig(layer.value) : layer.value;
|
|
121
|
+
const providers = { ...(merged.providers ?? {}) };
|
|
122
|
+
for (const [providerId, profile] of Object.entries(value.providers ?? {})) {
|
|
123
|
+
const previous = providers[providerId];
|
|
124
|
+
providers[providerId] = {
|
|
125
|
+
...previous,
|
|
126
|
+
...profile,
|
|
127
|
+
...(previous?.models === undefined && profile.models === undefined ? {} : { models: { ...previous?.models, ...profile.models } }),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
116
130
|
merged = {
|
|
117
131
|
...merged,
|
|
118
132
|
...value,
|
|
119
|
-
...(merged.providers === undefined && value.providers === undefined ? {} : { providers
|
|
133
|
+
...(merged.providers === undefined && value.providers === undefined ? {} : { providers }),
|
|
120
134
|
...(merged.models === undefined && value.models === undefined ? {} : { models: { ...merged.models, ...value.models } }),
|
|
121
135
|
};
|
|
122
136
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { SlashCommand } from '../types.js';
|
|
2
2
|
export interface SlashCommandDependencies {
|
|
3
|
+
onAutomation?: (args: string) => void | Promise<void>;
|
|
3
4
|
onMouse?: (args: string) => void | Promise<void>;
|
|
4
5
|
onCopy?: () => void | Promise<void>;
|
|
5
6
|
onHelp?: () => void | Promise<void>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useMemo, useCallback } from 'react';
|
|
2
2
|
export function useSlashCommands(deps) {
|
|
3
3
|
const commands = useMemo(() => [
|
|
4
|
+
{ name: '/automation', description: '自动化会话 [status|pause|takeover|resume|stop] [id]', execute: async (args) => deps.onAutomation?.(args) },
|
|
4
5
|
{ name: '/mouse', description: '滚轮和触控板滚动 [on|off]', execute: async (args) => deps.onMouse?.(args) },
|
|
5
6
|
{ name: '/copy', description: '复制选区或最近一条完整回复', execute: async () => deps.onCopy?.() },
|
|
6
7
|
{ name: '/help', aliases: ['/h'], description: '显示帮助信息', execute: async () => deps.onHelp?.() },
|
|
@@ -1,5 +1,61 @@
|
|
|
1
1
|
// Generated from CHANGELOG.md and CHANGELOG.en.md by scripts/release/sync-cli-highlights.mjs. Do not edit manually.
|
|
2
2
|
export const releaseHighlights = {
|
|
3
|
+
"0.6.0": [
|
|
4
|
+
{
|
|
5
|
+
"zh": "新增受管理的浏览器与桌面自动化,支持页面观察、截图操作、上传下载、暂停及人工接管,并在 CLI 和 H5 中提供入口。",
|
|
6
|
+
"en": "Added managed browser and desktop automation with page observation, screenshot actions, uploads and downloads, pause, and human takeover through CLI and H5 controls."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"zh": "Web 工作台展示任务执行过程、阶段进度和工具调用详情,便于查看运行状态与审批内容。",
|
|
10
|
+
"en": "The Web workbench displays task execution traces, stage progress, and tool-call details to make run status and approval requests easier to inspect."
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"zh": "改善长任务的上下文压缩、工具输入恢复、循环检测和预算处理,保留中断续跑的状态及调用记录。",
|
|
14
|
+
"en": "Improved context compaction, tool-input recovery, loop detection, and budget handling for long tasks, preserving state and call accounting across interrupted runs."
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"zh": "按明确的并行要求调度独立阶段,修复并行工作单元的状态访问与依赖证据传递。",
|
|
18
|
+
"en": "Schedule independent stages in parallel when explicitly requested, and fix worker state access and dependency evidence propagation."
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"zh": "保存计划后执行与内容绑定的确认,计划变更需重新确认;审批继续保留人工回退和操作范围限制。",
|
|
22
|
+
"en": "Bind execution confirmation to the saved plan and require confirmation again after changes; approvals retain human fallback and operation scope checks."
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"zh": "保护原有测试与验收命令,在修改前阻止破坏性补丁,同时允许授权的新测试补充和修正。",
|
|
26
|
+
"en": "Protect original tests and acceptance commands by rejecting destructive patches before writes, while allowing authorized additions and corrections to new tests."
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"zh": "修复验证证据的新鲜度、无效引用恢复及已通过测试覆盖先前失败子集的判定,减少重复验证与错误阻塞。",
|
|
30
|
+
"en": "Fix evidence freshness, recovery from invalid citations, and recognition of passing test supersets that cover earlier failed subsets, reducing redundant checks and false blocks."
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"zh": "修复 Windows 安装依赖只读隔离、相邻源码的读取边界、脚本审批路径显示及沙箱权限分批初始化,减少无关脚本的递归依赖授权开销,完善跨平台回归验证。",
|
|
34
|
+
"en": "Fix Windows read-only isolation for installed dependencies, read boundaries for adjacent source, script approval path display, and batched sandbox permission initialization; reduce recursive dependency grants for unrelated scripts and improve cross-platform regression checks."
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"zh": "H5 停止服务时等待正在启动的助理运行完成清理,保留后续排队消息;会话文件统一原子写入,修复并发读取异常和请求挂起。",
|
|
38
|
+
"en": "H5 shutdown waits for assistant launches already in progress to settle and preserves later queued messages; atomic conversation writes prevent concurrent read errors and hanging requests."
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
"0.5.1": [
|
|
42
|
+
{
|
|
43
|
+
"zh": "模型目录按供应商管理,支持不同供应商登记同名模型;默认模型、助理聊天、任务路由、即时预回复和审批复核统一选择已登记模型。",
|
|
44
|
+
"en": "Models are managed under providers, allowing different providers to register the same model name; defaults, assistant chat, task routing, instant replies, and approval review all select registered models."
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"zh": "项目侧栏支持组织会话与项目,并完善工作区信任边界及模型配置迁移。",
|
|
48
|
+
"en": "Added project sidebar organization and improved workspace trust boundaries and model configuration migration."
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"zh": "即时预回复按所选供应商模型读取协议与连接配置;审批复核继续限制为本机 Ollama,并在异常时交回人工。",
|
|
52
|
+
"en": "Instant replies use the selected provider's protocol and connection settings; approval review remains restricted to local Ollama and falls back to human review on errors."
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"zh": "改善 Jarvis 经验记录、上下文筛选和持久化交付流程。",
|
|
56
|
+
"en": "Improved Jarvis experience recording, context filtering, and durable delivery workflows."
|
|
57
|
+
}
|
|
58
|
+
],
|
|
3
59
|
"0.5.0": [
|
|
4
60
|
{
|
|
5
61
|
"zh": "新增独立 Web 工作台,统一助理聊天、项目工作会话、历史记录、设置与本地终端。",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hunterzhu/pulse-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "A local-first AI task assistant for coding, research, and file workflows.",
|
|
5
5
|
"license": "PolyForm-Noncommercial-1.0.0",
|
|
6
6
|
"keywords": [
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"registry": "https://registry.npmjs.org"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@hunterzhu/pulse-server": "0.
|
|
39
|
+
"@hunterzhu/pulse-server": "0.6.0",
|
|
40
40
|
"chalk": "^5.4.1",
|
|
41
41
|
"highlight.js": "^11.10.0",
|
|
42
42
|
"ink": "^7.1.1",
|