@hunterzhu/pulse-cli 0.4.6 → 0.4.7

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
@@ -15,6 +15,7 @@ import { runScheduledCommand } from './commands/scheduled.js';
15
15
  import { runServiceCommand } from './commands/service.js';
16
16
  import { runTemplateCommand } from './commands/template.js';
17
17
  import { runMcpDoctor } from './commands/mcp.js';
18
+ import { runH5Command } from './commands/h5.js';
18
19
  import { isSameModulePath } from './utils/is-main-module.js';
19
20
  import { formatVersionOutput, shouldUseColor } from './presentation.js';
20
21
  const packageManifest = createRequire(import.meta.url)('../package.json');
@@ -32,6 +33,8 @@ Usage:
32
33
  pulse sessions [options] list saved conversations
33
34
  pulse resume <conversation-id> [task] continue or recover a conversation
34
35
  pulse doctor [options] check local configuration
36
+ pulse h5 [start|qr|devices|revoke|install|status|stop|uninstall]
37
+ mobile web assistant and launchd daemon
35
38
  pulse schedule <add|list|pause|resume|remove|daemon> [options]
36
39
  manage recurring tasks (daemon requires read-only or auto approval)
37
40
  pulse service <install|status|start|stop|uninstall>
@@ -138,7 +141,7 @@ export function parse(argv) {
138
141
  options[key] = true;
139
142
  continue;
140
143
  }
141
- if (!command && ['run', 'sessions', 'resume', 'doctor', 'setup', 'schedule', 'service', 'template', 'mcp'].includes(arg)) {
144
+ if (!command && ['run', 'sessions', 'resume', 'doctor', 'setup', 'schedule', 'service', 'template', 'mcp', 'h5'].includes(arg)) {
142
145
  command = arg;
143
146
  }
144
147
  else {
@@ -439,6 +442,7 @@ export async function hostOptions(parsed) {
439
442
  ...(allowNetwork === undefined ? {} : { allowNetwork }),
440
443
  ...(config.networkHosts === undefined ? {} : { networkHosts: config.networkHosts }),
441
444
  jarvis,
445
+ ...(config.frontModel ? { frontModel: config.frontModel } : {}),
442
446
  };
443
447
  }
444
448
  export async function main() {
@@ -468,6 +472,8 @@ export async function main() {
468
472
  }
469
473
  if (parsed.command === 'service')
470
474
  return runServiceCommand(options, parsed.positionals);
475
+ if (parsed.command === 'h5')
476
+ return runH5Command(options, parsed.positionals, parsed.options);
471
477
  if (parsed.command === 'mcp' && parsed.positionals[0] === 'doctor')
472
478
  return runMcpDoctor(options, parsed.positionals.slice(1));
473
479
  if (parsed.command === 'template')
@@ -0,0 +1,5 @@
1
+ import { type LocalHostOptions } from '@hunterzhu/pulse-server';
2
+ /** Network H5 keeps ask/read-only unless this command explicitly requests auto. */
3
+ export declare function h5RuntimeOptions(options: LocalHostOptions, cliOptions?: Record<string, string | boolean>): LocalHostOptions;
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>;
@@ -0,0 +1,285 @@
1
+ import { access, mkdir, rm, writeFile } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { spawnSync } from 'node:child_process';
6
+ import QRCode from 'qrcode';
7
+ import { createLocalHost, H5Server, H5AuthStore, DEFAULT_H5_PORT } from '@hunterzhu/pulse-server';
8
+ const serviceId = 'com.hunterzhu.pulse.h5';
9
+ function checked(file, args) {
10
+ const result = spawnSync(file, args, { encoding: 'utf8' });
11
+ if (result.status !== 0) {
12
+ throw new Error(`${file} ${args[0]} failed: ${(result.stderr || result.stdout || '').trim()}`);
13
+ }
14
+ }
15
+ function xml(value) {
16
+ return value
17
+ .replaceAll('&', '&amp;')
18
+ .replaceAll('<', '&lt;')
19
+ .replaceAll('>', '&gt;')
20
+ .replaceAll('"', '&quot;')
21
+ .replaceAll("'", '&apos;');
22
+ }
23
+ function quoteSystemd(value) {
24
+ return `"${value.replaceAll('\\', '\\\\').replaceAll('"', '\\"').replaceAll('%', '%%')}"`;
25
+ }
26
+ const h5Actions = new Set(['start', 'daemon', 'qr', 'devices', 'revoke', 'install', 'status', 'stop', 'uninstall']);
27
+ /** Network H5 keeps ask/read-only unless this command explicitly requests auto. */
28
+ export function h5RuntimeOptions(options, cliOptions = {}) {
29
+ const requested = cliOptions['approval-mode'];
30
+ if (requested === 'auto' || cliOptions['auto-approve'] === true)
31
+ return { ...options, approvalMode: 'auto' };
32
+ if (requested === 'read-only' || cliOptions['read-only'] === true)
33
+ return { ...options, approvalMode: 'read-only' };
34
+ if (requested === 'ask')
35
+ return { ...options, approvalMode: 'ask' };
36
+ if (options.approvalMode === 'auto')
37
+ return { ...options, approvalMode: 'ask' };
38
+ return options;
39
+ }
40
+ function h5Port(cliOptions) {
41
+ if (typeof cliOptions.port !== 'string')
42
+ return DEFAULT_H5_PORT;
43
+ const port = Number(cliOptions.port);
44
+ return Number.isInteger(port) && port >= 0 && port <= 65535 ? port : undefined;
45
+ }
46
+ export function h5DaemonArguments(executable, entry, port) {
47
+ return [executable, entry, 'h5', 'daemon', '--port', String(port)];
48
+ }
49
+ export async function runH5Command(options, args, cliOptions = {}) {
50
+ const [action = 'start', targetId] = args;
51
+ if (!h5Actions.has(action)) {
52
+ process.stderr.write(`未知命令: pulse h5 ${action}\n可用子命令: start, qr, devices, revoke, install, status, stop, uninstall\n`);
53
+ return 1;
54
+ }
55
+ const port = h5Port(cliOptions);
56
+ if (port === undefined && ['start', 'daemon', 'qr', 'install'].includes(action)) {
57
+ process.stderr.write('错误: 端口必须是 0 到 65535 的整数。\n');
58
+ return 1;
59
+ }
60
+ if (action === 'install' && port === 0) {
61
+ process.stderr.write('错误: 后台服务需要固定端口,不能使用端口 0。\n');
62
+ return 1;
63
+ }
64
+ const home = process.env.PULSE_HOME ?? homedir();
65
+ const data = options.dataDir ?? process.env.PULSE_DATA_DIR ?? join(home, '.pulse', 'data');
66
+ const cwd = options.cwd ?? process.cwd();
67
+ const platform = process.platform;
68
+ // 1. Devices list
69
+ if (action === 'devices') {
70
+ const auth = new H5AuthStore(data);
71
+ await auth.init();
72
+ const devices = await auth.listDevices();
73
+ if (devices.length === 0) {
74
+ process.stdout.write('暂无已配对的设备。\n');
75
+ return 0;
76
+ }
77
+ process.stdout.write('已配对设备列表:\n');
78
+ process.stdout.write('ID\t\t\t\t\t设备名称\t\t配对时间\t\t最近活动\n');
79
+ process.stdout.write('------------------------------------------------------------------------------------\n');
80
+ for (const d of devices) {
81
+ process.stdout.write(`${d.id}\t${d.name}\t${new Date(d.createdAt).toLocaleString()}\t${new Date(d.lastUsedAt).toLocaleString()}\n`);
82
+ }
83
+ return 0;
84
+ }
85
+ // 2. Revoke device
86
+ if (action === 'revoke') {
87
+ if (!targetId?.trim()) {
88
+ process.stderr.write('错误: 请提供要吊销的设备 ID,例如: pulse h5 revoke dev-xxx\n');
89
+ return 1;
90
+ }
91
+ const auth = new H5AuthStore(data);
92
+ await auth.init();
93
+ const ok = await auth.revokeDevice(targetId.trim());
94
+ if (ok) {
95
+ process.stdout.write(`已成功吊销设备 ${targetId}。\n`);
96
+ return 0;
97
+ }
98
+ else {
99
+ process.stderr.write(`未找到设备 ${targetId}。\n`);
100
+ return 1;
101
+ }
102
+ }
103
+ // 3. QR code retrieval (queries running server or generates one-off)
104
+ if (action === 'qr') {
105
+ try {
106
+ const res = await fetch(`http://127.0.0.1:${port}/api/pair/qr`, { signal: AbortSignal.timeout(1500) });
107
+ if (res.ok) {
108
+ const info = (await res.json());
109
+ process.stdout.write(`\n📱 手机扫码连接 Pulse 个人助理:\n`);
110
+ process.stdout.write(`局域网地址: ${info.pairUrl}\n`);
111
+ process.stdout.write(`配对码: ${info.code} (5 分钟内有效)\n\n`);
112
+ const terminalQr = await QRCode.toString(info.pairUrl, { type: 'terminal', small: true });
113
+ process.stdout.write(terminalQr + '\n');
114
+ return 0;
115
+ }
116
+ }
117
+ catch {
118
+ // Server not running
119
+ }
120
+ process.stdout.write(`H5 服务当前未在端口 ${port} 运行。\n可运行 'pulse h5' 在前台启动,或运行 'pulse h5 install' 配置开机后台运行。\n`);
121
+ return 1;
122
+ }
123
+ // 4. Service install (launchd on macOS / systemd on Linux)
124
+ if (action === 'install') {
125
+ if (!['darwin', 'linux'].includes(platform)) {
126
+ throw new Error(`SERVICE_PLATFORM_UNSUPPORTED:${platform}`);
127
+ }
128
+ const executable = process.execPath;
129
+ const entry = fileURLToPath(new URL('../bin.js', import.meta.url));
130
+ const config = process.env.PULSE_CONFIG ?? join(home, '.pulse', 'config.json');
131
+ const daemonArguments = h5DaemonArguments(executable, entry, port ?? DEFAULT_H5_PORT);
132
+ if (platform === 'darwin') {
133
+ const directory = join(home, 'Library', 'LaunchAgents');
134
+ const file = join(directory, `${serviceId}.plist`);
135
+ await mkdir(directory, { recursive: true });
136
+ await writeFile(file, `<?xml version="1.0" encoding="UTF-8"?>
137
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
138
+ <plist version="1.0">
139
+ <dict>
140
+ <key>Label</key>
141
+ <string>${serviceId}</string>
142
+ <key>ProgramArguments</key>
143
+ <array>
144
+ ${daemonArguments.map((argument) => ` <string>${xml(argument)}</string>`).join('\n')}
145
+ </array>
146
+ <key>WorkingDirectory</key>
147
+ <string>${xml(cwd)}</string>
148
+ <key>EnvironmentVariables</key>
149
+ <dict>
150
+ <key>PULSE_HOME</key>
151
+ <string>${xml(home)}</string>
152
+ <key>PULSE_CONFIG</key>
153
+ <string>${xml(config)}</string>
154
+ <key>PULSE_DATA_DIR</key>
155
+ <string>${xml(data)}</string>
156
+ </dict>
157
+ <key>RunAtLoad</key>
158
+ <true/>
159
+ <key>KeepAlive</key>
160
+ <dict>
161
+ <key>SuccessfulExit</key>
162
+ <false/>
163
+ </dict>
164
+ <key>ThrottleInterval</key>
165
+ <integer>10</integer>
166
+ <key>StandardOutPath</key>
167
+ <string>${xml(join(data, 'h5-service.log'))}</string>
168
+ <key>StandardErrorPath</key>
169
+ <string>${xml(join(data, 'h5-service.error.log'))}</string>
170
+ </dict>
171
+ </plist>
172
+ `, { mode: 0o600 });
173
+ await mkdir(data, { recursive: true });
174
+ checked('launchctl', ['bootstrap', `gui/${process.getuid?.() ?? 0}`, file]);
175
+ process.stdout.write(`已安装 Pulse H5 launchd 守护服务;状态查看: pulse h5 status\n`);
176
+ return 0;
177
+ }
178
+ else if (platform === 'linux') {
179
+ const directory = join(home, '.config', 'systemd', 'user');
180
+ const file = join(directory, 'pulse-h5.service');
181
+ await mkdir(directory, { recursive: true });
182
+ await writeFile(file, `[Unit]
183
+ Description=Pulse H5 mobile web assistant service
184
+
185
+ [Service]
186
+ Type=simple
187
+ WorkingDirectory=${quoteSystemd(cwd)}
188
+ Environment=${quoteSystemd(`PULSE_HOME=${home}`)}
189
+ Environment=${quoteSystemd(`PULSE_CONFIG=${config}`)}
190
+ Environment=${quoteSystemd(`PULSE_DATA_DIR=${data}`)}
191
+ ExecStart=${daemonArguments.map(quoteSystemd).join(' ')}
192
+ Restart=on-failure
193
+ RestartSec=10s
194
+
195
+ [Install]
196
+ WantedBy=default.target
197
+ `, { mode: 0o600 });
198
+ checked('systemctl', ['--user', 'daemon-reload']);
199
+ checked('systemctl', ['--user', 'enable', '--now', 'pulse-h5.service']);
200
+ process.stdout.write(`已安装 Pulse H5 systemd 用户服务;状态查看: pulse h5 status\n`);
201
+ return 0;
202
+ }
203
+ }
204
+ // 5. Service status / stop / uninstall
205
+ if (['status', 'stop', 'uninstall'].includes(action)) {
206
+ if (platform === 'darwin') {
207
+ const file = join(home, 'Library', 'LaunchAgents', `${serviceId}.plist`);
208
+ if (action === 'status' && (await access(file).then(() => false, () => true))) {
209
+ process.stdout.write('Pulse H5 守护服务未安装。\n');
210
+ return 0;
211
+ }
212
+ if (action === 'uninstall') {
213
+ spawnSync('launchctl', ['bootout', `gui/${process.getuid?.() ?? 0}`, file], { stdio: 'ignore' });
214
+ await rm(file, { force: true });
215
+ process.stdout.write('已卸载 Pulse H5 守护服务。\n');
216
+ return 0;
217
+ }
218
+ const verb = action === 'status' ? 'print' : 'kill';
219
+ const extraArgs = action === 'stop' ? ['SIGTERM'] : [];
220
+ const result = spawnSync('launchctl', [verb, ...extraArgs, `gui/${process.getuid?.() ?? 0}/${serviceId}`], { encoding: 'utf8' });
221
+ process.stdout.write(result.stdout || result.stderr || `${action}: ${result.status === 0 ? 'ok' : 'not running'}\n`);
222
+ return result.status === 0 ? 0 : 1;
223
+ }
224
+ else if (platform === 'linux') {
225
+ const unit = 'pulse-h5.service';
226
+ if (action === 'status' && (await access(join(home, '.config', 'systemd', 'user', unit)).then(() => false, () => true))) {
227
+ process.stdout.write('Pulse H5 守护服务未安装。\n');
228
+ return 0;
229
+ }
230
+ const verb = action === 'status' ? 'status' : action === 'stop' ? 'stop' : 'disable';
231
+ const result = spawnSync('systemctl', ['--user', verb, ...(action === 'uninstall' ? ['--now'] : []), unit], { encoding: 'utf8' });
232
+ if (action === 'uninstall') {
233
+ await rm(join(home, '.config', 'systemd', 'user', unit), { force: true });
234
+ process.stdout.write('已卸载 Pulse H5 systemd 服务。\n');
235
+ }
236
+ else {
237
+ process.stdout.write(result.stdout || result.stderr || '');
238
+ }
239
+ return result.status === 0 ? 0 : 1;
240
+ }
241
+ process.stderr.write(`当前平台不支持 H5 守护进程管理: ${platform}\n`);
242
+ return 1;
243
+ }
244
+ if (action !== 'start' && action !== 'daemon') {
245
+ process.stderr.write(`未知命令: pulse h5 ${action}\n`);
246
+ return 1;
247
+ }
248
+ // 6. Foreground server / daemon worker
249
+ const isDaemon = action === 'daemon';
250
+ const runtimeOptions = h5RuntimeOptions(options, cliOptions);
251
+ if (options.approvalMode === 'auto' && runtimeOptions.approvalMode === 'ask') {
252
+ process.stderr.write('H5 网络服务已将审批模式从 auto 降为 ask。若确需自动批准,请显式传入 --approval-mode auto。\n');
253
+ }
254
+ const host = createLocalHost(runtimeOptions);
255
+ const server = new H5Server(host, { dataDir: data, port: port ?? DEFAULT_H5_PORT });
256
+ const started = await server.start();
257
+ if (!isDaemon) {
258
+ process.stdout.write(`\n🚀 Pulse H5 个人助理服务已启动!\n`);
259
+ process.stdout.write(`电脑本机管理页: http://localhost:${started.port}/pair\n`);
260
+ process.stdout.write(`手机扫码连接页: ${started.pairUrl}\n`);
261
+ process.stdout.write(`当前局域网 IP: ${started.lanIp}\n\n`);
262
+ try {
263
+ const qr = await QRCode.toString(started.pairUrl, { type: 'terminal', small: true });
264
+ process.stdout.write(qr + '\n');
265
+ }
266
+ catch {
267
+ // ignore
268
+ }
269
+ process.stdout.write(`提示: 手机连上同一 Wi-Fi 后扫码即可使用。按 Ctrl+C 可停止服务。\n\n`);
270
+ }
271
+ await new Promise((resolve) => {
272
+ let shuttingDown = false;
273
+ const shutdown = () => {
274
+ if (shuttingDown) {
275
+ process.exit(130);
276
+ return;
277
+ }
278
+ shuttingDown = true;
279
+ void server.stop().then(() => host.close()).then(() => resolve(), () => resolve());
280
+ };
281
+ process.once('SIGINT', shutdown);
282
+ process.once('SIGTERM', shutdown);
283
+ });
284
+ return 0;
285
+ }
package/dist/config.d.ts CHANGED
@@ -78,6 +78,13 @@ export interface PulseCliConfig {
78
78
  networkHosts?: string[];
79
79
  allowNetwork?: boolean;
80
80
  jarvis?: PulseCliJarvisConfig;
81
+ frontModel?: {
82
+ enabled?: boolean;
83
+ provider?: string;
84
+ model?: string;
85
+ baseURL?: string;
86
+ timeoutMs?: number;
87
+ };
81
88
  }
82
89
  export interface PulseCliJarvisConfig {
83
90
  /** Master switch to enable or disable Jarvis cognitive memory architecture. */
@@ -1,5 +1,31 @@
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.4.7": [
4
+ {
5
+ "zh": "新增 H5 手机助理:支持扫码配对、设备管理、实时会话与工具审批,以及 macOS 和 Linux 后台服务。",
6
+ "en": "Added an H5 mobile assistant with QR pairing, device management, live conversations, tool approvals, and background services on macOS and Linux."
7
+ },
8
+ {
9
+ "zh": "支持本地 Ollama 即时回复与任务意图整理,并结合最近对话处理澄清回答。",
10
+ "en": "Added local Ollama instant replies and structured task briefs that use recent dialogue to interpret clarification answers."
11
+ },
12
+ {
13
+ "zh": "Jarvis 事件、候选记忆和会话关闭增加持久化待发送队列及幂等请求标识,失败后可恢复重试,收到确认后才推进状态。",
14
+ "en": "Jarvis events, candidate memories, and session completion use a durable outbox and idempotency keys; failed writes can be retried after restoration, and state advances only after acknowledgement."
15
+ },
16
+ {
17
+ "zh": "执行中的用户补充输入可记录为 Jarvis 观察事件;候选记忆使用 Jarvis 可解析的会话来源。",
18
+ "en": "Mid-run user input is recorded as Jarvis observation events; candidate memories reference sessions that Jarvis can resolve as evidence."
19
+ },
20
+ {
21
+ "zh": "H5 默认端口改为 7332,避免与 Jarvis MCP 冲突;后台服务保留自定义端口并拒绝随机端口。",
22
+ "en": "Changed the default H5 port to 7332 to avoid conflicts with Jarvis MCP; background services preserve custom ports and reject random ports."
23
+ },
24
+ {
25
+ "zh": "修复手机页事件与审批流程、会话工作目录限制、Host 校验和标题转义;网络服务默认保持人工审批。",
26
+ "en": "Fixed mobile event and approval flows, session workspace restrictions, Host validation, and title escaping; network services keep manual approval by default."
27
+ }
28
+ ],
3
29
  "0.4.6": [
4
30
  {
5
31
  "zh": "支持通过 CLI、环境变量或用户配置启用 Jarvis 会话、项目上下文、执行事件和候选记忆;默认关闭,服务不可用时继续独立运行。",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hunterzhu/pulse-cli",
3
- "version": "0.4.6",
3
+ "version": "0.4.7",
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.6",
39
+ "@hunterzhu/pulse-server": "0.4.7",
40
40
  "chalk": "^5.4.1",
41
41
  "highlight.js": "^11.10.0",
42
42
  "ink": "^7.1.1",
@@ -44,10 +44,12 @@
44
44
  "ink-spinner": "^5.0.0",
45
45
  "ink-text-input": "^6.0.0",
46
46
  "marked": "^14.0.0",
47
+ "qrcode": "^1.5.4",
47
48
  "react": "^19.0.0",
48
49
  "wrap-ansi": "10.0.2"
49
50
  },
50
51
  "devDependencies": {
52
+ "@types/qrcode": "^1.5.6",
51
53
  "@types/react": "^19.0.0"
52
54
  }
53
55
  }