goto-assistant 0.1.2 → 0.1.4
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 +39 -30
- package/dist/agents/openai.js +38 -2
- package/dist/agents/openai.js.map +1 -1
- package/dist/config.js +1 -1
- package/dist/config.js.map +1 -1
- package/package.json +2 -1
- package/public/setup.html +13 -91
- package/public/setup.js +127 -0
package/README.md
CHANGED
|
@@ -26,37 +26,46 @@ PORT=3001 npx goto-assistant
|
|
|
26
26
|
## Architecture
|
|
27
27
|
|
|
28
28
|
```
|
|
29
|
-
|
|
30
|
-
│ Browser
|
|
31
|
-
│ ┌──────────────┐ ┌──────────────┐
|
|
32
|
-
│ │ index.html │ │ setup.html │
|
|
33
|
-
│ │ (Chat UI) │ │ (Config) │
|
|
34
|
-
│ └──────┬───────┘ └──────┬───────┘
|
|
35
|
-
│ │ WebSocket │ HTTP POST
|
|
36
|
-
|
|
29
|
+
┌──────────────────────────────────────────────────────────────────┐
|
|
30
|
+
│ Browser │
|
|
31
|
+
│ ┌──────────────┐ ┌──────────────┐ │
|
|
32
|
+
│ │ index.html │ │ setup.html │ │
|
|
33
|
+
│ │ (Chat UI) │ │ (Config) │ │
|
|
34
|
+
│ └──────┬───────┘ └──────┬───────┘ │
|
|
35
|
+
│ │ WebSocket │ HTTP POST │
|
|
36
|
+
└─────────┼──────────────────┼──────────────────────────────────────┘
|
|
37
37
|
│ │
|
|
38
|
-
|
|
39
|
-
│ server.ts │
|
|
40
|
-
│ ┌──────┴───────┐ ┌──────┴───────┐
|
|
41
|
-
│ │ WebSocket │ │ REST API │
|
|
42
|
-
│ │ Handler │ │ /api/* │
|
|
43
|
-
│ └──────┬───────┘ └──────────────┘
|
|
44
|
-
│ │
|
|
45
|
-
│ ┌──────┴───────┐
|
|
46
|
-
│ │ router.ts │──── routes by provider
|
|
47
|
-
│ └──┬────────┬──┘
|
|
48
|
-
│ │ │
|
|
49
|
-
│ ┌──┴──┐ ┌──┴───┐
|
|
50
|
-
│ │Claude│ │OpenAI
|
|
51
|
-
│ │Agent │ │Agent │
|
|
52
|
-
│ │ SDK │ │ SDK │
|
|
53
|
-
│ └──┬──┘ └──┬───┘
|
|
54
|
-
│ │ │
|
|
55
|
-
│
|
|
56
|
-
│
|
|
57
|
-
│
|
|
58
|
-
│
|
|
59
|
-
|
|
38
|
+
┌─────────┼──────────────────┼──────────────────────────────────────┐
|
|
39
|
+
│ server.ts │ │
|
|
40
|
+
│ ┌──────┴───────┐ ┌──────┴───────┐ │
|
|
41
|
+
│ │ WebSocket │ │ REST API │ │
|
|
42
|
+
│ │ Handler │ │ /api/* │ │
|
|
43
|
+
│ └──────┬───────┘ └──────────────┘ │
|
|
44
|
+
│ │ │
|
|
45
|
+
│ ┌──────┴───────┐ │
|
|
46
|
+
│ │ router.ts │──── routes by provider │
|
|
47
|
+
│ └──┬────────┬──┘ │
|
|
48
|
+
│ │ │ │
|
|
49
|
+
│ ┌──┴──┐ ┌──┴───┐ ┌───────────────────────────────────┐ │
|
|
50
|
+
│ │Claude│ │OpenAI│───▶│ MCP Servers │ │
|
|
51
|
+
│ │Agent │ │Agent │ │ │ │
|
|
52
|
+
│ │ SDK │ │ SDK │ │ ┌────────┐ ┌────────────┐ ··· │ │
|
|
53
|
+
│ └──┬──┘ └──┬───┘ │ │ memory │ │ filesystem │ │ │
|
|
54
|
+
│ │ │ │ └────────┘ └────────────┘ │ │
|
|
55
|
+
│ │ │ │ ▲ ▲ │ │
|
|
56
|
+
│ │ │ │ │ │ │ │
|
|
57
|
+
│ │ │ │ ┌────┴────────────┴──────────┐ │ │
|
|
58
|
+
│ │ │ │ │ mcp-cron │ │ │
|
|
59
|
+
│ │ │ │ │ AI tasks (w/ MCP access) │ │ │
|
|
60
|
+
│ │ │ │ │ + shell commands │ │ │
|
|
61
|
+
│ │ │ │ └────────────────────────────┘ │ │
|
|
62
|
+
│ │ │ └───────────────────────────────────┘ │
|
|
63
|
+
│ │ │ │
|
|
64
|
+
│ ┌──┴────────┴──┐ ┌────────────┐ │
|
|
65
|
+
│ │ sessions.ts │───▶│ SQLite DB │ │
|
|
66
|
+
│ │ (persistence)│ │ data/ │ │
|
|
67
|
+
│ └──────────────┘ └────────────┘ │
|
|
68
|
+
└──────────────────────────────────────────────────────────────────┘
|
|
60
69
|
```
|
|
61
70
|
|
|
62
71
|
## Development Setup
|
package/dist/agents/openai.js
CHANGED
|
@@ -1,7 +1,40 @@
|
|
|
1
|
-
import { Agent, run, MCPServerStdio } from "@openai/agents";
|
|
1
|
+
import { Agent, run, MCPServerStdio, shellTool } from "@openai/agents";
|
|
2
|
+
import { exec } from "node:child_process";
|
|
3
|
+
import { promisify } from "node:util";
|
|
2
4
|
import { MEMORY_FILE_PATH, MEMORY_SERVER_NAME } from "../config.js";
|
|
3
5
|
import { parseMessageContent } from "../sessions.js";
|
|
4
6
|
import { getUpload } from "../uploads.js";
|
|
7
|
+
const execAsync = promisify(exec);
|
|
8
|
+
class LocalShell {
|
|
9
|
+
async run(action) {
|
|
10
|
+
const output = [];
|
|
11
|
+
for (const command of action.commands) {
|
|
12
|
+
let stdout = "";
|
|
13
|
+
let stderr = "";
|
|
14
|
+
let outcome = { type: "exit", exitCode: 0 };
|
|
15
|
+
try {
|
|
16
|
+
const result = await execAsync(command, {
|
|
17
|
+
timeout: action.timeoutMs ?? 30_000,
|
|
18
|
+
maxBuffer: action.maxOutputLength ?? 1024 * 1024,
|
|
19
|
+
});
|
|
20
|
+
stdout = result.stdout;
|
|
21
|
+
stderr = result.stderr;
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
const err = error;
|
|
25
|
+
stdout = err.stdout ?? "";
|
|
26
|
+
stderr = err.stderr ?? err.message ?? "";
|
|
27
|
+
outcome = err.killed || err.signal === "SIGTERM"
|
|
28
|
+
? { type: "timeout" }
|
|
29
|
+
: { type: "exit", exitCode: typeof err.code === "number" ? err.code : 1 };
|
|
30
|
+
}
|
|
31
|
+
output.push({ stdout, stderr, outcome });
|
|
32
|
+
if (outcome.type === "timeout")
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
return { output };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
5
38
|
export async function runOpenAI(prompt, config, mcpServersConfig, onChunk, attachments, history) {
|
|
6
39
|
const env = {
|
|
7
40
|
...process.env,
|
|
@@ -30,9 +63,12 @@ export async function runOpenAI(prompt, config, mcpServersConfig, onChunk, attac
|
|
|
30
63
|
}
|
|
31
64
|
const agent = new Agent({
|
|
32
65
|
name: "goto-assistant",
|
|
33
|
-
instructions: "You are a helpful personal AI assistant. You have access to MCP tools for memory, filesystem, browser automation, and scheduled tasks. Use them when appropriate. IMPORTANT: At the start of each conversation, you MUST call the memory read_graph tool to retrieve all known context about the user before responding to their first message.",
|
|
66
|
+
instructions: "You are a helpful personal AI assistant. You have access to MCP tools for memory, filesystem, browser automation, and scheduled tasks. You also have a shell tool to execute commands on the host machine. Use them when appropriate. IMPORTANT: At the start of each conversation, you MUST call the memory read_graph tool to retrieve all known context about the user before responding to their first message.",
|
|
34
67
|
model: config.openai.model,
|
|
35
68
|
mcpServers,
|
|
69
|
+
tools: [
|
|
70
|
+
shellTool({ shell: new LocalShell() }),
|
|
71
|
+
],
|
|
36
72
|
});
|
|
37
73
|
// Build conversation input with history
|
|
38
74
|
const inputMessages = [];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"openai.js","sourceRoot":"","sources":["../../src/agents/openai.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"openai.js","sourceRoot":"","sources":["../../src/agents/openai.ts"],"names":[],"mappings":"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"}
|
package/dist/config.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
export const DATA_DIR = path.join(process.cwd(),
|
|
3
|
+
export const DATA_DIR = process.env.GOTO_DATA_DIR || path.join(process.cwd(), "data");
|
|
4
4
|
const CONFIG_PATH = path.join(DATA_DIR, "config.json");
|
|
5
5
|
export const MCP_CONFIG_PATH = path.join(DATA_DIR, "mcp.json");
|
|
6
6
|
export const MEMORY_FILE_PATH = path.join(DATA_DIR, "memory.json");
|
package/dist/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAe7B,MAAM,CAAC,MAAM,QAAQ,GAAG,
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "goto-assistant",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Lightweight, self-hosted personal AI assistant",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"packageManager": "pnpm@10.29.3",
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"@types/multer": "^2.0.0",
|
|
56
56
|
"@types/ws": "^8.0.0",
|
|
57
57
|
"eslint": "^9.39.2",
|
|
58
|
+
"jsdom": "^28.0.0",
|
|
58
59
|
"tsx": "^4.0.0",
|
|
59
60
|
"typescript": "^5.0.0",
|
|
60
61
|
"typescript-eslint": "^8.55.0",
|
package/public/setup.html
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
<title>goto-assistant — Setup</title>
|
|
7
7
|
<link rel="stylesheet" href="/style.css">
|
|
8
8
|
<script src="/cron-sync.js"></script>
|
|
9
|
+
<script src="/setup.js"></script>
|
|
9
10
|
</head>
|
|
10
11
|
<body>
|
|
11
12
|
<div class="setup-container">
|
|
@@ -54,74 +55,10 @@
|
|
|
54
55
|
</div>
|
|
55
56
|
|
|
56
57
|
<script>
|
|
57
|
-
const defaultServers = [
|
|
58
|
-
{ name: 'cron', command: 'npx', args: '-y mcp-cron --transport stdio --prevent-sleep --mcp-config-path ./data/mcp.json --ai-provider anthropic --ai-model claude-sonnet-4-5-20250929', env: {} },
|
|
59
|
-
{ name: 'memory', command: 'npx', args: '-y @modelcontextprotocol/server-memory', env: {} },
|
|
60
|
-
{ name: 'filesystem', command: 'npx', args: '-y @modelcontextprotocol/server-filesystem .', env: {} },
|
|
61
|
-
];
|
|
62
|
-
|
|
63
|
-
function getProvider() {
|
|
64
|
-
return document.querySelector('input[name="provider"]:checked').value;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Render MCP servers
|
|
68
|
-
function renderServers(servers) {
|
|
69
|
-
const container = document.getElementById('mcpServers');
|
|
70
|
-
container.innerHTML = '';
|
|
71
|
-
servers.forEach((s, i) => {
|
|
72
|
-
const div = document.createElement('div');
|
|
73
|
-
div.className = 'mcp-server';
|
|
74
|
-
const envRows = Object.entries(s.env || {}).map(([k, v], ei) =>
|
|
75
|
-
`<div class="env-row">
|
|
76
|
-
<input type="text" placeholder="Key" value="${k}" data-server="${i}" data-env-key="${ei}">
|
|
77
|
-
<input type="text" placeholder="Value" value="${v}" data-server="${i}" data-env-val="${ei}">
|
|
78
|
-
<button class="btn-icon" onclick="removeEnv(${i},${ei})">×</button>
|
|
79
|
-
</div>`
|
|
80
|
-
).join('');
|
|
81
|
-
div.innerHTML = `
|
|
82
|
-
<div class="mcp-server-header">
|
|
83
|
-
<input type="text" value="${s.name}" data-server="${i}" data-field="name" placeholder="Server name">
|
|
84
|
-
<button class="btn-icon" onclick="removeServer(${i})">×</button>
|
|
85
|
-
</div>
|
|
86
|
-
<div class="form-group">
|
|
87
|
-
<label>Command</label>
|
|
88
|
-
<input type="text" value="${s.command}" data-server="${i}" data-field="command">
|
|
89
|
-
</div>
|
|
90
|
-
<div class="form-group">
|
|
91
|
-
<label>Args</label>
|
|
92
|
-
<input type="text" value="${s.args}" data-server="${i}" data-field="args">
|
|
93
|
-
</div>
|
|
94
|
-
<div class="form-group">
|
|
95
|
-
<label>Environment Variables</label>
|
|
96
|
-
${envRows}
|
|
97
|
-
<button class="btn btn-secondary" style="margin-top:4px;font-size:12px;padding:4px 10px" onclick="addEnv(${i})">+ Add Env</button>
|
|
98
|
-
</div>`;
|
|
99
|
-
container.appendChild(div);
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
|
|
103
58
|
let servers = [...defaultServers.map(s => ({ ...s, env: { ...s.env } }))];
|
|
104
59
|
let isEditing = false; // true when revisiting setup with existing config
|
|
105
60
|
renderServers(servers);
|
|
106
61
|
|
|
107
|
-
function readServers() {
|
|
108
|
-
const container = document.getElementById('mcpServers');
|
|
109
|
-
const items = container.querySelectorAll('.mcp-server');
|
|
110
|
-
return Array.from(items).map((item, i) => {
|
|
111
|
-
const name = item.querySelector('[data-field="name"]').value;
|
|
112
|
-
const command = item.querySelector('[data-field="command"]').value;
|
|
113
|
-
const args = item.querySelector('[data-field="args"]').value;
|
|
114
|
-
const envRows = item.querySelectorAll('.env-row');
|
|
115
|
-
const env = {};
|
|
116
|
-
envRows.forEach(row => {
|
|
117
|
-
const k = row.querySelector('[placeholder="Key"]').value.trim();
|
|
118
|
-
const v = row.querySelector('[placeholder="Value"]').value.trim();
|
|
119
|
-
if (k) env[k] = v;
|
|
120
|
-
});
|
|
121
|
-
return { name, command, args, env };
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
|
|
125
62
|
window.removeServer = (i) => { servers.splice(i, 1); renderServers(servers); };
|
|
126
63
|
window.addEnv = (i) => {
|
|
127
64
|
servers = readServers();
|
|
@@ -141,39 +78,23 @@
|
|
|
141
78
|
renderServers(servers);
|
|
142
79
|
});
|
|
143
80
|
|
|
144
|
-
//
|
|
145
|
-
function
|
|
81
|
+
// Thin wrapper that passes mutable state to the extracted syncCronConfig
|
|
82
|
+
function doSyncCronConfig() {
|
|
146
83
|
servers = readServers();
|
|
147
|
-
|
|
148
|
-
if (!cron) return;
|
|
149
|
-
|
|
150
|
-
const provider = getProvider();
|
|
151
|
-
const apiKey = document.getElementById('apiKey').value.trim();
|
|
152
|
-
const model = document.getElementById('model').value;
|
|
153
|
-
const baseUrl = document.getElementById('baseUrl').value.trim();
|
|
154
|
-
|
|
155
|
-
const result = buildCronConfig({
|
|
156
|
-
provider, apiKey, model, baseUrl,
|
|
157
|
-
currentArgs: cron.args,
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
cron.args = result.args;
|
|
161
|
-
|
|
162
|
-
// Update env: remove old API_KEY entries, set the correct one
|
|
163
|
-
const oldKeys = Object.keys(cron.env).filter(k => k.includes('API_KEY'));
|
|
164
|
-
oldKeys.forEach(k => delete cron.env[k]);
|
|
165
|
-
cron.env[result.envKey] = result.envValue;
|
|
166
|
-
|
|
84
|
+
servers = syncCronConfig(servers, isEditing, buildCronConfig);
|
|
167
85
|
renderServers(servers);
|
|
168
86
|
}
|
|
169
87
|
|
|
170
88
|
// Sync cron when provider, API key, base URL, or model changes
|
|
171
89
|
document.querySelectorAll('input[name="provider"]').forEach(radio => {
|
|
172
|
-
radio.addEventListener('change',
|
|
90
|
+
radio.addEventListener('change', () => {
|
|
91
|
+
handleProviderSwitch(isEditing, window._savedConfig);
|
|
92
|
+
doSyncCronConfig();
|
|
93
|
+
});
|
|
173
94
|
});
|
|
174
|
-
document.getElementById('apiKey').addEventListener('input',
|
|
175
|
-
document.getElementById('baseUrl').addEventListener('input',
|
|
176
|
-
document.getElementById('model').addEventListener('change',
|
|
95
|
+
document.getElementById('apiKey').addEventListener('input', doSyncCronConfig);
|
|
96
|
+
document.getElementById('baseUrl').addEventListener('input', doSyncCronConfig);
|
|
97
|
+
document.getElementById('model').addEventListener('change', doSyncCronConfig);
|
|
177
98
|
|
|
178
99
|
// Load models
|
|
179
100
|
document.getElementById('loadModelsBtn').addEventListener('click', async () => {
|
|
@@ -214,7 +135,7 @@
|
|
|
214
135
|
if (!model && !isEditing) { showStatus('Select a model first', 'error'); return; }
|
|
215
136
|
|
|
216
137
|
// Sync cron one final time before saving
|
|
217
|
-
|
|
138
|
+
doSyncCronConfig();
|
|
218
139
|
servers = readServers();
|
|
219
140
|
|
|
220
141
|
const mcpServers = {};
|
|
@@ -276,6 +197,7 @@
|
|
|
276
197
|
const mcpData = await mcpRes.json();
|
|
277
198
|
if (data.configured && data.config) {
|
|
278
199
|
isEditing = true;
|
|
200
|
+
window._savedConfig = data.config;
|
|
279
201
|
document.getElementById('backBtn').style.display = '';
|
|
280
202
|
document.getElementById('apiKey').placeholder = 'Leave empty to keep existing key';
|
|
281
203
|
const c = data.config;
|
package/public/setup.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// setup.js — extracted functions from setup.html for testability.
|
|
2
|
+
// Loaded as a plain <script> in the browser; importable via require() in tests.
|
|
3
|
+
|
|
4
|
+
// eslint-disable-next-line no-unused-vars
|
|
5
|
+
var defaultServers = [
|
|
6
|
+
{ name: 'cron', command: 'npx', args: '-y mcp-cron --transport stdio --prevent-sleep --mcp-config-path ./data/mcp.json --ai-provider anthropic --ai-model claude-sonnet-4-5-20250929', env: {} },
|
|
7
|
+
{ name: 'memory', command: 'npx', args: '-y @modelcontextprotocol/server-memory', env: {} },
|
|
8
|
+
{ name: 'filesystem', command: 'npx', args: '-y @modelcontextprotocol/server-filesystem .', env: {} },
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
// eslint-disable-next-line no-unused-vars
|
|
12
|
+
function getProvider() {
|
|
13
|
+
return document.querySelector('input[name="provider"]:checked').value;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// eslint-disable-next-line no-unused-vars
|
|
17
|
+
function renderServers(servers) {
|
|
18
|
+
var container = document.getElementById('mcpServers');
|
|
19
|
+
container.innerHTML = '';
|
|
20
|
+
servers.forEach(function (s, i) {
|
|
21
|
+
var div = document.createElement('div');
|
|
22
|
+
div.className = 'mcp-server';
|
|
23
|
+
var envRows = Object.entries(s.env || {}).map(function (_ref, ei) {
|
|
24
|
+
var k = _ref[0], v = _ref[1];
|
|
25
|
+
return '<div class="env-row">' +
|
|
26
|
+
'<input type="text" placeholder="Key" value="' + k + '" data-server="' + i + '" data-env-key="' + ei + '">' +
|
|
27
|
+
'<input type="text" placeholder="Value" value="' + v + '" data-server="' + i + '" data-env-val="' + ei + '">' +
|
|
28
|
+
'<button class="btn-icon" onclick="removeEnv(' + i + ',' + ei + ')">×</button>' +
|
|
29
|
+
'</div>';
|
|
30
|
+
}).join('');
|
|
31
|
+
div.innerHTML =
|
|
32
|
+
'<div class="mcp-server-header">' +
|
|
33
|
+
'<input type="text" value="' + s.name + '" data-server="' + i + '" data-field="name" placeholder="Server name">' +
|
|
34
|
+
'<button class="btn-icon" onclick="removeServer(' + i + ')">×</button>' +
|
|
35
|
+
'</div>' +
|
|
36
|
+
'<div class="form-group">' +
|
|
37
|
+
'<label>Command</label>' +
|
|
38
|
+
'<input type="text" value="' + s.command + '" data-server="' + i + '" data-field="command">' +
|
|
39
|
+
'</div>' +
|
|
40
|
+
'<div class="form-group">' +
|
|
41
|
+
'<label>Args</label>' +
|
|
42
|
+
'<input type="text" value="' + s.args + '" data-server="' + i + '" data-field="args">' +
|
|
43
|
+
'</div>' +
|
|
44
|
+
'<div class="form-group">' +
|
|
45
|
+
'<label>Environment Variables</label>' +
|
|
46
|
+
envRows +
|
|
47
|
+
'<button class="btn btn-secondary" style="margin-top:4px;font-size:12px;padding:4px 10px" onclick="addEnv(' + i + ')">+ Add Env</button>' +
|
|
48
|
+
'</div>';
|
|
49
|
+
container.appendChild(div);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// eslint-disable-next-line no-unused-vars
|
|
54
|
+
function readServers() {
|
|
55
|
+
var container = document.getElementById('mcpServers');
|
|
56
|
+
var items = container.querySelectorAll('.mcp-server');
|
|
57
|
+
return Array.from(items).map(function (item) {
|
|
58
|
+
var name = item.querySelector('[data-field="name"]').value;
|
|
59
|
+
var command = item.querySelector('[data-field="command"]').value;
|
|
60
|
+
var args = item.querySelector('[data-field="args"]').value;
|
|
61
|
+
var envRows = item.querySelectorAll('.env-row');
|
|
62
|
+
var env = {};
|
|
63
|
+
envRows.forEach(function (row) {
|
|
64
|
+
var k = row.querySelector('[placeholder="Key"]').value.trim();
|
|
65
|
+
var v = row.querySelector('[placeholder="Value"]').value.trim();
|
|
66
|
+
if (k) env[k] = v;
|
|
67
|
+
});
|
|
68
|
+
return { name: name, command: command, args: args, env: env };
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Sync cron server config. Takes servers array, isEditing flag, and buildCronConfigFn.
|
|
73
|
+
// Returns updated servers array (mutates in place for convenience, also returns).
|
|
74
|
+
// eslint-disable-next-line no-unused-vars
|
|
75
|
+
function syncCronConfig(servers, isEditing, buildCronConfigFn) {
|
|
76
|
+
var cron = servers.find(function (s) { return s.name === 'cron'; });
|
|
77
|
+
if (!cron) return servers;
|
|
78
|
+
|
|
79
|
+
var provider = getProvider();
|
|
80
|
+
var apiKey = document.getElementById('apiKey').value.trim();
|
|
81
|
+
var model = document.getElementById('model').value;
|
|
82
|
+
var baseUrl = document.getElementById('baseUrl').value.trim();
|
|
83
|
+
|
|
84
|
+
var result = buildCronConfigFn({
|
|
85
|
+
provider: provider,
|
|
86
|
+
apiKey: apiKey,
|
|
87
|
+
model: model,
|
|
88
|
+
baseUrl: baseUrl,
|
|
89
|
+
currentArgs: cron.args,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
cron.args = result.args;
|
|
93
|
+
|
|
94
|
+
// Always rename the env key to match the provider.
|
|
95
|
+
// In edit mode with no new key entered, preserve the existing value.
|
|
96
|
+
var oldKeys = Object.keys(cron.env).filter(function (k) { return k.includes('API_KEY'); });
|
|
97
|
+
if (apiKey || !isEditing) {
|
|
98
|
+
oldKeys.forEach(function (k) { delete cron.env[k]; });
|
|
99
|
+
cron.env[result.envKey] = result.envValue;
|
|
100
|
+
} else if (oldKeys.length > 0 && oldKeys[0] !== result.envKey) {
|
|
101
|
+
var existingValue = cron.env[oldKeys[0]];
|
|
102
|
+
oldKeys.forEach(function (k) { delete cron.env[k]; });
|
|
103
|
+
cron.env[result.envKey] = existingValue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return servers;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Handle provider switch: pre-fill baseUrl and model from saved config.
|
|
110
|
+
// eslint-disable-next-line no-unused-vars
|
|
111
|
+
function handleProviderSwitch(isEditing, savedConfig) {
|
|
112
|
+
if (!isEditing || !savedConfig) return;
|
|
113
|
+
|
|
114
|
+
var p = getProvider();
|
|
115
|
+
var pc = savedConfig[p] || {};
|
|
116
|
+
document.getElementById('baseUrl').value = pc.baseUrl || '';
|
|
117
|
+
var select = document.getElementById('model');
|
|
118
|
+
if (pc.model) {
|
|
119
|
+
select.innerHTML = '<option value="' + pc.model + '">' + pc.model + '</option>';
|
|
120
|
+
} else {
|
|
121
|
+
select.innerHTML = '<option value="">— Load models —</option>';
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
126
|
+
module.exports = { defaultServers: defaultServers, getProvider: getProvider, renderServers: renderServers, readServers: readServers, syncCronConfig: syncCronConfig, handleProviderSwitch: handleProviderSwitch };
|
|
127
|
+
}
|