cli-jaw 0.1.11 → 0.1.12
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.ko.md +44 -13
- package/README.md +12 -11
- package/README.zh-CN.md +43 -12
- package/dist/bin/commands/doctor.js +13 -2
- package/dist/bin/commands/doctor.js.map +1 -1
- package/dist/bin/commands/mcp.js +15 -18
- package/dist/bin/commands/mcp.js.map +1 -1
- package/dist/bin/commands/serve.js +3 -28
- package/dist/bin/commands/serve.js.map +1 -1
- package/dist/bin/commands/skill.js +9 -6
- package/dist/bin/commands/skill.js.map +1 -1
- package/dist/lib/mcp-sync.js +123 -31
- package/dist/lib/mcp-sync.js.map +1 -1
- package/{scripts → dist/scripts}/check-copilot-gap.js +24 -17
- package/dist/scripts/check-copilot-gap.js.map +1 -0
- package/{scripts/check-deps-offline.mjs → dist/scripts/check-deps-offline.js} +24 -20
- package/dist/scripts/check-deps-offline.js.map +1 -0
- package/dist/scripts/fresh-install-smoke.js +120 -0
- package/dist/scripts/fresh-install-smoke.js.map +1 -0
- package/{scripts/i18n-registry.py → dist/scripts/i18n-registry.js} +115 -122
- package/dist/scripts/i18n-registry.js.map +1 -0
- package/dist/server.js +34 -26
- package/dist/server.js.map +1 -1
- package/dist/src/cli/command-context.js +13 -3
- package/dist/src/cli/command-context.js.map +1 -1
- package/dist/src/prompt/builder.js +28 -1
- package/dist/src/prompt/builder.js.map +1 -1
- package/package.json +9 -5
- package/public/dist/bundle.js +72 -77
- package/public/dist/bundle.js.map +4 -4
- package/public/index.html +1 -3
- package/public/js/{api.js → api.ts} +18 -12
- package/public/js/{constants.js → constants.ts} +44 -24
- package/public/js/features/{appname.js → appname.ts} +13 -12
- package/public/js/features/{chat.js → chat.ts} +46 -37
- package/public/js/features/{employees.js → employees.ts} +67 -38
- package/public/js/features/heartbeat.ts +90 -0
- package/public/js/features/{i18n.js → i18n.ts} +20 -20
- package/public/js/features/memory.ts +125 -0
- package/public/js/features/{settings.js → settings.ts} +125 -93
- package/public/js/features/{sidebar.js → sidebar.ts} +15 -16
- package/public/js/features/{skills.js → skills.ts} +29 -16
- package/public/js/features/{slash-commands.js → slash-commands.ts} +34 -29
- package/public/js/features/{theme.js → theme.ts} +4 -4
- package/public/js/{locale.js → locale.ts} +3 -3
- package/public/js/main.ts +280 -0
- package/public/js/{render.js → render.ts} +34 -107
- package/public/js/state.ts +38 -0
- package/public/js/{ui.js → ui.ts} +60 -63
- package/public/js/{ws.js → ws.ts} +46 -20
- package/public/locales/en.json +1 -0
- package/public/locales/ko.json +1 -0
- package/scripts/check-copilot-gap.ts +75 -0
- package/scripts/check-deps-offline.ts +98 -0
- package/scripts/fresh-install-smoke.ts +130 -0
- package/scripts/i18n-registry.ts +230 -0
- package/scripts/postinstall-guard.cjs +5 -0
- package/dist/bin/cli-claw.js +0 -96
- package/dist/bin/cli-claw.js.map +0 -1
- package/public/js/features/heartbeat.js +0 -80
- package/public/js/features/memory.js +0 -85
- package/public/js/main.js +0 -278
- package/public/js/state.js +0 -16
|
@@ -1,27 +1,41 @@
|
|
|
1
1
|
// ── Settings Feature ──
|
|
2
2
|
import { MODEL_MAP, loadCliRegistry, getCliKeys, getCliMeta } from '../constants.js';
|
|
3
|
+
import type { CliEntry } from '../constants.js';
|
|
3
4
|
import { escapeHtml } from '../render.js';
|
|
4
5
|
import { syncStoredLocale } from '../locale.js';
|
|
5
6
|
import { t } from './i18n.js';
|
|
6
7
|
import { api, apiJson, apiFire } from '../api.js';
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
interface PerCliConfig { model?: string; effort?: string; }
|
|
10
|
+
interface TelegramConfig { enabled?: boolean; token?: string; allowedChatIds?: number[]; }
|
|
11
|
+
interface QuotaWindow { label: string; percent: number; }
|
|
12
|
+
interface QuotaEntry { account?: { email?: string; type?: string; plan?: string; tier?: string }; windows?: QuotaWindow[]; }
|
|
13
|
+
interface SettingsData {
|
|
14
|
+
cli: string; workingDir: string; permissions: string; locale?: string;
|
|
15
|
+
perCli?: Record<string, PerCliConfig>;
|
|
16
|
+
activeOverrides?: Record<string, PerCliConfig>;
|
|
17
|
+
telegram?: TelegramConfig;
|
|
18
|
+
fallbackOrder?: string[];
|
|
19
|
+
memory?: { cli?: string };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function toCap(cli: string): string {
|
|
9
23
|
return cli.charAt(0).toUpperCase() + cli.slice(1);
|
|
10
24
|
}
|
|
11
25
|
|
|
12
|
-
function getModelSelect(cli) {
|
|
13
|
-
return document.getElementById('model' + toCap(cli));
|
|
26
|
+
function getModelSelect(cli: string): HTMLSelectElement | null {
|
|
27
|
+
return document.getElementById('model' + toCap(cli)) as HTMLSelectElement | null;
|
|
14
28
|
}
|
|
15
29
|
|
|
16
|
-
function getCustomModelInput(cli) {
|
|
17
|
-
return document.getElementById('customModel' + toCap(cli));
|
|
30
|
+
function getCustomModelInput(cli: string): HTMLInputElement | null {
|
|
31
|
+
return document.getElementById('customModel' + toCap(cli)) as HTMLInputElement | null;
|
|
18
32
|
}
|
|
19
33
|
|
|
20
|
-
function getEffortSelect(cli) {
|
|
21
|
-
return document.getElementById('effort' + toCap(cli));
|
|
34
|
+
function getEffortSelect(cli: string): HTMLSelectElement | null {
|
|
35
|
+
return document.getElementById('effort' + toCap(cli)) as HTMLSelectElement | null;
|
|
22
36
|
}
|
|
23
37
|
|
|
24
|
-
function setSelectOptions(selectEl, values, { includeCustom = false, includeDefault = false, selected = '' } = {}) {
|
|
38
|
+
function setSelectOptions(selectEl: HTMLSelectElement | null, values: string[], { includeCustom = false, includeDefault = false, selected = '' } = {}): void {
|
|
25
39
|
if (!selectEl) return;
|
|
26
40
|
const defaultHtml = includeDefault ? '<option value="default">default</option>' : '';
|
|
27
41
|
const customHtml = includeCustom ? `<option value="__custom__">${t('model.customOption')}</option>` : '';
|
|
@@ -33,7 +47,7 @@ function setSelectOptions(selectEl, values, { includeCustom = false, includeDefa
|
|
|
33
47
|
}
|
|
34
48
|
}
|
|
35
49
|
|
|
36
|
-
function appendCustomOption(selectEl, value) {
|
|
50
|
+
function appendCustomOption(selectEl: HTMLSelectElement | null, value: string): void {
|
|
37
51
|
if (!selectEl || !value) return;
|
|
38
52
|
if (Array.from(selectEl.options).some(o => o.value === value)) return;
|
|
39
53
|
const opt = document.createElement('option');
|
|
@@ -44,10 +58,10 @@ function appendCustomOption(selectEl, value) {
|
|
|
44
58
|
else selectEl.appendChild(opt);
|
|
45
59
|
}
|
|
46
60
|
|
|
47
|
-
function syncCliOptionSelects(settings = null) {
|
|
61
|
+
function syncCliOptionSelects(settings: SettingsData | null = null): void {
|
|
48
62
|
const cliKeys = getCliKeys();
|
|
49
63
|
|
|
50
|
-
const selCli = document.getElementById('selCli');
|
|
64
|
+
const selCli = document.getElementById('selCli') as HTMLSelectElement | null;
|
|
51
65
|
if (selCli) {
|
|
52
66
|
const current = settings?.cli || selCli.value || cliKeys[0] || 'claude';
|
|
53
67
|
selCli.innerHTML = cliKeys.map(cli => {
|
|
@@ -57,7 +71,7 @@ function syncCliOptionSelects(settings = null) {
|
|
|
57
71
|
if (Array.from(selCli.options).some(o => o.value === current)) selCli.value = current;
|
|
58
72
|
}
|
|
59
73
|
|
|
60
|
-
const memCli = document.getElementById('memCli');
|
|
74
|
+
const memCli = document.getElementById('memCli') as HTMLSelectElement | null;
|
|
61
75
|
if (memCli) {
|
|
62
76
|
const current = settings?.memory?.cli || memCli.value || '';
|
|
63
77
|
memCli.innerHTML = '<option value="">(active CLI)</option>' +
|
|
@@ -66,7 +80,7 @@ function syncCliOptionSelects(settings = null) {
|
|
|
66
80
|
}
|
|
67
81
|
}
|
|
68
82
|
|
|
69
|
-
function syncPerCliModelAndEffortControls(settings = null) {
|
|
83
|
+
function syncPerCliModelAndEffortControls(settings: SettingsData | null = null): void {
|
|
70
84
|
for (const cli of getCliKeys()) {
|
|
71
85
|
const modelSel = getModelSelect(cli);
|
|
72
86
|
if (modelSel) {
|
|
@@ -96,8 +110,8 @@ function syncPerCliModelAndEffortControls(settings = null) {
|
|
|
96
110
|
}
|
|
97
111
|
}
|
|
98
112
|
|
|
99
|
-
function syncActiveEffortOptions(cli, selected = '') {
|
|
100
|
-
const selEffort = document.getElementById('selEffort');
|
|
113
|
+
function syncActiveEffortOptions(cli: string, selected = ''): void {
|
|
114
|
+
const selEffort = document.getElementById('selEffort') as HTMLSelectElement | null;
|
|
101
115
|
if (!selEffort) return;
|
|
102
116
|
const meta = getCliMeta(cli);
|
|
103
117
|
if (meta?.effortNote) {
|
|
@@ -118,24 +132,26 @@ function syncActiveEffortOptions(cli, selected = '') {
|
|
|
118
132
|
if (Array.from(selEffort.options).some(o => o.value === selected)) selEffort.value = selected;
|
|
119
133
|
}
|
|
120
134
|
|
|
121
|
-
export async function loadSettings() {
|
|
135
|
+
export async function loadSettings(): Promise<void> {
|
|
122
136
|
await loadCliRegistry();
|
|
123
|
-
const s = await api('/api/settings');
|
|
137
|
+
const s = await api<SettingsData>('/api/settings');
|
|
124
138
|
if (!s) return;
|
|
125
|
-
syncStoredLocale(s.locale);
|
|
139
|
+
syncStoredLocale(s.locale ?? '');
|
|
126
140
|
syncCliOptionSelects(s);
|
|
127
141
|
syncPerCliModelAndEffortControls(s);
|
|
128
142
|
|
|
129
|
-
const selCli = document.getElementById('selCli');
|
|
130
|
-
if (Array.from(selCli.options).some(o => o.value === s.cli)) {
|
|
143
|
+
const selCli = document.getElementById('selCli') as HTMLSelectElement | null;
|
|
144
|
+
if (selCli && Array.from(selCli.options).some(o => o.value === s.cli)) {
|
|
131
145
|
selCli.value = s.cli;
|
|
132
146
|
}
|
|
133
|
-
document.getElementById('inpCwd')
|
|
134
|
-
|
|
147
|
+
const cwdEl = document.getElementById('inpCwd');
|
|
148
|
+
if (cwdEl) cwdEl.textContent = s.workingDir;
|
|
149
|
+
const headerEl = document.getElementById('headerCli');
|
|
150
|
+
if (headerEl) headerEl.textContent = s.cli;
|
|
135
151
|
setPerm(s.permissions, false);
|
|
136
152
|
|
|
137
153
|
if (s.perCli) {
|
|
138
|
-
for (const [cli, cfg] of Object.entries(s.perCli)) {
|
|
154
|
+
for (const [cli, cfg] of Object.entries(s.perCli) as [string, PerCliConfig][]) {
|
|
139
155
|
const modelEl = getModelSelect(cli);
|
|
140
156
|
const effortEl = getEffortSelect(cli);
|
|
141
157
|
if (modelEl && cfg.model) {
|
|
@@ -151,7 +167,8 @@ export async function loadSettings() {
|
|
|
151
167
|
const pc = s.perCli?.[s.cli] || {};
|
|
152
168
|
const activeModel = ao.model || pc.model;
|
|
153
169
|
const activeEffort = ao.effort || pc.effort || '';
|
|
154
|
-
|
|
170
|
+
const selModel = document.getElementById('selModel') as HTMLSelectElement | null;
|
|
171
|
+
if (activeModel && selModel) selModel.value = activeModel;
|
|
155
172
|
syncActiveEffortOptions(s.cli, activeEffort);
|
|
156
173
|
|
|
157
174
|
loadTelegramSettings(s);
|
|
@@ -159,11 +176,17 @@ export async function loadSettings() {
|
|
|
159
176
|
loadMcpServers();
|
|
160
177
|
}
|
|
161
178
|
|
|
162
|
-
|
|
179
|
+
interface McpData { servers: Record<string, { command: string; args?: string[] }>; }
|
|
180
|
+
interface McpSyncResult { results: Record<string, boolean>; }
|
|
181
|
+
interface McpInstallEntry { status: string; bin?: string; }
|
|
182
|
+
interface McpInstallResult { results: Record<string, McpInstallEntry>; }
|
|
183
|
+
|
|
184
|
+
export async function loadMcpServers(): Promise<void> {
|
|
163
185
|
try {
|
|
164
|
-
const d = await api('/api/mcp');
|
|
186
|
+
const d = await api<McpData>('/api/mcp');
|
|
165
187
|
if (!d) return;
|
|
166
188
|
const el = document.getElementById('mcpServerList');
|
|
189
|
+
if (!el) return;
|
|
167
190
|
const names = Object.entries(d.servers || {});
|
|
168
191
|
if (!names.length) { el.textContent = t('mcp.noServers'); return; }
|
|
169
192
|
el.innerHTML = names.map(([n, s]) =>
|
|
@@ -172,49 +195,52 @@ export async function loadMcpServers() {
|
|
|
172
195
|
} catch { }
|
|
173
196
|
}
|
|
174
197
|
|
|
175
|
-
export async function syncMcpServers() {
|
|
198
|
+
export async function syncMcpServers(): Promise<void> {
|
|
176
199
|
const resultEl = document.getElementById('mcpSyncResult');
|
|
200
|
+
if (!resultEl) return;
|
|
177
201
|
resultEl.style.display = 'block';
|
|
178
202
|
resultEl.textContent = t('mcp.syncing');
|
|
179
203
|
try {
|
|
180
|
-
const d = await apiJson('/api/mcp/sync', 'POST', {});
|
|
204
|
+
const d = await apiJson('/api/mcp/sync', 'POST', {}) as McpSyncResult | null;
|
|
181
205
|
if (!d) { resultEl.textContent = '❌ sync failed'; return; }
|
|
182
206
|
const r = d.results || {};
|
|
183
207
|
resultEl.innerHTML = Object.entries(r).map(([k, v]) =>
|
|
184
208
|
`${v ? '✅' : '⏭️'} ${k}`
|
|
185
209
|
).join(' ');
|
|
186
|
-
} catch (e) { resultEl.textContent = '❌ ' + e.message; }
|
|
210
|
+
} catch (e) { resultEl.textContent = '❌ ' + (e as Error).message; }
|
|
187
211
|
}
|
|
188
212
|
|
|
189
|
-
export async function installMcpGlobal() {
|
|
213
|
+
export async function installMcpGlobal(): Promise<void> {
|
|
190
214
|
const resultEl = document.getElementById('mcpSyncResult');
|
|
215
|
+
if (!resultEl) return;
|
|
191
216
|
resultEl.style.display = 'block';
|
|
192
217
|
resultEl.textContent = t('mcp.installing');
|
|
193
218
|
try {
|
|
194
|
-
const d = await apiJson('/api/mcp/install', 'POST', {});
|
|
219
|
+
const d = await apiJson('/api/mcp/install', 'POST', {}) as McpInstallResult | null;
|
|
195
220
|
if (!d) { resultEl.textContent = '❌ install failed'; return; }
|
|
196
221
|
resultEl.innerHTML = Object.entries(d.results || {}).map(([k, v]) => {
|
|
197
222
|
const icon = v.status === 'installed' ? '✅' : v.status === 'skip' ? '⏭️' : '❌';
|
|
198
223
|
return `${icon} <b>${k}</b>: ${v.status}${v.bin ? ' → ' + v.bin : ''}`;
|
|
199
224
|
}).join('<br>');
|
|
200
225
|
loadMcpServers();
|
|
201
|
-
} catch (e) { resultEl.textContent = '❌ ' + e.message; }
|
|
226
|
+
} catch (e) { resultEl.textContent = '❌ ' + (e as Error).message; }
|
|
202
227
|
}
|
|
203
228
|
|
|
204
|
-
export async function updateSettings() {
|
|
229
|
+
export async function updateSettings(): Promise<void> {
|
|
205
230
|
const s = {
|
|
206
|
-
cli: document.getElementById('selCli')
|
|
231
|
+
cli: (document.getElementById('selCli') as HTMLSelectElement)?.value || 'claude',
|
|
207
232
|
};
|
|
208
|
-
document.getElementById('headerCli')
|
|
233
|
+
const hdr = document.getElementById('headerCli');
|
|
234
|
+
if (hdr) hdr.textContent = s.cli;
|
|
209
235
|
await apiJson('/api/settings', 'PUT', s);
|
|
210
236
|
}
|
|
211
237
|
|
|
212
|
-
export function setPerm(
|
|
238
|
+
export function setPerm(_p: string, save = true): void {
|
|
213
239
|
// Auto-fixed since Phase 3.1 — no UI toggle, just persist
|
|
214
240
|
if (save) apiFire('/api/settings', 'PUT', { permissions: 'auto' });
|
|
215
241
|
}
|
|
216
242
|
|
|
217
|
-
export function getModelValue(cli) {
|
|
243
|
+
export function getModelValue(cli: string): string {
|
|
218
244
|
const sel = getModelSelect(cli);
|
|
219
245
|
if (!sel) return 'default';
|
|
220
246
|
if (sel.value === '__custom__') {
|
|
@@ -224,7 +250,7 @@ export function getModelValue(cli) {
|
|
|
224
250
|
return sel.value;
|
|
225
251
|
}
|
|
226
252
|
|
|
227
|
-
export function handleModelSelect(cli, selectEl) {
|
|
253
|
+
export function handleModelSelect(cli: string, selectEl: HTMLSelectElement): void {
|
|
228
254
|
const customInput = getCustomModelInput(cli);
|
|
229
255
|
if (!customInput) return;
|
|
230
256
|
if (selectEl.value === '__custom__') {
|
|
@@ -236,7 +262,7 @@ export function handleModelSelect(cli, selectEl) {
|
|
|
236
262
|
}
|
|
237
263
|
}
|
|
238
264
|
|
|
239
|
-
export function applyCustomModel(cli, inputEl) {
|
|
265
|
+
export function applyCustomModel(cli: string, inputEl: HTMLInputElement): void {
|
|
240
266
|
const val = inputEl.value.trim();
|
|
241
267
|
if (!val) return;
|
|
242
268
|
const select = getModelSelect(cli);
|
|
@@ -247,8 +273,8 @@ export function applyCustomModel(cli, inputEl) {
|
|
|
247
273
|
savePerCli();
|
|
248
274
|
}
|
|
249
275
|
|
|
250
|
-
export async function savePerCli() {
|
|
251
|
-
const perCli = {};
|
|
276
|
+
export async function savePerCli(): Promise<void> {
|
|
277
|
+
const perCli: Record<string, PerCliConfig> = {};
|
|
252
278
|
for (const cli of getCliKeys()) {
|
|
253
279
|
const modelEl = getModelSelect(cli);
|
|
254
280
|
if (!modelEl) continue;
|
|
@@ -261,12 +287,13 @@ export async function savePerCli() {
|
|
|
261
287
|
await apiJson('/api/settings', 'PUT', { perCli });
|
|
262
288
|
}
|
|
263
289
|
|
|
264
|
-
export function onCliChange(save = true) {
|
|
265
|
-
const cli = document.getElementById('selCli')
|
|
290
|
+
export function onCliChange(save = true): void {
|
|
291
|
+
const cli = (document.getElementById('selCli') as HTMLSelectElement)?.value || 'claude';
|
|
266
292
|
const models = MODEL_MAP[cli] || [];
|
|
267
|
-
const modelSel = document.getElementById('selModel');
|
|
293
|
+
const modelSel = document.getElementById('selModel') as HTMLSelectElement | null;
|
|
268
294
|
setSelectOptions(modelSel, models, { includeCustom: true, includeDefault: true });
|
|
269
|
-
document.getElementById('headerCli')
|
|
295
|
+
const hdrCli = document.getElementById('headerCli');
|
|
296
|
+
if (hdrCli) hdrCli.textContent = cli;
|
|
270
297
|
syncActiveEffortOptions(cli);
|
|
271
298
|
|
|
272
299
|
const oldInput = document.getElementById('selModelCustom');
|
|
@@ -277,16 +304,17 @@ export function onCliChange(save = true) {
|
|
|
277
304
|
inp.placeholder = t('model.placeholder');
|
|
278
305
|
inp.style.display = 'none';
|
|
279
306
|
inp.onchange = function () {
|
|
280
|
-
const val = this.value.trim();
|
|
281
|
-
if (!val) return;
|
|
307
|
+
const val = (this as HTMLInputElement).value.trim();
|
|
308
|
+
if (!val || !modelSel) return;
|
|
282
309
|
appendCustomOption(modelSel, val);
|
|
283
310
|
modelSel.value = val;
|
|
284
|
-
this.style.display = 'none';
|
|
311
|
+
(this as HTMLInputElement).style.display = 'none';
|
|
285
312
|
saveActiveCliSettings();
|
|
286
313
|
};
|
|
287
|
-
modelSel
|
|
314
|
+
if (!modelSel) { if (save) updateSettings(); return; }
|
|
315
|
+
modelSel.parentElement?.appendChild(inp);
|
|
288
316
|
modelSel.onchange = function () {
|
|
289
|
-
if (this.value === '__custom__') {
|
|
317
|
+
if ((this as HTMLSelectElement).value === '__custom__') {
|
|
290
318
|
inp.style.display = 'block';
|
|
291
319
|
inp.focus();
|
|
292
320
|
} else {
|
|
@@ -295,13 +323,13 @@ export function onCliChange(save = true) {
|
|
|
295
323
|
}
|
|
296
324
|
};
|
|
297
325
|
|
|
298
|
-
api('/api/settings').then(s => {
|
|
326
|
+
api<SettingsData>('/api/settings').then(s => {
|
|
299
327
|
if (!s) return;
|
|
300
328
|
const ao = s.activeOverrides?.[cli] || {};
|
|
301
329
|
const pc = s.perCli?.[cli] || {};
|
|
302
330
|
const model = ao.model || pc.model;
|
|
303
331
|
const effort = ao.effort || pc.effort || '';
|
|
304
|
-
if (model) {
|
|
332
|
+
if (model && modelSel) {
|
|
305
333
|
appendCustomOption(modelSel, model);
|
|
306
334
|
modelSel.value = model;
|
|
307
335
|
}
|
|
@@ -311,49 +339,51 @@ export function onCliChange(save = true) {
|
|
|
311
339
|
if (save) updateSettings();
|
|
312
340
|
}
|
|
313
341
|
|
|
314
|
-
export async function saveActiveCliSettings() {
|
|
315
|
-
const cli = document.getElementById('selCli')
|
|
316
|
-
const modelSel = document.getElementById('selModel');
|
|
342
|
+
export async function saveActiveCliSettings(): Promise<void> {
|
|
343
|
+
const cli = (document.getElementById('selCli') as HTMLSelectElement)?.value || 'claude';
|
|
344
|
+
const modelSel = document.getElementById('selModel') as HTMLSelectElement | null;
|
|
317
345
|
let model = modelSel?.value || 'default';
|
|
318
346
|
if (model === '__custom__') {
|
|
319
|
-
model = document.getElementById('selModelCustom')?.value?.trim() || 'default';
|
|
347
|
+
model = (document.getElementById('selModelCustom') as HTMLInputElement | null)?.value?.trim() || 'default';
|
|
320
348
|
}
|
|
321
|
-
const effortEl = document.getElementById('selEffort');
|
|
322
|
-
const overrides = {};
|
|
349
|
+
const effortEl = document.getElementById('selEffort') as HTMLSelectElement | null;
|
|
350
|
+
const overrides: Record<string, PerCliConfig> = {};
|
|
323
351
|
overrides[cli] = { model };
|
|
324
|
-
if (!effortEl
|
|
352
|
+
if (effortEl && !effortEl.disabled) overrides[cli].effort = effortEl.value || '';
|
|
325
353
|
await apiJson('/api/settings', 'PUT', { activeOverrides: overrides });
|
|
326
354
|
}
|
|
327
355
|
|
|
328
356
|
// ── Telegram ──
|
|
329
|
-
export async function saveTelegramSettings() {
|
|
330
|
-
const token = document.getElementById('tgToken')
|
|
331
|
-
const chatIdsRaw = document.getElementById('tgChatIds')
|
|
357
|
+
export async function saveTelegramSettings(): Promise<void> {
|
|
358
|
+
const token = (document.getElementById('tgToken') as HTMLInputElement)?.value.trim() || '';
|
|
359
|
+
const chatIdsRaw = (document.getElementById('tgChatIds') as HTMLInputElement)?.value.trim() || '';
|
|
332
360
|
const allowedChatIds = chatIdsRaw
|
|
333
361
|
? chatIdsRaw.split(',').map(s => parseInt(s.trim(), 10)).filter(n => !isNaN(n))
|
|
334
362
|
: [];
|
|
335
363
|
await apiJson('/api/settings', 'PUT', { telegram: { token, allowedChatIds } });
|
|
336
364
|
}
|
|
337
365
|
|
|
338
|
-
export async function setTelegram(enabled) {
|
|
339
|
-
document.getElementById('tgOn')
|
|
340
|
-
document.getElementById('tgOff')
|
|
366
|
+
export async function setTelegram(enabled: boolean): Promise<void> {
|
|
367
|
+
document.getElementById('tgOn')?.classList.toggle('active', enabled);
|
|
368
|
+
document.getElementById('tgOff')?.classList.toggle('active', !enabled);
|
|
341
369
|
await apiJson('/api/settings', 'PUT', { telegram: { enabled } });
|
|
342
370
|
}
|
|
343
371
|
|
|
344
|
-
function loadTelegramSettings(s) {
|
|
372
|
+
function loadTelegramSettings(s: SettingsData): void {
|
|
345
373
|
if (!s.telegram) return;
|
|
346
374
|
const tg = s.telegram;
|
|
347
|
-
document.getElementById('tgOn')
|
|
348
|
-
document.getElementById('tgOff')
|
|
349
|
-
|
|
350
|
-
if (tg.
|
|
351
|
-
|
|
375
|
+
document.getElementById('tgOn')?.classList.toggle('active', !!tg.enabled);
|
|
376
|
+
document.getElementById('tgOff')?.classList.toggle('active', !tg.enabled);
|
|
377
|
+
const tgToken = document.getElementById('tgToken') as HTMLInputElement | null;
|
|
378
|
+
if (tg.token && tgToken) tgToken.value = tg.token;
|
|
379
|
+
const tgChatIds = document.getElementById('tgChatIds') as HTMLInputElement | null;
|
|
380
|
+
if (tg.allowedChatIds?.length && tgChatIds) {
|
|
381
|
+
tgChatIds.value = tg.allowedChatIds.join(', ');
|
|
352
382
|
}
|
|
353
383
|
}
|
|
354
384
|
|
|
355
385
|
// ── Fallback Order ──
|
|
356
|
-
export function loadFallbackOrder(s) {
|
|
386
|
+
export function loadFallbackOrder(s: SettingsData): void {
|
|
357
387
|
const container = document.getElementById('fallbackOrderList');
|
|
358
388
|
if (!container) return;
|
|
359
389
|
const allClis = Object.keys(s.perCli || {});
|
|
@@ -379,8 +409,8 @@ export function loadFallbackOrder(s) {
|
|
|
379
409
|
container.innerHTML = html;
|
|
380
410
|
}
|
|
381
411
|
|
|
382
|
-
export async function saveFallbackOrder() {
|
|
383
|
-
const selects = document.querySelectorAll('#fallbackOrderList select');
|
|
412
|
+
export async function saveFallbackOrder(): Promise<void> {
|
|
413
|
+
const selects = document.querySelectorAll<HTMLSelectElement>('#fallbackOrderList select');
|
|
384
414
|
const fallbackOrder = [...selects].map(s => s.value).filter(Boolean);
|
|
385
415
|
await apiJson('/api/settings', 'PUT', { fallbackOrder });
|
|
386
416
|
}
|
|
@@ -388,10 +418,10 @@ export async function saveFallbackOrder() {
|
|
|
388
418
|
// ── CLI Status ──
|
|
389
419
|
import { state } from '../state.js';
|
|
390
420
|
|
|
391
|
-
export async function loadCliStatus(force = false) {
|
|
421
|
+
export async function loadCliStatus(force = false): Promise<void> {
|
|
392
422
|
const interval = Number(localStorage.getItem('cliStatusInterval') || 300);
|
|
393
423
|
if (!force && state.cliStatusCache && interval > 0 && (Date.now() - state.cliStatusTs) < interval * 1000) {
|
|
394
|
-
renderCliStatus(state.cliStatusCache);
|
|
424
|
+
renderCliStatus({ cliStatus: (state.cliStatusCache as Record<string, unknown>)?.cliStatus as Record<string, { available: boolean }> | null, quota: (state.cliStatusCache as Record<string, unknown>)?.quota as Record<string, QuotaEntry> | null });
|
|
395
425
|
return;
|
|
396
426
|
}
|
|
397
427
|
|
|
@@ -399,20 +429,20 @@ export async function loadCliStatus(force = false) {
|
|
|
399
429
|
if (el) el.innerHTML = '<div style="color:var(--text-dim);font-size:11px">Loading...</div>';
|
|
400
430
|
|
|
401
431
|
const [cliStatus, quota] = await Promise.all([
|
|
402
|
-
api('/api/cli-status'),
|
|
403
|
-
api('/api/quota'),
|
|
432
|
+
api<Record<string, { available: boolean }>>('/api/cli-status'),
|
|
433
|
+
api<Record<string, QuotaEntry>>('/api/quota'),
|
|
404
434
|
]);
|
|
405
435
|
|
|
406
|
-
state.cliStatusCache = { cliStatus, quota }
|
|
436
|
+
state.cliStatusCache = { cliStatus, quota } as Record<string, unknown>;
|
|
407
437
|
state.cliStatusTs = Date.now();
|
|
408
|
-
renderCliStatus(
|
|
438
|
+
renderCliStatus({ cliStatus, quota });
|
|
409
439
|
}
|
|
410
440
|
|
|
411
|
-
function renderCliStatus(data) {
|
|
441
|
+
function renderCliStatus(data: { cliStatus: Record<string, { available: boolean }> | null; quota: Record<string, QuotaEntry> | null }): void {
|
|
412
442
|
const { cliStatus, quota } = data;
|
|
413
443
|
const el = document.getElementById('cliStatusList');
|
|
414
444
|
|
|
415
|
-
const AUTH_HINTS = {
|
|
445
|
+
const AUTH_HINTS: Record<string, { install: string; auth: string }> = {
|
|
416
446
|
claude: { install: 'npm i -g @anthropic-ai/claude-code', auth: 'claude auth' },
|
|
417
447
|
codex: { install: 'npm i -g @openai/codex', auth: 'codex login' },
|
|
418
448
|
gemini: { install: 'npm i -g @google/gemini-cli', auth: `gemini (${t('cli.gemini.auth')})` },
|
|
@@ -428,7 +458,7 @@ function renderCliStatus(data) {
|
|
|
428
458
|
}
|
|
429
459
|
|
|
430
460
|
for (const [name, info] of Object.entries(cliStatus)) {
|
|
431
|
-
const q = quota[name];
|
|
461
|
+
const q = quota?.[name];
|
|
432
462
|
const dotClass = info.available ? 'ok' : 'missing';
|
|
433
463
|
|
|
434
464
|
let accountLine = '';
|
|
@@ -486,25 +516,27 @@ function renderCliStatus(data) {
|
|
|
486
516
|
`;
|
|
487
517
|
}
|
|
488
518
|
|
|
489
|
-
el.innerHTML = html;
|
|
519
|
+
if (el) el.innerHTML = html;
|
|
490
520
|
}
|
|
491
521
|
|
|
492
522
|
// ── Prompt Modal ──
|
|
493
|
-
export function openPromptModal() {
|
|
494
|
-
api('/api/prompt').then(data => {
|
|
523
|
+
export function openPromptModal(): void {
|
|
524
|
+
api<{ content?: string }>('/api/prompt').then(data => {
|
|
495
525
|
if (!data) return;
|
|
496
|
-
document.getElementById('modalPromptEditor')
|
|
497
|
-
|
|
526
|
+
const editor = document.getElementById('modalPromptEditor') as HTMLTextAreaElement | null;
|
|
527
|
+
if (editor) editor.value = data.content || '';
|
|
528
|
+
document.getElementById('promptModal')?.classList.add('open');
|
|
498
529
|
});
|
|
499
530
|
}
|
|
500
531
|
|
|
501
|
-
export function closePromptModal(e) {
|
|
532
|
+
export function closePromptModal(e?: Event): void {
|
|
502
533
|
if (e && e.target !== e.currentTarget) return;
|
|
503
|
-
document.getElementById('promptModal')
|
|
534
|
+
document.getElementById('promptModal')?.classList.remove('open');
|
|
504
535
|
}
|
|
505
536
|
|
|
506
|
-
export async function savePromptFromModal() {
|
|
507
|
-
const
|
|
537
|
+
export async function savePromptFromModal(): Promise<void> {
|
|
538
|
+
const editor = document.getElementById('modalPromptEditor') as HTMLTextAreaElement | null;
|
|
539
|
+
const content = editor?.value || '';
|
|
508
540
|
await apiJson('/api/prompt', 'PUT', { content });
|
|
509
|
-
document.getElementById('promptModal')
|
|
541
|
+
document.getElementById('promptModal')?.classList.remove('open');
|
|
510
542
|
}
|
|
@@ -3,11 +3,16 @@
|
|
|
3
3
|
// - Wide viewport (>900px): toggle *-collapsed classes
|
|
4
4
|
// - Narrow viewport (≤900px): CSS auto-collapses, toggle *-expanded to override
|
|
5
5
|
|
|
6
|
+
interface SidebarState {
|
|
7
|
+
left?: boolean;
|
|
8
|
+
right?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
6
11
|
const STORAGE_KEY = 'sidebarState';
|
|
7
12
|
const BREAKPOINT = 900;
|
|
8
13
|
|
|
9
|
-
export function initSidebar() {
|
|
10
|
-
const saved = JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}');
|
|
14
|
+
export function initSidebar(): void {
|
|
15
|
+
const saved: SidebarState = JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}');
|
|
11
16
|
if (saved.left) document.body.classList.add('left-collapsed');
|
|
12
17
|
if (saved.right) document.body.classList.add('right-collapsed');
|
|
13
18
|
|
|
@@ -17,43 +22,37 @@ export function initSidebar() {
|
|
|
17
22
|
// On resize: sync classes with viewport mode
|
|
18
23
|
window.addEventListener('resize', () => {
|
|
19
24
|
if (window.innerWidth > BREAKPOINT) {
|
|
20
|
-
// Wide: remove expanded, restore collapsed from storage
|
|
21
25
|
document.body.classList.remove('left-expanded', 'right-expanded');
|
|
22
|
-
const s = JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}');
|
|
26
|
+
const s: SidebarState = JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}');
|
|
23
27
|
document.body.classList.toggle('left-collapsed', !!s.left);
|
|
24
28
|
document.body.classList.toggle('right-collapsed', !!s.right);
|
|
25
29
|
} else {
|
|
26
|
-
// Narrow: suspend collapsed (CSS media query handles auto-collapse)
|
|
27
30
|
document.body.classList.remove('left-collapsed', 'right-collapsed');
|
|
28
31
|
}
|
|
29
32
|
syncIcons();
|
|
30
33
|
});
|
|
31
34
|
|
|
32
|
-
// If starting narrow, suspend collapsed
|
|
33
35
|
if (window.innerWidth <= BREAKPOINT) {
|
|
34
36
|
document.body.classList.remove('left-collapsed', 'right-collapsed');
|
|
35
37
|
}
|
|
36
|
-
|
|
37
38
|
syncIcons();
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
function isNarrow() {
|
|
41
|
+
function isNarrow(): boolean {
|
|
41
42
|
return window.innerWidth <= BREAKPOINT;
|
|
42
43
|
}
|
|
43
44
|
|
|
44
|
-
export function toggleLeft() {
|
|
45
|
+
export function toggleLeft(): void {
|
|
45
46
|
if (isNarrow()) {
|
|
46
|
-
// Narrow mode: toggle expanded override
|
|
47
47
|
document.body.classList.toggle('left-expanded');
|
|
48
48
|
} else {
|
|
49
|
-
// Wide mode: toggle collapsed
|
|
50
49
|
document.body.classList.toggle('left-collapsed');
|
|
51
50
|
}
|
|
52
51
|
save();
|
|
53
52
|
syncIcons();
|
|
54
53
|
}
|
|
55
54
|
|
|
56
|
-
export function toggleRight() {
|
|
55
|
+
export function toggleRight(): void {
|
|
57
56
|
if (isNarrow()) {
|
|
58
57
|
document.body.classList.toggle('right-expanded');
|
|
59
58
|
} else {
|
|
@@ -63,24 +62,24 @@ export function toggleRight() {
|
|
|
63
62
|
syncIcons();
|
|
64
63
|
}
|
|
65
64
|
|
|
66
|
-
function isLeftOpen() {
|
|
65
|
+
function isLeftOpen(): boolean {
|
|
67
66
|
if (isNarrow()) return document.body.classList.contains('left-expanded');
|
|
68
67
|
return !document.body.classList.contains('left-collapsed');
|
|
69
68
|
}
|
|
70
69
|
|
|
71
|
-
function isRightOpen() {
|
|
70
|
+
function isRightOpen(): boolean {
|
|
72
71
|
if (isNarrow()) return document.body.classList.contains('right-expanded');
|
|
73
72
|
return !document.body.classList.contains('right-collapsed');
|
|
74
73
|
}
|
|
75
74
|
|
|
76
|
-
function syncIcons() {
|
|
75
|
+
function syncIcons(): void {
|
|
77
76
|
const leftBtn = document.getElementById('toggleLeft');
|
|
78
77
|
const rightBtn = document.getElementById('toggleRight');
|
|
79
78
|
if (leftBtn) leftBtn.textContent = isLeftOpen() ? '◀' : '▶';
|
|
80
79
|
if (rightBtn) rightBtn.textContent = isRightOpen() ? '▶' : '◀';
|
|
81
80
|
}
|
|
82
81
|
|
|
83
|
-
function save() {
|
|
82
|
+
function save(): void {
|
|
84
83
|
localStorage.setItem(STORAGE_KEY, JSON.stringify({
|
|
85
84
|
left: document.body.classList.contains('left-collapsed'),
|
|
86
85
|
right: document.body.classList.contains('right-collapsed'),
|
|
@@ -4,35 +4,48 @@ import { t, fetchWithLocale } from './i18n.js';
|
|
|
4
4
|
import { apiJson } from '../api.js';
|
|
5
5
|
import { escapeHtml } from '../render.js';
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
interface SkillItem {
|
|
8
|
+
id: string;
|
|
9
|
+
name?: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
emoji?: string;
|
|
12
|
+
category?: string;
|
|
13
|
+
enabled: boolean;
|
|
14
|
+
requires?: { env?: string[]; bins?: string[] };
|
|
15
|
+
install?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const KNOWN_CATS = ['productivity', 'communication', 'devtools', 'ai-media', 'utility', 'smarthome', 'automation'];
|
|
19
|
+
|
|
20
|
+
export async function loadSkills(): Promise<void> {
|
|
8
21
|
try {
|
|
9
22
|
const res = await fetchWithLocale('/api/skills');
|
|
10
23
|
state.allSkills = await res.json();
|
|
11
24
|
renderSkills();
|
|
12
|
-
} catch
|
|
13
|
-
document.getElementById('skillsList')
|
|
14
|
-
|
|
25
|
+
} catch {
|
|
26
|
+
const el = document.getElementById('skillsList');
|
|
27
|
+
if (el) el.innerHTML = `<div style="color:var(--text-dim);font-size:11px">${t('skill.loadFail')}</div>`;
|
|
15
28
|
}
|
|
16
29
|
}
|
|
17
30
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
export function renderSkills() {
|
|
31
|
+
export function renderSkills(): void {
|
|
21
32
|
const list = document.getElementById('skillsList');
|
|
22
33
|
const count = document.getElementById('skillsCount');
|
|
23
|
-
|
|
34
|
+
if (!list || !count) return;
|
|
35
|
+
const skills = state.allSkills as SkillItem[];
|
|
36
|
+
let filtered = skills;
|
|
24
37
|
if (state.currentSkillFilter === 'installed') {
|
|
25
|
-
filtered =
|
|
38
|
+
filtered = skills.filter(s => s.enabled);
|
|
26
39
|
} else if (state.currentSkillFilter === 'other') {
|
|
27
|
-
filtered =
|
|
40
|
+
filtered = skills.filter(s => !KNOWN_CATS.includes(s.category || ''));
|
|
28
41
|
} else if (state.currentSkillFilter !== 'all') {
|
|
29
|
-
filtered =
|
|
42
|
+
filtered = skills.filter(s => s.category === state.currentSkillFilter);
|
|
30
43
|
}
|
|
31
|
-
const enabledCount =
|
|
32
|
-
count.textContent = t('skill.count', { active: enabledCount, total:
|
|
44
|
+
const enabledCount = skills.filter(s => s.enabled).length;
|
|
45
|
+
count.textContent = t('skill.count', { active: enabledCount, total: skills.length });
|
|
33
46
|
|
|
34
47
|
list.innerHTML = filtered.map(s => {
|
|
35
|
-
const reqParts = [];
|
|
48
|
+
const reqParts: string[] = [];
|
|
36
49
|
if (s.requires?.env) reqParts.push('🔑 ' + s.requires.env.join(', '));
|
|
37
50
|
if (s.requires?.bins) reqParts.push('⚙️ ' + s.requires.bins.join(', '));
|
|
38
51
|
if (s.install) reqParts.push(s.install);
|
|
@@ -50,7 +63,7 @@ export function renderSkills() {
|
|
|
50
63
|
}).join('');
|
|
51
64
|
}
|
|
52
65
|
|
|
53
|
-
export async function toggleSkill(id, currentlyEnabled) {
|
|
66
|
+
export async function toggleSkill(id: string, currentlyEnabled: boolean): Promise<void> {
|
|
54
67
|
const endpoint = currentlyEnabled ? '/api/skills/disable' : '/api/skills/enable';
|
|
55
68
|
try {
|
|
56
69
|
await apiJson(endpoint, 'POST', { id });
|
|
@@ -60,7 +73,7 @@ export async function toggleSkill(id, currentlyEnabled) {
|
|
|
60
73
|
}
|
|
61
74
|
}
|
|
62
75
|
|
|
63
|
-
export function filterSkills(cat, btn) {
|
|
76
|
+
export function filterSkills(cat: string, btn?: Element): void {
|
|
64
77
|
state.currentSkillFilter = cat;
|
|
65
78
|
document.querySelectorAll('.skill-filter').forEach(b => b.classList.remove('active'));
|
|
66
79
|
if (btn) btn.classList.add('active');
|