@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/cli.js CHANGED
@@ -41,7 +41,7 @@ const subArgs = args.slice(1);
41
41
  const SERVICE_COMMANDS = ['install', 'uninstall', 'start', 'stop', 'restart', 'status', 'logs'];
42
42
 
43
43
  if (command === 'doctor') {
44
- handleDoctorCommand();
44
+ await handleDoctorCommand();
45
45
  } else if (command === 'llm') {
46
46
  await handleLlmCommand(subArgs);
47
47
  } else if (command === 'upgrade') {
@@ -51,7 +51,7 @@ if (command === 'doctor') {
51
51
  } else if (command === '--help' || command === '-h') {
52
52
  printHelp();
53
53
  } else if (SERVICE_COMMANDS.includes(command)) {
54
- handleServiceCommand(command, subArgs);
54
+ await handleServiceCommand(command, subArgs);
55
55
  } else {
56
56
  // Normal agent startup — parse flags and set env vars
57
57
  parseAndStart(args);
@@ -64,35 +64,40 @@ function printHelp() {
64
64
  Usage:
65
65
  yeaft-agent [options] Run agent in foreground
66
66
  yeaft-agent install [options] Install as system service
67
- yeaft-agent uninstall Remove system service
68
- yeaft-agent start Start installed service
69
- yeaft-agent stop Stop installed service
70
- yeaft-agent restart Restart installed service
71
- yeaft-agent status Show service status
72
- yeaft-agent logs View service logs (follow mode)
67
+ yeaft-agent uninstall [options] Remove system service
68
+ yeaft-agent start [options] Start installed service
69
+ yeaft-agent stop [options] Stop installed service
70
+ yeaft-agent restart [options] Restart installed service
71
+ yeaft-agent status [options] Show service status
72
+ yeaft-agent logs [options] View service logs (follow mode)
73
73
  yeaft-agent doctor Diagnose service configuration
74
74
  yeaft-agent llm <command> Configure local Yeaft LLM providers/models
75
75
  yeaft-agent upgrade Upgrade to latest version
76
76
  yeaft-agent --version Show version
77
77
 
78
78
  Options:
79
+ --instance <id> Local service instance id (default: default)
79
80
  --server <url> WebSocket server URL (default: ws://localhost:3456)
80
81
  --name <name> Agent display name (default: Worker-{platform}-{pid})
81
82
  --secret <secret> Agent secret for authentication
82
83
  --work-dir <dir> Default working directory (default: cwd)
84
+ --yeaft-dir <dir> Yeaft data directory for this instance
83
85
  --auto-upgrade Check for updates on startup
84
86
 
85
87
  Environment variables (alternative to flags):
88
+ YEAFT_AGENT_INSTANCE Local service instance id
86
89
  SERVER_URL WebSocket server URL
87
90
  AGENT_NAME Agent display name
88
91
  AGENT_SECRET Agent secret
89
92
  WORK_DIR Working directory
93
+ YEAFT_DIR Yeaft data directory
90
94
 
91
95
  Examples:
92
96
  yeaft-agent --server wss://your-server.com --name my-worker --secret xxx
93
97
  yeaft-agent install --server wss://your-server.com --name my-worker --secret xxx
94
- yeaft-agent status
95
- yeaft-agent logs
98
+ yeaft-agent install --instance second --server wss://your-server.com --name my-worker-2 --secret xxx
99
+ yeaft-agent status --instance second
100
+ yeaft-agent logs --instance second
96
101
  `);
97
102
  }
98
103
 
@@ -380,13 +385,13 @@ function parseLlmArgs(args) {
380
385
  async function handleServiceCommand(command, args) {
381
386
  const service = await import('./service.js');
382
387
  switch (command) {
383
- case 'install': service.install(args); break;
384
- case 'uninstall': service.uninstall(); break;
385
- case 'start': service.start(); break;
386
- case 'stop': service.stop(); break;
387
- case 'restart': service.restart(); break;
388
- case 'status': service.status(); break;
389
- case 'logs': service.logs(); break;
388
+ case 'install': await service.install(args); break;
389
+ case 'uninstall': await service.uninstall(args); break;
390
+ case 'start': await service.start(args); break;
391
+ case 'stop': await service.stop(args); break;
392
+ case 'restart': await service.restart(args); break;
393
+ case 'status': await service.status(args); break;
394
+ case 'logs': await service.logs(args); break;
390
395
  }
391
396
  }
392
397
 
@@ -402,6 +407,9 @@ function parseAndStart(args) {
402
407
  const next = args[i + 1];
403
408
 
404
409
  switch (arg) {
410
+ case '--instance':
411
+ if (next) { process.env.YEAFT_AGENT_INSTANCE = process.env.YEAFT_AGENT_INSTANCE || next; i++; }
412
+ break;
405
413
  case '--server':
406
414
  if (next) { process.env.SERVER_URL = process.env.SERVER_URL || next; i++; }
407
415
  break;
@@ -414,6 +422,9 @@ function parseAndStart(args) {
414
422
  case '--work-dir':
415
423
  if (next) { process.env.WORK_DIR = process.env.WORK_DIR || next; i++; }
416
424
  break;
425
+ case '--yeaft-dir':
426
+ if (next) { process.env.YEAFT_DIR = process.env.YEAFT_DIR || next; i++; }
427
+ break;
417
428
  case '--auto-upgrade':
418
429
  checkForUpdates();
419
430
  break;
@@ -5,12 +5,15 @@ import { startAgentHeartbeat, stopAgentHeartbeat, scheduleReconnect } from './he
5
5
  import { handleMessage } from './message-router.js';
6
6
 
7
7
  export function connect() {
8
- // Don't include secret in URL - it will be sent via WebSocket message after connection
9
- // 使用 agentName 作为唯一标识(不再使用随机 UUID)
8
+ // Don't include secret in URL - it will be sent via WebSocket message after connection.
9
+ // instanceId is the stable local service identity; agentName is display-only.
10
+ // Old configs without instanceId still use agentName for backward-compatible identity.
11
+ const instanceId = ctx.CONFIG.instanceId || ctx.CONFIG.agentName;
10
12
  const params = new URLSearchParams({
11
13
  type: 'agent',
12
- id: ctx.CONFIG.agentName, // 直接用名称作为 ID
14
+ id: instanceId,
13
15
  name: ctx.CONFIG.agentName,
16
+ instanceId,
14
17
  workDir: ctx.CONFIG.workDir,
15
18
  capabilities: ctx.agentCapabilities.join(',')
16
19
  });
@@ -38,7 +38,7 @@ import { loadMcpServers, updateMcpConfig } from '../mcp.js';
38
38
  import { getLlmConfig, updateLlmConfig, getYeaftSettings, updateYeaftSettings, getSearchSettings, updateSearchSettings, fetchTavilyUsage } from '../yeaft/config-api.js';
39
39
  import { discoverLlmModels } from '../llm-model-discovery.js';
40
40
  import { fetchModelsDev } from '../yeaft/llm/models-dev.js';
41
- import { handleYeaftSessionSend, handleYeaftSubAgentPrompt, handleYeaftModeSwitch, handleYeaftModelSwitch, resetYeaftSession, handleYeaftLoadHistory, handleYeaftLoadMoreHistory, handleYeaftAbortThread, handleYeaftAbortAll, handleYeaftAbortTurn, handleYeaftVpSubscribe, handleYeaftVpCreate, handleYeaftVpUpdate, handleYeaftVpDelete, handleYeaftVpRead, handleYeaftListSessions, handleYeaftCreateSession, handleYeaftRenameSession, handleYeaftUpdateSession, handleYeaftUpdateSessionConfig, handleYeaftArchiveSession, handleYeaftDeleteSession, handleYeaftSessionAddMember, handleYeaftSessionRemoveMember, handleYeaftSessionSetDefaultVp, handleYeaftScanWorkdirSessions, handleYeaftRestoreSession, handleYeaftDreamTrigger, handleYeaftFetchToolStats, handleYeaftFetchDebugHistory, handleYeaftMcpList, handleYeaftMcpAdd, handleYeaftMcpRemove, handleYeaftMcpReload, broadcastLanguageChange, broadcastYeaftSessionSnapshotEager } from '../yeaft/web-bridge.js';
41
+ import { handleYeaftSessionSend, handleYeaftSubAgentPrompt, handleYeaftTaskCancel, handleYeaftModeSwitch, handleYeaftModelSwitch, resetYeaftSession, handleYeaftLoadHistory, handleYeaftLoadMoreHistory, handleYeaftAbortThread, handleYeaftAbortAll, handleYeaftAbortTurn, handleYeaftVpSubscribe, handleYeaftVpCreate, handleYeaftVpUpdate, handleYeaftVpDelete, handleYeaftVpRead, handleYeaftListSessions, handleYeaftCreateSession, handleYeaftRenameSession, handleYeaftUpdateSession, handleYeaftUpdateSessionConfig, handleYeaftArchiveSession, handleYeaftDeleteSession, handleYeaftSessionAddMember, handleYeaftSessionRemoveMember, handleYeaftSessionSetDefaultVp, handleYeaftScanWorkdirSessions, handleYeaftRestoreSession, handleYeaftDreamTrigger, handleYeaftFetchToolStats, handleYeaftFetchDebugHistory, handleYeaftMcpList, handleYeaftMcpAdd, handleYeaftMcpRemove, handleYeaftMcpReload, broadcastLanguageChange, broadcastYeaftSessionSnapshotEager } from '../yeaft/web-bridge.js';
42
42
  import { startYeaftStatusRefresh, refreshYeaftStatus } from '../yeaft/status-cache.js';
43
43
 
44
44
  export async function handleMessage(msg) {
@@ -58,11 +58,13 @@ export async function handleMessage(msg) {
58
58
  console.log('[WS] Server accepts plaintext, disabling outbound encryption');
59
59
  }
60
60
 
61
- // 只保存基本配置(不再保存 agentId,因为现在用 agentName 作为 ID)
61
+ // 只保存基本配置。instanceId 是本地服务实例身份;agentName 只用于展示。
62
62
  ctx.saveConfig({
63
+ instanceId: ctx.CONFIG.instanceId,
63
64
  serverUrl: ctx.CONFIG.serverUrl,
64
65
  agentName: ctx.CONFIG.agentName,
65
66
  workDir: ctx.CONFIG.workDir,
67
+ yeaftDir: ctx.CONFIG.yeaftDir,
66
68
  reconnectInterval: ctx.CONFIG.reconnectInterval
67
69
  // 不保存 agentSecret 到配置文件(安全考虑)
68
70
  });
@@ -658,6 +660,9 @@ export async function handleMessage(msg) {
658
660
  case 'yeaft_sub_agent_prompt':
659
661
  handleYeaftSubAgentPrompt(msg);
660
662
  break;
663
+ case 'yeaft_task_cancel':
664
+ handleYeaftTaskCancel(msg);
665
+ break;
661
666
 
662
667
  // wave-6b: manual dream trigger from VP detail page
663
668
  case 'yeaft_dream_trigger':
@@ -671,10 +676,9 @@ export async function handleMessage(msg) {
671
676
  await handleYeaftFetchToolStats(msg);
672
677
  break;
673
678
 
674
- // fix-vp-multi-thread (bug 4): hydrate the Yeaft debug panel from
675
- // the persistent SQLite trace. Without this, the panel only shows
676
- // turns that happened after the panel was opened — every previous
677
- // turn is invisible.
679
+ // Hydrate the Yeaft debug panel from the persistent file-backed trace.
680
+ // Without this, the panel only shows turns that happened after it was
681
+ // opened — every previous turn is invisible.
678
682
  case 'yeaft_fetch_debug_history':
679
683
  case 'unify_fetch_debug_history':
680
684
  await handleYeaftFetchDebugHistory(msg);
package/index.js CHANGED
@@ -9,7 +9,7 @@ import { exec } from 'child_process';
9
9
  import { promisify } from 'util';
10
10
  import { fileURLToPath } from 'url';
11
11
  import ctx from './context.js';
12
- import { getConfigPath, loadServiceConfig } from './service.js';
12
+ import { DEFAULT_INSTANCE_ID, getDefaultYeaftDir, validateInstanceId, getConfigPath, loadServiceConfig } from './service.js';
13
13
  import { loadNodePty } from './terminal.js';
14
14
  import { connect } from './connection.js';
15
15
  import { loadMcpServers } from './mcp.js';
@@ -60,6 +60,7 @@ function saveConfig(config) {
60
60
  }
61
61
 
62
62
  const fileConfig = loadConfig();
63
+ const INSTANCE_ID = validateInstanceId(process.env.YEAFT_AGENT_INSTANCE || fileConfig.instanceId || DEFAULT_INSTANCE_ID);
63
64
 
64
65
  // task-fix (5-bugs): the Yeaft web-bridge reads `ctx.CONFIG.yeaftDir`
65
66
  // for every group / VP / memory operation. If unset, `path.join(undefined, …)`
@@ -67,7 +68,7 @@ const fileConfig = loadConfig();
67
68
  // and the UI surfaces "群组操作失败: …" with a raw node error. Resolve the
68
69
  // default (`~/.yeaft`) here and make sure the directory exists before the
69
70
  // WebSocket connection goes live, so downstream code can assume a real path.
70
- const YEAFT_DIR = process.env.YEAFT_DIR || fileConfig.yeaftDir || join(homedir(), '.yeaft');
71
+ const YEAFT_DIR = process.env.YEAFT_DIR || fileConfig.yeaftDir || getDefaultYeaftDir(INSTANCE_ID);
71
72
  try {
72
73
  if (!existsSync(YEAFT_DIR)) {
73
74
  mkdirSync(YEAFT_DIR, { recursive: true });
@@ -78,6 +79,7 @@ try {
78
79
  }
79
80
 
80
81
  const CONFIG = {
82
+ instanceId: INSTANCE_ID,
81
83
  serverUrl: process.env.SERVER_URL || fileConfig.serverUrl,
82
84
  agentName: process.env.AGENT_NAME || fileConfig.agentName,
83
85
  workDir: process.env.WORK_DIR || fileConfig.workDir || process.cwd(),
package/llm-config-cli.js CHANGED
@@ -8,6 +8,8 @@ import {
8
8
  modelIdsFromProviderModels,
9
9
  } from './llm-model-discovery.js';
10
10
 
11
+ export const DEFAULT_GITHUB_COPILOT_MODEL = 'gpt-5.5';
12
+
11
13
  const VALID_PROTOCOLS = new Set(['anthropic', 'openai-responses']);
12
14
  const VALID_CREDENTIAL_PROVIDERS = new Set(['github-copilot']);
13
15
 
@@ -230,6 +232,52 @@ export async function useGitHubCopilot(config, options = {}) {
230
232
  return { config: next, provider, discovery };
231
233
  }
232
234
 
235
+ export function hasLocalLlmConfig(config = {}) {
236
+ const providers = Array.isArray(config.providers) ? config.providers.filter(Boolean) : [];
237
+ return providers.length > 0 || Boolean(config.primaryModel) || Boolean(config.fastModel);
238
+ }
239
+
240
+ export function isDefaultSeedLlmConfig(config = {}) {
241
+ const providers = Array.isArray(config.providers) ? config.providers.filter(Boolean) : [];
242
+ if (providers.length !== 1) return false;
243
+ const provider = providers[0];
244
+ return provider?.name === 'my-proxy'
245
+ && provider?.baseUrl === 'http://localhost:6628/v1'
246
+ && provider?.apiKey === 'proxy'
247
+ && typeof config.primaryModel === 'string'
248
+ && config.primaryModel.startsWith('my-proxy/');
249
+ }
250
+
251
+ export async function tryAutoConfigureGitHubCopilot(configPath = getDefaultYeaftConfigPath(), options = {}) {
252
+ let current;
253
+ try {
254
+ current = readLocalLlmConfig(configPath);
255
+ } catch (error) {
256
+ return { configured: false, reason: 'invalid-config', error, config: null };
257
+ }
258
+
259
+ const allowConfigured = Boolean(options.allowConfigured) || isDefaultSeedLlmConfig(current);
260
+ if (!allowConfigured && hasLocalLlmConfig(current)) {
261
+ return { configured: false, reason: 'already-configured', config: current };
262
+ }
263
+
264
+ try {
265
+ const result = await useGitHubCopilot(current, {
266
+ ...options,
267
+ model: options.model || DEFAULT_GITHUB_COPILOT_MODEL,
268
+ });
269
+ writeLocalLlmConfig(result.config, configPath);
270
+ return { configured: true, reason: 'configured', ...result };
271
+ } catch (error) {
272
+ return {
273
+ configured: false,
274
+ reason: error?.code === 'COPILOT_CREDENTIAL_MISSING' ? 'credential-missing' : 'unavailable',
275
+ error,
276
+ config: current,
277
+ };
278
+ }
279
+ }
280
+
233
281
 
234
282
  export async function useOpenAICompatible(config, options = {}, env = process.env) {
235
283
  const name = options.name ? String(options.name).trim() : 'openai';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.1104",
3
+ "version": "0.1.1107",
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
@@ -1,4 +1,4 @@
1
- /**
1
+ /*
2
2
  * Service — shared configuration and utility functions
3
3
  */
4
4
  import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
@@ -8,6 +8,7 @@ import { fileURLToPath } from 'url';
8
8
 
9
9
  const __dirname = dirname(fileURLToPath(import.meta.url));
10
10
  export const SERVICE_NAME = 'yeaft-agent';
11
+ export const DEFAULT_INSTANCE_ID = 'default';
11
12
 
12
13
  /**
13
14
  * Load .env file from agent directory (or cwd) into process.env
@@ -40,36 +41,98 @@ function loadDotenv() {
40
41
  }
41
42
  }
42
43
 
43
- // Standard config/log directory per platform
44
- export function getConfigDir() {
44
+ export function normalizeInstanceId(instanceId) {
45
+ const raw = String(instanceId || '').trim();
46
+ return raw || DEFAULT_INSTANCE_ID;
47
+ }
48
+
49
+ export function isDefaultInstance(instanceId) {
50
+ return normalizeInstanceId(instanceId) === DEFAULT_INSTANCE_ID;
51
+ }
52
+
53
+ export function validateInstanceId(instanceId) {
54
+ const normalized = normalizeInstanceId(instanceId);
55
+ if (!/^[A-Za-z0-9_.-]+$/.test(normalized)) {
56
+ throw new Error('Instance id may only contain letters, numbers, dot, underscore, or dash');
57
+ }
58
+ return normalized;
59
+ }
60
+
61
+ export function getInstanceIdFromArgs(args = [], env = process.env) {
62
+ let instanceId = env.YEAFT_AGENT_INSTANCE || '';
63
+ for (let i = 0; i < args.length; i++) {
64
+ const arg = args[i];
65
+ const next = args[i + 1];
66
+ if (arg === '--instance' && next) {
67
+ instanceId = next;
68
+ i++;
69
+ }
70
+ }
71
+ return validateInstanceId(instanceId || DEFAULT_INSTANCE_ID);
72
+ }
73
+
74
+ export function getServiceName(instanceId = DEFAULT_INSTANCE_ID) {
75
+ const normalized = validateInstanceId(instanceId);
76
+ return isDefaultInstance(normalized) ? SERVICE_NAME : `${SERVICE_NAME}@${normalized}`;
77
+ }
78
+
79
+ export function getPm2AppName(instanceId = DEFAULT_INSTANCE_ID) {
80
+ const normalized = validateInstanceId(instanceId);
81
+ return isDefaultInstance(normalized) ? SERVICE_NAME : `${SERVICE_NAME}-${normalized}`;
82
+ }
83
+
84
+ export function getLaunchdLabel(instanceId = DEFAULT_INSTANCE_ID) {
85
+ const normalized = validateInstanceId(instanceId);
86
+ return isDefaultInstance(normalized) ? 'com.yeaft.agent' : `com.yeaft.agent.${normalized}`;
87
+ }
88
+
89
+ export function getDefaultYeaftDir(instanceId = DEFAULT_INSTANCE_ID) {
90
+ const normalized = validateInstanceId(instanceId);
91
+ return isDefaultInstance(normalized)
92
+ ? join(homedir(), '.yeaft')
93
+ : join(homedir(), '.yeaft', 'instances', normalized);
94
+ }
95
+
96
+ function getBaseConfigDir() {
45
97
  if (platform() === 'win32') {
46
98
  return join(process.env.APPDATA || join(homedir(), 'AppData', 'Roaming'), SERVICE_NAME);
47
99
  }
48
100
  return join(homedir(), '.config', SERVICE_NAME);
49
101
  }
50
102
 
51
- export function getLogDir() {
52
- return join(getConfigDir(), 'logs');
103
+ // Standard config/log directory per platform. The default instance keeps the
104
+ // historical paths for compatibility; named instances live under instances/<id>.
105
+ export function getConfigDir(instanceId = process.env.YEAFT_AGENT_INSTANCE || DEFAULT_INSTANCE_ID) {
106
+ const normalized = validateInstanceId(instanceId);
107
+ const base = getBaseConfigDir();
108
+ return isDefaultInstance(normalized) ? base : join(base, 'instances', normalized);
109
+ }
110
+
111
+ export function getLogDir(instanceId = process.env.YEAFT_AGENT_INSTANCE || DEFAULT_INSTANCE_ID) {
112
+ return join(getConfigDir(instanceId), 'logs');
53
113
  }
54
114
 
55
- export function getConfigPath() {
56
- return join(getConfigDir(), 'config.json');
115
+ export function getConfigPath(instanceId = process.env.YEAFT_AGENT_INSTANCE || DEFAULT_INSTANCE_ID) {
116
+ return join(getConfigDir(instanceId), 'config.json');
57
117
  }
58
118
 
59
119
  /** Save agent configuration to standard location */
60
120
  export function saveServiceConfig(config) {
61
- const dir = getConfigDir();
121
+ const instanceId = validateInstanceId(config.instanceId || DEFAULT_INSTANCE_ID);
122
+ const dir = getConfigDir(instanceId);
62
123
  mkdirSync(dir, { recursive: true });
63
- mkdirSync(getLogDir(), { recursive: true });
64
- writeFileSync(getConfigPath(), JSON.stringify(config, null, 2));
124
+ mkdirSync(getLogDir(instanceId), { recursive: true });
125
+ writeFileSync(getConfigPath(instanceId), JSON.stringify({ ...config, instanceId }, null, 2));
65
126
  }
66
127
 
67
128
  /** Load agent configuration from standard location */
68
- export function loadServiceConfig() {
69
- const configPath = getConfigPath();
129
+ export function loadServiceConfig(instanceId = process.env.YEAFT_AGENT_INSTANCE || DEFAULT_INSTANCE_ID) {
130
+ const normalized = validateInstanceId(instanceId);
131
+ const configPath = getConfigPath(normalized);
70
132
  if (!existsSync(configPath)) return null;
71
133
  try {
72
- return JSON.parse(readFileSync(configPath, 'utf-8'));
134
+ const loaded = JSON.parse(readFileSync(configPath, 'utf-8'));
135
+ return { ...loaded, instanceId: loaded.instanceId || normalized };
73
136
  } catch {
74
137
  return null;
75
138
  }
@@ -86,18 +149,21 @@ export function getCliPath() {
86
149
  }
87
150
 
88
151
  /**
89
- * Parse --server/--name/--secret/--work-dir from args, merge with existing config
152
+ * Parse service options from args, merging with the selected instance config.
90
153
  */
91
154
  export function parseServiceArgs(args) {
92
155
  // Load .env if available (for dev / source-based usage)
93
156
  loadDotenv();
94
157
 
95
- const existing = loadServiceConfig() || {};
158
+ const instanceId = getInstanceIdFromArgs(args);
159
+ const existing = loadServiceConfig(instanceId) || {};
96
160
  const config = {
161
+ instanceId,
97
162
  serverUrl: existing.serverUrl || '',
98
163
  agentName: existing.agentName || '',
99
164
  agentSecret: existing.agentSecret || '',
100
165
  workDir: existing.workDir || '',
166
+ yeaftDir: existing.yeaftDir || '',
101
167
  };
102
168
 
103
169
  // Environment variables override saved config
@@ -105,16 +171,19 @@ export function parseServiceArgs(args) {
105
171
  if (process.env.AGENT_NAME) config.agentName = process.env.AGENT_NAME;
106
172
  if (process.env.AGENT_SECRET) config.agentSecret = process.env.AGENT_SECRET;
107
173
  if (process.env.WORK_DIR) config.workDir = process.env.WORK_DIR;
174
+ if (process.env.YEAFT_DIR) config.yeaftDir = process.env.YEAFT_DIR;
108
175
 
109
176
  // CLI args override everything
110
177
  for (let i = 0; i < args.length; i++) {
111
178
  const arg = args[i];
112
179
  const next = args[i + 1];
113
180
  switch (arg) {
181
+ case '--instance': if (next) { i++; } break;
114
182
  case '--server': if (next) { config.serverUrl = next; i++; } break;
115
183
  case '--name': if (next) { config.agentName = next; i++; } break;
116
184
  case '--secret': if (next) { config.agentSecret = next; i++; } break;
117
185
  case '--work-dir': if (next) { config.workDir = next; i++; } break;
186
+ case '--yeaft-dir': if (next) { config.yeaftDir = next; i++; } break;
118
187
  }
119
188
  }
120
189
 
@@ -122,6 +191,12 @@ export function parseServiceArgs(args) {
122
191
  }
123
192
 
124
193
  export function validateConfig(config) {
194
+ try {
195
+ validateInstanceId(config.instanceId || DEFAULT_INSTANCE_ID);
196
+ } catch (err) {
197
+ console.error(`Error: ${err.message}`);
198
+ process.exit(1);
199
+ }
125
200
  if (!config.serverUrl) {
126
201
  console.error('Error: --server <url> is required');
127
202
  process.exit(1);
package/service/index.js CHANGED
@@ -1,15 +1,17 @@
1
- /**
1
+ /*
2
2
  * Service — platform dispatcher
3
3
  * Routes install/uninstall/start/stop/restart/status/logs to the correct platform module.
4
4
  */
5
5
  import { existsSync } from 'fs';
6
+ import { join } from 'path';
6
7
  import { platform } from 'os';
7
8
  import {
8
- SERVICE_NAME, getConfigDir, getLogDir, getConfigPath,
9
+ getConfigDir, getLogDir, getConfigPath,
9
10
  saveServiceConfig, loadServiceConfig,
10
- parseServiceArgs, validateConfig
11
+ parseServiceArgs, validateConfig, getInstanceIdFromArgs, getDefaultYeaftDir
11
12
  } from './config.js';
12
13
  import { initYeaftDir } from '../yeaft/init.js';
14
+ import { DEFAULT_GITHUB_COPILOT_MODEL, tryAutoConfigureGitHubCopilot } from '../llm-config-cli.js';
13
15
  import { getSystemdServicePath, linuxInstall, linuxUninstall, linuxStart, linuxStop, linuxRestart, linuxStatus, linuxLogs } from './linux.js';
14
16
  import { getLaunchdPlistPath, macInstall, macUninstall, macStart, macStop, macRestart, macStatus, macLogs } from './macos.js';
15
17
  import { winInstall, winUninstall, winStart, winStop, winRestart, winStatus, winLogs } from './windows.js';
@@ -20,17 +22,42 @@ export {
20
22
  saveServiceConfig, loadServiceConfig,
21
23
  parseServiceArgs
22
24
  };
25
+ export {
26
+ SERVICE_NAME,
27
+ DEFAULT_INSTANCE_ID,
28
+ normalizeInstanceId,
29
+ isDefaultInstance,
30
+ validateInstanceId,
31
+ getInstanceIdFromArgs,
32
+ getServiceName,
33
+ getPm2AppName,
34
+ getLaunchdLabel,
35
+ getDefaultYeaftDir,
36
+ } from './config.js';
23
37
 
24
38
  const os = platform();
25
39
 
26
- function ensureInstalled() {
40
+ export async function autoConfigureGitHubCopilotIfAvailable(yeaftDir, options = {}) {
41
+ const result = await tryAutoConfigureGitHubCopilot(join(yeaftDir, 'config.json'), options);
42
+ if (result.configured) {
43
+ console.log(`Configured GitHub Copilot provider automatically with ${DEFAULT_GITHUB_COPILOT_MODEL}.`);
44
+ if (result.discovery?.warning) console.log(`Warning: ${result.discovery.warning}`);
45
+ } else if (result.reason === 'already-configured') {
46
+ console.log('LLM config already exists; skipped automatic GitHub Copilot setup.');
47
+ } else if (result.reason === 'invalid-config') {
48
+ console.log('Existing LLM config is invalid; skipped automatic GitHub Copilot setup.');
49
+ }
50
+ return result;
51
+ }
52
+
53
+ function ensureInstalled(instanceId) {
27
54
  if (os === 'linux') {
28
- if (!existsSync(getSystemdServicePath())) {
55
+ if (!existsSync(getSystemdServicePath(instanceId))) {
29
56
  console.error('Service not installed. Run "yeaft-agent install" first.');
30
57
  process.exit(1);
31
58
  }
32
59
  } else if (os === 'darwin') {
33
- if (!existsSync(getLaunchdPlistPath())) {
60
+ if (!existsSync(getLaunchdPlistPath(instanceId))) {
34
61
  console.error('Service not installed. Run "yeaft-agent install" first.');
35
62
  process.exit(1);
36
63
  }
@@ -38,24 +65,29 @@ function ensureInstalled() {
38
65
  // Windows check is done inside individual functions
39
66
  }
40
67
 
41
- export function install(args) {
68
+ export async function install(args) {
42
69
  const config = parseServiceArgs(args);
43
70
  validateConfig(config);
44
71
  saveServiceConfig(config);
45
72
 
46
73
  // Initialize ~/.yeaft/ directory + default config.json
47
74
  // so `yeaft` CLI is ready to use immediately after install
48
- const { dir, created } = initYeaftDir();
75
+ const effectiveYeaftDir = config.yeaftDir || getDefaultYeaftDir(config.instanceId);
76
+ const { dir, created } = initYeaftDir(effectiveYeaftDir);
77
+ await autoConfigureGitHubCopilotIfAvailable(dir, {
78
+ allowConfigured: created.includes(join(dir, 'config.json')),
79
+ });
49
80
  if (created.length > 0) {
50
81
  console.log(`Initialized ${dir}`);
51
82
  console.log(` Edit ${dir}/config.json to configure LLM providers.`);
52
83
  console.log('');
53
84
  }
54
85
 
55
- console.log(`Installing ${SERVICE_NAME} service...`);
56
- console.log(` Server: ${config.serverUrl}`);
57
- console.log(` Name: ${config.agentName || '(auto)'}`);
58
- console.log(` WorkDir: ${config.workDir || '(home)'}`);
86
+ console.log(`Installing yeaft-agent service...`);
87
+ console.log(` Instance: ${config.instanceId}`);
88
+ console.log(` Server: ${config.serverUrl}`);
89
+ console.log(` Name: ${config.agentName || '(auto)'}`);
90
+ console.log(` WorkDir: ${config.workDir || '(home)'}`);
59
91
  console.log('');
60
92
 
61
93
  if (os === 'linux') linuxInstall(config);
@@ -68,45 +100,51 @@ export function install(args) {
68
100
  }
69
101
  }
70
102
 
71
- export function uninstall() {
72
- console.log(`Uninstalling ${SERVICE_NAME} service...`);
73
- if (os === 'linux') linuxUninstall();
74
- else if (os === 'darwin') macUninstall();
75
- else if (os === 'win32') winUninstall();
103
+ export function uninstall(args = []) {
104
+ const instanceId = getInstanceIdFromArgs(args);
105
+ console.log(`Uninstalling yeaft-agent service (${instanceId})...`);
106
+ if (os === 'linux') linuxUninstall(instanceId);
107
+ else if (os === 'darwin') macUninstall(instanceId);
108
+ else if (os === 'win32') winUninstall(instanceId);
76
109
  else { console.error(`Unsupported platform: ${os}`); process.exit(1); }
77
110
  }
78
111
 
79
- export function start() {
80
- ensureInstalled();
81
- if (os === 'linux') linuxStart();
82
- else if (os === 'darwin') macStart();
83
- else if (os === 'win32') winStart();
112
+ export function start(args = []) {
113
+ const instanceId = getInstanceIdFromArgs(args);
114
+ ensureInstalled(instanceId);
115
+ if (os === 'linux') linuxStart(instanceId);
116
+ else if (os === 'darwin') macStart(instanceId);
117
+ else if (os === 'win32') winStart(instanceId);
84
118
  }
85
119
 
86
- export function stop() {
87
- ensureInstalled();
88
- if (os === 'linux') linuxStop();
89
- else if (os === 'darwin') macStop();
90
- else if (os === 'win32') winStop();
120
+ export function stop(args = []) {
121
+ const instanceId = getInstanceIdFromArgs(args);
122
+ ensureInstalled(instanceId);
123
+ if (os === 'linux') linuxStop(instanceId);
124
+ else if (os === 'darwin') macStop(instanceId);
125
+ else if (os === 'win32') winStop(instanceId);
91
126
  }
92
127
 
93
- export function restart() {
94
- ensureInstalled();
95
- if (os === 'linux') linuxRestart();
96
- else if (os === 'darwin') macRestart();
97
- else if (os === 'win32') winRestart();
128
+ export function restart(args = []) {
129
+ const instanceId = getInstanceIdFromArgs(args);
130
+ ensureInstalled(instanceId);
131
+ if (os === 'linux') linuxRestart(instanceId);
132
+ else if (os === 'darwin') macRestart(instanceId);
133
+ else if (os === 'win32') winRestart(instanceId);
98
134
  }
99
135
 
100
- export function status() {
101
- if (os === 'linux') linuxStatus();
102
- else if (os === 'darwin') macStatus();
103
- else if (os === 'win32') winStatus();
136
+ export function status(args = []) {
137
+ const instanceId = getInstanceIdFromArgs(args);
138
+ if (os === 'linux') linuxStatus(instanceId);
139
+ else if (os === 'darwin') macStatus(instanceId);
140
+ else if (os === 'win32') winStatus(instanceId);
104
141
  }
105
142
 
106
- export function logs() {
107
- if (os === 'linux') linuxLogs();
108
- else if (os === 'darwin') macLogs();
109
- else if (os === 'win32') winLogs();
143
+ export function logs(args = []) {
144
+ const instanceId = getInstanceIdFromArgs(args);
145
+ if (os === 'linux') linuxLogs(instanceId);
146
+ else if (os === 'darwin') macLogs(instanceId);
147
+ else if (os === 'win32') winLogs(instanceId);
110
148
  }
111
149
 
112
150
  export { doctor };