@yeaft/webchat-agent 1.0.357 → 1.0.359

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/cli.js CHANGED
@@ -64,13 +64,7 @@ if (command === 'doctor') {
64
64
  } else if (command === 'llm') {
65
65
  await handleLlmCommand(subArgs);
66
66
  } else if (command === 'local') {
67
- try {
68
- const { runLocal } = await import('./local-run.js');
69
- await runLocal(subArgs);
70
- } catch (error) {
71
- console.error(`Local run failed: ${error.message}`);
72
- process.exit(1);
73
- }
67
+ await handleLocalCommand(subArgs);
74
68
  } else if (command === 'upgrade') {
75
69
  await upgrade(subArgs);
76
70
  } else if (command === '--version' || command === '-v') {
@@ -84,6 +78,37 @@ if (command === 'doctor') {
84
78
  parseAndStart(args);
85
79
  }
86
80
 
81
+ /** Dispatch the local foreground and managed-service command family. */
82
+ export async function handleLocalCommand(subArgs, options = {}) {
83
+ const printHelpFn = options.printHelp || printHelp;
84
+ const warn = options.warn || console.warn;
85
+ const loadLocalService = options.loadLocalService || (() => import('./local-service.js'));
86
+ const loadLocalRun = options.loadLocalRun || (() => import('./local-run.js'));
87
+ try {
88
+ const localCommand = subArgs[0];
89
+ if (localCommand === '--help' || localCommand === '-h') {
90
+ printHelpFn();
91
+ return;
92
+ }
93
+ warnDeprecatedInstanceArg(subArgs, warn);
94
+ if (SERVICE_COMMANDS.includes(localCommand)) {
95
+ const { handleLocalServiceCommand } = await loadLocalService();
96
+ await handleLocalServiceCommand(localCommand, subArgs.slice(1));
97
+ return;
98
+ }
99
+ const { runLocal } = await loadLocalRun();
100
+ await runLocal(subArgs);
101
+ } catch (error) {
102
+ const message = `Local run failed: ${error.message}`;
103
+ if (options.onError) {
104
+ options.onError(message, error);
105
+ return;
106
+ }
107
+ console.error(message);
108
+ process.exit(1);
109
+ }
110
+ }
111
+
87
112
  function printHelp() {
88
113
  console.log(`
89
114
  ${pkg.name} v${pkg.version}
@@ -91,6 +116,11 @@ function printHelp() {
91
116
  Usage:
92
117
  yeaft-agent [options] Run agent in foreground
93
118
  yeaft-agent local [options] Run local Web UI, server, and agent
119
+ yeaft-agent local --background Run local mode in the background
120
+ yeaft-agent local install [options] Install local mode as a managed service
121
+ yeaft-agent local uninstall [options] Remove the local managed service
122
+ yeaft-agent local start|stop|restart|status|logs [options]
123
+ Control the local managed service
94
124
  yeaft-agent install [options] Install as system service
95
125
  yeaft-agent uninstall [options] Remove system service
96
126
  yeaft-agent start [options] Start installed service
@@ -108,6 +138,7 @@ function printHelp() {
108
138
  --server <url> WebSocket server URL (default: ws://localhost:3456)
109
139
  --name <name> Agent name and instance id (default: computer name; invalid chars become -)
110
140
  --port <port> Local server port (local command only; default: 6868)
141
+ --background, -d Detach local mode after spawning it (local command only)
111
142
  --secret <secret> Agent secret for authentication
112
143
  --work-dir <dir> Default working directory (default: cwd)
113
144
  --yeaft-dir <dir> Yeaft data directory for this instance
@@ -124,6 +155,8 @@ function printHelp() {
124
155
  Examples:
125
156
  yeaft-agent local
126
157
  yeaft-agent local --name my-worker --port 7000
158
+ yeaft-agent local --name my-worker --background
159
+ yeaft-agent local install --name my-worker --port 7000
127
160
  yeaft-agent --server wss://your-server.com --name my-worker --secret xxx
128
161
  yeaft-agent install --server wss://your-server.com --name my-worker --secret xxx
129
162
  yeaft-agent install --server wss://your-server.com --name my-worker-2 --secret xxx
package/index.js CHANGED
@@ -10,7 +10,14 @@ import { exec } from 'child_process';
10
10
  import { promisify } from 'util';
11
11
  import { fileURLToPath } from 'url';
12
12
  import ctx from './context.js';
13
- import { getDefaultAgentName, getDefaultYeaftDir, resolveRuntimeIdentity, getConfigPath, loadServiceConfig } from './service.js';
13
+ import {
14
+ getDefaultAgentName,
15
+ getDefaultYeaftDir,
16
+ resolveRuntimeIdentity,
17
+ getConfigPath,
18
+ loadServiceConfig,
19
+ shouldLoadLegacyLocalConfig,
20
+ } from './service.js';
14
21
  import { loadNodePty } from './terminal.js';
15
22
  import { connect } from './connection.js';
16
23
  import { loadMcpServers } from './mcp.js';
@@ -37,7 +44,8 @@ const pkg = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf-8'));
37
44
  ctx.agentVersion = pkg.version;
38
45
  ctx.pkgName = pkg.name;
39
46
 
40
- // 配置文件路径(向后兼容:先查当前目录 .claude-agent.json
47
+ // Legacy direct launches may still read cwd/.claude-agent.json. Explicit
48
+ // service instances must stay scoped to their standard per-instance config.
41
49
  const LOCAL_CONFIG_FILE = join(process.cwd(), '.claude-agent.json');
42
50
  const IS_LOCAL_RUN = process.env.YEAFT_LOCAL_RUN === 'true';
43
51
  const DEFAULT_AGENT_NAME = getDefaultAgentName();
@@ -56,8 +64,10 @@ function loadConfig() {
56
64
  // must not inherit a remote agent's persisted configuration.
57
65
  if (IS_LOCAL_RUN) return defaults;
58
66
 
59
- // Priority 1: Local .claude-agent.json (backward compat)
60
- if (existsSync(LOCAL_CONFIG_FILE)) {
67
+ // Priority 1: Local .claude-agent.json (backward compat for unscoped launches only).
68
+ // A named service can share its cwd with an unrelated legacy launch, so the
69
+ // process-level instance identity must fence this unscoped file out.
70
+ if (shouldLoadLegacyLocalConfig(process.env) && existsSync(LOCAL_CONFIG_FILE)) {
61
71
  try {
62
72
  const saved = JSON.parse(readFileSync(LOCAL_CONFIG_FILE, 'utf-8'));
63
73
  const { agentId, ...rest } = saved;
@@ -77,7 +87,7 @@ function loadConfig() {
77
87
  }
78
88
 
79
89
  function saveConfig(config) {
80
- if (IS_LOCAL_RUN) return;
90
+ if (IS_LOCAL_RUN || !shouldLoadLegacyLocalConfig(process.env)) return;
81
91
  writeFileSync(LOCAL_CONFIG_FILE, JSON.stringify(config, null, 2));
82
92
  }
83
93
 
package/local-run.js CHANGED
@@ -5,18 +5,23 @@ import { createServer } from 'net';
5
5
  import { dirname, join } from 'path';
6
6
  import { fileURLToPath } from 'url';
7
7
  import { WebSocket } from 'ws';
8
- import { resolveDisplayName, validateInstanceId } from './service/config.js';
8
+ import { resolveServiceInstanceId, resolveYeaftDir } from './service/config.js';
9
9
 
10
10
  const DEFAULT_PORT = 6868;
11
11
  const LOCAL_HOST = '127.0.0.1';
12
12
 
13
13
  export function parseLocalArgs(args, env = process.env) {
14
- const options = { name: resolveDisplayName(args, env), port: DEFAULT_PORT };
14
+ const options = {
15
+ name: resolveServiceInstanceId(args, env),
16
+ port: DEFAULT_PORT,
17
+ background: false,
18
+ yeaftDir: null,
19
+ };
15
20
  for (let i = 0; i < args.length; i++) {
16
21
  const arg = args[i];
17
22
  const value = args[i + 1];
18
- if (arg === '--name') {
19
- if (!value || value.startsWith('-')) throw new Error('--name requires a value');
23
+ if (arg === '--name' || arg === '--instance' || arg === '--yeaft-dir') {
24
+ if (!value || value.startsWith('-')) throw new Error(`${arg} requires a value`);
20
25
  i++;
21
26
  } else if (arg === '--port') {
22
27
  if (!value || value.startsWith('-')) throw new Error('--port requires a value');
@@ -24,11 +29,13 @@ export function parseLocalArgs(args, env = process.env) {
24
29
  options.port = Number(value);
25
30
  if (options.port < 1 || options.port > 65535) throw new Error(`Invalid port: ${value}`);
26
31
  i++;
32
+ } else if (arg === '--background' || arg === '-d') {
33
+ options.background = true;
27
34
  } else {
28
35
  throw new Error(`Unknown local option: ${arg}`);
29
36
  }
30
37
  }
31
- validateInstanceId(options.name);
38
+ options.yeaftDir = resolveYeaftDir(args, env, options.name);
32
39
  return options;
33
40
  }
34
41
 
@@ -127,8 +134,12 @@ function readAgentList(wsUrl) {
127
134
 
128
135
  export async function runLocal(args, options = {}) {
129
136
  const config = parseLocalArgs(args);
137
+ if (config.background && options.backgroundHandled !== true) {
138
+ return launchLocalInBackground(args, options);
139
+ }
130
140
  const paths = options.paths || runtimePaths();
131
141
  const dataDir = options.dataDir || join(homedir(), '.yeaft', 'server');
142
+ const yeaftDir = options.yeaftDir || config.yeaftDir;
132
143
  const url = `http://${LOCAL_HOST}:${config.port}`;
133
144
  const children = new Set();
134
145
  const signalHandlers = new Map();
@@ -173,6 +184,7 @@ export async function runLocal(args, options = {}) {
173
184
  SERVER_DATA_DIR: dataDir,
174
185
  PERF_TRACE_DIR: join(dataDir, 'perf-traces'),
175
186
  SKIP_AUTH: 'true',
187
+ YEAFT_LOCAL_RUN: 'true',
176
188
  WEB_DIR: paths.webDir,
177
189
  },
178
190
  });
@@ -189,6 +201,7 @@ export async function runLocal(args, options = {}) {
189
201
  AGENT_NAME: config.name,
190
202
  YEAFT_AGENT_INSTANCE: config.name,
191
203
  AGENT_SECRET: '',
204
+ YEAFT_DIR: yeaftDir,
192
205
  YEAFT_LOCAL_RUN: 'true',
193
206
  YEAFT_SKIP_STARTUP_INSTALLS: 'true',
194
207
  },
@@ -204,7 +217,7 @@ export async function runLocal(args, options = {}) {
204
217
  server.once('exit', fail('Local server'));
205
218
  agent.once('exit', fail('Local agent'));
206
219
 
207
- await waitForAgent(`ws://${LOCAL_HOST}:${config.port}`, config.name, agent);
220
+ await (options.waitForAgent || waitForAgent)(`ws://${LOCAL_HOST}:${config.port}`, config.name, agent);
208
221
 
209
222
  console.log(`Yeaft local is available at ${url}`);
210
223
  return { url, server, agent, stop };
@@ -213,3 +226,29 @@ export async function runLocal(args, options = {}) {
213
226
  throw error;
214
227
  }
215
228
  }
229
+
230
+ function localDaemonArgs(args) {
231
+ return args.filter(arg => arg !== '--background' && arg !== '-d');
232
+ }
233
+
234
+ export async function launchLocalInBackground(args, options = {}) {
235
+ const config = parseLocalArgs(args);
236
+ const spawnProcess = options.spawn || spawn;
237
+ const cliPath = options.cliPath || join(dirname(fileURLToPath(import.meta.url)), 'cli.js');
238
+ const child = spawnProcess(process.execPath, [cliPath, 'local', ...localDaemonArgs(args)], {
239
+ detached: true,
240
+ stdio: 'ignore',
241
+ windowsHide: true,
242
+ env: {
243
+ ...process.env,
244
+ YEAFT_LOCAL_RUN_BACKGROUND: 'true',
245
+ },
246
+ });
247
+ child.unref();
248
+ const url = `http://${LOCAL_HOST}:${config.port}`;
249
+ const result = { url, pid: child.pid, background: true };
250
+ if (options.quiet !== true) {
251
+ console.log(`Yeaft local is starting in the background at ${url} (PID ${child.pid}).`);
252
+ }
253
+ return result;
254
+ }
@@ -674,7 +674,14 @@ export async function handleAgentOutput(agentId, agent, msg) {
674
674
  }
675
675
  }
676
676
  }
677
- if (catalogChanged) await broadcastSessionCatalog(agent.ownerId);
677
+ // Nested `session_list_updated` events carry the same authoritative
678
+ // snapshot as the top-level alias below. Re-project the server-owned
679
+ // catalog after reconciliation so the unified sidebar updates in both
680
+ // local no-auth and deployed runtimes. Roster changes are handled by
681
+ // their own metadata branch above.
682
+ if ((event?.type === 'session_list_updated' || catalogChanged) && agent.ownerId) {
683
+ await broadcastSessionCatalog(agent.ownerId);
684
+ }
678
685
  break;
679
686
  }
680
687
 
@@ -4,6 +4,7 @@ import { CONFIG } from './config.js';
4
4
  import { verifyAgent } from './auth.js';
5
5
  import { encodeKey } from './encryption.js';
6
6
  import { agents, pendingAgentConnections } from './context.js';
7
+ import { userDb } from './database.js';
7
8
  import {
8
9
  parseMessage, broadcastAgentList, clearAgentDirCache
9
10
  } from './ws-utils.js';
@@ -114,18 +115,37 @@ export function handleAgentConnection(ws, url) {
114
115
 
115
116
  const capabilities = Array.isArray(msg.capabilities) ? msg.capabilities : urlCapabilities;
116
117
  const agentVersion = msg.version || null;
118
+ // Local no-auth mode still has one durable browser owner. This makes
119
+ // the server-side Session catalog persistent without changing generic
120
+ // development-server behavior, which remains ownerless.
121
+ const localOwner = skipAgentAuth && process.env.YEAFT_LOCAL_RUN === 'true'
122
+ ? userDb.getOrCreate('dev-user')
123
+ : null;
124
+ const ownerId = localOwner?.id || authResult.userId;
125
+ const ownerUsername = localOwner?.username || authResult.username;
117
126
  // Authenticated Agents use an owner-scoped key. SKIP_AUTH preserves
118
127
  // its historical unscoped id while still receiving version metadata.
119
128
  resolvedAgentId = skipAgentAuth
120
129
  ? clientAgentId
121
- : buildAgentMapKey(authResult.userId, pending.instanceId || pending.agentId || pending.agentName);
130
+ : buildAgentMapKey(ownerId, pending.instanceId || pending.agentId || pending.agentName);
122
131
  if (!claimAgentConnection(resolvedAgentId, connectionGeneration)) {
123
132
  resolvedAgentId = null;
124
133
  pruneAgentConnectionGenerations();
125
134
  ws.close(1008, 'Superseded by a newer Agent connection');
126
135
  return;
127
136
  }
128
- completeAgentRegistration(ws, resolvedAgentId, pending.agentName, pending.workDir, authResult.sessionKey, capabilities, authResult.userId, authResult.username, agentVersion, pending.instanceId || pending.agentId || pending.agentName);
137
+ completeAgentRegistration(
138
+ ws,
139
+ resolvedAgentId,
140
+ pending.agentName,
141
+ pending.workDir,
142
+ authResult.sessionKey,
143
+ capabilities,
144
+ ownerId,
145
+ ownerUsername,
146
+ agentVersion,
147
+ pending.instanceId || pending.agentId || pending.agentName,
148
+ );
129
149
  pruneAgentConnectionGenerations();
130
150
  }
131
151
  } catch (e) {
@@ -1 +1 @@
1
- {"version":"1.0.357"}
1
+ {"version":"1.0.359"}
@@ -0,0 +1,219 @@
1
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'fs';
2
+ import { execSync } from 'child_process';
3
+ import { homedir, platform } from 'os';
4
+ import { join, dirname } from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ import {
7
+ getConfigDir,
8
+ getDefaultYeaftDir,
9
+ getNodePath,
10
+ resolveServiceInstanceId,
11
+ resolveYeaftDir,
12
+ validateInstanceId,
13
+ } from './service/config.js';
14
+
15
+ const LOCAL_SERVICE_PREFIX = 'yeaft-local';
16
+ const DEFAULT_PORT = 6868;
17
+ const SYSTEMD_CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/;
18
+
19
+ /** Parse local service identity, runtime port, and resolved persistent data root. */
20
+ export function parseLocalServiceArgs(args, env = process.env, options = {}) {
21
+ const parsed = {
22
+ name: resolveServiceInstanceId(args, env),
23
+ port: DEFAULT_PORT,
24
+ yeaftDir: null,
25
+ };
26
+ for (let index = 0; index < args.length; index += 1) {
27
+ const arg = args[index];
28
+ const value = args[index + 1];
29
+ if (arg === '--name' || arg === '--instance' || arg === '--yeaft-dir') {
30
+ if (!value || value.startsWith('-')) throw new Error(`${arg} requires a value`);
31
+ index += 1;
32
+ } else if (arg === '--port') {
33
+ if (!value || value.startsWith('-')) throw new Error('--port requires a value');
34
+ if (!/^\d+$/.test(value)) throw new Error(`Invalid port: ${value}`);
35
+ parsed.port = Number(value);
36
+ if (parsed.port < 1 || parsed.port > 65535) throw new Error(`Invalid port: ${value}`);
37
+ index += 1;
38
+ } else {
39
+ throw new Error(`Unknown local service option: ${arg}`);
40
+ }
41
+ }
42
+ parsed.name = validateInstanceId(parsed.name);
43
+ const existing = options.existing || null;
44
+ const hasExplicitYeaftDir = args.includes('--yeaft-dir') || Boolean(env.YEAFT_DIR);
45
+ parsed.yeaftDir = hasExplicitYeaftDir
46
+ ? resolveYeaftDir(args, env, parsed.name)
47
+ : existing?.yeaftDir || getDefaultYeaftDir(parsed.name);
48
+ assertSystemdValue(parsed.yeaftDir, 'Yeaft data directory');
49
+ return parsed;
50
+ }
51
+
52
+ function assertSystemdValue(value, label) {
53
+ if (SYSTEMD_CONTROL_CHARACTERS.test(String(value))) {
54
+ throw new Error(`${label} cannot contain control characters when installing a systemd service`);
55
+ }
56
+ }
57
+
58
+ export function getLocalServiceName(name) {
59
+ return `${LOCAL_SERVICE_PREFIX}@${validateInstanceId(name)}`;
60
+ }
61
+
62
+ export function getLocalServiceConfigPath(name) {
63
+ return join(getConfigDir(name), 'local.json');
64
+ }
65
+
66
+ export function getLocalSystemdServicePath(name) {
67
+ return join(homedir(), '.config', 'systemd', 'user', `${getLocalServiceName(name)}.service`);
68
+ }
69
+
70
+ export function getLocalSystemdUnitPath(name) {
71
+ return getLocalSystemdServicePath(name);
72
+ }
73
+
74
+ function shellQuote(value) {
75
+ return `'${String(value).replaceAll("'", "'\\''")}'`;
76
+ }
77
+
78
+ function systemdEscape(value) {
79
+ assertSystemdValue(value, 'systemd unit value');
80
+ return String(value)
81
+ .replaceAll('%', '%%')
82
+ .replaceAll('\\', '\\\\')
83
+ .replaceAll('"', '\\"');
84
+ }
85
+
86
+ /** Read the persisted local service settings, including its resolved data root. */
87
+ export function readLocalServiceConfig(name) {
88
+ const path = getLocalServiceConfigPath(name);
89
+ if (!existsSync(path)) return null;
90
+ try {
91
+ const parsed = JSON.parse(readFileSync(path, 'utf8'));
92
+ if (!parsed || typeof parsed !== 'object') return null;
93
+ const configName = validateInstanceId(parsed.name || name);
94
+ return {
95
+ name: configName,
96
+ port: Number(parsed.port) || DEFAULT_PORT,
97
+ yeaftDir: typeof parsed.yeaftDir === 'string' && parsed.yeaftDir
98
+ ? parsed.yeaftDir
99
+ : getDefaultYeaftDir(configName),
100
+ };
101
+ } catch {
102
+ return null;
103
+ }
104
+ }
105
+
106
+ /** Persist the local service settings selected at install time. */
107
+ export function writeLocalServiceConfig(config) {
108
+ const path = getLocalServiceConfigPath(config.name);
109
+ mkdirSync(dirname(path), { recursive: true });
110
+ writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`);
111
+ }
112
+
113
+ function requireLocalServiceConfig(name) {
114
+ const config = readLocalServiceConfig(name);
115
+ if (!config) {
116
+ throw new Error(`Local service not installed for ${name}. Run "yeaft-agent local install --name ${name}" first.`);
117
+ }
118
+ return config;
119
+ }
120
+
121
+ export function generateLocalSystemdUnit(config, options = {}) {
122
+ const nodePath = getNodePath();
123
+ const cliPath = options.cliPath || join(dirname(fileURLToPath(import.meta.url)), 'cli.js');
124
+ const localDataDir = options.dataDir || join(homedir(), '.yeaft', 'server');
125
+ const yeaftDir = config.yeaftDir || getDefaultYeaftDir(config.name);
126
+ const logDir = join(getConfigDir(config.name), 'logs');
127
+ const workingDirectory = options.workingDirectory || dirname(cliPath);
128
+ const command = [
129
+ shellQuote(nodePath),
130
+ shellQuote(cliPath),
131
+ 'local',
132
+ '--name',
133
+ shellQuote(config.name),
134
+ '--port',
135
+ String(config.port),
136
+ ].join(' ');
137
+ return `[Unit]
138
+ Description=Yeaft Local Web UI (${config.name})
139
+ After=network-online.target
140
+ Wants=network-online.target
141
+
142
+ [Service]
143
+ Type=simple
144
+ ExecStart=${command}
145
+ Restart=on-failure
146
+ RestartSec=5
147
+ WorkingDirectory=${workingDirectory}
148
+ Environment="YEAFT_LOCAL_RUN=true"
149
+ Environment="YEAFT_AGENT_INSTANCE=${systemdEscape(config.name)}"
150
+ Environment="YEAFT_DIR=${systemdEscape(yeaftDir)}"
151
+ Environment="SERVER_DATA_DIR=${systemdEscape(localDataDir)}"
152
+ StandardOutput=append:${logDir}/out.log
153
+ StandardError=append:${logDir}/error.log
154
+
155
+ [Install]
156
+ WantedBy=default.target
157
+ `;
158
+ }
159
+
160
+ function installLinux(config) {
161
+ const path = getLocalSystemdServicePath(config.name);
162
+ const logDir = join(getConfigDir(config.name), 'logs');
163
+ mkdirSync(dirname(path), { recursive: true });
164
+ mkdirSync(logDir, { recursive: true });
165
+ writeFileSync(path, generateLocalSystemdUnit(config));
166
+ execSync('systemctl --user daemon-reload');
167
+ execSync(`systemctl --user enable --now ${getLocalServiceName(config.name)}`);
168
+ console.log(`Local service installed and started: ${getLocalServiceName(config.name)}`);
169
+ console.log(`Open http://127.0.0.1:${config.port}`);
170
+ console.log(`For boot without an active login session: sudo loginctl enable-linger $(whoami)`);
171
+ }
172
+
173
+ function uninstallLinux(name) {
174
+ const serviceName = getLocalServiceName(name);
175
+ const path = getLocalSystemdServicePath(name);
176
+ try { execSync(`systemctl --user disable --now ${serviceName}`, { stdio: 'ignore' }); } catch {}
177
+ if (existsSync(path)) unlinkSync(path);
178
+ try { execSync('systemctl --user daemon-reload'); } catch {}
179
+ console.log(`Local service uninstalled: ${serviceName}`);
180
+ }
181
+
182
+ function controlLinux(command, name) {
183
+ const serviceName = getLocalServiceName(name);
184
+ if (command === 'status') {
185
+ try { execSync(`systemctl --user status ${serviceName} --no-pager`, { stdio: 'inherit' }); } catch {}
186
+ return;
187
+ }
188
+ if (command === 'logs') {
189
+ execSync(`journalctl --user -u ${serviceName} -f --no-pager -n 100`, { stdio: 'inherit' });
190
+ return;
191
+ }
192
+ execSync(`systemctl --user ${command} ${serviceName}`, { stdio: 'inherit' });
193
+ const verb = command === 'start' ? 'started'
194
+ : command === 'stop' ? 'stopped'
195
+ : command === 'restart' ? 'restarted'
196
+ : command;
197
+ console.log(`Local service ${verb}: ${serviceName}`);
198
+ }
199
+
200
+ export async function handleLocalServiceCommand(command, args = []) {
201
+ const identity = parseLocalServiceArgs(args);
202
+ if (platform() !== 'linux') {
203
+ throw new Error(`Local managed service is currently supported on Linux only (current platform: ${platform()}). Use \`yeaft-agent local --background\` on this platform.`);
204
+ }
205
+ if (command === 'install') {
206
+ const config = parseLocalServiceArgs(args, process.env, {
207
+ existing: readLocalServiceConfig(identity.name),
208
+ });
209
+ writeLocalServiceConfig(config);
210
+ installLinux(config);
211
+ return;
212
+ }
213
+ if (command === 'uninstall') {
214
+ uninstallLinux(identity.name);
215
+ return;
216
+ }
217
+ const config = requireLocalServiceConfig(identity.name);
218
+ controlLinux(command, config.name);
219
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "1.0.357",
3
+ "version": "1.0.359",
4
4
  "description": "Remote worker agent for Yeaft Web Code Agent — connects the native Yeaft engine, CLI providers, and workbench tools",
5
5
  "main": "index.js",
6
6
  "type": "module",
package/service/config.js CHANGED
@@ -98,6 +98,10 @@ export function resolveRuntimeIdentity(fileConfig = {}, env = process.env) {
98
98
  return { agentName, instanceId };
99
99
  }
100
100
 
101
+ export function shouldLoadLegacyLocalConfig(env = process.env) {
102
+ return !String(env.YEAFT_AGENT_INSTANCE || '').trim();
103
+ }
104
+
101
105
  export function resolveServiceInstanceId(args = [], env = process.env, options = {}) {
102
106
  const { deprecatedInstanceId, explicitName } = readIdentityArgs(args);
103
107
  if (explicitName) return explicitName;
@@ -108,6 +112,19 @@ export function resolveServiceInstanceId(args = [], env = process.env, options =
108
112
  return validateInstanceId(options.fallbackName || getDefaultAgentName());
109
113
  }
110
114
 
115
+ /** Resolve the CLI/env/default Yeaft data root for one Agent instance. */
116
+ export function resolveYeaftDir(args = [], env = process.env, instanceId = DEFAULT_INSTANCE_ID) {
117
+ let explicitYeaftDir = null;
118
+ for (let index = 0; index < args.length; index += 1) {
119
+ if (args[index] !== '--yeaft-dir') continue;
120
+ const value = args[index + 1];
121
+ if (!value || value.startsWith('-')) throw new Error('--yeaft-dir requires a value');
122
+ explicitYeaftDir = value;
123
+ index += 1;
124
+ }
125
+ return explicitYeaftDir || env.YEAFT_DIR || getDefaultYeaftDir(instanceId);
126
+ }
127
+
111
128
  /** Legacy alias for resolveServiceInstanceId(). */
112
129
  export function getInstanceIdFromArgs(args = [], env = process.env, options = {}) {
113
130
  return resolveServiceInstanceId(args, env, options);
package/service/index.js CHANGED
@@ -29,6 +29,7 @@ export {
29
29
  resolveDisplayName,
30
30
  resolveRuntimeIdentity,
31
31
  resolveServiceInstanceId,
32
+ shouldLoadLegacyLocalConfig,
32
33
  normalizeInstanceId,
33
34
  isDefaultInstance,
34
35
  validateInstanceId,
package/service.js CHANGED
@@ -22,6 +22,7 @@ export {
22
22
  resolveDisplayName,
23
23
  resolveRuntimeIdentity,
24
24
  resolveServiceInstanceId,
25
+ shouldLoadLegacyLocalConfig,
25
26
  normalizeInstanceId,
26
27
  isDefaultInstance,
27
28
  validateInstanceId,