@hunterzhu/pulse-cli 0.4.7 → 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 CHANGED
@@ -4,7 +4,8 @@ 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 { defaultPulseConfig, ensurePulseUserConfig, expandHome, loadPulseConfig } from './config.js';
7
+ import { modelSelectionId, normalizeModelConfig, resolveModelReference } from '@hunterzhu/pulse-server';
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';
10
11
  import { runDoctor } from './commands/doctor.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);
191
+ }
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}`);
184
196
  }
185
- const configuredModelEntry = requestedModel === undefined
186
- ? undefined
187
- : Object.entries(effectiveModels).find(([name, item]) => name === requestedModel || item.displayName === requestedModel);
197
+ const configuredModelEntry = selections.find(item => item.id === activeModelName);
188
198
  if (!configuredModelEntry)
189
- throw new Error(`UNKNOWN_MODEL_DISPLAY_NAME:${requestedModel ?? '(missing)'}`);
190
- const [, modelSelection] = configuredModelEntry;
191
- const activeModelName = modelSelection.displayName;
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,25 +253,17 @@ export async function hostOptions(parsed) {
244
253
  ...(item.toolChoice === undefined ? {} : { toolChoice: item.toolChoice }),
245
254
  }]];
246
255
  }));
247
- const modelDisplayNames = new Set();
248
- const providerModels = Object.fromEntries(Object.entries(effectiveModels).flatMap(([name, item]) => {
249
- if (!effectiveProviders[item.provider])
250
- throw new Error(`MODEL_PROVIDER_NOT_FOUND:${item.provider}`);
251
- const displayName = item.displayName.trim();
252
- if (!displayName)
253
- throw new Error(`MODEL_DISPLAY_NAME_REQUIRED:${name}`);
254
- if (modelDisplayNames.has(displayName))
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 taskRouting = config.taskRouting;
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;
264
+ if (!providerModels[assistantModelName])
265
+ throw new Error(`UNKNOWN_ASSISTANT_MODEL:${assistantModelName}`);
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]));
266
267
  for (const [task, models] of Object.entries(taskRouting ?? {})) {
267
268
  if (!Array.isArray(models) || models.length === 0 || models.some((model) => typeof model !== 'string' || !providerModels[model])) {
268
269
  throw new Error(`INVALID_TASK_MODEL_ROUTE:${task}`);
@@ -422,13 +423,14 @@ export async function hostOptions(parsed) {
422
423
  provider,
423
424
  providerProfiles,
424
425
  providerModels,
425
- modelPricing: Object.fromEntries(Object.values(config.models ?? {}).flatMap((item) => item.pricing ? [[item.displayName, item.pricing]] : [])),
426
+ modelPricing: Object.fromEntries(selections.flatMap(({ id, model }) => model.pricing ? [[id, model.pricing]] : [])),
426
427
  ...(taskRouting === undefined ? {} : { taskRouting }),
427
428
  capabilityPacks,
428
429
  enabledCapabilityPacks,
429
430
  capabilityConfig: capabilitySettings.skills === undefined ? {} : { skills: skillNames },
430
431
  activeProviderCode: providerName,
431
432
  activeModel: activeModelName,
433
+ assistantModel: assistantModelName,
432
434
  ...(mockResponse === undefined ? {} : { mockResponse }),
433
435
  ...(mockTaskAssessments === undefined ? {} : { mockTaskAssessments }),
434
436
  ...(mockStageAssessments === undefined ? {} : { mockStageAssessments }),
@@ -442,7 +444,21 @@ export async function hostOptions(parsed) {
442
444
  ...(allowNetwork === undefined ? {} : { allowNetwork }),
443
445
  ...(config.networkHosts === undefined ? {} : { networkHosts: config.networkHosts }),
444
446
  jarvis,
445
- ...(config.frontModel ? { frontModel: config.frontModel } : {}),
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
+ })() } : {}),
446
462
  };
447
463
  }
448
464
  export async function main() {
@@ -472,8 +488,11 @@ export async function main() {
472
488
  }
473
489
  if (parsed.command === 'service')
474
490
  return runServiceCommand(options, parsed.positionals);
475
- if (parsed.command === 'h5')
476
- return runH5Command(options, parsed.positionals, parsed.options);
491
+ if (parsed.command === 'h5') {
492
+ const configPath = expandHome(option(parsed.options, 'config') ?? process.env.PULSE_CONFIG) ?? defaultPulseConfigPath();
493
+ const prepare = (candidatePath) => hostOptions({ ...parsed, options: { ...parsed.options, config: candidatePath } });
494
+ return runH5Command(options, parsed.positionals, parsed.options, { path: configPath, prepare });
495
+ }
477
496
  if (parsed.command === 'mcp' && parsed.positionals[0] === 'doctor')
478
497
  return runMcpDoctor(options, parsed.positionals.slice(1));
479
498
  if (parsed.command === 'template')
@@ -2,4 +2,7 @@ import { type LocalHostOptions } from '@hunterzhu/pulse-server';
2
2
  /** Network H5 keeps ask/read-only unless this command explicitly requests auto. */
3
3
  export declare function h5RuntimeOptions(options: LocalHostOptions, cliOptions?: Record<string, string | boolean>): LocalHostOptions;
4
4
  export declare function h5DaemonArguments(executable: string, entry: string, port: number): string[];
5
- export declare function runH5Command(options: LocalHostOptions, args: string[], cliOptions?: Record<string, string | boolean>): Promise<number>;
5
+ export declare function runH5Command(options: LocalHostOptions, args: string[], cliOptions?: Record<string, string | boolean>, configuration?: {
6
+ path: string;
7
+ prepare: (candidatePath: string) => Promise<LocalHostOptions>;
8
+ }): Promise<number>;
@@ -4,7 +4,7 @@ import { join } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { spawnSync } from 'node:child_process';
6
6
  import QRCode from 'qrcode';
7
- import { createLocalHost, H5Server, H5AuthStore, DEFAULT_H5_PORT } from '@hunterzhu/pulse-server';
7
+ import { createLocalHost, H5Server, H5AuthStore, DEFAULT_H5_PORT, runH5WorkWorker } from '@hunterzhu/pulse-server';
8
8
  const serviceId = 'com.hunterzhu.pulse.h5';
9
9
  function checked(file, args) {
10
10
  const result = spawnSync(file, args, { encoding: 'utf8' });
@@ -23,7 +23,7 @@ function xml(value) {
23
23
  function quoteSystemd(value) {
24
24
  return `"${value.replaceAll('\\', '\\\\').replaceAll('"', '\\"').replaceAll('%', '%%')}"`;
25
25
  }
26
- const h5Actions = new Set(['start', 'daemon', 'qr', 'devices', 'revoke', 'install', 'status', 'stop', 'uninstall']);
26
+ const h5Actions = new Set(['start', 'daemon', 'qr', 'devices', 'revoke', 'install', 'status', 'stop', 'uninstall', 'work-worker']);
27
27
  /** Network H5 keeps ask/read-only unless this command explicitly requests auto. */
28
28
  export function h5RuntimeOptions(options, cliOptions = {}) {
29
29
  const requested = cliOptions['approval-mode'];
@@ -46,7 +46,7 @@ function h5Port(cliOptions) {
46
46
  export function h5DaemonArguments(executable, entry, port) {
47
47
  return [executable, entry, 'h5', 'daemon', '--port', String(port)];
48
48
  }
49
- export async function runH5Command(options, args, cliOptions = {}) {
49
+ export async function runH5Command(options, args, cliOptions = {}, configuration) {
50
50
  const [action = 'start', targetId] = args;
51
51
  if (!h5Actions.has(action)) {
52
52
  process.stderr.write(`未知命令: pulse h5 ${action}\n可用子命令: start, qr, devices, revoke, install, status, stop, uninstall\n`);
@@ -65,6 +65,16 @@ export async function runH5Command(options, args, cliOptions = {}) {
65
65
  const data = options.dataDir ?? process.env.PULSE_DATA_DIR ?? join(home, '.pulse', 'data');
66
66
  const cwd = options.cwd ?? process.cwd();
67
67
  const platform = process.platform;
68
+ // Internal child process. The parent sends the task over versioned IPC so
69
+ // prompts and credentials never appear in the process argument list.
70
+ if (action === 'work-worker') {
71
+ if (!targetId || !/^conv-[0-9a-f-]{36}$/i.test(targetId)) {
72
+ process.stderr.write('工作进程会话标识无效。\n');
73
+ return 1;
74
+ }
75
+ const host = createLocalHost(h5RuntimeOptions(options, cliOptions));
76
+ return runH5WorkWorker(host, targetId);
77
+ }
68
78
  // 1. Devices list
69
79
  if (action === 'devices') {
70
80
  const auth = new H5AuthStore(data);
@@ -252,7 +262,20 @@ WantedBy=default.target
252
262
  process.stderr.write('H5 网络服务已将审批模式从 auto 降为 ask。若确需自动批准,请显式传入 --approval-mode auto。\n');
253
263
  }
254
264
  const host = createLocalHost(runtimeOptions);
255
- const server = new H5Server(host, { dataDir: data, port: port ?? DEFAULT_H5_PORT });
265
+ const server = new H5Server(host, {
266
+ dataDir: data,
267
+ port: port ?? DEFAULT_H5_PORT,
268
+ ...(process.env.PULSE_H5_HOST ? { host: process.env.PULSE_H5_HOST } : {}),
269
+ ...(configuration ? { configuration } : {}),
270
+ workWorker: {
271
+ entry: fileURLToPath(new URL('../bin.js', import.meta.url)),
272
+ dataDir: data,
273
+ ...(configuration ? { configPath: configuration.path } : {}),
274
+ approvalMode: runtimeOptions.approvalMode,
275
+ trustWorkspace: cliOptions['trust-workspace'] === true,
276
+ cwd,
277
+ },
278
+ });
256
279
  const started = await server.start();
257
280
  if (!isDaemon) {
258
281
  process.stdout.write(`\n🚀 Pulse H5 个人助理服务已启动!\n`);
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
- /** Provider map key, not the adapter id. */
19
- provider: string;
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: string;
25
+ displayName?: string;
24
26
  maxContextTokens?: number;
25
27
  maxOutputTokens?: number;
26
28
  reasoningEffort?: 'low' | 'medium' | 'high';
@@ -61,11 +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
- /** Globally unique Pulse model names mapped to provider/model codes. */
66
+ /** @deprecated Legacy flat model catalog. New configs nest models under providers. */
65
67
  models?: Record<string, PulseCliModel>;
66
- /** Active Pulse model display name. */
68
+ /** Active Pulse model reference, preferably providerId/modelId. */
67
69
  activeModel?: string;
68
- /** Ordered model display names per task; later entries act as fallbacks. */
70
+ /** Dedicated model for the persistent Pulse assistant chat; defaults to activeModel. */
71
+ assistantModel?: string;
72
+ /** Ordered providerId/modelId references per task; later entries act as fallbacks. */
69
73
  taskRouting?: Partial<Record<PulseModelTask, string[]>>;
70
74
  capabilities?: PulseCliCapabilities;
71
75
  approvalMode?: 'read-only' | 'ask' | 'auto';
@@ -85,6 +89,12 @@ export interface PulseCliConfig {
85
89
  baseURL?: string;
86
90
  timeoutMs?: number;
87
91
  };
92
+ approvalReview?: {
93
+ provider?: 'ollama';
94
+ model?: string;
95
+ baseURL?: string;
96
+ timeoutMs?: number;
97
+ };
88
98
  }
89
99
  export interface PulseCliJarvisConfig {
90
100
  /** Master switch to enable or disable Jarvis cognitive memory architecture. */
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
- models: {
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: { ...merged.providers, ...value.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,57 @@
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
+ ],
21
+ "0.5.0": [
22
+ {
23
+ "zh": "新增独立 Web 工作台,统一助理聊天、项目工作会话、历史记录、设置与本地终端。",
24
+ "en": "Added a standalone Web workbench for assistant chats, project work sessions, history, settings, and local terminals."
25
+ },
26
+ {
27
+ "zh": "项目任务使用独立工作进程和 Git 工作区,继承当前项目改动,验收通过后合入结果;非 Git 项目使用互斥锁串行执行。",
28
+ "en": "Project tasks run in separate worker processes and Git worktrees that inherit current project edits and merge accepted results; non-Git projects use exclusive locks for serial execution."
29
+ },
30
+ {
31
+ "zh": "支持助理调用项目、工作会话及 Jarvis 工具,跟踪任务状态,并通过持久化消息队列恢复中断交互。",
32
+ "en": "The assistant can invoke project, work-session, and Jarvis tools, track task status, and recover interrupted interactions through a persistent message inbox."
33
+ },
34
+ {
35
+ "zh": "多个澄清问题可分组一次回答;Jarvis 检索结果按当前请求筛选后进入执行上下文。",
36
+ "en": "Related clarification questions can be answered as one group; retrieved Jarvis context is selected for the current request before execution."
37
+ },
38
+ {
39
+ "zh": "工作区交付比较任务起始快照,正确合入已提交成果,并支持解决冲突后重试;非 Git 失败重试保留共享目录锁语义。",
40
+ "en": "Workspace delivery compares against the task's starting snapshot, merges already committed results, and supports retries after conflict resolution; non-Git retries retain shared-directory locking."
41
+ },
42
+ {
43
+ "zh": "Jarvis 同步保留观察与结果事件类型,兼容旧队列;队列满时保留未送达和待审核记录,明确拒绝无法接纳的新请求。",
44
+ "en": "Jarvis synchronization preserves observation and result event types and supports older queues; full queues retain undelivered writes and candidates under review, explicitly rejecting new requests that cannot be accepted."
45
+ },
46
+ {
47
+ "zh": "改善 H5 实时消息、工具审批、设备认证和工作进程配置传递,打包时包含完整 Web 资源。",
48
+ "en": "Improved H5 live messages, tool approvals, device authentication, and worker configuration transfer, and included the complete Web assets in packages."
49
+ },
50
+ {
51
+ "zh": "修复任务接收确认和服务状态提示,兼容已有运行记录及工具定义。",
52
+ "en": "Fixed task acknowledgement and service status messages while retaining compatibility with existing run records and tool definitions."
53
+ }
54
+ ],
3
55
  "0.4.7": [
4
56
  {
5
57
  "zh": "新增 H5 手机助理:支持扫码配对、设备管理、实时会话与工具审批,以及 macOS 和 Linux 后台服务。",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hunterzhu/pulse-cli",
3
- "version": "0.4.7",
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.4.7",
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",