cli-jaw 0.1.10 → 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 (67) 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/agent/spawn.js +22 -8
  25. package/dist/src/agent/spawn.js.map +1 -1
  26. package/dist/src/cli/command-context.js +13 -3
  27. package/dist/src/cli/command-context.js.map +1 -1
  28. package/dist/src/prompt/builder.js +28 -1
  29. package/dist/src/prompt/builder.js.map +1 -1
  30. package/dist/src/telegram/bot.js +1 -1
  31. package/dist/src/telegram/bot.js.map +1 -1
  32. package/package.json +9 -5
  33. package/public/dist/bundle.js +72 -77
  34. package/public/dist/bundle.js.map +4 -4
  35. package/public/index.html +1 -3
  36. package/public/js/{api.js → api.ts} +18 -12
  37. package/public/js/{constants.js → constants.ts} +44 -24
  38. package/public/js/features/{appname.js → appname.ts} +13 -12
  39. package/public/js/features/{chat.js → chat.ts} +46 -37
  40. package/public/js/features/{employees.js → employees.ts} +67 -38
  41. package/public/js/features/heartbeat.ts +90 -0
  42. package/public/js/features/{i18n.js → i18n.ts} +20 -20
  43. package/public/js/features/memory.ts +125 -0
  44. package/public/js/features/{settings.js → settings.ts} +125 -93
  45. package/public/js/features/{sidebar.js → sidebar.ts} +15 -16
  46. package/public/js/features/{skills.js → skills.ts} +29 -16
  47. package/public/js/features/{slash-commands.js → slash-commands.ts} +34 -29
  48. package/public/js/features/{theme.js → theme.ts} +4 -4
  49. package/public/js/{locale.js → locale.ts} +3 -3
  50. package/public/js/main.ts +280 -0
  51. package/public/js/{render.js → render.ts} +34 -107
  52. package/public/js/state.ts +38 -0
  53. package/public/js/{ui.js → ui.ts} +60 -63
  54. package/public/js/{ws.js → ws.ts} +46 -20
  55. package/public/locales/en.json +1 -0
  56. package/public/locales/ko.json +1 -0
  57. package/scripts/check-copilot-gap.ts +75 -0
  58. package/scripts/check-deps-offline.ts +98 -0
  59. package/scripts/fresh-install-smoke.ts +130 -0
  60. package/scripts/i18n-registry.ts +230 -0
  61. package/scripts/postinstall-guard.cjs +5 -0
  62. package/dist/bin/cli-claw.js +0 -96
  63. package/dist/bin/cli-claw.js.map +0 -1
  64. package/public/js/features/heartbeat.js +0 -80
  65. package/public/js/features/memory.js +0 -85
  66. package/public/js/main.js +0 -278
  67. package/public/js/state.js +0 -16
package/public/index.html CHANGED
@@ -25,9 +25,7 @@
25
25
  <script defer src="https://cdn.jsdelivr.net/npm/katex@0.16/dist/katex.min.js"></script>
26
26
  <script defer src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
27
27
  <script defer src="https://cdn.jsdelivr.net/npm/dompurify@3/dist/purify.min.js"></script>
28
- <link rel="modulepreload" href="/js/state.js">
29
- <link rel="modulepreload" href="/js/ws.js">
30
- <link rel="modulepreload" href="/js/ui.js">
28
+ <!-- modulepreload removed: esbuild bundles all modules into dist/bundle.js -->
31
29
  </head>
32
30
 
33
31
  <body>
@@ -1,12 +1,18 @@
1
1
  // ── API Fetch Wrapper ──
2
2
  // All API calls centralized for error handling + ok/data unwrapping
3
3
 
4
+ interface ApiResponse<T = unknown> {
5
+ ok?: boolean;
6
+ data?: T;
7
+ error?: string;
8
+ }
9
+
4
10
  /**
5
- * @param {string} path - API path (e.g. '/api/settings')
6
- * @param {RequestInit} opts - fetch options
7
- * @returns {Promise<any|null>} - data on success, null on failure
11
+ * @param path - API path (e.g. '/api/settings')
12
+ * @param opts - fetch options
13
+ * @returns data on success, null on failure
8
14
  */
9
- export async function api(path, opts = {}) {
15
+ export async function api<T = unknown>(path: string, opts: RequestInit = {}): Promise<T | null> {
10
16
  try {
11
17
  const res = await fetch(path, opts);
12
18
  if (!res.ok) {
@@ -15,18 +21,18 @@ export async function api(path, opts = {}) {
15
21
  }
16
22
  const contentType = res.headers.get('content-type') || '';
17
23
  if (!contentType.includes('json')) return null;
18
- const json = await res.json();
24
+ const json = (await res.json()) as ApiResponse<T>;
19
25
  // Phase 9.2 dual-response compat: { ok, data } or bare response
20
26
  if (json && typeof json === 'object' && 'ok' in json && 'data' in json) {
21
27
  if (!json.ok) {
22
28
  console.warn(`[api] ${path} → ok:false`, json.error || '');
23
29
  return null;
24
30
  }
25
- return json.data;
31
+ return json.data as T;
26
32
  }
27
- return json;
33
+ return json as unknown as T;
28
34
  } catch (e) {
29
- console.warn(`[api] ${path} failed:`, e.message);
35
+ console.warn(`[api] ${path} failed:`, (e as Error).message);
30
36
  return null;
31
37
  }
32
38
  }
@@ -34,8 +40,8 @@ export async function api(path, opts = {}) {
34
40
  /**
35
41
  * POST/PUT/DELETE JSON request
36
42
  */
37
- export async function apiJson(path, method, body) {
38
- return api(path, {
43
+ export async function apiJson<T = unknown>(path: string, method: string, body: unknown): Promise<T | null> {
44
+ return api<T>(path, {
39
45
  method,
40
46
  headers: { 'Content-Type': 'application/json' },
41
47
  body: JSON.stringify(body),
@@ -45,8 +51,8 @@ export async function apiJson(path, method, body) {
45
51
  /**
46
52
  * fire-and-forget: ignores result
47
53
  */
48
- export function apiFire(path, method = 'POST', body) {
49
- const opts = { method };
54
+ export function apiFire(path: string, method: string = 'POST', body?: unknown): void {
55
+ const opts: RequestInit = { method };
50
56
  if (body) {
51
57
  opts.headers = { 'Content-Type': 'application/json' };
52
58
  opts.body = JSON.stringify(body);
@@ -1,7 +1,16 @@
1
1
  // ── Shared constants (frontend) ──
2
2
  import { api } from './api.js';
3
3
 
4
- const FALLBACK_CLI_REGISTRY = {
4
+ export interface CliEntry {
5
+ label: string;
6
+ efforts: string[];
7
+ models: string[];
8
+ effortNote?: string;
9
+ }
10
+
11
+ export type CliRegistry = Record<string, CliEntry>;
12
+
13
+ const FALLBACK_CLI_REGISTRY: CliRegistry = {
5
14
  claude: {
6
15
  label: 'Claude',
7
16
  efforts: ['low', 'medium', 'high'],
@@ -53,36 +62,39 @@ const FALLBACK_CLI_REGISTRY = {
53
62
  },
54
63
  };
55
64
 
56
- function toModelMap(registry) {
57
- const out = {};
58
- for (const [key, value] of Object.entries(registry || {})) {
65
+ type ModelMap = Record<string, string[]>;
66
+
67
+ function toModelMap(registry: CliRegistry): ModelMap {
68
+ const out: ModelMap = {};
69
+ for (const [key, value] of Object.entries(registry)) {
59
70
  out[key] = Array.isArray(value?.models) ? [...value.models] : [];
60
71
  }
61
72
  return out;
62
73
  }
63
74
 
64
- function normalizeRegistry(input) {
65
- const out = {};
75
+ function normalizeRegistry(input: Record<string, unknown>): CliRegistry {
76
+ const out: CliRegistry = {};
66
77
  for (const [key, value] of Object.entries(input || {})) {
67
78
  if (!value || typeof value !== 'object') continue;
68
- const normalized = {
69
- label: value.label || key,
70
- efforts: Array.isArray(value.efforts) ? [...value.efforts] : [],
71
- models: Array.isArray(value.models) ? [...value.models] : [],
79
+ const v = value as Record<string, unknown>;
80
+ const normalized: CliEntry = {
81
+ label: (v.label as string) || key,
82
+ efforts: Array.isArray(v.efforts) ? [...v.efforts] as string[] : [],
83
+ models: Array.isArray(v.models) ? [...v.models] as string[] : [],
72
84
  };
73
- if (typeof value.effortNote === 'string' && value.effortNote.trim()) {
74
- normalized.effortNote = value.effortNote;
85
+ if (typeof v.effortNote === 'string' && v.effortNote.trim()) {
86
+ normalized.effortNote = v.effortNote;
75
87
  }
76
88
  out[key] = normalized;
77
89
  }
78
90
  return out;
79
91
  }
80
92
 
81
- export let CLI_REGISTRY = normalizeRegistry(FALLBACK_CLI_REGISTRY);
82
- export let CLI_KEYS = Object.keys(CLI_REGISTRY);
83
- export let MODEL_MAP = toModelMap(CLI_REGISTRY);
93
+ export let CLI_REGISTRY: CliRegistry = normalizeRegistry(FALLBACK_CLI_REGISTRY as unknown as Record<string, unknown>);
94
+ export let CLI_KEYS: string[] = Object.keys(CLI_REGISTRY);
95
+ export let MODEL_MAP: ModelMap = toModelMap(CLI_REGISTRY);
84
96
 
85
- function applyRegistry(registry) {
97
+ function applyRegistry(registry: Record<string, unknown>): boolean {
86
98
  const normalized = normalizeRegistry(registry);
87
99
  if (!Object.keys(normalized).length) return false;
88
100
  CLI_REGISTRY = normalized;
@@ -91,29 +103,37 @@ function applyRegistry(registry) {
91
103
  return true;
92
104
  }
93
105
 
94
- export async function loadCliRegistry() {
106
+ export async function loadCliRegistry(): Promise<CliRegistry> {
95
107
  try {
96
- const data = await api('/api/cli-registry');
108
+ const data = await api<Record<string, unknown>>('/api/cli-registry');
97
109
  if (!data || !applyRegistry(data)) throw new Error('invalid registry');
98
110
  } catch (e) {
99
- console.warn('[cli-registry] fallback:', e.message);
100
- applyRegistry(FALLBACK_CLI_REGISTRY);
111
+ console.warn('[cli-registry] fallback:', (e as Error).message);
112
+ applyRegistry(FALLBACK_CLI_REGISTRY as unknown as Record<string, unknown>);
101
113
  }
102
114
  return CLI_REGISTRY;
103
115
  }
104
116
 
105
- export function getCliKeys() {
117
+ export function getCliKeys(): string[] {
106
118
  return CLI_KEYS;
107
119
  }
108
120
 
109
- export function getCliMeta(cli) {
121
+ export function getCliMeta(cli: string): CliEntry | null {
110
122
  return CLI_REGISTRY[cli] || null;
111
123
  }
112
124
 
113
- export const ROLE_PRESETS = [
125
+ export interface RolePreset {
126
+ value: string;
127
+ labelKey: string;
128
+ label: string;
129
+ prompt: string;
130
+ skill: string | null;
131
+ }
132
+
133
+ export const ROLE_PRESETS: readonly RolePreset[] = [
114
134
  { value: 'frontend', labelKey: 'role.label.frontend', label: 'Frontend', prompt: 'UI/UX, CSS, components', skill: 'dev-frontend' },
115
135
  { value: 'backend', labelKey: 'role.label.backend', label: 'Backend', prompt: 'API, DB, server logic', skill: 'dev-backend' },
116
136
  { value: 'data', labelKey: 'role.label.data', label: 'Data', prompt: 'Data pipeline, analysis, ML', skill: 'dev-data' },
117
137
  { value: 'docs', labelKey: 'role.label.docs', label: 'Docs', prompt: 'Documentation, README, API docs', skill: 'documentation' },
118
138
  { value: 'custom', labelKey: 'role.label.custom', label: 'Custom...', prompt: '', skill: null },
119
- ];
139
+ ] as const;
@@ -5,39 +5,40 @@
5
5
  const STORAGE_KEY = 'agentName';
6
6
  const DEFAULT_NAME = 'CLI-JAW';
7
7
 
8
- let currentName = DEFAULT_NAME;
8
+ let currentName: string = DEFAULT_NAME;
9
9
 
10
- export function getAppName() {
10
+ export function getAppName(): string {
11
11
  return currentName;
12
12
  }
13
13
 
14
- export function setAppName(name) {
14
+ export function setAppName(name: string): void {
15
15
  currentName = (name || '').trim() || DEFAULT_NAME;
16
16
  localStorage.setItem(STORAGE_KEY, currentName);
17
17
  // Update input field
18
- const input = document.getElementById('appNameInput');
18
+ const input = document.getElementById('appNameInput') as HTMLInputElement | null;
19
19
  if (input) input.value = currentName;
20
20
  }
21
21
 
22
- export function initAppName() {
22
+ export function initAppName(): void {
23
23
  currentName = localStorage.getItem(STORAGE_KEY) || DEFAULT_NAME;
24
24
 
25
25
  // Sync input
26
- const input = document.getElementById('appNameInput');
26
+ const input = document.getElementById('appNameInput') as HTMLInputElement | null;
27
27
  if (input) input.value = currentName;
28
28
 
29
29
  // Save button
30
30
  document.getElementById('appNameSave')?.addEventListener('click', () => {
31
- const inp = document.getElementById('appNameInput');
31
+ const inp = document.getElementById('appNameInput') as HTMLInputElement | null;
32
32
  if (inp) setAppName(inp.value);
33
33
  });
34
34
 
35
35
  // Enter key
36
- document.getElementById('appNameInput')?.addEventListener('keydown', (e) => {
37
- if (e.key === 'Enter') {
38
- e.preventDefault();
39
- setAppName(e.target.value);
40
- e.target.blur();
36
+ document.getElementById('appNameInput')?.addEventListener('keydown', (e: Event) => {
37
+ const ke = e as KeyboardEvent;
38
+ if (ke.key === 'Enter') {
39
+ ke.preventDefault();
40
+ setAppName((ke.target as HTMLInputElement).value);
41
+ (ke.target as HTMLInputElement).blur();
41
42
  }
42
43
  });
43
44
  }
@@ -6,9 +6,13 @@ import { t } from './i18n.js';
6
6
  import * as slashCmd from './slash-commands.js';
7
7
  import { api, apiJson, apiFire } from '../api.js';
8
8
 
9
- export async function sendMessage() {
10
- const input = document.getElementById('chatInput');
9
+ interface CommandResult { code?: string; text?: string; type?: string; }
10
+ interface MessageResult { queued?: boolean; pending?: number; continued?: boolean; error?: string; }
11
+
12
+ export async function sendMessage(): Promise<void> {
13
+ const input = document.getElementById('chatInput') as HTMLTextAreaElement | null;
11
14
  const btn = document.getElementById('btnSend');
15
+ if (!input || !btn) return;
12
16
 
13
17
  // Stop mode: clicking ■ stops the agent
14
18
  if (btn.classList.contains('stop-mode') && !input.value.trim() && !state.attachedFiles.length) {
@@ -29,7 +33,7 @@ export async function sendMessage() {
29
33
  resetInputHeight();
30
34
  slashCmd.close();
31
35
  try {
32
- let signal, timer;
36
+ let signal: AbortSignal; let timer: ReturnType<typeof setTimeout> | undefined;
33
37
  if (typeof AbortSignal?.timeout === 'function') {
34
38
  signal = AbortSignal.timeout(10000);
35
39
  } else {
@@ -48,7 +52,7 @@ export async function sendMessage() {
48
52
  signal,
49
53
  });
50
54
  if (timer) clearTimeout(timer);
51
- const result = await res.json().catch(() => ({}));
55
+ const result: CommandResult = await res.json().catch(() => ({}));
52
56
  // not_command → fall through to normal chat
53
57
  if (result?.code === 'not_command') {
54
58
  addMessage('user', text);
@@ -57,30 +61,31 @@ export async function sendMessage() {
57
61
  }
58
62
  if (!res.ok && !result?.text) throw new Error(`HTTP ${res.status}`);
59
63
  if (result?.code === 'clear_screen') {
60
- document.getElementById('chatMessages').innerHTML = '';
64
+ const chatEl = document.getElementById('chatMessages');
65
+ if (chatEl) chatEl.innerHTML = '';
61
66
  }
62
67
  if (result?.text) addSystemMsg(result.text, '', result.type);
63
68
  } catch (err) {
64
- addSystemMsg(t('chat.cmd.fail', { msg: err.message }), '', 'error');
69
+ addSystemMsg(t('chat.cmd.fail', { msg: (err as Error).message }), '', 'error');
65
70
  }
66
71
  return;
67
72
  }
68
73
 
69
74
  if (state.attachedFiles.length) {
70
- const names = state.attachedFiles.map(f => f.name).join(', ');
75
+ const names = state.attachedFiles.map((f: File) => f.name).join(', ');
71
76
  const displayMsg = `[📎 ${names}] ${text}`;
72
77
  addMessage('user', displayMsg);
73
78
  input.value = '';
74
79
  resetInputHeight();
75
80
  try {
76
81
  // Upload all files in parallel
77
- const paths = await Promise.all(state.attachedFiles.map(f => uploadFile(f)));
82
+ const paths = await Promise.all(state.attachedFiles.map((f: File) => uploadFile(f)));
78
83
  let prompt = paths.map(p => t('chat.file.sent', { path: p })).join('\n');
79
84
  if (text) prompt += t('chat.file.sentWithMsg', { text });
80
85
  clearAttachedFiles();
81
86
  await apiJson('/api/message', 'POST', { prompt });
82
87
  } catch (err) {
83
- addSystemMsg(t('chat.file.uploadFail', { msg: err.message }));
88
+ addSystemMsg(t('chat.file.uploadFail', { msg: (err as Error).message }));
84
89
  clearAttachedFiles();
85
90
  }
86
91
  } else {
@@ -92,7 +97,7 @@ export async function sendMessage() {
92
97
  headers: { 'Content-Type': 'application/json' },
93
98
  body: JSON.stringify({ prompt: text }),
94
99
  });
95
- const data = await res.json().catch(() => ({}));
100
+ const data: MessageResult = await res.json().catch(() => ({}));
96
101
  if (!res.ok) {
97
102
  addSystemMsg(`❌ ${data.error || t('chat.requestFail', { status: res.status })}`, '', 'error');
98
103
  return;
@@ -106,11 +111,11 @@ export async function sendMessage() {
106
111
  }
107
112
  }
108
113
 
109
- export function handleKey(e) {
114
+ export function handleKey(e: KeyboardEvent): void {
110
115
  if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) { e.preventDefault(); sendMessage(); }
111
116
  }
112
117
 
113
- async function uploadFile(file) {
118
+ async function uploadFile(file: File): Promise<string> {
114
119
  const res = await fetch('/api/upload', {
115
120
  method: 'POST',
116
121
  headers: { 'X-Filename': encodeURIComponent(file.name) },
@@ -121,30 +126,31 @@ async function uploadFile(file) {
121
126
  return data.path;
122
127
  }
123
128
 
124
- export function attachFiles(files) {
129
+ export function attachFiles(files: File[]): void {
125
130
  for (const file of files) {
126
- // Skip duplicates by name
127
131
  if (state.attachedFiles.some(f => f.name === file.name)) continue;
128
132
  state.attachedFiles.push(file);
129
133
  }
130
134
  renderFilePreview();
131
- document.getElementById('chatInput').focus();
135
+ (document.getElementById('chatInput') as HTMLTextAreaElement | null)?.focus();
132
136
  }
133
137
 
134
- export function removeAttachedFile(index) {
138
+ export function removeAttachedFile(index: number): void {
135
139
  state.attachedFiles.splice(index, 1);
136
140
  renderFilePreview();
137
141
  }
138
142
 
139
- export function clearAttachedFiles() {
143
+ export function clearAttachedFiles(): void {
140
144
  state.attachedFiles = [];
141
145
  renderFilePreview();
142
- document.getElementById('fileInput').value = '';
146
+ const fi = document.getElementById('fileInput') as HTMLInputElement | null;
147
+ if (fi) fi.value = '';
143
148
  }
144
149
 
145
- function renderFilePreview() {
150
+ function renderFilePreview(): void {
146
151
  const preview = document.getElementById('filePreview');
147
152
  const listEl = document.getElementById('filePreviewList');
153
+ if (!preview) return;
148
154
  if (!state.attachedFiles.length) {
149
155
  preview.classList.remove('visible');
150
156
  if (listEl) listEl.innerHTML = '';
@@ -152,7 +158,7 @@ function renderFilePreview() {
152
158
  }
153
159
  preview.classList.add('visible');
154
160
  if (!listEl) return;
155
- listEl.innerHTML = state.attachedFiles.map((f, i) => {
161
+ listEl.innerHTML = state.attachedFiles.map((f: File, i: number) => {
156
162
  const size = (f.size / 1024).toFixed(1);
157
163
  const isImg = f.type.startsWith('image/');
158
164
  const thumb = isImg ? `<img src="${URL.createObjectURL(f)}" class="file-chip-thumb" alt="">` : '';
@@ -164,30 +170,32 @@ function renderFilePreview() {
164
170
  }).join('');
165
171
  }
166
172
 
167
- export async function clearChat() {
173
+ export async function clearChat(): Promise<void> {
168
174
  apiFire('/api/clear', 'POST');
169
- document.getElementById('chatMessages').innerHTML = '';
175
+ const chatEl = document.getElementById('chatMessages');
176
+ if (chatEl) chatEl.innerHTML = '';
170
177
  }
171
178
 
172
179
  // ── Auto-resize textarea ──
173
- function autoResize(el) {
180
+ function autoResize(el: HTMLTextAreaElement): void {
174
181
  el.style.height = 'auto';
175
182
  el.style.height = el.scrollHeight + 'px';
176
183
  }
177
184
 
178
- export function initAutoResize() {
179
- const el = document.getElementById('chatInput');
180
- el.addEventListener('input', () => autoResize(el));
185
+ export function initAutoResize(): void {
186
+ const el = document.getElementById('chatInput') as HTMLTextAreaElement | null;
187
+ if (el) el.addEventListener('input', () => autoResize(el));
181
188
  }
182
189
 
183
- export function resetInputHeight() {
184
- const el = document.getElementById('chatInput');
185
- el.style.height = 'auto';
190
+ export function resetInputHeight(): void {
191
+ const el = document.getElementById('chatInput') as HTMLTextAreaElement | null;
192
+ if (el) el.style.height = 'auto';
186
193
  }
187
194
 
188
- export function initDragDrop() {
195
+ export function initDragDrop(): void {
189
196
  const chatArea = document.querySelector('.chat-area');
190
197
  const overlay = document.getElementById('dragOverlay');
198
+ if (!chatArea || !overlay) return;
191
199
  let dragCounter = 0;
192
200
 
193
201
  chatArea.addEventListener('dragenter', (e) => {
@@ -205,26 +213,27 @@ export function initDragDrop() {
205
213
  e.preventDefault();
206
214
  dragCounter = 0;
207
215
  overlay.classList.remove('visible');
208
- const files = [...e.dataTransfer.files];
216
+ const de = e as DragEvent;
217
+ const files = [...(de.dataTransfer?.files || [])];
209
218
  if (files.length) attachFiles(files);
210
219
  });
211
220
 
212
- document.getElementById('fileInput').addEventListener('change', (e) => {
213
- const files = [...e.target.files];
221
+ (document.getElementById('fileInput') as HTMLInputElement | null)?.addEventListener('change', (e) => {
222
+ const target = e.target as HTMLInputElement;
223
+ const files = [...(target.files || [])];
214
224
  if (files.length) attachFiles(files);
215
- e.target.value = ''; // allow re-selecting same file
225
+ target.value = '';
216
226
  });
217
227
 
218
228
  // ── Clipboard paste (Cmd+V) ──
219
- document.addEventListener('paste', (e) => {
229
+ document.addEventListener('paste', (e: ClipboardEvent) => {
220
230
  const items = e.clipboardData?.items;
221
231
  if (!items) return;
222
- const files = [];
232
+ const files: File[] = [];
223
233
  for (const item of items) {
224
234
  if (item.kind !== 'file') continue;
225
235
  const blob = item.getAsFile();
226
236
  if (!blob) continue;
227
- // Pasted images often have no useful name; generate one
228
237
  if (!blob.name || blob.name === 'image.png') {
229
238
  const ts = new Date().toISOString().replace(/[:.]/g, '-');
230
239
  const ext = blob.type.split('/')[1] || 'png';
@@ -1,52 +1,76 @@
1
1
  // ── Employees Feature ──
2
2
  import { state } from '../state.js';
3
3
  import { MODEL_MAP, ROLE_PRESETS, getCliKeys } from '../constants.js';
4
+ import type { RolePreset } from '../constants.js';
4
5
  import { escapeHtml } from '../render.js';
5
6
  import { getAgentPhase } from '../ws.js';
6
7
  import { t } from './i18n.js';
7
8
  import { api, apiJson, apiFire } from '../api.js';
8
9
 
9
- export async function loadEmployees() {
10
- const data = await api('/api/employees');
10
+ interface Employee {
11
+ id: string;
12
+ name?: string;
13
+ cli: string;
14
+ model?: string;
15
+ role?: string;
16
+ status?: string;
17
+ phase?: string;
18
+ phaseLabel?: string;
19
+ }
20
+
21
+ const LEGACY_MAP: Record<string, string> = {
22
+ 'React/Vue 기반 UI 컴포넌트 개발, 스타일링': 'frontend',
23
+ 'API 서버, DB 스키마, 비즈니스 로직 구현': 'backend',
24
+ '프론트엔드와 백엔드 모두 담당': 'frontend',
25
+ 'CI/CD, Docker, 인프라 자동화': 'backend',
26
+ '테스트 작성, 버그 재현, 품질 관리': 'custom',
27
+ '데이터 파이프라인, ETL, 분석 쿼리': 'data',
28
+ 'API 문서화, README, 가이드 작성': 'docs',
29
+ 'UI/UX 구현, CSS, 컴포넌트 개발': 'frontend',
30
+ 'API, DB, 서버 로직 구현': 'backend',
31
+ '데이터 파이프라인, 분석, ML': 'data',
32
+ '문서화, README, API docs': 'docs',
33
+ 'UI/UX, CSS, components': 'frontend',
34
+ 'API, DB, server logic': 'backend',
35
+ 'Data pipeline, analysis, ML': 'data',
36
+ 'Documentation, README, API docs': 'docs',
37
+ };
38
+
39
+ const PHASE_COLORS: Record<string, string> = {
40
+ '1': '#60a5fa', '2': '#a78bfa', '3': '#34d399', '4': '#fbbf24', '5': '#f472b6'
41
+ };
42
+
43
+ export async function loadEmployees(): Promise<void> {
44
+ const data = await api<Employee[]>('/api/employees');
11
45
  state.employees = data || [];
12
46
  renderEmployees();
13
47
  }
14
48
 
15
- export function renderEmployees() {
49
+ export function renderEmployees(): void {
16
50
  const el = document.getElementById('employeesList');
17
- if (state.employees.length === 0) {
51
+ if (!el) return;
52
+ const employees = state.employees as Employee[];
53
+ if (employees.length === 0) {
18
54
  el.innerHTML = `<div style="color:var(--text-dim);font-size:11px;padding:4px 0">${t('emp.addPrompt')}</div>`;
19
55
  return;
20
56
  }
21
57
  const cliKeys = getCliKeys();
22
- el.innerHTML = state.employees.map(a => {
58
+ el.innerHTML = employees.map(a => {
23
59
  const models = MODEL_MAP[a.cli] || [];
24
- // Legacy role prompt → new preset migration
25
- const LEGACY_MAP = {
26
- // Legacy Korean roles (backward compat with old DB data)
27
- 'React/Vue 기반 UI 컴포넌트 개발, 스타일링': 'frontend',
28
- 'API 서버, DB 스키마, 비즈니스 로직 구현': 'backend',
29
- '프론트엔드와 백엔드 모두 담당': 'frontend',
30
- 'CI/CD, Docker, 인프라 자동화': 'backend',
31
- '테스트 작성, 버그 재현, 품질 관리': 'custom',
32
- '데이터 파이프라인, ETL, 분석 쿼리': 'data',
33
- 'API 문서화, README, 가이드 작성': 'docs',
34
- // Previous default Korean preset prompts
35
- 'UI/UX 구현, CSS, 컴포넌트 개발': 'frontend',
36
- 'API, DB, 서버 로직 구현': 'backend',
37
- '데이터 파이프라인, 분석, ML': 'data',
38
- '문서화, README, API docs': 'docs',
39
- // Phase 6.9: new English preset prompts
40
- 'UI/UX, CSS, components': 'frontend',
41
- 'API, DB, server logic': 'backend',
42
- 'Data pipeline, analysis, ML': 'data',
43
- 'Documentation, README, API docs': 'docs',
44
- };
45
- const legacyVal = LEGACY_MAP[a.role];
46
- const matched = legacyVal ? ROLE_PRESETS.find(r => r.value === legacyVal) : ROLE_PRESETS.find(r => r.prompt === a.role);
60
+ const legacyVal = LEGACY_MAP[a.role || ''];
61
+ const matched: RolePreset | undefined = legacyVal
62
+ ? ROLE_PRESETS.find(r => r.value === legacyVal)
63
+ : ROLE_PRESETS.find(r => r.prompt === a.role);
47
64
  const presetVal = matched ? matched.value : (a.role ? 'custom' : 'frontend');
48
65
  const isCustom = presetVal === 'custom';
49
66
 
67
+ const ps = getAgentPhase(a.id);
68
+ const p = ps?.phase || a.phase;
69
+ const pl = ps?.phaseLabel || a.phaseLabel;
70
+ const phaseBadge = p
71
+ ? `<span style="background:${PHASE_COLORS[p] || '#888'};color:#000;padding:1px 6px;border-radius:9px;font-size:9px">${pl || 'P' + p}</span>`
72
+ : '';
73
+
50
74
  return `
51
75
  <div class="settings-group" style="margin-bottom:8px;padding:8px 10px">
52
76
  <div style="display:flex;align-items:center;gap:6px;margin-bottom:6px">
@@ -77,38 +101,43 @@ export function renderEmployees() {
77
101
  ${ROLE_PRESETS.map(r => `<option value="${r.value}"${presetVal === r.value ? ' selected' : ''}>${r.label}</option>`).join('')}
78
102
  </select>
79
103
  <textarea data-emp-custom="${a.id}" style="display:${isCustom ? 'block' : 'none'};margin-top:4px;width:100%;height:40px;background:var(--bg);border:1px solid var(--border);color:var(--text);padding:4px 6px;border-radius:4px;font-size:10px;font-family:inherit;resize:vertical"
80
- placeholder="${t('emp.customRole')}">${isCustom ? escapeHtml(a.role) : ''}</textarea>
104
+ placeholder="${t('emp.customRole')}">${isCustom ? escapeHtml(a.role || '') : ''}</textarea>
81
105
  </div>
82
106
  <div style="margin-top:4px;font-size:10px;display:flex;align-items:center;gap:6px">
83
107
  <span style="color:${a.status === 'running' ? '#fbbf24' : 'var(--green)'}">● ${a.status || 'idle'}</span>
84
- ${(() => { const ps = getAgentPhase(a.id); const p = ps?.phase || a.phase; const pl = ps?.phaseLabel || a.phaseLabel; return p ? `<span style="background:${({ 1: '#60a5fa', 2: '#a78bfa', 3: '#34d399', 4: '#fbbf24', 5: '#f472b6' })[p] || '#888'};color:#000;padding:1px 6px;border-radius:9px;font-size:9px">${pl || 'P' + p}</span>` : ''; })()}
108
+ ${phaseBadge}
85
109
  </div>
86
110
  </div>`;
87
111
  }).join('');
88
112
  }
89
113
 
90
- export async function addEmployee() {
114
+ export async function addEmployee(): Promise<void> {
91
115
  await apiJson('/api/employees', 'POST', {});
92
116
  }
93
117
 
94
- export async function updateEmployee(id, data) {
118
+ export async function updateEmployee(id: string, data: Partial<Employee>): Promise<void> {
95
119
  await apiJson(`/api/employees/${id}`, 'PUT', data);
96
120
  }
97
121
 
98
- export async function deleteEmployee(id) {
122
+ export async function deleteEmployee(id: string): Promise<void> {
99
123
  apiFire(`/api/employees/${id}`, 'DELETE');
100
124
  }
101
125
 
102
- export function onEmpCliChange(id, cli) {
126
+ export function onEmpCliChange(id: string, cli: string): void {
103
127
  const models = MODEL_MAP[cli] || [];
104
- const sel = document.querySelector(`[data-emp-model="${id}"]`);
105
- sel.innerHTML = `<option value="default" selected>default</option>` + models.map(m => `<option>${escapeHtml(m)}</option>`).join('') + `<option value="__custom__">${t('emp.customModel')}</option>`;
128
+ const sel = document.querySelector(`[data-emp-model="${id}"]`) as HTMLSelectElement | null;
129
+ if (sel) {
130
+ sel.innerHTML = `<option value="default" selected>default</option>` +
131
+ models.map(m => `<option>${escapeHtml(m)}</option>`).join('') +
132
+ `<option value="__custom__">${t('emp.customModel')}</option>`;
133
+ }
106
134
  updateEmployee(id, { cli, model: 'default' });
107
135
  }
108
136
 
109
- export function onEmpRoleChange(id, presetVal) {
137
+ export function onEmpRoleChange(id: string, presetVal: string): void {
110
138
  const preset = ROLE_PRESETS.find(r => r.value === presetVal);
111
- const customEl = document.querySelector(`[data-emp-custom="${id}"]`);
139
+ const customEl = document.querySelector(`[data-emp-custom="${id}"]`) as HTMLTextAreaElement | null;
140
+ if (!customEl) return;
112
141
  if (presetVal === 'custom') {
113
142
  customEl.style.display = 'block';
114
143
  customEl.focus();