@yeaft/webchat-agent 0.1.1104 → 0.1.1107

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/service/linux.js CHANGED
@@ -1,31 +1,34 @@
1
- /**
1
+ /*
2
2
  * Service — Linux (systemd) platform implementation
3
3
  */
4
4
  import { execSync } from 'child_process';
5
5
  import { existsSync, writeFileSync, unlinkSync, mkdirSync } from 'fs';
6
6
  import { join, dirname } from 'path';
7
7
  import { homedir } from 'os';
8
- import { SERVICE_NAME, getLogDir, getNodePath, getCliPath } from './config.js';
8
+ import { getServiceName, getLogDir, getNodePath, getCliPath, DEFAULT_INSTANCE_ID } from './config.js';
9
9
 
10
10
  /** Pure path getter — no side effects (no directory creation). */
11
- export function getSystemdServicePath() {
12
- return join(homedir(), '.config', 'systemd', 'user', `${SERVICE_NAME}.service`);
11
+ export function getSystemdServicePath(instanceId = DEFAULT_INSTANCE_ID) {
12
+ return join(homedir(), '.config', 'systemd', 'user', `${getServiceName(instanceId)}.service`);
13
13
  }
14
14
 
15
15
  function generateSystemdUnit(config) {
16
16
  const nodePath = getNodePath();
17
17
  const cliPath = getCliPath();
18
+ const logDir = getLogDir(config.instanceId);
18
19
  const envLines = [];
20
+ if (config.instanceId) envLines.push(`Environment=YEAFT_AGENT_INSTANCE=${config.instanceId}`);
19
21
  if (config.serverUrl) envLines.push(`Environment=SERVER_URL=${config.serverUrl}`);
20
22
  if (config.agentName) envLines.push(`Environment=AGENT_NAME=${config.agentName}`);
21
23
  if (config.agentSecret) envLines.push(`Environment=AGENT_SECRET=${config.agentSecret}`);
22
24
  if (config.workDir) envLines.push(`Environment=WORK_DIR=${config.workDir}`);
25
+ if (config.yeaftDir) envLines.push(`Environment=YEAFT_DIR=${config.yeaftDir}`);
23
26
 
24
27
  // Include node's bin dir in PATH for claude CLI access
25
28
  const nodeBinDir = dirname(nodePath);
26
29
 
27
30
  return `[Unit]
28
- Description=Yeaft WebChat Agent
31
+ Description=Yeaft WebChat Agent (${config.instanceId || DEFAULT_INSTANCE_ID})
29
32
  After=network-online.target
30
33
  Wants=network-online.target
31
34
 
@@ -39,8 +42,8 @@ KillMode=process
39
42
  ${envLines.join('\n')}
40
43
  Environment=PATH=${nodeBinDir}:${homedir()}/.local/bin:${homedir()}/.npm-global/bin:/usr/local/bin:/usr/bin:/bin
41
44
 
42
- StandardOutput=append:${getLogDir()}/out.log
43
- StandardError=append:${getLogDir()}/error.log
45
+ StandardOutput=append:${logDir}/out.log
46
+ StandardError=append:${logDir}/error.log
44
47
 
45
48
  [Install]
46
49
  WantedBy=default.target
@@ -48,61 +51,68 @@ WantedBy=default.target
48
51
  }
49
52
 
50
53
  export function linuxInstall(config) {
51
- const servicePath = getSystemdServicePath();
54
+ const serviceName = getServiceName(config.instanceId);
55
+ const servicePath = getSystemdServicePath(config.instanceId);
52
56
  // Ensure systemd user directory exists before writing
53
57
  mkdirSync(join(homedir(), '.config', 'systemd', 'user'), { recursive: true });
58
+ mkdirSync(getLogDir(config.instanceId), { recursive: true });
54
59
  writeFileSync(servicePath, generateSystemdUnit(config));
55
60
  execSync('systemctl --user daemon-reload');
56
- execSync(`systemctl --user enable ${SERVICE_NAME}`);
57
- execSync(`systemctl --user start ${SERVICE_NAME}`);
58
- console.log(`Service installed and started.`);
61
+ execSync(`systemctl --user enable ${serviceName}`);
62
+ execSync(`systemctl --user start ${serviceName}`);
63
+ console.log(`Service installed and started: ${serviceName}`);
59
64
  console.log(`\nManage with:`);
60
- console.log(` yeaft-agent status`);
61
- console.log(` yeaft-agent logs`);
62
- console.log(` yeaft-agent restart`);
63
- console.log(` yeaft-agent uninstall`);
65
+ console.log(` yeaft-agent status --instance ${config.instanceId}`);
66
+ console.log(` yeaft-agent logs --instance ${config.instanceId}`);
67
+ console.log(` yeaft-agent restart --instance ${config.instanceId}`);
68
+ console.log(` yeaft-agent uninstall --instance ${config.instanceId}`);
64
69
  console.log(`\nTo run when not logged in:`);
65
70
  console.log(` sudo loginctl enable-linger $(whoami)`);
66
71
  }
67
72
 
68
- export function linuxUninstall() {
69
- try { execSync(`systemctl --user stop ${SERVICE_NAME} 2>/dev/null`); } catch {}
70
- try { execSync(`systemctl --user disable ${SERVICE_NAME} 2>/dev/null`); } catch {}
71
- const servicePath = getSystemdServicePath();
73
+ export function linuxUninstall(instanceId = DEFAULT_INSTANCE_ID) {
74
+ const serviceName = getServiceName(instanceId);
75
+ try { execSync(`systemctl --user stop ${serviceName} 2>/dev/null`); } catch {}
76
+ try { execSync(`systemctl --user disable ${serviceName} 2>/dev/null`); } catch {}
77
+ const servicePath = getSystemdServicePath(instanceId);
72
78
  if (existsSync(servicePath)) unlinkSync(servicePath);
73
79
  try { execSync('systemctl --user daemon-reload'); } catch {}
74
- console.log('Service uninstalled.');
80
+ console.log(`Service uninstalled: ${serviceName}`);
75
81
  }
76
82
 
77
- export function linuxStart() {
78
- execSync(`systemctl --user start ${SERVICE_NAME}`, { stdio: 'inherit' });
79
- console.log('Service started.');
83
+ export function linuxStart(instanceId = DEFAULT_INSTANCE_ID) {
84
+ const serviceName = getServiceName(instanceId);
85
+ execSync(`systemctl --user start ${serviceName}`, { stdio: 'inherit' });
86
+ console.log(`Service started: ${serviceName}`);
80
87
  }
81
88
 
82
- export function linuxStop() {
83
- execSync(`systemctl --user stop ${SERVICE_NAME}`, { stdio: 'inherit' });
84
- console.log('Service stopped.');
89
+ export function linuxStop(instanceId = DEFAULT_INSTANCE_ID) {
90
+ const serviceName = getServiceName(instanceId);
91
+ execSync(`systemctl --user stop ${serviceName}`, { stdio: 'inherit' });
92
+ console.log(`Service stopped: ${serviceName}`);
85
93
  }
86
94
 
87
- export function linuxRestart() {
88
- execSync(`systemctl --user restart ${SERVICE_NAME}`, { stdio: 'inherit' });
89
- console.log('Service restarted.');
95
+ export function linuxRestart(instanceId = DEFAULT_INSTANCE_ID) {
96
+ const serviceName = getServiceName(instanceId);
97
+ execSync(`systemctl --user restart ${serviceName}`, { stdio: 'inherit' });
98
+ console.log(`Service restarted: ${serviceName}`);
90
99
  }
91
100
 
92
101
  /**
93
102
  * Query systemd for the current service status.
94
103
  * Returns { running: boolean, pid: string|null }.
95
104
  */
96
- export function getLinuxServiceStatus() {
105
+ export function getLinuxServiceStatus(instanceId = DEFAULT_INSTANCE_ID) {
106
+ const serviceName = getServiceName(instanceId);
97
107
  try {
98
- const output = execSync(`systemctl --user is-active ${SERVICE_NAME}`, {
108
+ const output = execSync(`systemctl --user is-active ${serviceName}`, {
99
109
  encoding: 'utf-8',
100
110
  stdio: ['pipe', 'pipe', 'pipe'],
101
111
  }).trim();
102
112
  if (output === 'active') {
103
113
  let pid = null;
104
114
  try {
105
- pid = execSync(`systemctl --user show ${SERVICE_NAME} --property=MainPID --value`, {
115
+ pid = execSync(`systemctl --user show ${serviceName} --property=MainPID --value`, {
106
116
  encoding: 'utf-8',
107
117
  stdio: ['pipe', 'pipe', 'pipe'],
108
118
  }).trim();
@@ -116,20 +126,21 @@ export function getLinuxServiceStatus() {
116
126
  }
117
127
  }
118
128
 
119
- export function linuxStatus() {
129
+ export function linuxStatus(instanceId = DEFAULT_INSTANCE_ID) {
120
130
  try {
121
- execSync(`systemctl --user status ${SERVICE_NAME}`, { stdio: 'inherit' });
131
+ execSync(`systemctl --user status ${getServiceName(instanceId)}`, { stdio: 'inherit' });
122
132
  } catch {
123
133
  // systemctl status returns non-zero when service is stopped
124
134
  }
125
135
  }
126
136
 
127
- export function linuxLogs() {
137
+ export function linuxLogs(instanceId = DEFAULT_INSTANCE_ID) {
138
+ const serviceName = getServiceName(instanceId);
128
139
  try {
129
- execSync(`journalctl --user -u ${SERVICE_NAME} -f --no-pager -n 100`, { stdio: 'inherit' });
140
+ execSync(`journalctl --user -u ${serviceName} -f --no-pager -n 100`, { stdio: 'inherit' });
130
141
  } catch {
131
142
  // Fallback to log files
132
- const logFile = join(getLogDir(), 'out.log');
143
+ const logFile = join(getLogDir(instanceId), 'out.log');
133
144
  if (existsSync(logFile)) {
134
145
  execSync(`tail -f -n 100 ${logFile}`, { stdio: 'inherit' });
135
146
  } else {
package/service/macos.js CHANGED
@@ -1,34 +1,37 @@
1
- /**
1
+ /*
2
2
  * Service — macOS (launchd) platform implementation
3
3
  */
4
4
  import { execSync } from 'child_process';
5
5
  import { existsSync, writeFileSync, unlinkSync, mkdirSync } from 'fs';
6
6
  import { join } from 'path';
7
7
  import { homedir } from 'os';
8
- import { getLogDir, getNodePath, getCliPath } from './config.js';
8
+ import { getLaunchdLabel, getLogDir, getNodePath, getCliPath, DEFAULT_INSTANCE_ID } from './config.js';
9
9
 
10
10
  /** Pure path getter — no side effects (no directory creation). */
11
- export function getLaunchdPlistPath() {
12
- return join(homedir(), 'Library', 'LaunchAgents', 'com.yeaft.agent.plist');
11
+ export function getLaunchdPlistPath(instanceId = DEFAULT_INSTANCE_ID) {
12
+ return join(homedir(), 'Library', 'LaunchAgents', `${getLaunchdLabel(instanceId)}.plist`);
13
13
  }
14
14
 
15
15
  function generateLaunchdPlist(config) {
16
16
  const nodePath = getNodePath();
17
17
  const cliPath = getCliPath();
18
- const logDir = getLogDir();
18
+ const logDir = getLogDir(config.instanceId);
19
+ const label = getLaunchdLabel(config.instanceId);
19
20
 
20
21
  const envDict = [];
22
+ if (config.instanceId) envDict.push(` <key>YEAFT_AGENT_INSTANCE</key>\n <string>${config.instanceId}</string>`);
21
23
  if (config.serverUrl) envDict.push(` <key>SERVER_URL</key>\n <string>${config.serverUrl}</string>`);
22
24
  if (config.agentName) envDict.push(` <key>AGENT_NAME</key>\n <string>${config.agentName}</string>`);
23
25
  if (config.agentSecret) envDict.push(` <key>AGENT_SECRET</key>\n <string>${config.agentSecret}</string>`);
24
26
  if (config.workDir) envDict.push(` <key>WORK_DIR</key>\n <string>${config.workDir}</string>`);
27
+ if (config.yeaftDir) envDict.push(` <key>YEAFT_DIR</key>\n <string>${config.yeaftDir}</string>`);
25
28
 
26
29
  return `<?xml version="1.0" encoding="UTF-8"?>
27
30
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
28
31
  <plist version="1.0">
29
32
  <dict>
30
33
  <key>Label</key>
31
- <string>com.yeaft.agent</string>
34
+ <string>${label}</string>
32
35
  <key>ProgramArguments</key>
33
36
  <array>
34
37
  <string>${nodePath}</string>
@@ -59,62 +62,64 @@ ${envDict.join('\n')}
59
62
  }
60
63
 
61
64
  export function macInstall(config) {
62
- const plistPath = getLaunchdPlistPath();
65
+ const plistPath = getLaunchdPlistPath(config.instanceId);
63
66
  // Ensure LaunchAgents directory exists before writing
64
67
  mkdirSync(join(homedir(), 'Library', 'LaunchAgents'), { recursive: true });
68
+ mkdirSync(getLogDir(config.instanceId), { recursive: true });
65
69
  // Unload first if exists
66
70
  if (existsSync(plistPath)) {
67
71
  try { execSync(`launchctl unload ${plistPath} 2>/dev/null`); } catch {}
68
72
  }
69
73
  writeFileSync(plistPath, generateLaunchdPlist(config));
70
74
  execSync(`launchctl load ${plistPath}`);
71
- console.log('Service installed and started.');
75
+ console.log(`Service installed and started: ${getLaunchdLabel(config.instanceId)}`);
72
76
  console.log(`\nManage with:`);
73
- console.log(` yeaft-agent status`);
74
- console.log(` yeaft-agent logs`);
75
- console.log(` yeaft-agent restart`);
76
- console.log(` yeaft-agent uninstall`);
77
+ console.log(` yeaft-agent status --instance ${config.instanceId}`);
78
+ console.log(` yeaft-agent logs --instance ${config.instanceId}`);
79
+ console.log(` yeaft-agent restart --instance ${config.instanceId}`);
80
+ console.log(` yeaft-agent uninstall --instance ${config.instanceId}`);
77
81
  }
78
82
 
79
- export function macUninstall() {
80
- const plistPath = getLaunchdPlistPath();
83
+ export function macUninstall(instanceId = DEFAULT_INSTANCE_ID) {
84
+ const plistPath = getLaunchdPlistPath(instanceId);
81
85
  if (existsSync(plistPath)) {
82
86
  try { execSync(`launchctl unload ${plistPath}`); } catch {}
83
87
  unlinkSync(plistPath);
84
88
  }
85
- console.log('Service uninstalled.');
89
+ console.log(`Service uninstalled: ${getLaunchdLabel(instanceId)}`);
86
90
  }
87
91
 
88
- export function macStart() {
89
- const plistPath = getLaunchdPlistPath();
92
+ export function macStart(instanceId = DEFAULT_INSTANCE_ID) {
93
+ const plistPath = getLaunchdPlistPath(instanceId);
90
94
  if (!existsSync(plistPath)) {
91
95
  console.error('Service not installed. Run "yeaft-agent install" first.');
92
96
  process.exit(1);
93
97
  }
94
98
  execSync(`launchctl load ${plistPath}`);
95
- console.log('Service started.');
99
+ console.log(`Service started: ${getLaunchdLabel(instanceId)}`);
96
100
  }
97
101
 
98
- export function macStop() {
99
- const plistPath = getLaunchdPlistPath();
102
+ export function macStop(instanceId = DEFAULT_INSTANCE_ID) {
103
+ const plistPath = getLaunchdPlistPath(instanceId);
100
104
  if (existsSync(plistPath)) {
101
105
  execSync(`launchctl unload ${plistPath}`);
102
106
  }
103
- console.log('Service stopped.');
107
+ console.log(`Service stopped: ${getLaunchdLabel(instanceId)}`);
104
108
  }
105
109
 
106
- export function macRestart() {
107
- macStop();
108
- macStart();
110
+ export function macRestart(instanceId = DEFAULT_INSTANCE_ID) {
111
+ macStop(instanceId);
112
+ macStart(instanceId);
109
113
  }
110
114
 
111
115
  /**
112
116
  * Query launchd for the current service status.
113
117
  * Returns { running: boolean, pid: string|null, exitCode: string|null }.
114
118
  */
115
- export function getMacServiceStatus() {
119
+ export function getMacServiceStatus(instanceId = DEFAULT_INSTANCE_ID) {
120
+ const label = getLaunchdLabel(instanceId);
116
121
  try {
117
- const output = execSync('launchctl list | grep com.yeaft.agent', {
122
+ const output = execSync(`launchctl list | grep ${label}`, {
118
123
  encoding: 'utf-8',
119
124
  stdio: ['pipe', 'pipe', 'pipe'],
120
125
  });
@@ -133,8 +138,8 @@ export function getMacServiceStatus() {
133
138
  }
134
139
  }
135
140
 
136
- export function macStatus() {
137
- const status = getMacServiceStatus();
141
+ export function macStatus(instanceId = DEFAULT_INSTANCE_ID) {
142
+ const status = getMacServiceStatus(instanceId);
138
143
  if (status.running) {
139
144
  console.log(`Service is running (PID: ${status.pid})`);
140
145
  } else if (status.exitCode !== null) {
@@ -144,8 +149,8 @@ export function macStatus() {
144
149
  }
145
150
  }
146
151
 
147
- export function macLogs() {
148
- const logFile = join(getLogDir(), 'out.log');
152
+ export function macLogs(instanceId = DEFAULT_INSTANCE_ID) {
153
+ const logFile = join(getLogDir(instanceId), 'out.log');
149
154
  if (existsSync(logFile)) {
150
155
  execSync(`tail -f -n 100 ${logFile}`, { stdio: 'inherit' });
151
156
  } else {
@@ -1,17 +1,16 @@
1
- /**
1
+ /*
2
2
  * Service — Windows (pm2) platform implementation
3
3
  */
4
4
  import { execSync, spawn } from 'child_process';
5
5
  import { existsSync, mkdirSync, writeFileSync, readFileSync, unlinkSync } from 'fs';
6
6
  import { join, dirname } from 'path';
7
- import { SERVICE_NAME, getConfigDir, getLogDir, getNodePath, getCliPath } from './config.js';
7
+ import { getConfigDir, getLogDir, getNodePath, getCliPath, getPm2AppName, DEFAULT_INSTANCE_ID } from './config.js';
8
8
 
9
9
  const WIN_TASK_NAME = 'YeaftAgent';
10
- const PM2_APP_NAME = 'yeaft-agent';
11
10
 
12
11
  // Legacy paths for cleanup
13
- function getWinWrapperPath() { return join(getConfigDir(), 'run.vbs'); }
14
- function getWinBatPath() { return join(getConfigDir(), 'run.bat'); }
12
+ function getWinWrapperPath(instanceId = DEFAULT_INSTANCE_ID) { return join(getConfigDir(instanceId), 'run.vbs'); }
13
+ function getWinBatPath(instanceId = DEFAULT_INSTANCE_ID) { return join(getConfigDir(instanceId), 'run.bat'); }
15
14
 
16
15
  function ensurePm2() {
17
16
  try {
@@ -22,25 +21,28 @@ function ensurePm2() {
22
21
  }
23
22
  }
24
23
 
25
- export function getEcosystemPath() {
26
- return join(getConfigDir(), 'ecosystem.config.cjs');
24
+ export function getEcosystemPath(instanceId = DEFAULT_INSTANCE_ID) {
25
+ return join(getConfigDir(instanceId), 'ecosystem.config.cjs');
27
26
  }
28
27
 
29
28
  function generateEcosystem(config) {
30
29
  const nodePath = getNodePath();
31
30
  const cliPath = getCliPath();
32
31
  const cliDir = dirname(cliPath);
33
- const logDir = getLogDir();
32
+ const logDir = getLogDir(config.instanceId);
33
+ const pm2AppName = getPm2AppName(config.instanceId);
34
34
 
35
35
  const env = {};
36
+ if (config.instanceId) env.YEAFT_AGENT_INSTANCE = config.instanceId;
36
37
  if (config.serverUrl) env.SERVER_URL = config.serverUrl;
37
38
  if (config.agentName) env.AGENT_NAME = config.agentName;
38
39
  if (config.agentSecret) env.AGENT_SECRET = config.agentSecret;
39
40
  if (config.workDir) env.WORK_DIR = config.workDir;
41
+ if (config.yeaftDir) env.YEAFT_DIR = config.yeaftDir;
40
42
 
41
43
  return `module.exports = {
42
44
  apps: [{
43
- name: '${PM2_APP_NAME}',
45
+ name: '${pm2AppName}',
44
46
  script: '${cliPath.replace(/\\/g, '\\\\')}',
45
47
  interpreter: '${nodePath.replace(/\\/g, '\\\\')}',
46
48
  cwd: '${cliDir.replace(/\\/g, '\\\\')}',
@@ -61,15 +63,16 @@ function generateEcosystem(config) {
61
63
 
62
64
  export function winInstall(config) {
63
65
  ensurePm2();
64
- const logDir = getLogDir();
66
+ const logDir = getLogDir(config.instanceId);
67
+ const pm2AppName = getPm2AppName(config.instanceId);
65
68
  mkdirSync(logDir, { recursive: true });
66
69
 
67
70
  // Generate ecosystem config
68
- const ecoPath = getEcosystemPath();
71
+ const ecoPath = getEcosystemPath(config.instanceId);
69
72
  writeFileSync(ecoPath, generateEcosystem(config));
70
73
 
71
74
  // Stop existing instance if any
72
- try { execSync(`pm2 delete ${PM2_APP_NAME}`, { stdio: 'pipe' }); } catch {}
75
+ try { execSync(`pm2 delete ${pm2AppName}`, { stdio: 'pipe' }); } catch {}
73
76
 
74
77
  // Start with pm2
75
78
  execSync(`pm2 start "${ecoPath}"`, { stdio: 'inherit' });
@@ -81,7 +84,7 @@ export function winInstall(config) {
81
84
  // pm2-startup doesn't work well on Windows, use Startup folder approach
82
85
  const trayScript = join(dirname(getCliPath()), 'scripts', 'agent-tray.ps1');
83
86
  const startupDir = join(process.env.APPDATA, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup');
84
- const startupBat = join(startupDir, `${PM2_APP_NAME}.bat`);
87
+ const startupBat = join(startupDir, `${pm2AppName}.bat`);
85
88
  // Resurrect pm2 processes + launch tray icon
86
89
  let batContent = `@echo off\r\npm2 resurrect\r\n`;
87
90
  if (existsSync(trayScript)) {
@@ -96,41 +99,43 @@ export function winInstall(config) {
96
99
  }).unref();
97
100
  }
98
101
 
99
- console.log(`\nService installed and started.`);
102
+ console.log(`\nService installed and started: ${pm2AppName}`);
100
103
  console.log(` Ecosystem: ${ecoPath}`);
101
104
  console.log(` Startup: ${startupBat}`);
102
105
  console.log(`\nManage with:`);
103
- console.log(` yeaft-agent status`);
104
- console.log(` yeaft-agent logs`);
105
- console.log(` yeaft-agent restart`);
106
- console.log(` yeaft-agent uninstall`);
106
+ console.log(` yeaft-agent status --instance ${config.instanceId}`);
107
+ console.log(` yeaft-agent logs --instance ${config.instanceId}`);
108
+ console.log(` yeaft-agent restart --instance ${config.instanceId}`);
109
+ console.log(` yeaft-agent uninstall --instance ${config.instanceId}`);
107
110
  }
108
111
 
109
- export function winUninstall() {
110
- try { execSync(`pm2 delete ${PM2_APP_NAME}`, { stdio: 'pipe' }); } catch {}
112
+ export function winUninstall(instanceId = DEFAULT_INSTANCE_ID) {
113
+ const pm2AppName = getPm2AppName(instanceId);
114
+ try { execSync(`pm2 delete ${pm2AppName}`, { stdio: 'pipe' }); } catch {}
111
115
  try { execSync('pm2 save', { stdio: 'pipe' }); } catch {}
112
116
  // Clean up ecosystem config
113
- const ecoPath = getEcosystemPath();
117
+ const ecoPath = getEcosystemPath(instanceId);
114
118
  if (existsSync(ecoPath)) unlinkSync(ecoPath);
115
119
  // Clean up Startup bat
116
- const startupBat = join(process.env.APPDATA, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup', `${PM2_APP_NAME}.bat`);
120
+ const startupBat = join(process.env.APPDATA, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup', `${pm2AppName}.bat`);
117
121
  if (existsSync(startupBat)) unlinkSync(startupBat);
118
122
  // Clean up legacy files
119
- const vbsPath = getWinWrapperPath();
120
- const batPath = getWinBatPath();
123
+ const vbsPath = getWinWrapperPath(instanceId);
124
+ const batPath = getWinBatPath(instanceId);
121
125
  if (existsSync(vbsPath)) unlinkSync(vbsPath);
122
126
  if (existsSync(batPath)) unlinkSync(batPath);
123
127
  const startupVbs = join(process.env.APPDATA, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup', `${WIN_TASK_NAME}.vbs`);
124
128
  if (existsSync(startupVbs)) unlinkSync(startupVbs);
125
- console.log('Service uninstalled.');
129
+ console.log(`Service uninstalled: ${pm2AppName}`);
126
130
  }
127
131
 
128
- export function winStart() {
132
+ export function winStart(instanceId = DEFAULT_INSTANCE_ID) {
133
+ const pm2AppName = getPm2AppName(instanceId);
129
134
  try {
130
- execSync(`pm2 start ${PM2_APP_NAME}`, { stdio: 'inherit' });
135
+ execSync(`pm2 start ${pm2AppName}`, { stdio: 'inherit' });
131
136
  } catch {
132
137
  // Try ecosystem file
133
- const ecoPath = getEcosystemPath();
138
+ const ecoPath = getEcosystemPath(instanceId);
134
139
  if (existsSync(ecoPath)) {
135
140
  execSync(`pm2 start "${ecoPath}"`, { stdio: 'inherit' });
136
141
  } else {
@@ -140,17 +145,17 @@ export function winStart() {
140
145
  }
141
146
  }
142
147
 
143
- export function winStop() {
148
+ export function winStop(instanceId = DEFAULT_INSTANCE_ID) {
144
149
  try {
145
- execSync(`pm2 stop ${PM2_APP_NAME}`, { stdio: 'inherit' });
150
+ execSync(`pm2 stop ${getPm2AppName(instanceId)}`, { stdio: 'inherit' });
146
151
  } catch {
147
152
  console.error('Service not running or not installed.');
148
153
  }
149
154
  }
150
155
 
151
- export function winRestart() {
156
+ export function winRestart(instanceId = DEFAULT_INSTANCE_ID) {
152
157
  try {
153
- execSync(`pm2 restart ${PM2_APP_NAME}`, { stdio: 'inherit' });
158
+ execSync(`pm2 restart ${getPm2AppName(instanceId)}`, { stdio: 'inherit' });
154
159
  } catch {
155
160
  console.error('Service not running. Use "yeaft-agent start" to start.');
156
161
  }
@@ -160,14 +165,15 @@ export function winRestart() {
160
165
  * Query pm2 for the current service status.
161
166
  * Returns { running: boolean, pid: string|null }.
162
167
  */
163
- export function getWinServiceStatus() {
168
+ export function getWinServiceStatus(instanceId = DEFAULT_INSTANCE_ID) {
169
+ const pm2AppName = getPm2AppName(instanceId);
164
170
  try {
165
171
  const output = execSync('pm2 jlist', {
166
172
  encoding: 'utf-8',
167
173
  stdio: ['pipe', 'pipe', 'pipe'],
168
174
  });
169
175
  const apps = JSON.parse(output);
170
- const app = Array.isArray(apps) && apps.find(a => a.name === 'yeaft-agent');
176
+ const app = Array.isArray(apps) && apps.find(a => a.name === pm2AppName);
171
177
  if (app) {
172
178
  const running = app.pm2_env && app.pm2_env.status === 'online';
173
179
  const pid = running ? app.pid : null;
@@ -179,22 +185,23 @@ export function getWinServiceStatus() {
179
185
  }
180
186
  }
181
187
 
182
- export function winStatus() {
188
+ export function winStatus(instanceId = DEFAULT_INSTANCE_ID) {
183
189
  try {
184
- execSync(`pm2 describe ${PM2_APP_NAME}`, { stdio: 'inherit' });
190
+ execSync(`pm2 describe ${getPm2AppName(instanceId)}`, { stdio: 'inherit' });
185
191
  } catch {
186
192
  console.log('Service is not installed.');
187
193
  }
188
194
  }
189
195
 
190
- export function winLogs() {
191
- const child = spawn('pm2', ['logs', PM2_APP_NAME, '--lines', '100'], {
196
+ export function winLogs(instanceId = DEFAULT_INSTANCE_ID) {
197
+ const pm2AppName = getPm2AppName(instanceId);
198
+ const child = spawn('pm2', ['logs', pm2AppName, '--lines', '100'], {
192
199
  stdio: 'inherit',
193
200
  shell: true
194
201
  });
195
202
  child.on('error', () => {
196
203
  // Fallback to reading log file directly
197
- const logFile = join(getLogDir(), 'out.log');
204
+ const logFile = join(getLogDir(instanceId), 'out.log');
198
205
  if (existsSync(logFile)) {
199
206
  console.log(readFileSync(logFile, 'utf-8'));
200
207
  } else {
package/service.js CHANGED
@@ -16,6 +16,16 @@ export {
16
16
  saveServiceConfig,
17
17
  loadServiceConfig,
18
18
  parseServiceArgs,
19
+ SERVICE_NAME,
20
+ DEFAULT_INSTANCE_ID,
21
+ normalizeInstanceId,
22
+ isDefaultInstance,
23
+ validateInstanceId,
24
+ getInstanceIdFromArgs,
25
+ getServiceName,
26
+ getPm2AppName,
27
+ getLaunchdLabel,
28
+ getDefaultYeaftDir,
19
29
  install,
20
30
  uninstall,
21
31
  start,
package/terminal.js CHANGED
@@ -3,6 +3,8 @@ import { existsSync, chmodSync, statSync } from 'fs';
3
3
  import { join, dirname } from 'path';
4
4
  import { createRequire } from 'module';
5
5
  import ctx from './context.js';
6
+ import { getRuntimePlatformInfo } from './yeaft/runtime-platform.js';
7
+ import { wrapInvocationInSystemdUserScope } from './yeaft/systemd-scope.js';
6
8
 
7
9
  // Package name of the PTY backend. We use the Homebridge prebuilt fork
8
10
  // because upstream node-pty ships no Linux prebuilds and falls back to
@@ -91,12 +93,22 @@ export async function handleTerminalCreate(msg) {
91
93
  ? `${process.env.SystemRoot || 'C:\\Windows'}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`
92
94
  : (process.env.COMSPEC || 'cmd.exe')))
93
95
  : (process.env.SHELL || 'bash');
94
- const ptyProcess = pty.spawn(shell, [], {
96
+ const terminalEnv = { ...process.env };
97
+ const terminalInvocation = wrapInvocationInSystemdUserScope(
98
+ { command: shell, args: [], family: platform() === 'win32' ? 'powershell' : 'posix' },
99
+ {
100
+ runtimePlatform: getRuntimePlatformInfo(),
101
+ env: terminalEnv,
102
+ scopeId: `terminal-${terminalId}`,
103
+ scopePrefix: 'yeaft-terminal',
104
+ },
105
+ );
106
+ const ptyProcess = pty.spawn(terminalInvocation.command, terminalInvocation.args || [], {
95
107
  name: 'xterm-256color',
96
108
  cols: cols || 80,
97
109
  rows: rows || 24,
98
110
  cwd: workDir,
99
- env: process.env
111
+ env: terminalEnv
100
112
  });
101
113
 
102
114
  // 输出缓冲 - 每 16ms 批量发送
package/yeaft/cli.js CHANGED
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * Features:
9
9
  * --dry-run "prompt" — Assemble system prompt + messages, don't call LLM
10
- * --trace stats|recent|search <keyword>|tools|compact — Query/maintain debug.db
10
+ * --trace stats|recent|search <keyword>|tools|compact — Query/maintain debug trace files
11
11
  * -i / --interactive — REPL mode with / commands
12
12
  * <prompt> — One-shot query (Phase 1: engine.query)
13
13
  * --skip-mcp — Skip MCP server connections (faster startup)
@@ -117,12 +117,12 @@ function parseArgs(argv) {
117
117
  // ─── Trace query handler ───────────────────────────────────────
118
118
 
119
119
  function handleTraceQuery(args, config) {
120
- const dbPath = join(config.dir, 'debug.db');
120
+ const traceDir = config.dir;
121
121
  let trace;
122
122
  try {
123
- trace = new DebugTrace(dbPath);
123
+ trace = new DebugTrace(traceDir);
124
124
  } catch (e) {
125
- throw new Error(`Cannot open debug database at ${dbPath}: ${e.message}`);
125
+ throw new Error(`Cannot open debug trace store at ${traceDir}: ${e.message}`);
126
126
  }
127
127
 
128
128
  try {
@@ -133,7 +133,7 @@ function handleTraceQuery(args, config) {
133
133
  console.log(` Turns: ${s.turnCount}`);
134
134
  console.log(` Tools: ${s.toolCount}`);
135
135
  console.log(` Events: ${s.eventCount}`);
136
- console.log(` DB Size: ${(s.dbSizeBytes / 1024).toFixed(1)} KB`);
136
+ console.log(` Disk: ${(s.dbSizeBytes / 1024).toFixed(1)} KB`);
137
137
  break;
138
138
  }
139
139
  case 'recent': {
@@ -175,8 +175,8 @@ function handleTraceQuery(args, config) {
175
175
  }
176
176
  case 'compact': {
177
177
  const s = trace.stats();
178
- console.log(`Compacting debug database (${(s.dbSizeBytes / 1048576).toFixed(1)} MB, ${s.turnCount} turns)...`);
179
- console.log('This rebuilds the file and may take a while on a large DB. Do not interrupt.');
178
+ console.log(`Compacting debug trace files (${(s.dbSizeBytes / 1048576).toFixed(1)} MB, ${s.turnCount} turns)...`);
179
+ console.log('This prunes old request folders and may take a moment. Do not interrupt.');
180
180
  const { before, after } = trace.compact();
181
181
  const saved = Math.max(0, before - after);
182
182
  console.log(`Done. ${(before / 1048576).toFixed(1)} MB → ${(after / 1048576).toFixed(1)} MB (reclaimed ${(saved / 1048576).toFixed(1)} MB).`);