@swarmai/local-agent 0.1.0
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 +174 -0
- package/index.js +11 -0
- package/package.json +42 -0
- package/src/agenticChatClaudeMd.js +92 -0
- package/src/agenticChatHandler.js +566 -0
- package/src/aiProviderScanner.js +115 -0
- package/src/auth.js +180 -0
- package/src/cli.js +334 -0
- package/src/commands.js +1853 -0
- package/src/config.js +98 -0
- package/src/connection.js +470 -0
- package/src/mcpManager.js +276 -0
- package/src/startup.js +297 -0
- package/src/toolScanner.js +221 -0
- package/src/workspace.js +201 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Manager for Local Agent
|
|
3
|
+
*
|
|
4
|
+
* Manages MCP (Model Context Protocol) server connections locally.
|
|
5
|
+
* Uses @modelcontextprotocol/sdk to spawn stdio-based MCP server processes,
|
|
6
|
+
* discover their tools, and execute tool calls.
|
|
7
|
+
*
|
|
8
|
+
* Mirrors patterns from server/services/mcp/MCPClientManager.cjs
|
|
9
|
+
* but adapted for client-side use without database access.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const os = require('os');
|
|
13
|
+
|
|
14
|
+
const TOOL_CALL_TIMEOUT = 60000; // 60s
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Pre-configured MCP server recipes
|
|
18
|
+
*/
|
|
19
|
+
const MCP_RECIPES = {
|
|
20
|
+
playwright: {
|
|
21
|
+
name: 'playwright',
|
|
22
|
+
description: 'Browser automation via Playwright',
|
|
23
|
+
command: 'npx',
|
|
24
|
+
args: ['-y', '@playwright/mcp@latest'],
|
|
25
|
+
},
|
|
26
|
+
filesystem: {
|
|
27
|
+
name: 'filesystem',
|
|
28
|
+
description: 'Local filesystem access',
|
|
29
|
+
command: 'npx',
|
|
30
|
+
args: ['-y', '@modelcontextprotocol/server-filesystem', os.homedir()],
|
|
31
|
+
},
|
|
32
|
+
sqlite: {
|
|
33
|
+
name: 'sqlite',
|
|
34
|
+
description: 'SQLite database access',
|
|
35
|
+
command: 'npx',
|
|
36
|
+
args: ['-y', '@modelcontextprotocol/server-sqlite'],
|
|
37
|
+
},
|
|
38
|
+
git: {
|
|
39
|
+
name: 'git',
|
|
40
|
+
description: 'Git repository operations',
|
|
41
|
+
command: 'npx',
|
|
42
|
+
args: ['-y', '@modelcontextprotocol/server-github'],
|
|
43
|
+
},
|
|
44
|
+
docker: {
|
|
45
|
+
name: 'docker',
|
|
46
|
+
description: 'Docker container management',
|
|
47
|
+
command: 'npx',
|
|
48
|
+
args: ['-y', '@modelcontextprotocol/server-docker'],
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
class MCPManager {
|
|
53
|
+
constructor() {
|
|
54
|
+
/** @type {Map<string, { client: Object, transport: Object, tools: Array, config: Object }>} */
|
|
55
|
+
this._connections = new Map();
|
|
56
|
+
this._log = console.log;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Set log function (called from connection.js)
|
|
61
|
+
*/
|
|
62
|
+
setLogger(logFn) {
|
|
63
|
+
this._log = logFn || console.log;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Start all configured MCP servers
|
|
68
|
+
* @param {Object} mcpConfig - { serverName: { command, args, env } }
|
|
69
|
+
*/
|
|
70
|
+
async startAll(mcpConfig) {
|
|
71
|
+
if (!mcpConfig || typeof mcpConfig !== 'object') return;
|
|
72
|
+
|
|
73
|
+
const entries = Object.entries(mcpConfig);
|
|
74
|
+
if (entries.length === 0) return;
|
|
75
|
+
|
|
76
|
+
for (const [name, config] of entries) {
|
|
77
|
+
try {
|
|
78
|
+
await this.connect(name, config);
|
|
79
|
+
} catch (e) {
|
|
80
|
+
this._log(`MCP: Failed to start "${name}": ${e.message}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Connect to a single MCP server
|
|
87
|
+
* @param {string} name - Server name
|
|
88
|
+
* @param {Object} config - { command, args, env }
|
|
89
|
+
*/
|
|
90
|
+
async connect(name, config) {
|
|
91
|
+
if (this._connections.has(name)) {
|
|
92
|
+
this._log(`MCP: "${name}" already connected`);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!config.command) {
|
|
97
|
+
throw new Error(`MCP server "${name}" has no command configured`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const { Client } = require('@modelcontextprotocol/sdk/client');
|
|
101
|
+
const { StdioClientTransport } = require('@modelcontextprotocol/sdk/client/stdio.js');
|
|
102
|
+
|
|
103
|
+
const args = Array.isArray(config.args) ? config.args : [];
|
|
104
|
+
const env = config.env && typeof config.env === 'object'
|
|
105
|
+
? { ...process.env, ...config.env }
|
|
106
|
+
: undefined;
|
|
107
|
+
|
|
108
|
+
// On Windows, npx needs shell resolution
|
|
109
|
+
const isWindows = os.platform() === 'win32';
|
|
110
|
+
const command = isWindows && config.command === 'npx' ? 'npx.cmd' : config.command;
|
|
111
|
+
|
|
112
|
+
this._log(`MCP: Starting "${name}" (${config.command} ${args.join(' ')})`);
|
|
113
|
+
|
|
114
|
+
const transport = new StdioClientTransport({ command, args, env });
|
|
115
|
+
|
|
116
|
+
const client = new Client(
|
|
117
|
+
{ name: 'swarmai-local-agent', version: '0.1.0' },
|
|
118
|
+
{ capabilities: {} }
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
await client.connect(transport);
|
|
123
|
+
} catch (err) {
|
|
124
|
+
throw new Error(`Failed to connect MCP server "${name}": ${err.message}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const entry = { client, transport, tools: [], config };
|
|
128
|
+
this._connections.set(name, entry);
|
|
129
|
+
|
|
130
|
+
// Discover tools
|
|
131
|
+
try {
|
|
132
|
+
const result = await client.listTools();
|
|
133
|
+
entry.tools = (result.tools || []).map(t => ({
|
|
134
|
+
server: name,
|
|
135
|
+
name: t.name,
|
|
136
|
+
description: t.description || '',
|
|
137
|
+
inputSchema: t.inputSchema || { type: 'object', properties: {} },
|
|
138
|
+
}));
|
|
139
|
+
this._log(`MCP: "${name}" connected — ${entry.tools.length} tools discovered`);
|
|
140
|
+
} catch (err) {
|
|
141
|
+
this._log(`MCP: "${name}" connected but tool discovery failed: ${err.message}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Handle unexpected disconnect
|
|
145
|
+
if (transport.onclose) {
|
|
146
|
+
const originalOnClose = transport.onclose;
|
|
147
|
+
transport.onclose = () => {
|
|
148
|
+
originalOnClose();
|
|
149
|
+
if (this._connections.has(name)) {
|
|
150
|
+
this._log(`MCP: "${name}" disconnected unexpectedly`);
|
|
151
|
+
this._connections.delete(name);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Disconnect a single MCP server
|
|
159
|
+
* @param {string} name
|
|
160
|
+
*/
|
|
161
|
+
async disconnect(name) {
|
|
162
|
+
const entry = this._connections.get(name);
|
|
163
|
+
if (!entry) return;
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
await entry.client.close();
|
|
167
|
+
} catch (err) {
|
|
168
|
+
this._log(`MCP: Error closing "${name}": ${err.message}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
this._connections.delete(name);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Disconnect all MCP servers
|
|
176
|
+
*/
|
|
177
|
+
async disconnectAll() {
|
|
178
|
+
const names = [...this._connections.keys()];
|
|
179
|
+
const promises = names.map(name => this.disconnect(name));
|
|
180
|
+
await Promise.allSettled(promises);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Call a tool on a specific MCP server
|
|
185
|
+
* @param {string} serverName
|
|
186
|
+
* @param {string} toolName
|
|
187
|
+
* @param {Object} args
|
|
188
|
+
* @returns {Promise<Object>}
|
|
189
|
+
*/
|
|
190
|
+
async callTool(serverName, toolName, args = {}) {
|
|
191
|
+
const entry = this._connections.get(serverName);
|
|
192
|
+
if (!entry) {
|
|
193
|
+
throw new Error(`MCP server "${serverName}" not connected. Connected servers: ${this.getConnectedServers().join(', ') || 'none'}`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Validate tool exists
|
|
197
|
+
const tool = entry.tools.find(t => t.name === toolName);
|
|
198
|
+
if (!tool) {
|
|
199
|
+
const available = entry.tools.map(t => t.name).join(', ');
|
|
200
|
+
throw new Error(`Tool "${toolName}" not found on MCP server "${serverName}". Available: ${available || 'none'}`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Execute with timeout
|
|
204
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
205
|
+
setTimeout(() => reject(new Error(`MCP tool call timed out after ${TOOL_CALL_TIMEOUT / 1000}s`)), TOOL_CALL_TIMEOUT);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const callPromise = entry.client.callTool({
|
|
209
|
+
name: toolName,
|
|
210
|
+
arguments: args,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const result = await Promise.race([callPromise, timeoutPromise]);
|
|
214
|
+
return result.content || result;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Get all discovered tools from all connected servers (flat array)
|
|
219
|
+
* @returns {Array<{ server: string, name: string, description: string, inputSchema: Object }>}
|
|
220
|
+
*/
|
|
221
|
+
getAllTools() {
|
|
222
|
+
const tools = [];
|
|
223
|
+
for (const entry of this._connections.values()) {
|
|
224
|
+
tools.push(...entry.tools);
|
|
225
|
+
}
|
|
226
|
+
return tools;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Get list of connected server names
|
|
231
|
+
* @returns {string[]}
|
|
232
|
+
*/
|
|
233
|
+
getConnectedServers() {
|
|
234
|
+
return [...this._connections.keys()];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Get a pre-configured recipe
|
|
239
|
+
* @param {string} name
|
|
240
|
+
* @returns {Object|null}
|
|
241
|
+
*/
|
|
242
|
+
static getRecipe(name) {
|
|
243
|
+
return MCP_RECIPES[name] || null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Get all recipe names
|
|
248
|
+
* @returns {string[]}
|
|
249
|
+
*/
|
|
250
|
+
static getRecipeNames() {
|
|
251
|
+
return Object.keys(MCP_RECIPES);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Get recipe descriptions for display
|
|
256
|
+
* @returns {Array<{name: string, description: string}>}
|
|
257
|
+
*/
|
|
258
|
+
static getRecipeList() {
|
|
259
|
+
return Object.values(MCP_RECIPES).map(r => ({
|
|
260
|
+
name: r.name,
|
|
261
|
+
description: r.description,
|
|
262
|
+
}));
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Singleton
|
|
267
|
+
let _instance = null;
|
|
268
|
+
|
|
269
|
+
function getMcpManager() {
|
|
270
|
+
if (!_instance) {
|
|
271
|
+
_instance = new MCPManager();
|
|
272
|
+
}
|
|
273
|
+
return _instance;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
module.exports = { MCPManager, getMcpManager, MCP_RECIPES };
|
package/src/startup.js
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-OS auto-start at boot management
|
|
3
|
+
* Supports Windows (Registry), macOS (LaunchAgent), Linux (systemd user service)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const { execSync } = require('child_process');
|
|
10
|
+
|
|
11
|
+
const APP_NAME = 'swarmai-agent';
|
|
12
|
+
const APP_LABEL = 'com.swarmai.agent';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get the full path to the swarmai-agent executable
|
|
16
|
+
*/
|
|
17
|
+
function getExecutablePath() {
|
|
18
|
+
const platform = os.platform();
|
|
19
|
+
try {
|
|
20
|
+
if (platform === 'win32') {
|
|
21
|
+
return execSync('where swarmai-agent', { encoding: 'utf-8' }).trim().split('\n')[0].trim();
|
|
22
|
+
} else {
|
|
23
|
+
return execSync('which swarmai-agent', { encoding: 'utf-8' }).trim();
|
|
24
|
+
}
|
|
25
|
+
} catch {
|
|
26
|
+
// Fallback: use the current process
|
|
27
|
+
return process.argv[1] || 'swarmai-agent';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Enable auto-start at boot
|
|
33
|
+
*/
|
|
34
|
+
function enableStartup() {
|
|
35
|
+
const platform = os.platform();
|
|
36
|
+
const execPath = getExecutablePath();
|
|
37
|
+
|
|
38
|
+
switch (platform) {
|
|
39
|
+
case 'win32':
|
|
40
|
+
return enableWindows(execPath);
|
|
41
|
+
case 'darwin':
|
|
42
|
+
return enableMacOS(execPath);
|
|
43
|
+
case 'linux':
|
|
44
|
+
return enableLinux(execPath);
|
|
45
|
+
default:
|
|
46
|
+
throw new Error(`Unsupported platform: ${platform}. Supported: Windows, macOS, Linux`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Disable auto-start at boot
|
|
52
|
+
*/
|
|
53
|
+
function disableStartup() {
|
|
54
|
+
const platform = os.platform();
|
|
55
|
+
|
|
56
|
+
switch (platform) {
|
|
57
|
+
case 'win32':
|
|
58
|
+
return disableWindows();
|
|
59
|
+
case 'darwin':
|
|
60
|
+
return disableMacOS();
|
|
61
|
+
case 'linux':
|
|
62
|
+
return disableLinux();
|
|
63
|
+
default:
|
|
64
|
+
throw new Error(`Unsupported platform: ${platform}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Check if auto-start is enabled
|
|
70
|
+
*/
|
|
71
|
+
function isStartupEnabled() {
|
|
72
|
+
const platform = os.platform();
|
|
73
|
+
|
|
74
|
+
switch (platform) {
|
|
75
|
+
case 'win32':
|
|
76
|
+
return isEnabledWindows();
|
|
77
|
+
case 'darwin':
|
|
78
|
+
return isEnabledMacOS();
|
|
79
|
+
case 'linux':
|
|
80
|
+
return isEnabledLinux();
|
|
81
|
+
default:
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ================
|
|
87
|
+
// Windows: Registry
|
|
88
|
+
// ================
|
|
89
|
+
|
|
90
|
+
function enableWindows(execPath) {
|
|
91
|
+
// Sanitize execPath to prevent command injection via quotes in path
|
|
92
|
+
const safePath = execPath.replace(/"/g, '');
|
|
93
|
+
const cmd = `"${safePath}" start`;
|
|
94
|
+
try {
|
|
95
|
+
execSync(
|
|
96
|
+
`reg add "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run" /v "${APP_NAME}" /t REG_SZ /d "${cmd}" /f`,
|
|
97
|
+
{ encoding: 'utf-8', stdio: 'pipe' }
|
|
98
|
+
);
|
|
99
|
+
return { method: 'registry', path: 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run' };
|
|
100
|
+
} catch (err) {
|
|
101
|
+
throw new Error(`Failed to set Windows registry: ${err.message}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function disableWindows() {
|
|
106
|
+
try {
|
|
107
|
+
execSync(
|
|
108
|
+
`reg delete "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run" /v "${APP_NAME}" /f`,
|
|
109
|
+
{ encoding: 'utf-8', stdio: 'pipe' }
|
|
110
|
+
);
|
|
111
|
+
return true;
|
|
112
|
+
} catch {
|
|
113
|
+
return false; // Already removed
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function isEnabledWindows() {
|
|
118
|
+
try {
|
|
119
|
+
const output = execSync(
|
|
120
|
+
`reg query "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run" /v "${APP_NAME}"`,
|
|
121
|
+
{ encoding: 'utf-8', stdio: 'pipe' }
|
|
122
|
+
);
|
|
123
|
+
return output.includes(APP_NAME);
|
|
124
|
+
} catch {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ================
|
|
130
|
+
// macOS: LaunchAgent
|
|
131
|
+
// ================
|
|
132
|
+
|
|
133
|
+
function getLaunchAgentPath() {
|
|
134
|
+
return path.join(os.homedir(), 'Library', 'LaunchAgents', `${APP_LABEL}.plist`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function enableMacOS(execPath) {
|
|
138
|
+
const plistPath = getLaunchAgentPath();
|
|
139
|
+
const plistDir = path.dirname(plistPath);
|
|
140
|
+
|
|
141
|
+
if (!fs.existsSync(plistDir)) {
|
|
142
|
+
fs.mkdirSync(plistDir, { recursive: true });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
146
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
147
|
+
<plist version="1.0">
|
|
148
|
+
<dict>
|
|
149
|
+
<key>Label</key>
|
|
150
|
+
<string>${APP_LABEL}</string>
|
|
151
|
+
<key>ProgramArguments</key>
|
|
152
|
+
<array>
|
|
153
|
+
<string>${execPath}</string>
|
|
154
|
+
<string>start</string>
|
|
155
|
+
</array>
|
|
156
|
+
<key>RunAtLoad</key>
|
|
157
|
+
<true/>
|
|
158
|
+
<key>KeepAlive</key>
|
|
159
|
+
<true/>
|
|
160
|
+
<key>StandardOutPath</key>
|
|
161
|
+
<string>${path.join(os.homedir(), '.swarmai', 'agent.log')}</string>
|
|
162
|
+
<key>StandardErrorPath</key>
|
|
163
|
+
<string>${path.join(os.homedir(), '.swarmai', 'agent-error.log')}</string>
|
|
164
|
+
</dict>
|
|
165
|
+
</plist>`;
|
|
166
|
+
|
|
167
|
+
fs.writeFileSync(plistPath, plist, 'utf-8');
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
execSync(`launchctl load "${plistPath}"`, { stdio: 'pipe' });
|
|
171
|
+
} catch {
|
|
172
|
+
// May already be loaded
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return { method: 'launchagent', path: plistPath };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function disableMacOS() {
|
|
179
|
+
const plistPath = getLaunchAgentPath();
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
execSync(`launchctl unload "${plistPath}"`, { stdio: 'pipe' });
|
|
183
|
+
} catch {
|
|
184
|
+
// May not be loaded
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (fs.existsSync(plistPath)) {
|
|
188
|
+
fs.unlinkSync(plistPath);
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function isEnabledMacOS() {
|
|
195
|
+
return fs.existsSync(getLaunchAgentPath());
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ================
|
|
199
|
+
// Linux: systemd user service
|
|
200
|
+
// ================
|
|
201
|
+
|
|
202
|
+
function getSystemdServicePath() {
|
|
203
|
+
return path.join(os.homedir(), '.config', 'systemd', 'user', `${APP_NAME}.service`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function enableLinux(execPath) {
|
|
207
|
+
const servicePath = getSystemdServicePath();
|
|
208
|
+
const serviceDir = path.dirname(servicePath);
|
|
209
|
+
|
|
210
|
+
if (!fs.existsSync(serviceDir)) {
|
|
211
|
+
fs.mkdirSync(serviceDir, { recursive: true });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const service = `[Unit]
|
|
215
|
+
Description=SwarmAI Local Agent
|
|
216
|
+
After=network-online.target
|
|
217
|
+
Wants=network-online.target
|
|
218
|
+
|
|
219
|
+
[Service]
|
|
220
|
+
Type=simple
|
|
221
|
+
ExecStart=${execPath} start
|
|
222
|
+
Restart=on-failure
|
|
223
|
+
RestartSec=10
|
|
224
|
+
Environment=HOME=${os.homedir()}
|
|
225
|
+
|
|
226
|
+
[Install]
|
|
227
|
+
WantedBy=default.target
|
|
228
|
+
`;
|
|
229
|
+
|
|
230
|
+
fs.writeFileSync(servicePath, service, 'utf-8');
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
execSync('systemctl --user daemon-reload', { stdio: 'pipe' });
|
|
234
|
+
execSync(`systemctl --user enable ${APP_NAME}.service`, { stdio: 'pipe' });
|
|
235
|
+
return { method: 'systemd', path: servicePath };
|
|
236
|
+
} catch (err) {
|
|
237
|
+
// systemd might not be available, try XDG autostart fallback
|
|
238
|
+
return enableLinuxXdg(execPath);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function enableLinuxXdg(execPath) {
|
|
243
|
+
const autostartDir = path.join(os.homedir(), '.config', 'autostart');
|
|
244
|
+
const desktopPath = path.join(autostartDir, `${APP_NAME}.desktop`);
|
|
245
|
+
|
|
246
|
+
if (!fs.existsSync(autostartDir)) {
|
|
247
|
+
fs.mkdirSync(autostartDir, { recursive: true });
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const desktop = `[Desktop Entry]
|
|
251
|
+
Type=Application
|
|
252
|
+
Name=SwarmAI Local Agent
|
|
253
|
+
Exec=${execPath} start
|
|
254
|
+
Hidden=false
|
|
255
|
+
NoDisplay=true
|
|
256
|
+
X-GNOME-Autostart-enabled=true
|
|
257
|
+
`;
|
|
258
|
+
|
|
259
|
+
fs.writeFileSync(desktopPath, desktop, 'utf-8');
|
|
260
|
+
return { method: 'xdg-autostart', path: desktopPath };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function disableLinux() {
|
|
264
|
+
// Try systemd first
|
|
265
|
+
const servicePath = getSystemdServicePath();
|
|
266
|
+
if (fs.existsSync(servicePath)) {
|
|
267
|
+
try {
|
|
268
|
+
execSync(`systemctl --user disable ${APP_NAME}.service`, { stdio: 'pipe' });
|
|
269
|
+
execSync('systemctl --user daemon-reload', { stdio: 'pipe' });
|
|
270
|
+
} catch {
|
|
271
|
+
// systemd might not be available
|
|
272
|
+
}
|
|
273
|
+
fs.unlinkSync(servicePath);
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Try XDG autostart
|
|
278
|
+
const desktopPath = path.join(os.homedir(), '.config', 'autostart', `${APP_NAME}.desktop`);
|
|
279
|
+
if (fs.existsSync(desktopPath)) {
|
|
280
|
+
fs.unlinkSync(desktopPath);
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function isEnabledLinux() {
|
|
288
|
+
if (fs.existsSync(getSystemdServicePath())) return true;
|
|
289
|
+
if (fs.existsSync(path.join(os.homedir(), '.config', 'autostart', `${APP_NAME}.desktop`))) return true;
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
module.exports = {
|
|
294
|
+
enableStartup,
|
|
295
|
+
disableStartup,
|
|
296
|
+
isStartupEnabled,
|
|
297
|
+
};
|