shennian 0.2.12 → 0.2.13
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/README.md +6 -0
- package/dist/src/commands/daemon.d.ts +16 -1
- package/dist/src/commands/daemon.js +180 -45
- package/dist/src/index.js +48 -25
- package/dist/src/native-fusion/parsers.js +35 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,8 +15,14 @@
|
|
|
15
15
|
|------|------|
|
|
16
16
|
| `shennian` | 智能启动:必要时 pair 再启动 daemon |
|
|
17
17
|
| `shennian pair` | 重新与账号 / server 配对 |
|
|
18
|
+
| `shennian restart` | 重启 daemon;未运行则启动 |
|
|
18
19
|
| `shennian daemon status` | 查看 daemon 运行状态 |
|
|
20
|
+
| `shennian daemon stop` | 停止 daemon |
|
|
21
|
+
| `shennian daemon restart` | 重启 daemon |
|
|
22
|
+
| `shennian daemon logs -n 100` | 查看最近日志 |
|
|
19
23
|
| `shennian config` | 查看本地配置 |
|
|
24
|
+
| `shennian agent add/list/remove` | 管理自定义 Agent |
|
|
25
|
+
| `shennian upgrade` | 升级 CLI |
|
|
20
26
|
|
|
21
27
|
## 开发
|
|
22
28
|
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
|
+
type Platform = 'darwin' | 'linux' | 'win32';
|
|
2
3
|
type ServiceLaunchMode = 'direct' | 'global-shim' | 'npx';
|
|
4
|
+
export type DaemonStatus = {
|
|
5
|
+
running: boolean;
|
|
6
|
+
pid: number | null;
|
|
7
|
+
stale: boolean;
|
|
8
|
+
platform: Platform;
|
|
9
|
+
shennianDir: string;
|
|
10
|
+
pidFile: string;
|
|
11
|
+
logFile: string;
|
|
12
|
+
};
|
|
3
13
|
export type ServiceLaunchSpec = {
|
|
4
14
|
command: string;
|
|
5
15
|
args: string[];
|
|
@@ -12,6 +22,9 @@ export declare function resolveServiceLaunchSpec(input: {
|
|
|
12
22
|
shennianCommandPath?: string | null;
|
|
13
23
|
npxPath?: string | null;
|
|
14
24
|
}): ServiceLaunchSpec;
|
|
25
|
+
export declare function getDaemonStatus(opts?: {
|
|
26
|
+
cleanupStale?: boolean;
|
|
27
|
+
}): DaemonStatus;
|
|
15
28
|
export declare function buildWindowsLauncherCommand(spec: ServiceLaunchSpec, logFile: string): string;
|
|
16
29
|
export declare function buildWindowsStartupVbs(cmdPath: string): string;
|
|
17
30
|
export declare function buildWindowsScheduledTaskXml(input: {
|
|
@@ -25,7 +38,9 @@ export declare function captureEnvForService(): Record<string, string>;
|
|
|
25
38
|
* This is critical for launchd/systemd which start with minimal env.
|
|
26
39
|
*/
|
|
27
40
|
export declare function saveEnvSnapshot(): void;
|
|
28
|
-
export declare function startDaemonProcess(
|
|
41
|
+
export declare function startDaemonProcess(opts?: {
|
|
42
|
+
quiet?: boolean;
|
|
43
|
+
}): void;
|
|
29
44
|
/**
|
|
30
45
|
* Install the platform-native auto-start service.
|
|
31
46
|
* Returns true if the service was immediately started (so caller can skip startDaemonProcess).
|
|
@@ -75,6 +75,31 @@ function isRunning(pid) {
|
|
|
75
75
|
return false;
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
|
+
export function getDaemonStatus(opts = {}) {
|
|
79
|
+
const pid = readPid();
|
|
80
|
+
const running = pid !== null && isRunning(pid);
|
|
81
|
+
const stale = pid !== null && !running;
|
|
82
|
+
if (stale && opts.cleanupStale) {
|
|
83
|
+
try {
|
|
84
|
+
fs.unlinkSync(PID_FILE);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// Another process may have cleaned up the stale pid file first.
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
running,
|
|
92
|
+
pid,
|
|
93
|
+
stale,
|
|
94
|
+
platform: getPlatform(),
|
|
95
|
+
shennianDir: SHENNIAN_DIR,
|
|
96
|
+
pidFile: PID_FILE,
|
|
97
|
+
logFile: LOG_FILE,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function printJson(value) {
|
|
101
|
+
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
102
|
+
}
|
|
78
103
|
// ─── Service definitions ─────────────────────────────────────────────────────
|
|
79
104
|
const LAUNCHD_LABEL = 'com.shennian.agent';
|
|
80
105
|
const LAUNCHD_PLIST = path.join(os.homedir(), 'Library/LaunchAgents', `${LAUNCHD_LABEL}.plist`);
|
|
@@ -142,12 +167,7 @@ function isWindowsCmdScript(filePath) {
|
|
|
142
167
|
export function buildWindowsLauncherCommand(spec, logFile) {
|
|
143
168
|
const commandLine = [spec.command, ...spec.args].map(quoteCmdArg).join(' ');
|
|
144
169
|
const invocation = isWindowsCmdScript(spec.command) ? `call ${commandLine}` : commandLine;
|
|
145
|
-
return [
|
|
146
|
-
'@echo off',
|
|
147
|
-
'setlocal',
|
|
148
|
-
`${invocation} >> ${quoteCmdArg(logFile)} 2>&1`,
|
|
149
|
-
'',
|
|
150
|
-
].join('\r\n');
|
|
170
|
+
return ['@echo off', 'setlocal', `${invocation} >> ${quoteCmdArg(logFile)} 2>&1`, ''].join('\r\n');
|
|
151
171
|
}
|
|
152
172
|
export function buildWindowsStartupVbs(cmdPath) {
|
|
153
173
|
return [
|
|
@@ -269,7 +289,9 @@ function installWindowsScheduledTask() {
|
|
|
269
289
|
});
|
|
270
290
|
fs.writeFileSync(WINDOWS_TASK_XML, taskXml, 'utf8');
|
|
271
291
|
try {
|
|
272
|
-
execSync(`schtasks /create /tn "${WINDOWS_TASK_NAME}" /xml "${WINDOWS_TASK_XML}" /f`, {
|
|
292
|
+
execSync(`schtasks /create /tn "${WINDOWS_TASK_NAME}" /xml "${WINDOWS_TASK_XML}" /f`, {
|
|
293
|
+
stdio: 'pipe',
|
|
294
|
+
});
|
|
273
295
|
execSync(`schtasks /run /tn "${WINDOWS_TASK_NAME}"`, { stdio: 'pipe' });
|
|
274
296
|
return true;
|
|
275
297
|
}
|
|
@@ -279,12 +301,27 @@ function installWindowsScheduledTask() {
|
|
|
279
301
|
}
|
|
280
302
|
export function captureEnvForService() {
|
|
281
303
|
const keep = [
|
|
282
|
-
'PATH',
|
|
283
|
-
'
|
|
284
|
-
'
|
|
285
|
-
'
|
|
286
|
-
'
|
|
287
|
-
'
|
|
304
|
+
'PATH',
|
|
305
|
+
'HOME',
|
|
306
|
+
'USER',
|
|
307
|
+
'LOGNAME',
|
|
308
|
+
'SHELL',
|
|
309
|
+
'TMPDIR',
|
|
310
|
+
'LANG',
|
|
311
|
+
'LC_ALL',
|
|
312
|
+
'LC_CTYPE',
|
|
313
|
+
'SSH_AUTH_SOCK',
|
|
314
|
+
'XDG_CONFIG_HOME',
|
|
315
|
+
'XDG_DATA_HOME',
|
|
316
|
+
'TEMP',
|
|
317
|
+
'TMP',
|
|
318
|
+
'ANTHROPIC_API_KEY',
|
|
319
|
+
'ANTHROPIC_AUTH_TOKEN',
|
|
320
|
+
'ANTHROPIC_BASE_URL',
|
|
321
|
+
'OPENAI_API_KEY',
|
|
322
|
+
'OPENAI_BASE_URL',
|
|
323
|
+
'GEMINI_API_KEY',
|
|
324
|
+
'GOOGLE_API_KEY',
|
|
288
325
|
];
|
|
289
326
|
const env = {};
|
|
290
327
|
for (const k of keep) {
|
|
@@ -379,11 +416,13 @@ export function saveEnvSnapshot() {
|
|
|
379
416
|
fs.writeFileSync(resolveShennianPath('env.json'), JSON.stringify(snapshot, null, 2));
|
|
380
417
|
}
|
|
381
418
|
// ─── Exported helpers (used by pair.ts) ─────────────────────────────────────
|
|
382
|
-
export function startDaemonProcess() {
|
|
419
|
+
export function startDaemonProcess(opts = {}) {
|
|
383
420
|
fs.mkdirSync(SHENNIAN_DIR, { recursive: true });
|
|
384
421
|
const pid = readPid();
|
|
385
422
|
if (pid !== null && isRunning(pid)) {
|
|
386
|
-
|
|
423
|
+
if (!opts.quiet) {
|
|
424
|
+
console.log(chalk.green(`✓ Background service already running (PID ${pid})`));
|
|
425
|
+
}
|
|
387
426
|
return;
|
|
388
427
|
}
|
|
389
428
|
const logFd = fs.openSync(LOG_FILE, 'a');
|
|
@@ -397,8 +436,10 @@ export function startDaemonProcess() {
|
|
|
397
436
|
child.unref();
|
|
398
437
|
fs.closeSync(logFd);
|
|
399
438
|
fs.writeFileSync(PID_FILE, String(child.pid));
|
|
400
|
-
|
|
401
|
-
|
|
439
|
+
if (!opts.quiet) {
|
|
440
|
+
console.log(chalk.green(`✓ Background service started (PID ${child.pid})`));
|
|
441
|
+
console.log(chalk.gray(` Logs: ${LOG_FILE}`));
|
|
442
|
+
}
|
|
402
443
|
}
|
|
403
444
|
/**
|
|
404
445
|
* Install the platform-native auto-start service.
|
|
@@ -409,8 +450,8 @@ export function installService() {
|
|
|
409
450
|
const platform = getPlatform();
|
|
410
451
|
const spec = resolveCurrentServiceLaunchSpec();
|
|
411
452
|
if (spec.mode === 'direct' && isEphemeralCliPath(SHENNIAN_SCRIPT)) {
|
|
412
|
-
console.warn(chalk.yellow('⚠ Warning: Current CLI path is temporary (npx). Auto-start may not work after reboot.\n'
|
|
413
|
-
|
|
453
|
+
console.warn(chalk.yellow('⚠ Warning: Current CLI path is temporary (npx). Auto-start may not work after reboot.\n' +
|
|
454
|
+
' Run `npm install -g shennian` for reliable auto-start.'));
|
|
414
455
|
}
|
|
415
456
|
switch (platform) {
|
|
416
457
|
case 'darwin': {
|
|
@@ -438,7 +479,9 @@ export function installService() {
|
|
|
438
479
|
fs.mkdirSync(dir, { recursive: true });
|
|
439
480
|
fs.writeFileSync(SYSTEMD_UNIT, buildSystemdUnit());
|
|
440
481
|
try {
|
|
441
|
-
execSync('systemctl --user daemon-reload && systemctl --user enable shennian', {
|
|
482
|
+
execSync('systemctl --user daemon-reload && systemctl --user enable shennian', {
|
|
483
|
+
stdio: 'pipe',
|
|
484
|
+
});
|
|
442
485
|
}
|
|
443
486
|
catch {
|
|
444
487
|
// Some Linux environments lack systemd user services; keep best-effort behavior.
|
|
@@ -459,10 +502,20 @@ export function installService() {
|
|
|
459
502
|
}
|
|
460
503
|
}
|
|
461
504
|
// ─── Subcommand implementations ──────────────────────────────────────────────
|
|
462
|
-
function
|
|
505
|
+
function daemonStart(opts) {
|
|
506
|
+
startDaemonProcess({ quiet: opts.json });
|
|
507
|
+
if (opts.json)
|
|
508
|
+
printJson(getDaemonStatus());
|
|
509
|
+
}
|
|
510
|
+
function daemonStop(opts = {}) {
|
|
463
511
|
const pid = readPid();
|
|
464
512
|
if (pid === null) {
|
|
465
|
-
|
|
513
|
+
if (opts.json) {
|
|
514
|
+
printJson(getDaemonStatus());
|
|
515
|
+
}
|
|
516
|
+
else {
|
|
517
|
+
console.log(chalk.yellow('⚠ No running background service found'));
|
|
518
|
+
}
|
|
466
519
|
return;
|
|
467
520
|
}
|
|
468
521
|
if (!isRunning(pid)) {
|
|
@@ -472,7 +525,12 @@ function daemonStop() {
|
|
|
472
525
|
catch {
|
|
473
526
|
// Stale pid file is already gone.
|
|
474
527
|
}
|
|
475
|
-
|
|
528
|
+
if (opts.json) {
|
|
529
|
+
printJson(getDaemonStatus());
|
|
530
|
+
}
|
|
531
|
+
else {
|
|
532
|
+
console.log(chalk.yellow('⚠ Service process no longer exists, cleaned up'));
|
|
533
|
+
}
|
|
476
534
|
return;
|
|
477
535
|
}
|
|
478
536
|
try {
|
|
@@ -483,31 +541,100 @@ function daemonStop() {
|
|
|
483
541
|
catch {
|
|
484
542
|
// Best-effort cleanup after SIGTERM.
|
|
485
543
|
}
|
|
486
|
-
|
|
544
|
+
if (opts.json) {
|
|
545
|
+
printJson(getDaemonStatus());
|
|
546
|
+
}
|
|
547
|
+
else {
|
|
548
|
+
console.log(chalk.green(`✓ Background service stopped (PID ${pid})`));
|
|
549
|
+
}
|
|
487
550
|
}
|
|
488
551
|
catch (err) {
|
|
489
|
-
|
|
552
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
553
|
+
if (opts.json) {
|
|
554
|
+
printJson({ ...getDaemonStatus(), error: message });
|
|
555
|
+
}
|
|
556
|
+
else {
|
|
557
|
+
console.error(chalk.red(`✗ Failed to stop: ${message}`));
|
|
558
|
+
}
|
|
490
559
|
}
|
|
491
560
|
}
|
|
492
|
-
function
|
|
493
|
-
const
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
561
|
+
async function waitForPidExit(pid, timeoutMs = 5000) {
|
|
562
|
+
const startedAt = Date.now();
|
|
563
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
564
|
+
if (!isRunning(pid))
|
|
565
|
+
return true;
|
|
566
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
497
567
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
568
|
+
return !isRunning(pid);
|
|
569
|
+
}
|
|
570
|
+
async function daemonRestart(opts = {}) {
|
|
571
|
+
const pid = readPid();
|
|
572
|
+
if (pid !== null && isRunning(pid)) {
|
|
573
|
+
try {
|
|
574
|
+
process.kill(pid, 'SIGTERM');
|
|
575
|
+
const stopped = await waitForPidExit(pid);
|
|
576
|
+
if (!stopped) {
|
|
577
|
+
process.kill(pid, 'SIGKILL');
|
|
578
|
+
await waitForPidExit(pid, 2000);
|
|
579
|
+
}
|
|
580
|
+
try {
|
|
581
|
+
fs.unlinkSync(PID_FILE);
|
|
582
|
+
}
|
|
583
|
+
catch {
|
|
584
|
+
// Best-effort cleanup after restart stop.
|
|
585
|
+
}
|
|
586
|
+
if (!opts.json) {
|
|
587
|
+
console.log(chalk.green(`✓ Background service stopped (PID ${pid})`));
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
catch (err) {
|
|
591
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
592
|
+
if (opts.json) {
|
|
593
|
+
printJson({ ...getDaemonStatus(), error: message });
|
|
594
|
+
}
|
|
595
|
+
else {
|
|
596
|
+
console.error(chalk.red(`✗ Failed to stop: ${message}`));
|
|
597
|
+
}
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
501
600
|
}
|
|
502
|
-
else {
|
|
503
|
-
console.log(chalk.yellow(`● Background service stopped (PID ${pid} is stale)`));
|
|
601
|
+
else if (pid !== null) {
|
|
504
602
|
try {
|
|
505
603
|
fs.unlinkSync(PID_FILE);
|
|
506
604
|
}
|
|
507
605
|
catch {
|
|
508
|
-
//
|
|
606
|
+
// Stale pid file is already gone.
|
|
607
|
+
}
|
|
608
|
+
if (!opts.json) {
|
|
609
|
+
console.log(chalk.yellow(`⚠ Service process no longer exists, cleaned up stale PID ${pid}`));
|
|
509
610
|
}
|
|
510
611
|
}
|
|
612
|
+
else {
|
|
613
|
+
if (!opts.json) {
|
|
614
|
+
console.log(chalk.gray('● Background service not running, starting it now'));
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
startDaemonProcess({ quiet: opts.json });
|
|
618
|
+
if (opts.json)
|
|
619
|
+
printJson(getDaemonStatus());
|
|
620
|
+
}
|
|
621
|
+
function daemonStatus(opts = {}) {
|
|
622
|
+
const status = getDaemonStatus({ cleanupStale: true });
|
|
623
|
+
if (opts.json) {
|
|
624
|
+
printJson(status);
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
if (status.pid === null) {
|
|
628
|
+
console.log(chalk.gray('● Background service not running'));
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
if (status.running) {
|
|
632
|
+
console.log(chalk.green(`● Background service running (PID ${status.pid})`));
|
|
633
|
+
console.log(chalk.gray(` Logs: ${LOG_FILE}`));
|
|
634
|
+
}
|
|
635
|
+
else {
|
|
636
|
+
console.log(chalk.yellow(`● Background service stopped (PID ${status.pid} is stale)`));
|
|
637
|
+
}
|
|
511
638
|
}
|
|
512
639
|
function daemonLogs(opts) {
|
|
513
640
|
if (!fs.existsSync(LOG_FILE)) {
|
|
@@ -589,24 +716,32 @@ function daemonUninstall() {
|
|
|
589
716
|
}
|
|
590
717
|
// ─── Register command ─────────────────────────────────────────────────────────
|
|
591
718
|
export function registerDaemonCommand(program) {
|
|
592
|
-
const daemon = program
|
|
593
|
-
|
|
594
|
-
|
|
719
|
+
const daemon = program.command('daemon').description('Manage the background service');
|
|
720
|
+
program.command('restart').description('Restart the background service').action(() => daemonRestart());
|
|
721
|
+
daemon
|
|
722
|
+
.command('start')
|
|
723
|
+
.description('Start the background service')
|
|
724
|
+
.option('--json', 'Print machine-readable status JSON')
|
|
725
|
+
.action((opts) => daemonStart(opts));
|
|
595
726
|
daemon
|
|
596
727
|
.command('stop')
|
|
597
728
|
.description('Stop the background service')
|
|
598
|
-
.
|
|
729
|
+
.option('--json', 'Print machine-readable status JSON')
|
|
730
|
+
.action((opts) => daemonStop(opts));
|
|
731
|
+
daemon
|
|
732
|
+
.command('restart')
|
|
733
|
+
.description('Restart the background service')
|
|
734
|
+
.option('--json', 'Print machine-readable status JSON')
|
|
735
|
+
.action((opts) => daemonRestart(opts));
|
|
599
736
|
daemon
|
|
600
737
|
.command('status')
|
|
601
738
|
.description('Show background service status')
|
|
602
|
-
.
|
|
739
|
+
.option('--json', 'Print machine-readable status JSON')
|
|
740
|
+
.action((opts) => daemonStatus(opts));
|
|
603
741
|
daemon
|
|
604
742
|
.command('logs')
|
|
605
743
|
.description('Show recent logs')
|
|
606
744
|
.option('-n, --lines <n>', 'Number of lines', '50')
|
|
607
745
|
.action((opts) => daemonLogs({ lines: parseInt(opts.lines, 10) }));
|
|
608
|
-
daemon
|
|
609
|
-
.command('uninstall')
|
|
610
|
-
.description('Uninstall auto-start service')
|
|
611
|
-
.action(daemonUninstall);
|
|
746
|
+
daemon.command('uninstall').description('Uninstall auto-start service').action(daemonUninstall);
|
|
612
747
|
}
|
package/dist/src/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { Command } from 'commander';
|
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import os from 'node:os';
|
|
6
6
|
import fs from 'node:fs';
|
|
7
|
-
import { loadConfig, saveConfig, configPath, getShennianDir, resolveShennianPath } from './config/index.js';
|
|
7
|
+
import { loadConfig, saveConfig, configPath, getShennianDir, resolveShennianPath, } from './config/index.js';
|
|
8
8
|
import { CliRelayClient } from './relay/client.js';
|
|
9
9
|
import { registerPairCommand, runSmartStart } from './commands/pair.js';
|
|
10
10
|
import { registerDaemonCommand } from './commands/daemon.js';
|
|
@@ -37,10 +37,7 @@ function formatDisconnectInfo(info) {
|
|
|
37
37
|
return parts.join(' ');
|
|
38
38
|
}
|
|
39
39
|
const program = new Command();
|
|
40
|
-
program
|
|
41
|
-
.name('shennian')
|
|
42
|
-
.description('Shennian — AI Agent Mobile Console')
|
|
43
|
-
.version(cliVersion);
|
|
40
|
+
program.name('shennian').description('Shennian — AI Agent Mobile Console').version(cliVersion);
|
|
44
41
|
program
|
|
45
42
|
.option('--api <url>', 'Server URL override')
|
|
46
43
|
.option('--name <name>', 'Machine name', os.hostname())
|
|
@@ -81,12 +78,18 @@ program
|
|
|
81
78
|
}
|
|
82
79
|
}
|
|
83
80
|
}
|
|
84
|
-
catch {
|
|
85
|
-
|
|
86
|
-
process.on('exit', () => { try {
|
|
87
|
-
fs.unlinkSync(pidFile);
|
|
81
|
+
catch {
|
|
82
|
+
/* noop */
|
|
88
83
|
}
|
|
89
|
-
|
|
84
|
+
fs.writeFileSync(pidFile, String(process.pid));
|
|
85
|
+
process.on('exit', () => {
|
|
86
|
+
try {
|
|
87
|
+
fs.unlinkSync(pidFile);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
/* noop */
|
|
91
|
+
}
|
|
92
|
+
});
|
|
90
93
|
// Crash detection: if we're recovering from a failed upgrade, rollback and exit
|
|
91
94
|
const didRollback = await handleStartupCrashCheck();
|
|
92
95
|
if (didRollback) {
|
|
@@ -109,7 +112,11 @@ program
|
|
|
109
112
|
initCliLogReporter(serverUrl, config.machineId);
|
|
110
113
|
}
|
|
111
114
|
console.log(`[${new Date().toISOString()}] Connecting to ${wsUrl}... (v${cliVersion}) agents: ${agentList.join(',')}`);
|
|
112
|
-
reportLog({
|
|
115
|
+
reportLog({
|
|
116
|
+
level: 'info',
|
|
117
|
+
wsEvent: 'daemon.start',
|
|
118
|
+
metadata: { version: cliVersion, agents: agentList },
|
|
119
|
+
});
|
|
113
120
|
let nativeFusion = null;
|
|
114
121
|
const client = new CliRelayClient({
|
|
115
122
|
serverUrl: wsUrl,
|
|
@@ -132,7 +139,8 @@ program
|
|
|
132
139
|
void resolveAgentInfos(detectedAgents, {
|
|
133
140
|
serverUrl: config.serverUrl ?? serverUrl,
|
|
134
141
|
authToken: config.machineToken ?? config.accessToken,
|
|
135
|
-
})
|
|
142
|
+
})
|
|
143
|
+
.then((agents) => {
|
|
136
144
|
if (JSON.stringify(agents) === JSON.stringify(cachedAgentInfos))
|
|
137
145
|
return;
|
|
138
146
|
client.sendEvent({
|
|
@@ -143,8 +151,10 @@ program
|
|
|
143
151
|
agents,
|
|
144
152
|
},
|
|
145
153
|
});
|
|
146
|
-
})
|
|
147
|
-
|
|
154
|
+
})
|
|
155
|
+
.catch(() => { });
|
|
156
|
+
import('./upgrade/engine.js')
|
|
157
|
+
.then(({ readUpgradeAttempt, clearUpgradeAttempt }) => {
|
|
148
158
|
const attempt = readUpgradeAttempt();
|
|
149
159
|
clearUpgradeAttempt();
|
|
150
160
|
if (attempt) {
|
|
@@ -152,10 +162,16 @@ program
|
|
|
152
162
|
client.sendEvent({
|
|
153
163
|
type: 'event',
|
|
154
164
|
event: 'upgrade',
|
|
155
|
-
payload: {
|
|
165
|
+
payload: {
|
|
166
|
+
state: 'success',
|
|
167
|
+
machineId: 'self',
|
|
168
|
+
from: attempt.from,
|
|
169
|
+
to: attempt.to,
|
|
170
|
+
},
|
|
156
171
|
});
|
|
157
172
|
}
|
|
158
|
-
})
|
|
173
|
+
})
|
|
174
|
+
.catch(() => { });
|
|
159
175
|
void scheduleAutoUpgrade(client, config.autoUpgrade ?? 'patch');
|
|
160
176
|
nativeFusion?.handleConnected();
|
|
161
177
|
},
|
|
@@ -176,13 +192,20 @@ program
|
|
|
176
192
|
},
|
|
177
193
|
onReq: (req) => {
|
|
178
194
|
console.log(`[${new Date().toISOString()}] [req] ${req.method}`);
|
|
179
|
-
reportLog({
|
|
195
|
+
reportLog({
|
|
196
|
+
level: 'info',
|
|
197
|
+
type: 'ws',
|
|
198
|
+
wsEvent: req.method,
|
|
199
|
+
wsDirection: 'in',
|
|
200
|
+
traceId: req.traceId,
|
|
201
|
+
});
|
|
180
202
|
void sessionManager.handleReq(req);
|
|
181
203
|
},
|
|
182
204
|
});
|
|
183
|
-
nativeFusion =
|
|
184
|
-
|
|
185
|
-
|
|
205
|
+
nativeFusion =
|
|
206
|
+
process.env.SHENNIAN_NATIVE_FUSION_DISABLED === '1'
|
|
207
|
+
? null
|
|
208
|
+
: new NativeSessionFusionService(client);
|
|
186
209
|
const sessionManager = new SessionManager(client, nativeFusion);
|
|
187
210
|
fs.mkdirSync(SHENNIAN_DIR, { recursive: true });
|
|
188
211
|
fs.writeFileSync(PID_FILE, String(process.pid));
|
|
@@ -197,16 +220,16 @@ program
|
|
|
197
220
|
try {
|
|
198
221
|
fs.unlinkSync(PID_FILE);
|
|
199
222
|
}
|
|
200
|
-
catch {
|
|
223
|
+
catch {
|
|
224
|
+
/* noop */
|
|
225
|
+
}
|
|
201
226
|
process.exit(0);
|
|
202
227
|
};
|
|
203
228
|
process.on('SIGINT', shutdown);
|
|
204
229
|
process.on('SIGTERM', shutdown);
|
|
205
230
|
});
|
|
206
231
|
// config: view and modify settings
|
|
207
|
-
const configCmd = program
|
|
208
|
-
.command('config')
|
|
209
|
-
.description('View or modify configuration');
|
|
232
|
+
const configCmd = program.command('config').description('View or modify configuration');
|
|
210
233
|
configCmd
|
|
211
234
|
.command('show', { isDefault: true })
|
|
212
235
|
.description('Show current configuration')
|
|
@@ -238,7 +261,7 @@ configCmd
|
|
|
238
261
|
config.serverUrl = regionToUrl(region);
|
|
239
262
|
saveConfig(config);
|
|
240
263
|
console.log(chalk.green(`✓ Server set to ${SERVERS[region].label}`));
|
|
241
|
-
console.log(chalk.yellow(' Restart the daemon for this to take effect: shennian
|
|
264
|
+
console.log(chalk.yellow(' Restart the daemon for this to take effect: shennian restart'));
|
|
242
265
|
return;
|
|
243
266
|
}
|
|
244
267
|
console.error(chalk.red(`✗ Unknown config key: ${key}. Supported: server`));
|
|
@@ -4,7 +4,7 @@ import { createHash } from 'node:crypto';
|
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import os from 'node:os';
|
|
6
6
|
import path from 'node:path';
|
|
7
|
-
import { buildUserMessagePayload } from '@shennian/wire';
|
|
7
|
+
import { buildUserMessagePayload, isToolPayload } from '@shennian/wire';
|
|
8
8
|
function normalizeText(text) {
|
|
9
9
|
return stripGitDirectiveArtifacts(text.replace(/\r\n/g, '\n').trim());
|
|
10
10
|
}
|
|
@@ -256,7 +256,7 @@ function parseCodexUserMessage(payload) {
|
|
|
256
256
|
}
|
|
257
257
|
return { payload: text, titleText: text };
|
|
258
258
|
}
|
|
259
|
-
function pushCodexEvent(events, filePath, lineOffset, kind, sourceSessionKey, ts, payload, title, modelId, workDir, role = 'agent') {
|
|
259
|
+
function pushCodexEvent(events, filePath, lineOffset, kind, sourceSessionKey, ts, payload, title, modelId, workDir, role = 'agent', terminal = false) {
|
|
260
260
|
if (!payload)
|
|
261
261
|
return;
|
|
262
262
|
events.push({
|
|
@@ -271,6 +271,7 @@ function pushCodexEvent(events, filePath, lineOffset, kind, sourceSessionKey, ts
|
|
|
271
271
|
title,
|
|
272
272
|
modelId,
|
|
273
273
|
workDir,
|
|
274
|
+
terminal,
|
|
274
275
|
});
|
|
275
276
|
}
|
|
276
277
|
function pushCodexToolEvent(events, filePath, lineOffset, kind, sourceSessionKey, ts, toolName, title, modelId, workDir, args, result) {
|
|
@@ -327,6 +328,17 @@ function parseCodexEventMessage(events, filePath, lineOffset, payload, sourceSes
|
|
|
327
328
|
pushCodexEvent(events, filePath, lineOffset, eventType, sourceSessionKey, ts, text, title, modelId, workDir);
|
|
328
329
|
return;
|
|
329
330
|
}
|
|
331
|
+
if (eventType === 'turn_aborted' || eventType === 'error') {
|
|
332
|
+
const text = typeof payload.message === 'string'
|
|
333
|
+
? normalizeText(payload.message)
|
|
334
|
+
: typeof payload.error === 'string'
|
|
335
|
+
? normalizeText(payload.error)
|
|
336
|
+
: '';
|
|
337
|
+
if (!text)
|
|
338
|
+
return;
|
|
339
|
+
pushCodexEvent(events, filePath, lineOffset, eventType, sourceSessionKey, ts, text, title, modelId, workDir, 'agent', true);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
330
342
|
if (eventType === 'exec_command_end') {
|
|
331
343
|
const command = Array.isArray(payload.command) ? payload.command : payload.parsed_cmd ?? payload.command;
|
|
332
344
|
const exitCode = payload.exit_code ?? payload.exitCode;
|
|
@@ -565,6 +577,7 @@ export function parseCodexRolloutChunk(filePath, startOffset) {
|
|
|
565
577
|
const lines = chunk.split('\n').filter(Boolean);
|
|
566
578
|
const events = [];
|
|
567
579
|
let { sourceSessionKey, workDir, modelId, title } = readCodexSessionMeta(filePath);
|
|
580
|
+
let pendingTerminal = false;
|
|
568
581
|
for (let index = 0, offset = startOffset; index < lines.length; index++) {
|
|
569
582
|
const line = lines[index];
|
|
570
583
|
const parsed = safeParse(line);
|
|
@@ -595,12 +608,32 @@ export function parseCodexRolloutChunk(filePath, startOffset) {
|
|
|
595
608
|
}
|
|
596
609
|
if (type === 'event_msg') {
|
|
597
610
|
const eventType = typeof payload.type === 'string' ? payload.type : '';
|
|
611
|
+
if (eventType === 'turn_completed') {
|
|
612
|
+
for (let i = events.length - 1; i >= 0; i -= 1) {
|
|
613
|
+
const event = events[i];
|
|
614
|
+
if (event?.agentType !== 'codex')
|
|
615
|
+
continue;
|
|
616
|
+
if (event.role !== 'agent')
|
|
617
|
+
continue;
|
|
618
|
+
if (isToolPayload(event.payload))
|
|
619
|
+
continue;
|
|
620
|
+
event.terminal = true;
|
|
621
|
+
pendingTerminal = false;
|
|
622
|
+
break;
|
|
623
|
+
}
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
598
626
|
if (eventType === 'user_message') {
|
|
599
627
|
const parsedUser = parseCodexUserMessage(payload);
|
|
600
628
|
if (parsedUser?.titleText && !title)
|
|
601
629
|
title = parsedUser.titleText.slice(0, 80);
|
|
602
630
|
}
|
|
631
|
+
const beforeCount = events.length;
|
|
603
632
|
parseCodexEventMessage(events, filePath, lineOffset, payload, sourceSessionKey, ts, title, modelId, workDir);
|
|
633
|
+
if (pendingTerminal && events.length > beforeCount) {
|
|
634
|
+
events[events.length - 1].terminal = true;
|
|
635
|
+
pendingTerminal = false;
|
|
636
|
+
}
|
|
604
637
|
}
|
|
605
638
|
}
|
|
606
639
|
return { nextOffset, events };
|