mcp-for-i 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 +53 -0
- package/dist/config/store.js +365 -0
- package/dist/controlplane/autostart.js +93 -0
- package/dist/controlplane/connectionService.js +79 -0
- package/dist/controlplane/index.js +120 -0
- package/dist/controlplane/runtimeService.js +263 -0
- package/dist/controlplane/server.js +180 -0
- package/dist/controlplane/ui.js +741 -0
- package/dist/ibmi/CompileTools.js +91 -0
- package/dist/ibmi/IBMiContent.js +386 -0
- package/dist/ibmi/LocalLanguageActions.js +183 -0
- package/dist/ibmi/Search.js +135 -0
- package/dist/ibmi/Tools.js +188 -0
- package/dist/ibmi/client.js +213 -0
- package/dist/ibmi/sourceDates.js +60 -0
- package/dist/ibmi/types.js +1 -0
- package/dist/ibmi/variables.js +53 -0
- package/dist/index.js +59 -0
- package/dist/mcp/context.js +235 -0
- package/dist/mcp/logger.js +73 -0
- package/dist/mcp/tools.js +1335 -0
- package/dist/mcp/validation.js +74 -0
- package/dist/security/credentialStore.js +42 -0
- package/docs/configuration.md +57 -0
- package/docs/examples.md +73 -0
- package/docs/installation.md +61 -0
- package/docs/overview.md +14 -0
- package/docs/security.md +8 -0
- package/docs/tools.md +105 -0
- package/package.json +49 -0
|
@@ -0,0 +1,741 @@
|
|
|
1
|
+
export function renderControlPlaneHtml() {
|
|
2
|
+
return `<!doctype html>
|
|
3
|
+
<html lang="en">
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>MCP-for-i Control Plane</title>
|
|
8
|
+
<style>
|
|
9
|
+
:root {
|
|
10
|
+
color-scheme: dark;
|
|
11
|
+
--bg: #060b11;
|
|
12
|
+
--bg-grad-1: rgba(20, 184, 166, 0.17);
|
|
13
|
+
--bg-grad-2: rgba(59, 130, 246, 0.18);
|
|
14
|
+
--surface: rgba(15, 23, 32, 0.9);
|
|
15
|
+
--surface-2: #131d2a;
|
|
16
|
+
--line: #263547;
|
|
17
|
+
--line-strong: #3c5270;
|
|
18
|
+
--text: #e6eef8;
|
|
19
|
+
--muted: #8da4bb;
|
|
20
|
+
--accent: #14b8a6;
|
|
21
|
+
--accent-2: #38bdf8;
|
|
22
|
+
--danger-1: #f97316;
|
|
23
|
+
--danger-2: #ef4444;
|
|
24
|
+
--ok: #34d399;
|
|
25
|
+
--shadow: 0 18px 42px rgba(0, 0, 0, 0.34);
|
|
26
|
+
--ring: 0 0 0 3px rgba(20, 184, 166, 0.22);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
body[data-theme="light"] {
|
|
30
|
+
color-scheme: light;
|
|
31
|
+
--bg: #f4f7fb;
|
|
32
|
+
--bg-grad-1: rgba(20, 184, 166, 0.14);
|
|
33
|
+
--bg-grad-2: rgba(14, 165, 233, 0.12);
|
|
34
|
+
--surface: rgba(255, 255, 255, 0.92);
|
|
35
|
+
--surface-2: #f8fbff;
|
|
36
|
+
--line: #d5e0ec;
|
|
37
|
+
--line-strong: #b8c9dd;
|
|
38
|
+
--text: #0e1b2a;
|
|
39
|
+
--muted: #516377;
|
|
40
|
+
--accent: #0f766e;
|
|
41
|
+
--accent-2: #0284c7;
|
|
42
|
+
--danger-1: #dc2626;
|
|
43
|
+
--danger-2: #b91c1c;
|
|
44
|
+
--ok: #059669;
|
|
45
|
+
--shadow: 0 14px 34px rgba(13, 35, 66, 0.14);
|
|
46
|
+
--ring: 0 0 0 3px rgba(2, 132, 199, 0.2);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
* { box-sizing: border-box; }
|
|
50
|
+
|
|
51
|
+
body {
|
|
52
|
+
margin: 0;
|
|
53
|
+
min-height: 100vh;
|
|
54
|
+
color: var(--text);
|
|
55
|
+
background:
|
|
56
|
+
radial-gradient(circle at 12% 10%, var(--bg-grad-1), transparent 34%),
|
|
57
|
+
radial-gradient(circle at 88% 8%, var(--bg-grad-2), transparent 35%),
|
|
58
|
+
linear-gradient(180deg, var(--bg), var(--bg));
|
|
59
|
+
font: 14px/1.55 "Inter", "Segoe UI", "SF Pro Text", "Helvetica Neue", sans-serif;
|
|
60
|
+
letter-spacing: 0.01em;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.page {
|
|
64
|
+
max-width: 1220px;
|
|
65
|
+
margin: 0 auto;
|
|
66
|
+
padding: 24px;
|
|
67
|
+
display: grid;
|
|
68
|
+
gap: 18px;
|
|
69
|
+
grid-template-columns: 1.1fr 1fr;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.shell,
|
|
73
|
+
.card {
|
|
74
|
+
border-radius: 14px;
|
|
75
|
+
border: 1px solid var(--line);
|
|
76
|
+
background: linear-gradient(180deg, rgba(255,255,255,0.03), rgba(255,255,255,0)), var(--surface);
|
|
77
|
+
box-shadow: var(--shadow);
|
|
78
|
+
backdrop-filter: blur(8px);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.shell {
|
|
82
|
+
grid-column: 1 / -1;
|
|
83
|
+
padding: 18px 20px;
|
|
84
|
+
display: flex;
|
|
85
|
+
justify-content: space-between;
|
|
86
|
+
align-items: center;
|
|
87
|
+
gap: 14px;
|
|
88
|
+
flex-wrap: wrap;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.title-wrap h1 {
|
|
92
|
+
margin: 0;
|
|
93
|
+
font-size: 24px;
|
|
94
|
+
letter-spacing: 0.2px;
|
|
95
|
+
font-weight: 700;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.title-wrap p {
|
|
99
|
+
margin: 5px 0 0;
|
|
100
|
+
color: var(--muted);
|
|
101
|
+
max-width: 850px;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.top-actions {
|
|
105
|
+
display: flex;
|
|
106
|
+
align-items: center;
|
|
107
|
+
gap: 10px;
|
|
108
|
+
flex-wrap: wrap;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.status {
|
|
112
|
+
display: inline-flex;
|
|
113
|
+
align-items: center;
|
|
114
|
+
gap: 8px;
|
|
115
|
+
padding: 7px 12px;
|
|
116
|
+
border-radius: 999px;
|
|
117
|
+
border: 1px solid var(--line);
|
|
118
|
+
background: rgba(0, 0, 0, 0.12);
|
|
119
|
+
color: var(--muted);
|
|
120
|
+
font-weight: 600;
|
|
121
|
+
font-size: 12px;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.dot {
|
|
125
|
+
width: 8px;
|
|
126
|
+
height: 8px;
|
|
127
|
+
border-radius: 50%;
|
|
128
|
+
background: var(--ok);
|
|
129
|
+
box-shadow: 0 0 12px color-mix(in srgb, var(--ok) 75%, transparent);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.card { padding: 16px; }
|
|
133
|
+
|
|
134
|
+
.card h2 {
|
|
135
|
+
margin: 0 0 12px;
|
|
136
|
+
font-size: 15px;
|
|
137
|
+
letter-spacing: 0.03em;
|
|
138
|
+
text-transform: uppercase;
|
|
139
|
+
color: var(--muted);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.grid-2 {
|
|
143
|
+
display: grid;
|
|
144
|
+
gap: 10px;
|
|
145
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.field label {
|
|
149
|
+
display: block;
|
|
150
|
+
margin-bottom: 6px;
|
|
151
|
+
color: var(--muted);
|
|
152
|
+
font-size: 11px;
|
|
153
|
+
font-weight: 700;
|
|
154
|
+
letter-spacing: 0.09em;
|
|
155
|
+
text-transform: uppercase;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
input,
|
|
159
|
+
select,
|
|
160
|
+
textarea {
|
|
161
|
+
width: 100%;
|
|
162
|
+
border: 1px solid var(--line);
|
|
163
|
+
border-radius: 10px;
|
|
164
|
+
background: var(--surface-2);
|
|
165
|
+
color: var(--text);
|
|
166
|
+
padding: 10px 11px;
|
|
167
|
+
outline: none;
|
|
168
|
+
transition: border-color .15s ease, box-shadow .15s ease, transform .1s ease;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
input:focus,
|
|
172
|
+
select:focus,
|
|
173
|
+
textarea:focus {
|
|
174
|
+
border-color: var(--accent);
|
|
175
|
+
box-shadow: var(--ring);
|
|
176
|
+
transform: translateY(-1px);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.actions {
|
|
180
|
+
margin-top: 12px;
|
|
181
|
+
display: flex;
|
|
182
|
+
gap: 8px;
|
|
183
|
+
flex-wrap: wrap;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
button {
|
|
187
|
+
border-radius: 10px;
|
|
188
|
+
border: 1px solid transparent;
|
|
189
|
+
padding: 9px 13px;
|
|
190
|
+
font-size: 13px;
|
|
191
|
+
font-weight: 700;
|
|
192
|
+
cursor: pointer;
|
|
193
|
+
transition: transform 0.12s ease, filter 0.15s ease, box-shadow 0.15s ease;
|
|
194
|
+
background: linear-gradient(135deg, var(--accent), var(--accent-2));
|
|
195
|
+
color: #062228;
|
|
196
|
+
box-shadow: 0 8px 18px color-mix(in srgb, var(--accent) 35%, transparent);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
button:hover {
|
|
200
|
+
transform: translateY(-1px);
|
|
201
|
+
filter: brightness(1.05);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
button:focus-visible {
|
|
205
|
+
outline: none;
|
|
206
|
+
box-shadow: var(--ring);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
button.secondary,
|
|
210
|
+
button.theme,
|
|
211
|
+
button.table-action {
|
|
212
|
+
background: var(--surface-2);
|
|
213
|
+
color: var(--text);
|
|
214
|
+
border-color: var(--line);
|
|
215
|
+
box-shadow: none;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
button.secondary:hover,
|
|
219
|
+
button.theme:hover,
|
|
220
|
+
button.table-action:hover {
|
|
221
|
+
border-color: var(--line-strong);
|
|
222
|
+
filter: none;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
button.danger {
|
|
226
|
+
background: linear-gradient(135deg, var(--danger-1), var(--danger-2));
|
|
227
|
+
color: #fff;
|
|
228
|
+
box-shadow: 0 8px 18px rgba(239, 68, 68, 0.28);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
button.table-action {
|
|
232
|
+
padding: 6px 9px;
|
|
233
|
+
font-size: 12px;
|
|
234
|
+
font-weight: 600;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.hint {
|
|
238
|
+
margin-top: 8px;
|
|
239
|
+
color: var(--muted);
|
|
240
|
+
font-size: 12px;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.logs {
|
|
244
|
+
margin-top: 10px;
|
|
245
|
+
max-height: 320px;
|
|
246
|
+
overflow: auto;
|
|
247
|
+
border-radius: 10px;
|
|
248
|
+
border: 1px solid var(--line);
|
|
249
|
+
background: color-mix(in srgb, var(--surface-2) 95%, #000 5%);
|
|
250
|
+
padding: 10px;
|
|
251
|
+
white-space: pre-wrap;
|
|
252
|
+
font: 12px/1.45 ui-monospace, "Cascadia Code", "SFMono-Regular", Consolas, monospace;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
table {
|
|
256
|
+
width: 100%;
|
|
257
|
+
border-collapse: collapse;
|
|
258
|
+
font-size: 13px;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
th,
|
|
262
|
+
td {
|
|
263
|
+
text-align: left;
|
|
264
|
+
padding: 10px 8px;
|
|
265
|
+
border-bottom: 1px solid var(--line);
|
|
266
|
+
vertical-align: top;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
th {
|
|
270
|
+
color: var(--muted);
|
|
271
|
+
font-size: 11px;
|
|
272
|
+
letter-spacing: 0.09em;
|
|
273
|
+
text-transform: uppercase;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
tbody tr {
|
|
277
|
+
transition: background-color 0.12s ease;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
tbody tr:hover {
|
|
281
|
+
background: color-mix(in srgb, var(--accent) 9%, transparent);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
tbody tr.selected {
|
|
285
|
+
background: color-mix(in srgb, var(--accent) 13%, transparent);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
tr:last-child td { border-bottom: none; }
|
|
289
|
+
|
|
290
|
+
.pill {
|
|
291
|
+
display: inline-block;
|
|
292
|
+
border-radius: 999px;
|
|
293
|
+
border: 1px solid var(--line);
|
|
294
|
+
background: color-mix(in srgb, var(--surface-2) 95%, transparent);
|
|
295
|
+
padding: 3px 8px;
|
|
296
|
+
font-size: 11px;
|
|
297
|
+
color: var(--muted);
|
|
298
|
+
text-transform: uppercase;
|
|
299
|
+
letter-spacing: 0.08em;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.footer-note {
|
|
303
|
+
grid-column: 1 / -1;
|
|
304
|
+
color: var(--muted);
|
|
305
|
+
font-size: 12px;
|
|
306
|
+
padding: 2px 2px 6px;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
@media (max-width: 1080px) {
|
|
310
|
+
.page { grid-template-columns: 1fr; }
|
|
311
|
+
.grid-2 { grid-template-columns: 1fr; }
|
|
312
|
+
.shell { align-items: flex-start; }
|
|
313
|
+
}
|
|
314
|
+
</style>
|
|
315
|
+
</head>
|
|
316
|
+
<body>
|
|
317
|
+
<div class="page">
|
|
318
|
+
<section class="shell">
|
|
319
|
+
<div class="title-wrap">
|
|
320
|
+
<h1>MCP-for-i Control Plane</h1>
|
|
321
|
+
<p>Secure onboarding, profile management, and runtime updates for agent-driven IBM i workflows.</p>
|
|
322
|
+
</div>
|
|
323
|
+
<div class="top-actions">
|
|
324
|
+
<div class="status"><span class="dot"></span><span id="healthText">Checking local service...</span></div>
|
|
325
|
+
<button id="themeBtn" class="theme" type="button">Switch Theme</button>
|
|
326
|
+
</div>
|
|
327
|
+
</section>
|
|
328
|
+
|
|
329
|
+
<section class="card">
|
|
330
|
+
<h2>Connection Profile</h2>
|
|
331
|
+
<div class="grid-2">
|
|
332
|
+
<div class="field">
|
|
333
|
+
<label>Name</label>
|
|
334
|
+
<input id="name" placeholder="DEV400" />
|
|
335
|
+
</div>
|
|
336
|
+
<div class="field">
|
|
337
|
+
<label>Host</label>
|
|
338
|
+
<input id="host" placeholder="dev400.company.com" />
|
|
339
|
+
</div>
|
|
340
|
+
<div class="field">
|
|
341
|
+
<label>Port</label>
|
|
342
|
+
<input id="port" type="number" value="22" />
|
|
343
|
+
</div>
|
|
344
|
+
<div class="field">
|
|
345
|
+
<label>Username</label>
|
|
346
|
+
<input id="username" placeholder="DEVUSER" />
|
|
347
|
+
</div>
|
|
348
|
+
<div class="field">
|
|
349
|
+
<label>Private Key Path</label>
|
|
350
|
+
<input id="privateKeyPath" placeholder="C:/keys/id_rsa" />
|
|
351
|
+
</div>
|
|
352
|
+
<div class="field">
|
|
353
|
+
<label>Password (stored in keychain)</label>
|
|
354
|
+
<input id="password" type="password" placeholder="Optional" />
|
|
355
|
+
</div>
|
|
356
|
+
<div class="field">
|
|
357
|
+
<label>Policy</label>
|
|
358
|
+
<select id="policyProfile">
|
|
359
|
+
<option value="guarded">guarded</option>
|
|
360
|
+
<option value="read-only">read-only</option>
|
|
361
|
+
<option value="power-user">power-user</option>
|
|
362
|
+
</select>
|
|
363
|
+
</div>
|
|
364
|
+
<div class="field">
|
|
365
|
+
<label>Current Library (optional)</label>
|
|
366
|
+
<input id="currentLibrary" placeholder="QGPL" />
|
|
367
|
+
</div>
|
|
368
|
+
</div>
|
|
369
|
+
<div class="actions">
|
|
370
|
+
<button id="saveBtn" type="button">Save Connection</button>
|
|
371
|
+
<button id="renameBtn" class="secondary" type="button">Rename Selected</button>
|
|
372
|
+
<button id="deleteBtn" class="danger" type="button">Delete Selected</button>
|
|
373
|
+
<button id="clearBtn" class="secondary" type="button">Clear Form</button>
|
|
374
|
+
</div>
|
|
375
|
+
<div class="hint">Credentials never persist in plain config. Passwords go only to the local control service and OS keychain.</div>
|
|
376
|
+
</section>
|
|
377
|
+
|
|
378
|
+
<section class="card">
|
|
379
|
+
<h2>Runtime Actions</h2>
|
|
380
|
+
<div class="grid-2">
|
|
381
|
+
<div class="field">
|
|
382
|
+
<label>Skills Repo URL</label>
|
|
383
|
+
<input id="skillsRepoUrl" placeholder="https://github.com/ashishthomas-pcr/mcp-for-i-skills.git" />
|
|
384
|
+
</div>
|
|
385
|
+
<div class="field">
|
|
386
|
+
<label>Skills Branch</label>
|
|
387
|
+
<input id="skillsBranch" placeholder="main" />
|
|
388
|
+
</div>
|
|
389
|
+
</div>
|
|
390
|
+
<div class="actions">
|
|
391
|
+
<button id="installBtn" type="button">Install/Repair MCP</button>
|
|
392
|
+
<button id="updateMcpBtn" class="secondary" type="button">Update MCP from GitHub</button>
|
|
393
|
+
<button id="updateSkillsBtn" class="secondary" type="button">Update Skills</button>
|
|
394
|
+
<button id="refreshStatusBtn" class="secondary" type="button">Refresh Status</button>
|
|
395
|
+
<button id="setupAutostartBtn" class="secondary" type="button">Enable Background Startup</button>
|
|
396
|
+
<button id="removeAutostartBtn" class="secondary" type="button">Disable Background Startup</button>
|
|
397
|
+
<button id="autostartStatusBtn" class="secondary" type="button">Refresh Startup Status</button>
|
|
398
|
+
</div>
|
|
399
|
+
<div id="autostartInfo" class="hint">Startup status: checking...</div>
|
|
400
|
+
<div id="logs" class="logs">No jobs yet.</div>
|
|
401
|
+
<div class="hint">Actions stream logs live. Skills update can clone or pull from the repo and branch shown above.</div>
|
|
402
|
+
</section>
|
|
403
|
+
|
|
404
|
+
<section class="card" style="grid-column: 1 / -1;">
|
|
405
|
+
<h2>Saved Connections</h2>
|
|
406
|
+
<table>
|
|
407
|
+
<thead>
|
|
408
|
+
<tr>
|
|
409
|
+
<th>Name</th>
|
|
410
|
+
<th>Host</th>
|
|
411
|
+
<th>User</th>
|
|
412
|
+
<th>Policy</th>
|
|
413
|
+
<th>Actions</th>
|
|
414
|
+
</tr>
|
|
415
|
+
</thead>
|
|
416
|
+
<tbody id="connectionsBody"></tbody>
|
|
417
|
+
</table>
|
|
418
|
+
</section>
|
|
419
|
+
|
|
420
|
+
<div class="footer-note">Tip: select a connection from the table to edit, rename, or rotate password.</div>
|
|
421
|
+
</div>
|
|
422
|
+
|
|
423
|
+
<script>
|
|
424
|
+
const THEME_KEY = "mcp_for_i_theme";
|
|
425
|
+
const SKILLS_REPO_KEY = "mcp_for_i_skills_repo";
|
|
426
|
+
const SKILLS_BRANCH_KEY = "mcp_for_i_skills_branch";
|
|
427
|
+
const DEFAULT_SKILLS_REPO = "https://github.com/ashishthomas-pcr/mcp-for-i-skills.git";
|
|
428
|
+
const DEFAULT_SKILLS_BRANCH = "main";
|
|
429
|
+
|
|
430
|
+
const state = {
|
|
431
|
+
selectedName: "",
|
|
432
|
+
jobs: {},
|
|
433
|
+
autostart: null
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
const $ = id => document.getElementById(id);
|
|
437
|
+
|
|
438
|
+
function readStoredTheme() {
|
|
439
|
+
try {
|
|
440
|
+
const stored = localStorage.getItem(THEME_KEY);
|
|
441
|
+
if (stored === "light" || stored === "dark") return stored;
|
|
442
|
+
} catch {}
|
|
443
|
+
if (window.matchMedia && window.matchMedia("(prefers-color-scheme: light)").matches) {
|
|
444
|
+
return "light";
|
|
445
|
+
}
|
|
446
|
+
return "dark";
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function applyTheme(theme) {
|
|
450
|
+
document.body.setAttribute("data-theme", theme);
|
|
451
|
+
const btn = $("themeBtn");
|
|
452
|
+
if (btn) {
|
|
453
|
+
btn.textContent = theme === "dark" ? "Switch to Light" : "Switch to Dark";
|
|
454
|
+
}
|
|
455
|
+
try {
|
|
456
|
+
localStorage.setItem(THEME_KEY, theme);
|
|
457
|
+
} catch {}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function toggleTheme() {
|
|
461
|
+
const current = document.body.getAttribute("data-theme") === "light" ? "light" : "dark";
|
|
462
|
+
applyTheme(current === "dark" ? "light" : "dark");
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function loadSkillsUpdateSettings() {
|
|
466
|
+
let repoUrl = DEFAULT_SKILLS_REPO;
|
|
467
|
+
let branch = DEFAULT_SKILLS_BRANCH;
|
|
468
|
+
try {
|
|
469
|
+
const storedRepo = localStorage.getItem(SKILLS_REPO_KEY);
|
|
470
|
+
const storedBranch = localStorage.getItem(SKILLS_BRANCH_KEY);
|
|
471
|
+
if (storedRepo && storedRepo.trim()) repoUrl = storedRepo.trim();
|
|
472
|
+
if (storedBranch && storedBranch.trim()) branch = storedBranch.trim();
|
|
473
|
+
} catch {}
|
|
474
|
+
$("skillsRepoUrl").value = repoUrl;
|
|
475
|
+
$("skillsBranch").value = branch;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function getSkillsUpdatePayload() {
|
|
479
|
+
const repoUrl = $("skillsRepoUrl").value.trim();
|
|
480
|
+
const branch = $("skillsBranch").value.trim();
|
|
481
|
+
try {
|
|
482
|
+
localStorage.setItem(SKILLS_REPO_KEY, repoUrl || DEFAULT_SKILLS_REPO);
|
|
483
|
+
localStorage.setItem(SKILLS_BRANCH_KEY, branch || DEFAULT_SKILLS_BRANCH);
|
|
484
|
+
} catch {}
|
|
485
|
+
return {
|
|
486
|
+
repoUrl: repoUrl || DEFAULT_SKILLS_REPO,
|
|
487
|
+
branch: branch || DEFAULT_SKILLS_BRANCH
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
async function api(path, options = {}) {
|
|
492
|
+
const res = await fetch(path, {
|
|
493
|
+
headers: { "Content-Type": "application/json" },
|
|
494
|
+
...options
|
|
495
|
+
});
|
|
496
|
+
if (!res.ok) {
|
|
497
|
+
const text = await res.text();
|
|
498
|
+
throw new Error(text || ("HTTP " + res.status));
|
|
499
|
+
}
|
|
500
|
+
if (res.status === 204) return null;
|
|
501
|
+
return res.json();
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function formToPayload() {
|
|
505
|
+
const name = $("name").value.trim();
|
|
506
|
+
const payload = {
|
|
507
|
+
name,
|
|
508
|
+
host: $("host").value.trim(),
|
|
509
|
+
port: Number($("port").value || 22),
|
|
510
|
+
username: $("username").value.trim(),
|
|
511
|
+
privateKeyPath: $("privateKeyPath").value.trim() || undefined,
|
|
512
|
+
password: $("password").value || undefined,
|
|
513
|
+
storePassword: true,
|
|
514
|
+
policy: { profile: $("policyProfile").value },
|
|
515
|
+
settings: {}
|
|
516
|
+
};
|
|
517
|
+
const currentLibrary = $("currentLibrary").value.trim();
|
|
518
|
+
if (currentLibrary) payload.settings.currentLibrary = currentLibrary;
|
|
519
|
+
return payload;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function resetForm() {
|
|
523
|
+
state.selectedName = "";
|
|
524
|
+
$("name").value = "";
|
|
525
|
+
$("host").value = "";
|
|
526
|
+
$("port").value = "22";
|
|
527
|
+
$("username").value = "";
|
|
528
|
+
$("privateKeyPath").value = "";
|
|
529
|
+
$("password").value = "";
|
|
530
|
+
$("policyProfile").value = "guarded";
|
|
531
|
+
$("currentLibrary").value = "";
|
|
532
|
+
renderConnections(window.__connectionsCache || []);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function renderConnections(list) {
|
|
536
|
+
window.__connectionsCache = list;
|
|
537
|
+
const body = $("connectionsBody");
|
|
538
|
+
body.innerHTML = "";
|
|
539
|
+
for (const conn of list) {
|
|
540
|
+
const tr = document.createElement("tr");
|
|
541
|
+
if (conn.name === state.selectedName) tr.className = "selected";
|
|
542
|
+
const policy = (conn.policy && conn.policy.profile) || "guarded";
|
|
543
|
+
tr.innerHTML = \`
|
|
544
|
+
<td><strong>\${escapeHtml(conn.name)}</strong></td>
|
|
545
|
+
<td>\${escapeHtml(conn.host || "")}:\${escapeHtml(String(conn.port || 22))}</td>
|
|
546
|
+
<td>\${escapeHtml(conn.username || "")}</td>
|
|
547
|
+
<td><span class="pill">\${escapeHtml(policy)}</span></td>
|
|
548
|
+
<td>
|
|
549
|
+
<button class="table-action" data-action="select" data-name="\${encodeURIComponent(conn.name)}" type="button">Select</button>
|
|
550
|
+
<button class="table-action" data-action="password" data-name="\${encodeURIComponent(conn.name)}" type="button">Set Password</button>
|
|
551
|
+
</td>\`;
|
|
552
|
+
body.appendChild(tr);
|
|
553
|
+
}
|
|
554
|
+
body.querySelectorAll("button").forEach(btn => {
|
|
555
|
+
btn.addEventListener("click", onTableAction);
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
async function loadConnections() {
|
|
560
|
+
const data = await api("/api/connections");
|
|
561
|
+
renderConnections(data.connections || []);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
async function loadHealth() {
|
|
565
|
+
const health = await api("/api/health");
|
|
566
|
+
$("healthText").textContent = health.ok ? "Local control service online" : "Service unavailable";
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function renderLogs(jobs) {
|
|
570
|
+
state.jobs = jobs || {};
|
|
571
|
+
const lines = [];
|
|
572
|
+
for (const key of Object.keys(jobs || {})) {
|
|
573
|
+
const j = jobs[key];
|
|
574
|
+
lines.push(\`[\${key}] \${j.status}\`);
|
|
575
|
+
if (j.startedAt) lines.push(\` started: \${j.startedAt}\`);
|
|
576
|
+
if (j.finishedAt) lines.push(\` finished: \${j.finishedAt}\`);
|
|
577
|
+
if (j.error) lines.push(\` error: \${j.error}\`);
|
|
578
|
+
for (const line of j.output || []) lines.push(\` \${line}\`);
|
|
579
|
+
lines.push("");
|
|
580
|
+
}
|
|
581
|
+
$("logs").textContent = lines.length ? lines.join("\\n") : "No jobs yet.";
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
async function loadRuntimeStatus() {
|
|
585
|
+
const status = await api("/api/runtime/status");
|
|
586
|
+
renderLogs(status.jobs || {});
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function renderAutostart(status) {
|
|
590
|
+
state.autostart = status || null;
|
|
591
|
+
const el = $("autostartInfo");
|
|
592
|
+
if (!status) {
|
|
593
|
+
el.textContent = "Startup status: unavailable";
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
if (status.supported === false) {
|
|
597
|
+
el.textContent = \`Startup status: not managed on platform '\${status.platform}'. Running now: \${status.running ? "yes" : "no"}.\`;
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
const installed = status.installed ? "enabled" : "disabled";
|
|
601
|
+
const stateText = status.state ? \` (\${status.state})\` : "";
|
|
602
|
+
el.textContent = \`Startup status: \${installed}\${stateText}. Running now: \${status.running ? "yes" : "no"}.\`;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
async function loadAutostartStatus() {
|
|
606
|
+
const data = await api("/api/runtime/autostart/status");
|
|
607
|
+
renderAutostart(data.status || null);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
async function onSave() {
|
|
611
|
+
const payload = formToPayload();
|
|
612
|
+
if (!payload.name || !payload.host || !payload.username) {
|
|
613
|
+
alert("Name, host, and username are required.");
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
if (state.selectedName && state.selectedName === payload.name) {
|
|
617
|
+
await api("/api/connections/" + encodeURIComponent(state.selectedName), {
|
|
618
|
+
method: "PUT",
|
|
619
|
+
body: JSON.stringify(payload)
|
|
620
|
+
});
|
|
621
|
+
} else {
|
|
622
|
+
await api("/api/connections", {
|
|
623
|
+
method: "POST",
|
|
624
|
+
body: JSON.stringify(payload)
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
state.selectedName = payload.name;
|
|
628
|
+
await loadConnections();
|
|
629
|
+
$("password").value = "";
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
async function onRename() {
|
|
633
|
+
if (!state.selectedName) {
|
|
634
|
+
alert("Select a connection first.");
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
const newName = prompt("Rename connection to:", state.selectedName);
|
|
638
|
+
if (!newName || !newName.trim() || newName === state.selectedName) return;
|
|
639
|
+
await api("/api/connections/" + encodeURIComponent(state.selectedName) + "/rename", {
|
|
640
|
+
method: "POST",
|
|
641
|
+
body: JSON.stringify({ newName: newName.trim() })
|
|
642
|
+
});
|
|
643
|
+
state.selectedName = newName.trim();
|
|
644
|
+
await loadConnections();
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
async function onDelete() {
|
|
648
|
+
if (!state.selectedName) {
|
|
649
|
+
alert("Select a connection first.");
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
if (!confirm("Delete connection '" + state.selectedName + "'?")) return;
|
|
653
|
+
await api("/api/connections/" + encodeURIComponent(state.selectedName), { method: "DELETE" });
|
|
654
|
+
resetForm();
|
|
655
|
+
await loadConnections();
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
async function onTableAction(e) {
|
|
659
|
+
const btn = e.currentTarget;
|
|
660
|
+
const action = btn.dataset.action;
|
|
661
|
+
const name = decodeURIComponent(btn.dataset.name || "");
|
|
662
|
+
if (!name) return;
|
|
663
|
+
if (action === "select") {
|
|
664
|
+
const res = await api("/api/connections/" + encodeURIComponent(name));
|
|
665
|
+
const conn = res.connection;
|
|
666
|
+
state.selectedName = conn.name;
|
|
667
|
+
$("name").value = conn.name;
|
|
668
|
+
$("host").value = conn.host || "";
|
|
669
|
+
$("port").value = String(conn.port || 22);
|
|
670
|
+
$("username").value = conn.username || "";
|
|
671
|
+
$("privateKeyPath").value = conn.privateKeyPath || "";
|
|
672
|
+
$("policyProfile").value = (conn.policy && conn.policy.profile) || "guarded";
|
|
673
|
+
$("currentLibrary").value = (conn.settings && conn.settings.currentLibrary) || "";
|
|
674
|
+
$("password").value = "";
|
|
675
|
+
renderConnections(window.__connectionsCache || []);
|
|
676
|
+
}
|
|
677
|
+
if (action === "password") {
|
|
678
|
+
const pwd = prompt("Set new password for " + name + ":");
|
|
679
|
+
if (!pwd) return;
|
|
680
|
+
await api("/api/connections/" + encodeURIComponent(name) + "/password", {
|
|
681
|
+
method: "POST",
|
|
682
|
+
body: JSON.stringify({ password: pwd })
|
|
683
|
+
});
|
|
684
|
+
alert("Password updated in keychain.");
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
async function triggerJob(path, payload) {
|
|
689
|
+
await api(path, {
|
|
690
|
+
method: "POST",
|
|
691
|
+
body: payload ? JSON.stringify(payload) : undefined
|
|
692
|
+
});
|
|
693
|
+
await loadRuntimeStatus();
|
|
694
|
+
await loadAutostartStatus();
|
|
695
|
+
startJobPolling();
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
let pollId = 0;
|
|
699
|
+
function startJobPolling() {
|
|
700
|
+
if (pollId) return;
|
|
701
|
+
pollId = setInterval(async () => {
|
|
702
|
+
await loadRuntimeStatus();
|
|
703
|
+
const jobs = Object.values(state.jobs || {});
|
|
704
|
+
const running = jobs.some(j => j.status === "running");
|
|
705
|
+
if (!running) {
|
|
706
|
+
clearInterval(pollId);
|
|
707
|
+
pollId = 0;
|
|
708
|
+
}
|
|
709
|
+
}, 1500);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function escapeHtml(str) {
|
|
713
|
+
return String(str)
|
|
714
|
+
.replaceAll("&", "&")
|
|
715
|
+
.replaceAll("<", "<")
|
|
716
|
+
.replaceAll(">", ">")
|
|
717
|
+
.replaceAll('"', """);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
$("themeBtn").addEventListener("click", toggleTheme);
|
|
721
|
+
$("saveBtn").addEventListener("click", onSave);
|
|
722
|
+
$("renameBtn").addEventListener("click", onRename);
|
|
723
|
+
$("deleteBtn").addEventListener("click", onDelete);
|
|
724
|
+
$("clearBtn").addEventListener("click", resetForm);
|
|
725
|
+
$("installBtn").addEventListener("click", () => triggerJob("/api/runtime/install"));
|
|
726
|
+
$("updateMcpBtn").addEventListener("click", () => triggerJob("/api/runtime/update/mcp"));
|
|
727
|
+
$("updateSkillsBtn").addEventListener("click", () => triggerJob("/api/runtime/update/skills", getSkillsUpdatePayload()));
|
|
728
|
+
$("refreshStatusBtn").addEventListener("click", loadRuntimeStatus);
|
|
729
|
+
$("setupAutostartBtn").addEventListener("click", () => triggerJob("/api/runtime/autostart/setup"));
|
|
730
|
+
$("removeAutostartBtn").addEventListener("click", () => triggerJob("/api/runtime/autostart/remove"));
|
|
731
|
+
$("autostartStatusBtn").addEventListener("click", loadAutostartStatus);
|
|
732
|
+
|
|
733
|
+
applyTheme(readStoredTheme());
|
|
734
|
+
loadSkillsUpdateSettings();
|
|
735
|
+
Promise.all([loadHealth(), loadConnections(), loadRuntimeStatus(), loadAutostartStatus()]).catch(err => {
|
|
736
|
+
$("logs").textContent = "Startup error: " + err.message;
|
|
737
|
+
});
|
|
738
|
+
</script>
|
|
739
|
+
</body>
|
|
740
|
+
</html>`;
|
|
741
|
+
}
|