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.
- 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
|
@@ -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 = {};
|
|
6
|
-
let fallbackDict = {};
|
|
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
|
+
}
|