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.
Files changed (63) hide show
  1. package/README.ko.md +44 -13
  2. package/README.md +12 -11
  3. package/README.zh-CN.md +43 -12
  4. package/dist/bin/commands/doctor.js +13 -2
  5. package/dist/bin/commands/doctor.js.map +1 -1
  6. package/dist/bin/commands/mcp.js +15 -18
  7. package/dist/bin/commands/mcp.js.map +1 -1
  8. package/dist/bin/commands/serve.js +3 -28
  9. package/dist/bin/commands/serve.js.map +1 -1
  10. package/dist/bin/commands/skill.js +9 -6
  11. package/dist/bin/commands/skill.js.map +1 -1
  12. package/dist/lib/mcp-sync.js +123 -31
  13. package/dist/lib/mcp-sync.js.map +1 -1
  14. package/{scripts → dist/scripts}/check-copilot-gap.js +24 -17
  15. package/dist/scripts/check-copilot-gap.js.map +1 -0
  16. package/{scripts/check-deps-offline.mjs → dist/scripts/check-deps-offline.js} +24 -20
  17. package/dist/scripts/check-deps-offline.js.map +1 -0
  18. package/dist/scripts/fresh-install-smoke.js +120 -0
  19. package/dist/scripts/fresh-install-smoke.js.map +1 -0
  20. package/{scripts/i18n-registry.py → dist/scripts/i18n-registry.js} +115 -122
  21. package/dist/scripts/i18n-registry.js.map +1 -0
  22. package/dist/server.js +34 -26
  23. package/dist/server.js.map +1 -1
  24. package/dist/src/cli/command-context.js +13 -3
  25. package/dist/src/cli/command-context.js.map +1 -1
  26. package/dist/src/prompt/builder.js +28 -1
  27. package/dist/src/prompt/builder.js.map +1 -1
  28. package/package.json +9 -5
  29. package/public/dist/bundle.js +72 -77
  30. package/public/dist/bundle.js.map +4 -4
  31. package/public/index.html +1 -3
  32. package/public/js/{api.js → api.ts} +18 -12
  33. package/public/js/{constants.js → constants.ts} +44 -24
  34. package/public/js/features/{appname.js → appname.ts} +13 -12
  35. package/public/js/features/{chat.js → chat.ts} +46 -37
  36. package/public/js/features/{employees.js → employees.ts} +67 -38
  37. package/public/js/features/heartbeat.ts +90 -0
  38. package/public/js/features/{i18n.js → i18n.ts} +20 -20
  39. package/public/js/features/memory.ts +125 -0
  40. package/public/js/features/{settings.js → settings.ts} +125 -93
  41. package/public/js/features/{sidebar.js → sidebar.ts} +15 -16
  42. package/public/js/features/{skills.js → skills.ts} +29 -16
  43. package/public/js/features/{slash-commands.js → slash-commands.ts} +34 -29
  44. package/public/js/features/{theme.js → theme.ts} +4 -4
  45. package/public/js/{locale.js → locale.ts} +3 -3
  46. package/public/js/main.ts +280 -0
  47. package/public/js/{render.js → render.ts} +34 -107
  48. package/public/js/state.ts +38 -0
  49. package/public/js/{ui.js → ui.ts} +60 -63
  50. package/public/js/{ws.js → ws.ts} +46 -20
  51. package/public/locales/en.json +1 -0
  52. package/public/locales/ko.json +1 -0
  53. package/scripts/check-copilot-gap.ts +75 -0
  54. package/scripts/check-deps-offline.ts +98 -0
  55. package/scripts/fresh-install-smoke.ts +130 -0
  56. package/scripts/i18n-registry.ts +230 -0
  57. package/scripts/postinstall-guard.cjs +5 -0
  58. package/dist/bin/cli-claw.js +0 -96
  59. package/dist/bin/cli-claw.js.map +0 -1
  60. package/public/js/features/heartbeat.js +0 -80
  61. package/public/js/features/memory.js +0 -85
  62. package/public/js/main.js +0 -278
  63. 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
- function toCap(cli) {
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').textContent = s.workingDir;
134
- document.getElementById('headerCli').textContent = s.cli;
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
- if (activeModel) document.getElementById('selModel').value = activeModel;
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
- export async function loadMcpServers() {
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(' &nbsp; ');
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').value,
231
+ cli: (document.getElementById('selCli') as HTMLSelectElement)?.value || 'claude',
207
232
  };
208
- document.getElementById('headerCli').textContent = s.cli;
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(p, save = true) {
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').value;
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').textContent = cli;
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.parentElement.appendChild(inp);
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').value;
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?.disabled) overrides[cli].effort = effortEl?.value || '';
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').value.trim();
331
- const chatIdsRaw = document.getElementById('tgChatIds').value.trim();
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').classList.toggle('active', enabled);
340
- document.getElementById('tgOff').classList.toggle('active', !enabled);
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').classList.toggle('active', !!tg.enabled);
348
- document.getElementById('tgOff').classList.toggle('active', !tg.enabled);
349
- if (tg.token) document.getElementById('tgToken').value = tg.token;
350
- if (tg.allowedChatIds?.length) {
351
- document.getElementById('tgChatIds').value = tg.allowedChatIds.join(', ');
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(state.cliStatusCache);
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').value = data.content || '';
497
- document.getElementById('promptModal').classList.add('open');
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').classList.remove('open');
534
+ document.getElementById('promptModal')?.classList.remove('open');
504
535
  }
505
536
 
506
- export async function savePromptFromModal() {
507
- const content = document.getElementById('modalPromptEditor').value;
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').classList.remove('open');
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
- export async function loadSkills() {
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 (e) {
13
- document.getElementById('skillsList').innerHTML =
14
- `<div style="color:var(--text-dim);font-size:11px">${t('skill.loadFail')}</div>`;
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
- const KNOWN_CATS = ['productivity', 'communication', 'devtools', 'ai-media', 'utility', 'smarthome', 'automation'];
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
- let filtered = state.allSkills;
34
+ if (!list || !count) return;
35
+ const skills = state.allSkills as SkillItem[];
36
+ let filtered = skills;
24
37
  if (state.currentSkillFilter === 'installed') {
25
- filtered = state.allSkills.filter(s => s.enabled);
38
+ filtered = skills.filter(s => s.enabled);
26
39
  } else if (state.currentSkillFilter === 'other') {
27
- filtered = state.allSkills.filter(s => !KNOWN_CATS.includes(s.category));
40
+ filtered = skills.filter(s => !KNOWN_CATS.includes(s.category || ''));
28
41
  } else if (state.currentSkillFilter !== 'all') {
29
- filtered = state.allSkills.filter(s => s.category === state.currentSkillFilter);
42
+ filtered = skills.filter(s => s.category === state.currentSkillFilter);
30
43
  }
31
- const enabledCount = state.allSkills.filter(s => s.enabled).length;
32
- count.textContent = t('skill.count', { active: enabledCount, total: state.allSkills.length });
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');