@hunterzhu/pulse-cli 0.4.5 → 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 +63 -2
- package/dist/commands/h5.d.ts +5 -0
- package/dist/commands/h5.js +285 -0
- package/dist/config.d.ts +23 -1
- package/dist/config.js +8 -1
- package/dist/release-highlights.generated.js +48 -0
- package/package.json +4 -2
package/dist/bin.js
CHANGED
|
@@ -3,7 +3,7 @@ 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 { createMcpCapabilityPack, createPdfCapabilityPack, createSkillCapabilityPack, createSpreadsheetCapabilityPack } from '@hunterzhu/pulse-server';
|
|
6
|
+
import { assertJarvisApiUrl, createMcpCapabilityPack, createPdfCapabilityPack, createSkillCapabilityPack, createSpreadsheetCapabilityPack } from '@hunterzhu/pulse-server';
|
|
7
7
|
import { defaultPulseConfig, ensurePulseUserConfig, expandHome, loadPulseConfig } from './config.js';
|
|
8
8
|
import { runInteractive } from './commands/interactive.js';
|
|
9
9
|
import { runOneShot } from './commands/run.js';
|
|
@@ -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>
|
|
@@ -66,12 +69,36 @@ Options:
|
|
|
66
69
|
--mock-after-tool-response <text> deterministic response after tool execution
|
|
67
70
|
--task-controller force the multi-stage task controller
|
|
68
71
|
--no-task-controller keep a single ReAct loop
|
|
72
|
+
--jarvis enable Jarvis cognitive memory architecture
|
|
73
|
+
--no-jarvis disable Jarvis cognitive memory architecture
|
|
74
|
+
--jarvis-url <url> Jarvis API base URL (default http://127.0.0.1:7330)
|
|
75
|
+
--jarvis-token <token> bearer token for the Jarvis API
|
|
76
|
+
--jarvis-budget <n> context token budget sent to Jarvis (default 4000)
|
|
77
|
+
--jarvis-allow-remote allow a Jarvis URL that is not loopback
|
|
69
78
|
--live doctor: make one real provider request
|
|
70
79
|
--no-color disable terminal styling
|
|
71
80
|
--help, -h show this help
|
|
72
81
|
--version, -v show the version
|
|
73
82
|
setup --force write a user config template (also created on first run)
|
|
74
83
|
`;
|
|
84
|
+
const knownBooleanFlags = new Set([
|
|
85
|
+
'resume',
|
|
86
|
+
'read-only',
|
|
87
|
+
'auto-approve',
|
|
88
|
+
'allow-network',
|
|
89
|
+
'no-network',
|
|
90
|
+
'trust-workspace',
|
|
91
|
+
'task-controller',
|
|
92
|
+
'no-task-controller',
|
|
93
|
+
'live',
|
|
94
|
+
'no-color',
|
|
95
|
+
'help',
|
|
96
|
+
'version',
|
|
97
|
+
'force',
|
|
98
|
+
'jarvis',
|
|
99
|
+
'no-jarvis',
|
|
100
|
+
'jarvis-allow-remote',
|
|
101
|
+
]);
|
|
75
102
|
export function parse(argv) {
|
|
76
103
|
const options = {};
|
|
77
104
|
const positionals = [];
|
|
@@ -95,6 +122,10 @@ export function parse(argv) {
|
|
|
95
122
|
options[key] = inline;
|
|
96
123
|
continue;
|
|
97
124
|
}
|
|
125
|
+
if (knownBooleanFlags.has(key)) {
|
|
126
|
+
options[key] = true;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
98
129
|
const next = forwarded[index + 1];
|
|
99
130
|
if (next && !next.startsWith('-')) {
|
|
100
131
|
options[key] = next;
|
|
@@ -110,7 +141,7 @@ export function parse(argv) {
|
|
|
110
141
|
options[key] = true;
|
|
111
142
|
continue;
|
|
112
143
|
}
|
|
113
|
-
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)) {
|
|
114
145
|
command = arg;
|
|
115
146
|
}
|
|
116
147
|
else {
|
|
@@ -357,6 +388,32 @@ export async function hostOptions(parsed) {
|
|
|
357
388
|
}
|
|
358
389
|
}
|
|
359
390
|
const explicitTaskController = parsed.options['task-controller'] === true ? true : parsed.options['no-task-controller'] === true ? false : mockParallelPlanOption !== undefined ? true : undefined;
|
|
391
|
+
const explicitJarvis = parsed.options['jarvis'] === true
|
|
392
|
+
? true
|
|
393
|
+
: parsed.options['no-jarvis'] === true
|
|
394
|
+
? false
|
|
395
|
+
: process.env.PULSE_JARVIS_ENABLED !== undefined
|
|
396
|
+
? process.env.PULSE_JARVIS_ENABLED === '1' || process.env.PULSE_JARVIS_ENABLED.toLowerCase() === 'true'
|
|
397
|
+
: config.jarvis?.enabled;
|
|
398
|
+
const jarvisUrl = option(parsed.options, 'jarvis-url') ?? process.env.PULSE_JARVIS_API_URL ?? config.jarvis?.apiUrl;
|
|
399
|
+
const jarvisToken = option(parsed.options, 'jarvis-token') ?? process.env.PULSE_JARVIS_TOKEN ?? config.jarvis?.token;
|
|
400
|
+
const jarvisBudget = parsePositiveInteger(option(parsed.options, 'jarvis-budget') ?? process.env.PULSE_JARVIS_BUDGET) ?? config.jarvis?.contextTokenBudget;
|
|
401
|
+
const jarvisAutoCandidate = config.jarvis?.autoCandidate ?? true;
|
|
402
|
+
const jarvisAllowRemote = parsed.options['jarvis-allow-remote'] === true
|
|
403
|
+
|| process.env.PULSE_JARVIS_ALLOW_REMOTE === '1'
|
|
404
|
+
|| process.env.PULSE_JARVIS_ALLOW_REMOTE?.toLowerCase() === 'true'
|
|
405
|
+
|| config.jarvis?.allowRemote === true;
|
|
406
|
+
const jarvisEnabled = explicitJarvis ?? false;
|
|
407
|
+
if (jarvisEnabled)
|
|
408
|
+
assertJarvisApiUrl(jarvisUrl ?? 'http://127.0.0.1:7330', jarvisAllowRemote);
|
|
409
|
+
const jarvis = {
|
|
410
|
+
enabled: jarvisEnabled,
|
|
411
|
+
...(jarvisUrl ? { apiUrl: jarvisUrl } : {}),
|
|
412
|
+
...(jarvisToken ? { token: jarvisToken } : {}),
|
|
413
|
+
...(jarvisBudget ? { contextTokenBudget: jarvisBudget } : {}),
|
|
414
|
+
autoCandidate: jarvisAutoCandidate,
|
|
415
|
+
allowRemote: jarvisAllowRemote,
|
|
416
|
+
};
|
|
360
417
|
return {
|
|
361
418
|
...(explicitTaskController === undefined ? {} : { taskController: explicitTaskController }),
|
|
362
419
|
...(cwd === undefined ? {} : { cwd }),
|
|
@@ -384,6 +441,8 @@ export async function hostOptions(parsed) {
|
|
|
384
441
|
...(configuredAutoCompactPercent === undefined ? {} : { autoCompactPercent: Math.min(90, configuredAutoCompactPercent) }),
|
|
385
442
|
...(allowNetwork === undefined ? {} : { allowNetwork }),
|
|
386
443
|
...(config.networkHosts === undefined ? {} : { networkHosts: config.networkHosts }),
|
|
444
|
+
jarvis,
|
|
445
|
+
...(config.frontModel ? { frontModel: config.frontModel } : {}),
|
|
387
446
|
};
|
|
388
447
|
}
|
|
389
448
|
export async function main() {
|
|
@@ -413,6 +472,8 @@ export async function main() {
|
|
|
413
472
|
}
|
|
414
473
|
if (parsed.command === 'service')
|
|
415
474
|
return runServiceCommand(options, parsed.positionals);
|
|
475
|
+
if (parsed.command === 'h5')
|
|
476
|
+
return runH5Command(options, parsed.positionals, parsed.options);
|
|
416
477
|
if (parsed.command === 'mcp' && parsed.positionals[0] === 'doctor')
|
|
417
478
|
return runMcpDoctor(options, parsed.positionals.slice(1));
|
|
418
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('&', '&')
|
|
18
|
+
.replaceAll('<', '<')
|
|
19
|
+
.replaceAll('>', '>')
|
|
20
|
+
.replaceAll('"', '"')
|
|
21
|
+
.replaceAll("'", ''');
|
|
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
|
@@ -77,6 +77,28 @@ export interface PulseCliConfig {
|
|
|
77
77
|
/** Explicit network destinations; workspace configuration cannot grant these. */
|
|
78
78
|
networkHosts?: string[];
|
|
79
79
|
allowNetwork?: boolean;
|
|
80
|
+
jarvis?: PulseCliJarvisConfig;
|
|
81
|
+
frontModel?: {
|
|
82
|
+
enabled?: boolean;
|
|
83
|
+
provider?: string;
|
|
84
|
+
model?: string;
|
|
85
|
+
baseURL?: string;
|
|
86
|
+
timeoutMs?: number;
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
export interface PulseCliJarvisConfig {
|
|
90
|
+
/** Master switch to enable or disable Jarvis cognitive memory architecture. */
|
|
91
|
+
enabled?: boolean;
|
|
92
|
+
/** Base URL of the running Jarvis server. Defaults to http://127.0.0.1:7330 */
|
|
93
|
+
apiUrl?: string;
|
|
94
|
+
/** Optional bearer token if Jarvis requires auth. */
|
|
95
|
+
token?: string;
|
|
96
|
+
/** Max token budget for dynamic context package built by Jarvis. Defaults to 4000. */
|
|
97
|
+
contextTokenBudget?: number;
|
|
98
|
+
/** Whether to automatically record candidate memories upon task completion. Defaults to true. */
|
|
99
|
+
autoCandidate?: boolean;
|
|
100
|
+
/** Allow an API URL that is not loopback. Defaults to false. */
|
|
101
|
+
allowRemote?: boolean;
|
|
80
102
|
}
|
|
81
103
|
export declare const defaultPulseConfig: PulseCliConfig;
|
|
82
104
|
export declare function defaultPulseConfigPath(): string;
|
|
@@ -84,7 +106,7 @@ export declare function defaultPulseHomePath(): string;
|
|
|
84
106
|
/** Create the user config on first run without replacing an existing file. */
|
|
85
107
|
export declare function ensurePulseUserConfig(path?: string): Promise<void>;
|
|
86
108
|
export declare function expandHome(path: string | undefined): string | undefined;
|
|
87
|
-
/** Workspace files must not escalate approval, network, provider credentials, or the
|
|
109
|
+
/** Workspace files must not escalate approval, network, provider credentials, the system prompt, or the Jarvis endpoint. */
|
|
88
110
|
export declare function sanitizeWorkspaceConfig(value: PulseCliConfig): PulseCliConfig;
|
|
89
111
|
export declare function mergePulseConfigs(layers: Array<{
|
|
90
112
|
value: PulseCliConfig;
|
package/dist/config.js
CHANGED
|
@@ -27,6 +27,13 @@ export const defaultPulseConfig = {
|
|
|
27
27
|
maxTurns: 32,
|
|
28
28
|
autoCompactPercent: 90,
|
|
29
29
|
allowNetwork: false,
|
|
30
|
+
jarvis: {
|
|
31
|
+
enabled: false,
|
|
32
|
+
apiUrl: 'http://127.0.0.1:7330',
|
|
33
|
+
contextTokenBudget: 4_000,
|
|
34
|
+
autoCandidate: true,
|
|
35
|
+
allowRemote: false,
|
|
36
|
+
},
|
|
30
37
|
};
|
|
31
38
|
export function defaultPulseConfigPath() {
|
|
32
39
|
return join(defaultPulseHomePath(), 'config.json');
|
|
@@ -69,7 +76,7 @@ function asConfig(value) {
|
|
|
69
76
|
return undefined;
|
|
70
77
|
return value;
|
|
71
78
|
}
|
|
72
|
-
/** Workspace files must not escalate approval, network, provider credentials, or the
|
|
79
|
+
/** Workspace files must not escalate approval, network, provider credentials, the system prompt, or the Jarvis endpoint. */
|
|
73
80
|
export function sanitizeWorkspaceConfig(value) {
|
|
74
81
|
const providers = value.providers === undefined ? undefined : Object.fromEntries(Object.entries(value.providers).flatMap(([name, profile]) => {
|
|
75
82
|
const safe = {
|
|
@@ -1,5 +1,53 @@
|
|
|
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
|
+
],
|
|
29
|
+
"0.4.6": [
|
|
30
|
+
{
|
|
31
|
+
"zh": "支持通过 CLI、环境变量或用户配置启用 Jarvis 会话、项目上下文、执行事件和候选记忆;默认关闭,服务不可用时继续独立运行。",
|
|
32
|
+
"en": "Jarvis sessions, project context, execution events, and candidate memories can be enabled through CLI flags, environment variables, or user configuration; integration is off by default and execution continues independently if the service is unavailable."
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"zh": "工具事件仅发送执行标识、工具名称和状态,避免参数、文件内容和输出中的凭据进入记忆;发送的文本增加常见凭据脱敏。",
|
|
36
|
+
"en": "Tool events send only execution identifiers, tool names, and statuses to keep credentials in arguments, file contents, and outputs out of memory; outbound text redacts common credential forms."
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"zh": "持久化每次运行的 Jarvis 会话、上下文和已记录事件,恢复运行时保留上下文并避免重放已记录事件。",
|
|
40
|
+
"en": "Persist each run's Jarvis session, context, and recorded events so restored runs retain context without replaying recorded events."
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"zh": "事件记录不再依赖界面消费,结束前等待本次运行的写入完成;并行运行不会互相等待记忆写入。",
|
|
44
|
+
"en": "Event recording no longer depends on UI consumption, and completion waits for the current run's writes; concurrent runs do not wait for each other's memory writes."
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"zh": "候选记忆标题统一为单行并限制长度;仅验收通过的任务可记录为成功,工作区配置不能启用 Jarvis 或覆盖其端点。",
|
|
48
|
+
"en": "Candidate memory titles are normalized to a single line and capped in length; only accepted tasks are recorded as successful, and workspace configuration cannot enable Jarvis or override its endpoint."
|
|
49
|
+
}
|
|
50
|
+
],
|
|
3
51
|
"0.4.5": [
|
|
4
52
|
{
|
|
5
53
|
"zh": "复杂任务不再受全局模型调用次数上限截断,继续通过阶段、运行时限和循环检测控制执行;旧会话保持原有预算语义。",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hunterzhu/pulse-cli",
|
|
3
|
-
"version": "0.4.
|
|
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.
|
|
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
|
}
|