cli-jaw 0.1.11 → 1.0.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.
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
@@ -0,0 +1,90 @@
1
+ // ── Heartbeat Feature ──
2
+ import { state } from '../state.js';
3
+ import type { HeartbeatJob } from '../state.js';
4
+ import { t } from './i18n.js';
5
+ import { api, apiJson } from '../api.js';
6
+ import { escapeHtml } from '../render.js';
7
+
8
+ interface HeartbeatData {
9
+ jobs: HeartbeatJob[];
10
+ }
11
+
12
+ export async function openHeartbeatModal(): Promise<void> {
13
+ const data = await api<HeartbeatData>('/api/heartbeat');
14
+ state.heartbeatJobs = data?.jobs || [];
15
+ renderHeartbeatJobs();
16
+ document.getElementById('heartbeatModal')?.classList.add('open');
17
+ }
18
+
19
+ export function closeHeartbeatModal(e?: Event): void {
20
+ if (e && e.target !== e.currentTarget) return;
21
+ document.getElementById('heartbeatModal')?.classList.remove('open');
22
+ }
23
+
24
+ export function renderHeartbeatJobs(): void {
25
+ const container = document.getElementById('hbJobsList');
26
+ if (!container) return;
27
+ const jobs = state.heartbeatJobs as HeartbeatJob[];
28
+ if (jobs.length === 0) {
29
+ container.innerHTML = `<p style="color:var(--text-dim);font-size:12px;text-align:center">${t('hb.empty')}</p>`;
30
+ } else {
31
+ container.innerHTML = jobs.map((job, i) => `
32
+ <div class="hb-job-card">
33
+ <div class="hb-job-header">
34
+ <input type="text" value="${escapeHtml(String(job.name || ''))}" placeholder="${t('hb.name')}"
35
+ data-hb-name="${i}">
36
+ <span style="font-size:11px;color:var(--text-dim)">every</span>
37
+ <input type="number" value="${(job.schedule as Record<string, unknown>)?.minutes || 5}" min="1"
38
+ data-hb-minutes="${i}">
39
+ <span style="font-size:11px;color:var(--text-dim)">min</span>
40
+ <button class="hb-toggle ${job.enabled ? 'on' : 'off'}"
41
+ data-hb-toggle="${i}"></button>
42
+ <button class="hb-del" data-hb-remove="${i}">✕</button>
43
+ </div>
44
+ <textarea class="hb-prompt" rows="2" placeholder="${t('hb.prompt')}"
45
+ data-hb-prompt="${i}">${escapeHtml(String(job.prompt || ''))}</textarea>
46
+ </div>
47
+ `).join('');
48
+ }
49
+ const active = jobs.filter(j => j.enabled).length;
50
+ const btn = document.getElementById('hbSidebarBtn');
51
+ if (btn) btn.textContent = `💓 Heartbeat (${active})`;
52
+ }
53
+
54
+ export function addHeartbeatJob(): void {
55
+ (state.heartbeatJobs as HeartbeatJob[]).push({
56
+ id: 'hb_' + Date.now(),
57
+ name: '',
58
+ enabled: true,
59
+ schedule: { kind: 'every', minutes: 5 },
60
+ prompt: ''
61
+ });
62
+ renderHeartbeatJobs();
63
+ saveHeartbeatJobs();
64
+ }
65
+
66
+ export function removeHeartbeatJob(i: number): void {
67
+ state.heartbeatJobs.splice(i, 1);
68
+ renderHeartbeatJobs();
69
+ saveHeartbeatJobs();
70
+ }
71
+
72
+ export function toggleHeartbeatJob(i: number): void {
73
+ const jobs = state.heartbeatJobs as HeartbeatJob[];
74
+ jobs[i].enabled = !jobs[i].enabled;
75
+ renderHeartbeatJobs();
76
+ saveHeartbeatJobs();
77
+ }
78
+
79
+ export async function saveHeartbeatJobs(): Promise<void> {
80
+ await apiJson('/api/heartbeat', 'PUT', { jobs: state.heartbeatJobs });
81
+ }
82
+
83
+ export async function initHeartbeatBadge(): Promise<void> {
84
+ try {
85
+ const d = await api<HeartbeatData>('/api/heartbeat');
86
+ const active = (d?.jobs || []).filter(j => j.enabled).length;
87
+ const btn = document.getElementById('hbSidebarBtn');
88
+ if (btn) btn.textContent = `💓 Heartbeat (${active})`;
89
+ } catch { /* ignore */ }
90
+ }
@@ -1,24 +1,24 @@
1
1
  // ── Frontend i18n module ──
2
2
  // Phase 7: client-side translation with lazy-loaded locale JSON
3
3
 
4
+ type LocaleDict = Record<string, string>;
5
+
4
6
  let currentLocale = 'ko';
5
- let dict = {}; // current locale dictionary
6
- let fallbackDict = {}; // ko fallback
7
+ let dict: LocaleDict = {};
8
+ let fallbackDict: LocaleDict = {};
7
9
 
8
10
  /**
9
11
  * Initialize i18n: restore from localStorage, detect from browser, load locale
10
12
  */
11
- export async function initI18n() {
12
- let saved = null;
13
+ export async function initI18n(): Promise<void> {
14
+ let saved: string | null = null;
13
15
  try { saved = localStorage.getItem('claw_locale'); } catch { /* Safari private */ }
14
16
 
15
17
  if (!saved) {
16
- // Detect from browser language
17
18
  const browserLang = (navigator.language || 'ko').split(/[-_]/)[0].toLowerCase();
18
19
  saved = ['en', 'ko'].includes(browserLang) ? browserLang : 'ko';
19
20
  }
20
21
 
21
- // Always load ko as fallback
22
22
  fallbackDict = await fetchLocale('ko');
23
23
  if (saved === 'ko') {
24
24
  dict = fallbackDict;
@@ -32,10 +32,10 @@ export async function initI18n() {
32
32
  /**
33
33
  * Fetch a locale JSON from the server
34
34
  */
35
- async function fetchLocale(lang) {
35
+ async function fetchLocale(lang: string): Promise<LocaleDict> {
36
36
  try {
37
37
  const { api } = await import('../api.js');
38
- return await api(`/api/i18n/${lang}`) || {};
38
+ return await api<LocaleDict>(`/api/i18n/${lang}`) || {};
39
39
  } catch { return {}; }
40
40
  }
41
41
 
@@ -43,8 +43,8 @@ async function fetchLocale(lang) {
43
43
  * Translate a key with optional parameter interpolation
44
44
  * Falls back: dict[key] → fallbackDict[key] → key itself
45
45
  */
46
- export function t(key, params = {}) {
47
- let val = dict[key] ?? fallbackDict[key] ?? key;
46
+ export function t(key: string, params: Record<string, unknown> = {}): string {
47
+ let val: string = dict[key] ?? fallbackDict[key] ?? key;
48
48
  for (const [k, v] of Object.entries(params)) {
49
49
  val = val.replaceAll(`{${k}}`, String(v));
50
50
  }
@@ -54,18 +54,18 @@ export function t(key, params = {}) {
54
54
  /**
55
55
  * Apply translations to all elements with data-i18n attributes
56
56
  */
57
- export function applyI18n() {
57
+ export function applyI18n(): void {
58
58
  document.querySelectorAll('[data-i18n]').forEach(el => {
59
59
  const key = el.getAttribute('data-i18n');
60
60
  if (key) el.textContent = t(key);
61
61
  });
62
62
  document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
63
63
  const key = el.getAttribute('data-i18n-placeholder');
64
- if (key) el.placeholder = t(key);
64
+ if (key) (el as HTMLInputElement).placeholder = t(key);
65
65
  });
66
66
  document.querySelectorAll('[data-i18n-title]').forEach(el => {
67
67
  const key = el.getAttribute('data-i18n-title');
68
- if (key) el.title = t(key);
68
+ if (key) (el as HTMLElement).title = t(key);
69
69
  });
70
70
  document.querySelectorAll('[data-i18n-aria]').forEach(el => {
71
71
  const key = el.getAttribute('data-i18n-aria');
@@ -76,7 +76,7 @@ export function applyI18n() {
76
76
  /**
77
77
  * Switch language, reload locale, rebind all UI
78
78
  */
79
- export async function setLang(lang) {
79
+ export async function setLang(lang: string): Promise<void> {
80
80
  if (lang === currentLocale) return;
81
81
  if (lang === 'ko') {
82
82
  dict = fallbackDict;
@@ -91,32 +91,32 @@ export async function setLang(lang) {
91
91
  try {
92
92
  const { loadEmployees } = await import('./employees.js');
93
93
  loadEmployees();
94
- } catch { }
94
+ } catch { /* ignore */ }
95
95
  try {
96
96
  const { loadSkills } = await import('./skills.js');
97
97
  loadSkills();
98
- } catch { }
98
+ } catch { /* ignore */ }
99
99
  try {
100
100
  const { loadCommands } = await import('./slash-commands.js');
101
101
  loadCommands();
102
- } catch { }
102
+ } catch { /* ignore */ }
103
103
  try {
104
104
  const { loadSettings } = await import('./settings.js');
105
105
  loadSettings();
106
- } catch { }
106
+ } catch { /* ignore */ }
107
107
  }
108
108
 
109
109
  /**
110
110
  * Get current locale code
111
111
  */
112
- export function getLang() {
112
+ export function getLang(): string {
113
113
  return currentLocale;
114
114
  }
115
115
 
116
116
  /**
117
117
  * fetchWithLocale — wrapper that appends ?locale= to requests
118
118
  */
119
- export function fetchWithLocale(url, init = {}) {
119
+ export function fetchWithLocale(url: string, init: RequestInit = {}): Promise<Response> {
120
120
  const u = new URL(url, location.origin);
121
121
  if (!u.searchParams.has('locale')) {
122
122
  u.searchParams.set('locale', currentLocale);
@@ -0,0 +1,125 @@
1
+ // ── Memory Feature ──
2
+ import { escapeHtml } from '../render.js';
3
+ import { api, apiJson } from '../api.js';
4
+
5
+ interface MemoryFile {
6
+ name: string;
7
+ entries: number;
8
+ }
9
+
10
+ interface MemoryData {
11
+ enabled: boolean;
12
+ flushEvery: number;
13
+ cli?: string;
14
+ model?: string;
15
+ retentionDays: number;
16
+ path: string;
17
+ counter: number;
18
+ files: MemoryFile[];
19
+ }
20
+
21
+ interface MemoryFileContent {
22
+ name: string;
23
+ content: string;
24
+ }
25
+
26
+ export async function openMemoryModal(): Promise<void> {
27
+ const data = await api<MemoryData>('/api/memory-files');
28
+ if (!data) return;
29
+ const $ = (id: string) => document.getElementById(id);
30
+
31
+ $('memOn')?.classList.toggle('active', data.enabled);
32
+ $('memOff')?.classList.toggle('active', !data.enabled);
33
+ const flushEl = $('memFlushEvery') as HTMLSelectElement | null;
34
+ if (flushEl) flushEl.value = String(data.flushEvery);
35
+ const cliEl = $('memCli') as HTMLSelectElement | null;
36
+ if (cliEl) cliEl.value = data.cli || '';
37
+ const modelEl = $('memModel') as HTMLSelectElement | null;
38
+ if (modelEl) modelEl.value = data.model || '';
39
+ const retEl = $('memRetention') as HTMLSelectElement | null;
40
+ if (retEl) retEl.value = String(data.retentionDays);
41
+ const pathEl = $('memPath');
42
+ if (pathEl) pathEl.textContent = data.path;
43
+ const counterEl = $('memCounter');
44
+ if (counterEl) counterEl.textContent = String(data.counter);
45
+ const thresholdEl = $('memThreshold');
46
+ if (thresholdEl) thresholdEl.textContent = String(data.flushEvery);
47
+ renderMemFiles(data.files);
48
+ const sideBtn = $('memorySidebarBtn');
49
+ if (sideBtn) sideBtn.textContent = `🧠 Memory (${data.files.length})`;
50
+ $('memoryModal')?.classList.add('open');
51
+ }
52
+
53
+ export function closeMemoryModal(e?: Event): void {
54
+ if (e && e.target !== e.currentTarget) return;
55
+ document.getElementById('memoryModal')?.classList.remove('open');
56
+ }
57
+
58
+ export function switchMemTab(tab: string): void {
59
+ const settingsTab = document.getElementById('memTabSettings');
60
+ const filesTab = document.getElementById('memTabFiles');
61
+ if (settingsTab) settingsTab.style.display = tab === 'settings' ? '' : 'none';
62
+ if (filesTab) filesTab.style.display = tab === 'files' ? '' : 'none';
63
+ document.getElementById('memTabBtnSettings')?.classList.toggle('active', tab === 'settings');
64
+ document.getElementById('memTabBtnFiles')?.classList.toggle('active', tab === 'files');
65
+ }
66
+
67
+ export async function setMemEnabled(v: boolean): Promise<void> {
68
+ document.getElementById('memOn')?.classList.toggle('active', v);
69
+ document.getElementById('memOff')?.classList.toggle('active', !v);
70
+ await apiJson('/api/memory-files/settings', 'PUT', { enabled: v });
71
+ }
72
+
73
+ export async function saveMemSettings(): Promise<void> {
74
+ const flushEl = document.getElementById('memFlushEvery') as HTMLSelectElement | null;
75
+ const cliEl = document.getElementById('memCli') as HTMLSelectElement | null;
76
+ const modelEl = document.getElementById('memModel') as HTMLSelectElement | null;
77
+ const retEl = document.getElementById('memRetention') as HTMLSelectElement | null;
78
+ await apiJson('/api/memory-files/settings', 'PUT', {
79
+ flushEvery: +(flushEl?.value || 10),
80
+ cli: cliEl?.value || '',
81
+ model: modelEl?.value || '',
82
+ retentionDays: +(retEl?.value || 30),
83
+ });
84
+ const thresholdEl = document.getElementById('memThreshold');
85
+ if (thresholdEl && flushEl) thresholdEl.textContent = flushEl.value;
86
+ }
87
+
88
+ function renderMemFiles(files: MemoryFile[]): void {
89
+ const container = document.getElementById('memFilesList');
90
+ if (!container) return;
91
+ if (!files || files.length === 0) {
92
+ container.innerHTML = '<p style="color:var(--text-dim);font-size:12px;text-align:center">No memory files yet</p>';
93
+ return;
94
+ }
95
+ container.innerHTML = files.map(f => `
96
+ <div style="display:flex;align-items:center;justify-content:space-between;padding:6px 8px;border:1px solid var(--border);border-radius:4px;margin-bottom:4px;cursor:pointer"
97
+ data-mem-view="${escapeHtml(f.name)}">
98
+ <div>
99
+ <span style="font-size:12px;font-family:monospace">${escapeHtml(f.name)}</span>
100
+ <span style="font-size:10px;color:var(--accent);margin-left:6px">${f.entries} entries</span>
101
+ </div>
102
+ <button data-mem-delete="${escapeHtml(f.name)}" style="background:none;border:none;color:#f55;cursor:pointer;font-size:14px">🗑️</button>
103
+ </div>
104
+ `).join('');
105
+ }
106
+
107
+ export async function deleteMemFile(name: string): Promise<void> {
108
+ if (!confirm('Delete ' + name + '?')) return;
109
+ await apiJson('/api/memory-files/' + name, 'DELETE', {});
110
+ openMemoryModal();
111
+ }
112
+
113
+ export async function viewMemFile(name: string): Promise<void> {
114
+ const data = await api<MemoryFileContent>('/api/memory-files/' + name);
115
+ if (!data) return;
116
+ const container = document.getElementById('memFilesList');
117
+ if (!container) return;
118
+ container.innerHTML = `
119
+ <div style="margin-bottom:8px;display:flex;justify-content:space-between;align-items:center">
120
+ <span style="font-size:12px;font-weight:600">${data.name}</span>
121
+ <button data-mem-back style="background:none;border:none;color:var(--accent);cursor:pointer;font-size:11px">← back</button>
122
+ </div>
123
+ <pre style="background:var(--bg);padding:8px;border-radius:4px;font-size:11px;white-space:pre-wrap;max-height:50vh;overflow-y:auto;color:var(--text)">${escapeHtml(data.content)}</pre>
124
+ `;
125
+ }