@hunterzhu/pulse-cli 0.5.0 → 0.5.1
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 +48 -37
- package/dist/config.d.ts +8 -6
- package/dist/config.js +22 -9
- package/dist/release-highlights.generated.js +18 -0
- package/package.json +2 -2
package/dist/bin.js
CHANGED
|
@@ -4,6 +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 { assertJarvisApiUrl, 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';
|
|
@@ -162,7 +163,7 @@ export async function hostOptions(parsed) {
|
|
|
162
163
|
}
|
|
163
164
|
const loaded = (await loadPulseConfig(requestedCwd ?? process.cwd(), explicitConfig, parsed.options['trust-workspace'] === true)).value;
|
|
164
165
|
// 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 };
|
|
166
|
+
const config = normalizeModelConfig(loaded.activeModel || loaded.models || loaded.providers ? loaded : { ...defaultPulseConfig, ...loaded });
|
|
166
167
|
const mockResponse = option(parsed.options, 'mock-response');
|
|
167
168
|
const mockTaskAssessment = option(parsed.options, 'mock-task-assessment');
|
|
168
169
|
const mockStageAssessment = option(parsed.options, 'mock-stage-assessment');
|
|
@@ -171,25 +172,33 @@ export async function hostOptions(parsed) {
|
|
|
171
172
|
const mockParallelPlanOption = option(parsed.options, 'mock-parallel-plan');
|
|
172
173
|
const hasMockDemand = mockResponse !== undefined || mockTaskAssessment !== undefined || mockStageAssessment !== undefined || mockToolCallsOption !== undefined || mockAfterToolResponse !== undefined || mockParallelPlanOption !== undefined;
|
|
173
174
|
const cliModel = option(parsed.options, 'model');
|
|
174
|
-
const requestedModel = cliModel ?? (hasMockDemand ? 'mock' : process.env.PULSE_MODEL ?? config.activeModel);
|
|
175
|
-
const effectiveModels = { ...(config.models ?? {}) };
|
|
175
|
+
const requestedModel = cliModel ?? (hasMockDemand ? 'mock/mock' : process.env.PULSE_MODEL ?? config.activeModel);
|
|
176
176
|
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
|
-
}
|
|
177
|
+
if (requestedModel === 'mock/mock' || requestedModel === 'mock' || (hasMockDemand && cliModel === undefined)) {
|
|
181
178
|
if (!effectiveProviders.mock) {
|
|
182
179
|
effectiveProviders.mock = { provider: 'mock', name: 'Mock' };
|
|
183
180
|
}
|
|
181
|
+
effectiveProviders.mock.models ??= {};
|
|
182
|
+
effectiveProviders.mock.models.mock ??= { displayName: 'mock', modelCode: 'mock', maxContextTokens: 32_000, maxOutputTokens: 4_096, reasoningEffort: 'medium' };
|
|
183
|
+
}
|
|
184
|
+
const catalog = { ...config, providers: effectiveProviders };
|
|
185
|
+
const selections = Object.entries(effectiveProviders).flatMap(([providerId, profile]) => Object.entries(profile.models ?? {}).map(([modelId, model]) => ({ id: modelSelectionId(providerId, modelId), providerId, modelId, model })));
|
|
186
|
+
if (!requestedModel)
|
|
187
|
+
throw new Error('UNKNOWN_MODEL_DISPLAY_NAME:(missing)');
|
|
188
|
+
let activeModelName;
|
|
189
|
+
try {
|
|
190
|
+
activeModelName = resolveModelReference(catalog, requestedModel);
|
|
184
191
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
192
|
+
catch (error) {
|
|
193
|
+
if (error instanceof Error && error.message.startsWith('AMBIGUOUS_MODEL_REFERENCE:'))
|
|
194
|
+
throw error;
|
|
195
|
+
throw new Error(`UNKNOWN_MODEL_DISPLAY_NAME:${requestedModel}`);
|
|
196
|
+
}
|
|
197
|
+
const configuredModelEntry = selections.find(item => item.id === activeModelName);
|
|
188
198
|
if (!configuredModelEntry)
|
|
189
|
-
throw new Error(`UNKNOWN_MODEL_DISPLAY_NAME:${requestedModel
|
|
190
|
-
const
|
|
191
|
-
const
|
|
192
|
-
const providerName = modelSelection.provider;
|
|
199
|
+
throw new Error(`UNKNOWN_MODEL_DISPLAY_NAME:${requestedModel}`);
|
|
200
|
+
const modelSelection = configuredModelEntry.model;
|
|
201
|
+
const providerName = configuredModelEntry.providerId;
|
|
193
202
|
const profile = effectiveProviders[providerName];
|
|
194
203
|
if (!profile)
|
|
195
204
|
throw new Error(`MODEL_PROVIDER_NOT_FOUND:${providerName}`);
|
|
@@ -244,28 +253,17 @@ export async function hostOptions(parsed) {
|
|
|
244
253
|
...(item.toolChoice === undefined ? {} : { toolChoice: item.toolChoice }),
|
|
245
254
|
}]];
|
|
246
255
|
}));
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
throw new Error(`DUPLICATE_MODEL_DISPLAY_NAME:${displayName}`);
|
|
256
|
-
modelDisplayNames.add(displayName);
|
|
257
|
-
return [[displayName, {
|
|
258
|
-
provider: item.provider,
|
|
259
|
-
model: item.modelCode,
|
|
260
|
-
...(item.maxContextTokens === undefined ? {} : { maxContextTokens: item.maxContextTokens }),
|
|
261
|
-
...(item.maxOutputTokens === undefined ? {} : { maxOutputTokens: item.maxOutputTokens }),
|
|
262
|
-
...(item.reasoningEffort === undefined ? {} : { reasoningEffort: item.reasoningEffort }),
|
|
263
|
-
}]];
|
|
264
|
-
}));
|
|
265
|
-
const assistantModelName = config.assistantModel ?? activeModelName;
|
|
256
|
+
const providerModels = Object.fromEntries(selections.map(({ id, providerId, model }) => [id, {
|
|
257
|
+
provider: providerId,
|
|
258
|
+
model: model.modelCode,
|
|
259
|
+
...(model.maxContextTokens === undefined ? {} : { maxContextTokens: model.maxContextTokens }),
|
|
260
|
+
...(model.maxOutputTokens === undefined ? {} : { maxOutputTokens: model.maxOutputTokens }),
|
|
261
|
+
...(model.reasoningEffort === undefined ? {} : { reasoningEffort: model.reasoningEffort }),
|
|
262
|
+
}]));
|
|
263
|
+
const assistantModelName = config.assistantModel ? resolveModelReference(catalog, config.assistantModel) : activeModelName;
|
|
266
264
|
if (!providerModels[assistantModelName])
|
|
267
265
|
throw new Error(`UNKNOWN_ASSISTANT_MODEL:${assistantModelName}`);
|
|
268
|
-
const taskRouting = config.taskRouting;
|
|
266
|
+
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
267
|
for (const [task, models] of Object.entries(taskRouting ?? {})) {
|
|
270
268
|
if (!Array.isArray(models) || models.length === 0 || models.some((model) => typeof model !== 'string' || !providerModels[model])) {
|
|
271
269
|
throw new Error(`INVALID_TASK_MODEL_ROUTE:${task}`);
|
|
@@ -425,7 +423,7 @@ export async function hostOptions(parsed) {
|
|
|
425
423
|
provider,
|
|
426
424
|
providerProfiles,
|
|
427
425
|
providerModels,
|
|
428
|
-
modelPricing: Object.fromEntries(
|
|
426
|
+
modelPricing: Object.fromEntries(selections.flatMap(({ id, model }) => model.pricing ? [[id, model.pricing]] : [])),
|
|
429
427
|
...(taskRouting === undefined ? {} : { taskRouting }),
|
|
430
428
|
capabilityPacks,
|
|
431
429
|
enabledCapabilityPacks,
|
|
@@ -446,8 +444,21 @@ export async function hostOptions(parsed) {
|
|
|
446
444
|
...(allowNetwork === undefined ? {} : { allowNetwork }),
|
|
447
445
|
...(config.networkHosts === undefined ? {} : { networkHosts: config.networkHosts }),
|
|
448
446
|
jarvis,
|
|
449
|
-
...(config.frontModel ? { frontModel:
|
|
450
|
-
|
|
447
|
+
...(config.frontModel ? { frontModel: (() => {
|
|
448
|
+
const selected = typeof config.frontModel?.model === 'string' ? resolveModelReference(catalog, config.frontModel.model) : undefined;
|
|
449
|
+
const entry = selections.find(item => item.id === selected);
|
|
450
|
+
const source = entry && effectiveProviders[entry.providerId];
|
|
451
|
+
const keyEnv = source?.apiKeyEnv ?? (source?.provider === 'anthropic' ? 'ANTHROPIC_API_KEY' : 'OPENAI_API_KEY');
|
|
452
|
+
return { ...config.frontModel, ...(entry && source ? { provider: source.provider, model: entry.model.modelCode, ...(source.baseURL ? { baseURL: source.baseURL } : {}), ...(process.env[keyEnv] ? { apiKey: process.env[keyEnv] } : {}) } : {}) };
|
|
453
|
+
})() } : {}),
|
|
454
|
+
...(config.approvalReview?.model ? { approvalReview: (() => {
|
|
455
|
+
const selected = resolveModelReference(catalog, config.approvalReview.model);
|
|
456
|
+
const entry = selections.find(item => item.id === selected);
|
|
457
|
+
const source = entry && effectiveProviders[entry.providerId];
|
|
458
|
+
if (!entry || !source || source.provider !== 'ollama')
|
|
459
|
+
throw new Error('APPROVAL_REVIEW_REQUIRES_OLLAMA');
|
|
460
|
+
return { provider: 'ollama', model: entry.model.modelCode, baseURL: source.baseURL ?? 'http://127.0.0.1:11434', timeoutMs: Math.max(1_000, Math.min(60_000, config.approvalReview.timeoutMs ?? 15_000)) };
|
|
461
|
+
})() } : {}),
|
|
451
462
|
};
|
|
452
463
|
}
|
|
453
464
|
export async function main() {
|
package/dist/config.d.ts
CHANGED
|
@@ -13,14 +13,16 @@ export interface PulseCliProviderProfile {
|
|
|
13
13
|
name: string;
|
|
14
14
|
};
|
|
15
15
|
};
|
|
16
|
+
/** Models served by this provider. The provider plus map key is the model identity. */
|
|
17
|
+
models?: Record<string, Omit<PulseCliModel, 'provider'>>;
|
|
16
18
|
}
|
|
17
19
|
export interface PulseCliModel {
|
|
18
|
-
/**
|
|
19
|
-
provider
|
|
20
|
+
/** Legacy flat-config field; nested models inherit their provider from the parent. */
|
|
21
|
+
provider?: string;
|
|
20
22
|
/** Exact model identifier sent to the provider. */
|
|
21
23
|
modelCode: string;
|
|
22
24
|
/** Globally unique name shown by the CLI and accepted by `/model`. */
|
|
23
|
-
displayName
|
|
25
|
+
displayName?: string;
|
|
24
26
|
maxContextTokens?: number;
|
|
25
27
|
maxOutputTokens?: number;
|
|
26
28
|
reasoningEffort?: 'low' | 'medium' | 'high';
|
|
@@ -61,13 +63,13 @@ export interface PulseCliConfig {
|
|
|
61
63
|
systemPromptFile?: string;
|
|
62
64
|
/** Named provider profiles. The map key is a stable provider code. */
|
|
63
65
|
providers?: Record<string, PulseCliProviderProfile>;
|
|
64
|
-
/**
|
|
66
|
+
/** @deprecated Legacy flat model catalog. New configs nest models under providers. */
|
|
65
67
|
models?: Record<string, PulseCliModel>;
|
|
66
|
-
/** Active Pulse model
|
|
68
|
+
/** Active Pulse model reference, preferably providerId/modelId. */
|
|
67
69
|
activeModel?: string;
|
|
68
70
|
/** Dedicated model for the persistent Pulse assistant chat; defaults to activeModel. */
|
|
69
71
|
assistantModel?: string;
|
|
70
|
-
/** Ordered
|
|
72
|
+
/** Ordered providerId/modelId references per task; later entries act as fallbacks. */
|
|
71
73
|
taskRouting?: Partial<Record<PulseModelTask, string[]>>;
|
|
72
74
|
capabilities?: PulseCliCapabilities;
|
|
73
75
|
approvalMode?: 'read-only' | 'ask' | 'auto';
|
package/dist/config.js
CHANGED
|
@@ -3,26 +3,23 @@ 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,
|
|
28
25
|
autoCompactPercent: 90,
|
|
@@ -82,6 +79,13 @@ export function sanitizeWorkspaceConfig(value) {
|
|
|
82
79
|
const safe = {
|
|
83
80
|
provider: profile.provider,
|
|
84
81
|
...(profile.name === undefined ? {} : { name: profile.name }),
|
|
82
|
+
...(profile.models === undefined ? {} : { models: Object.fromEntries(Object.entries(profile.models).flatMap(([modelId, model]) => model.modelCode ? [[modelId, {
|
|
83
|
+
modelCode: model.modelCode,
|
|
84
|
+
...(model.displayName === undefined ? {} : { displayName: model.displayName }),
|
|
85
|
+
...(model.maxContextTokens === undefined ? {} : { maxContextTokens: model.maxContextTokens }),
|
|
86
|
+
...(model.maxOutputTokens === undefined ? {} : { maxOutputTokens: model.maxOutputTokens }),
|
|
87
|
+
...(model.reasoningEffort === undefined ? {} : { reasoningEffort: model.reasoningEffort }),
|
|
88
|
+
}]] : [])) }),
|
|
85
89
|
};
|
|
86
90
|
return Object.keys(safe).length ? [[name, safe]] : [];
|
|
87
91
|
}));
|
|
@@ -100,7 +104,7 @@ export function sanitizeWorkspaceConfig(value) {
|
|
|
100
104
|
return [[name, {
|
|
101
105
|
provider: model.provider,
|
|
102
106
|
modelCode: model.modelCode,
|
|
103
|
-
displayName: model.displayName,
|
|
107
|
+
...(model.displayName === undefined ? {} : { displayName: model.displayName }),
|
|
104
108
|
...(model.maxContextTokens === undefined ? {} : { maxContextTokens: model.maxContextTokens }),
|
|
105
109
|
...(model.maxOutputTokens === undefined ? {} : { maxOutputTokens: model.maxOutputTokens }),
|
|
106
110
|
...(model.reasoningEffort === undefined ? {} : { reasoningEffort: model.reasoningEffort }),
|
|
@@ -113,10 +117,19 @@ export function mergePulseConfigs(layers) {
|
|
|
113
117
|
let merged = {};
|
|
114
118
|
for (const layer of layers) {
|
|
115
119
|
const value = layer.trust === 'workspace' ? sanitizeWorkspaceConfig(layer.value) : layer.value;
|
|
120
|
+
const providers = { ...(merged.providers ?? {}) };
|
|
121
|
+
for (const [providerId, profile] of Object.entries(value.providers ?? {})) {
|
|
122
|
+
const previous = providers[providerId];
|
|
123
|
+
providers[providerId] = {
|
|
124
|
+
...previous,
|
|
125
|
+
...profile,
|
|
126
|
+
...(previous?.models === undefined && profile.models === undefined ? {} : { models: { ...previous?.models, ...profile.models } }),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
116
129
|
merged = {
|
|
117
130
|
...merged,
|
|
118
131
|
...value,
|
|
119
|
-
...(merged.providers === undefined && value.providers === undefined ? {} : { providers
|
|
132
|
+
...(merged.providers === undefined && value.providers === undefined ? {} : { providers }),
|
|
120
133
|
...(merged.models === undefined && value.models === undefined ? {} : { models: { ...merged.models, ...value.models } }),
|
|
121
134
|
};
|
|
122
135
|
}
|
|
@@ -1,5 +1,23 @@
|
|
|
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.5.1": [
|
|
4
|
+
{
|
|
5
|
+
"zh": "模型目录按供应商管理,支持不同供应商登记同名模型;默认模型、助理聊天、任务路由、即时预回复和审批复核统一选择已登记模型。",
|
|
6
|
+
"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."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"zh": "项目侧栏支持组织会话与项目,并完善工作区信任边界及模型配置迁移。",
|
|
10
|
+
"en": "Added project sidebar organization and improved workspace trust boundaries and model configuration migration."
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"zh": "即时预回复按所选供应商模型读取协议与连接配置;审批复核继续限制为本机 Ollama,并在异常时交回人工。",
|
|
14
|
+
"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."
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"zh": "改善 Jarvis 经验记录、上下文筛选和持久化交付流程。",
|
|
18
|
+
"en": "Improved Jarvis experience recording, context filtering, and durable delivery workflows."
|
|
19
|
+
}
|
|
20
|
+
],
|
|
3
21
|
"0.5.0": [
|
|
4
22
|
{
|
|
5
23
|
"zh": "新增独立 Web 工作台,统一助理聊天、项目工作会话、历史记录、设置与本地终端。",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hunterzhu/pulse-cli",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
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.5.
|
|
39
|
+
"@hunterzhu/pulse-server": "0.5.1",
|
|
40
40
|
"chalk": "^5.4.1",
|
|
41
41
|
"highlight.js": "^11.10.0",
|
|
42
42
|
"ink": "^7.1.1",
|