s-app-scoder-agent 0.6.0 → 0.8.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 (49) hide show
  1. package/.scoderrc.example.json +97 -0
  2. package/LICENSE +21 -21
  3. package/README.md +600 -468
  4. package/dist/agent.js +841 -197
  5. package/dist/agents.js +70 -0
  6. package/dist/compaction.js +114 -0
  7. package/dist/config.js +178 -12
  8. package/dist/context.js +119 -14
  9. package/dist/cost.js +56 -6
  10. package/dist/guardrails.js +423 -0
  11. package/dist/index.js +129 -76
  12. package/dist/json-schema.js +58 -0
  13. package/dist/llm.js +123 -9
  14. package/dist/mcp.js +397 -70
  15. package/dist/model-catalog.js +175 -0
  16. package/dist/permissions.js +328 -35
  17. package/dist/prompt-builder.js +47 -0
  18. package/dist/prompt.js +103 -0
  19. package/dist/provider-anthropic.js +113 -35
  20. package/dist/provider-registry.js +116 -0
  21. package/dist/retry.js +3 -2
  22. package/dist/sandbox.js +96 -0
  23. package/dist/server.js +32 -2
  24. package/dist/session.js +282 -11
  25. package/dist/shell-scanner.js +112 -0
  26. package/dist/skills.js +122 -0
  27. package/dist/snapshot.js +170 -0
  28. package/dist/steering.js +35 -0
  29. package/dist/tools.js +513 -19
  30. package/dist/tui/app.js +505 -198
  31. package/dist/tui/at-complete.js +93 -0
  32. package/dist/tui/chat-views.js +146 -0
  33. package/dist/tui/chrome.js +96 -0
  34. package/dist/tui/command-registry.js +189 -0
  35. package/dist/tui/commands.js +200 -2
  36. package/dist/tui/image.js +149 -0
  37. package/dist/tui/input.js +214 -0
  38. package/dist/tui/markdown.js +229 -0
  39. package/dist/tui/mouse.js +20 -0
  40. package/dist/tui/permission-modal.js +34 -0
  41. package/dist/tui/review.js +64 -0
  42. package/dist/tui/session-picker.js +68 -0
  43. package/dist/tui/sidebar.js +99 -0
  44. package/dist/tui/state.js +192 -6
  45. package/dist/tui/theme.js +90 -0
  46. package/dist/tui/vim.js +60 -0
  47. package/dist/watch.js +52 -0
  48. package/package.json +16 -8
  49. package/dist/tui-direct.mjs +0 -4
package/dist/tui/state.js CHANGED
@@ -9,13 +9,87 @@ export function initialState() {
9
9
  workdir: "",
10
10
  buildCmd: "",
11
11
  buildLamp: "none",
12
+ permission: "",
13
+ startedAt: null,
14
+ endedAt: null,
12
15
  usage: { calls: 0, inputTokens: 0, outputTokens: 0, cachedTokens: 0 },
13
16
  summary: null,
17
+ guardrailCount: 0,
18
+ lastGuardrail: null,
19
+ thinkingVisible: false,
20
+ phases: [],
21
+ mcpServers: {},
22
+ streamingOutputChars: 0,
14
23
  };
15
24
  }
16
25
  const MAX_ITEMS = 400;
17
26
  const MAX_OUTPUT_CHARS = 20_000;
18
27
  const MAX_TEXT_CHARS = 40_000;
28
+ /**
29
+ * T4: ước lượng chiều cao (rows) của một ChatItem khi render — dùng cho
30
+ * virtualization (chỉ render window quanh viewport thay vì cap cứng).
31
+ * Over-estimate nhẹ an toàn hơn under-estimate (viewport luôn đủ phủ).
32
+ * ponytail: heuristic theo chars, không đo Ink thật — nâng cấp thành đo
33
+ * height thật (useBoxMetrics per item) khi heuristic lệch đáng kể.
34
+ */
35
+ export function estimateItemRows(item) {
36
+ switch (item.kind) {
37
+ case "user":
38
+ return 2 + Math.ceil(item.text.length / 80);
39
+ case "assistant":
40
+ case "thinking":
41
+ return 1 + Math.ceil((item.text?.length ?? 0) / 80) + (item.text ? item.text.split("\n").length : 0);
42
+ case "tool":
43
+ return 1 + 2; // title + preview 2 lines
44
+ case "diff":
45
+ return 3 + Math.min(24, (item.text?.split("\n").length ?? 0)); // header + ≤24 lines + border
46
+ case "todo":
47
+ return 2 + item.items.length;
48
+ case "build":
49
+ return 1;
50
+ case "notice":
51
+ return 1 + Math.ceil(item.text.length / 2000);
52
+ }
53
+ }
54
+ /**
55
+ * T4: chọn window item để render: lấy từ cuối lên tới khi tổng rows ước
56
+ * lượng ≥ viewportRows + margin (render dư phía trên để scroll mượt), nhưng
57
+ * giữ toàn bộ khi tổng nhỏ. Trả về slice của items (không mutate).
58
+ */
59
+ export function visibleWindow(items, viewportRows, margin = 50) {
60
+ const budget = viewportRows + margin;
61
+ let acc = 0;
62
+ let start = items.length;
63
+ while (start > 0 && acc < budget) {
64
+ start--;
65
+ acc += estimateItemRows(items[start]);
66
+ }
67
+ return start === 0 ? items : items.slice(start);
68
+ }
69
+ /**
70
+ * Plan mới nhất cho sidebar — todo items được append mỗi lần update nhưng
71
+ * sidebar chỉ cần snapshot hiện tại (last-wins), null khi chưa có plan.
72
+ *
73
+ * Reset realtime: mỗi lần user gửi message mới, plan reset — chỉ hiện plan
74
+ * được viết SAU user message cuối cùng. Tránh stale plan từ task trước hiện
75
+ * trong task mới.
76
+ */
77
+ export function latestPlan(items) {
78
+ // Tìm user message cuối cùng — plan chỉ reset tại đây.
79
+ let lastUserIdx = -1;
80
+ for (let i = items.length - 1; i >= 0; i--) {
81
+ if (items[i].kind === "user") {
82
+ lastUserIdx = i;
83
+ break;
84
+ }
85
+ }
86
+ // Chỉ tìm todo SAU user cuối.
87
+ for (let i = items.length - 1; i > lastUserIdx; i--) {
88
+ if (items[i].kind === "todo")
89
+ return items[i];
90
+ }
91
+ return null;
92
+ }
19
93
  function appendItem(state, item) {
20
94
  state.items.push(item);
21
95
  if (state.items.length > MAX_ITEMS)
@@ -30,22 +104,69 @@ function capTail(text, max = MAX_OUTPUT_CHARS) {
30
104
  return text;
31
105
  return `…[truncated ${text.length - max} chars]\n${text.slice(text.length - max)}`;
32
106
  }
107
+ /**
108
+ * Map tên tool → phase của breadcrumb (plan→explore→edit→build→verify).
109
+ * Trả về null với tool không thuộc phase nào (mcp__*...).
110
+ */
111
+ export function toolPhase(name) {
112
+ if (name === "write_todos")
113
+ return "plan";
114
+ if (name === "list_files" || name === "read_file" || name === "web_fetch" || name === "load_skill")
115
+ return "explore";
116
+ if (name === "write_file" || name === "edit_file")
117
+ return "edit";
118
+ if (name === "run_command")
119
+ return "build";
120
+ return null;
121
+ }
122
+ /** Guardrail event: đếm + giữ decision gần nhất (ngắn, tự giải thích cho header). */
123
+ function applyGuardrail(state, ev) {
124
+ // history-trim là routine context management — đếm làm gì, hiện làm gì:
125
+ // header chỉ hiển thị những can thiệp THẬT sự đáng chú ý (inject/finish/verify).
126
+ if (ev.action === "trim")
127
+ return;
128
+ state.guardrailCount += 1;
129
+ // Thông điệp thân thiện theo rule — không dump raw "rule · reason" lên UI.
130
+ const HUMAN = {
131
+ "wrap-up": "sắp hết bước — yêu cầu agent chốt việc và chạy build",
132
+ "chat-done": "lượt chat thuần — kết thúc không cần build",
133
+ "completion-claim": "agent báo hoàn thành — buộc chạy build xác minh",
134
+ "text-only-streak": "agent chỉ nói không làm — dừng run",
135
+ "continue-nudge": "trả lời text chưa dùng tool — nhắc tiếp tục",
136
+ "doom-loop": "lặp lẫn lộn cùng tool call — yêu cầu đổi hướng",
137
+ "build-stop": "build pass — kết thúc run",
138
+ };
139
+ state.lastGuardrail = HUMAN[ev.rule] ?? `${ev.rule}: ${ev.reason}`;
140
+ }
33
141
  /**
34
142
  * Fold one AgentEvent into the state (mutates `state`, returns it for
35
143
  * reducer-style chaining). Unknown events are ignored.
36
144
  */
37
145
  export function applyEvent(state, ev) {
38
146
  switch (ev.type) {
147
+ case "permission_request": {
148
+ // T-03: surfaced via a dedicated React state hook in TuiApp (not the
149
+ // chat items — the modal is modal, not transcript).
150
+ break;
151
+ }
39
152
  case "run_start": {
40
153
  state.model = ev.model;
41
154
  state.provider = ev.provider;
42
155
  state.workdir = ev.workdir;
43
156
  state.buildCmd = ev.buildCmd;
44
157
  state.buildLamp = "none";
158
+ state.permission = ev.permission;
159
+ state.startedAt = Date.now();
160
+ state.endedAt = null;
45
161
  state.status = "running";
46
162
  state.step = 0;
47
163
  state.summary = null;
48
164
  state.usage = { calls: 0, inputTokens: 0, outputTokens: 0, cachedTokens: 0 };
165
+ // Reset tracking của run mới.
166
+ state.guardrailCount = 0;
167
+ state.lastGuardrail = null;
168
+ state.phases = [];
169
+ state.streamingOutputChars = 0;
49
170
  // The TUI pushes the user's message itself when submitting; avoid a
50
171
  // duplicate when run_start carries the same task.
51
172
  const last = lastItem(state);
@@ -54,6 +175,22 @@ export function applyEvent(state, ev) {
54
175
  }
55
176
  break;
56
177
  }
178
+ case "llm_reasoning": {
179
+ // T-10: thinking stream — chỉ append khi thinkingVisible; vẫn giữ trong
180
+ // reasoning buffer riêng để toggle bật lại thấy (đơn giản: drop khi off).
181
+ if (!state.thinkingVisible)
182
+ break;
183
+ const last = lastItem(state);
184
+ if (last && last.kind === "thinking" && last.streaming) {
185
+ last.text += ev.delta;
186
+ if (last.text.length > MAX_TEXT_CHARS)
187
+ last.text = last.text.slice(-MAX_TEXT_CHARS);
188
+ }
189
+ else {
190
+ appendItem(state, { kind: "thinking", text: ev.delta, streaming: true });
191
+ }
192
+ break;
193
+ }
57
194
  case "step": {
58
195
  state.step = ev.step;
59
196
  state.maxSteps = ev.maxSteps;
@@ -61,9 +198,12 @@ export function applyEvent(state, ev) {
61
198
  const last = lastItem(state);
62
199
  if (last && last.kind === "assistant" && last.streaming)
63
200
  last.streaming = false;
201
+ if (last && last.kind === "thinking" && last.streaming)
202
+ last.streaming = false;
64
203
  break;
65
204
  }
66
205
  case "llm_delta": {
206
+ state.streamingOutputChars += ev.delta.length;
67
207
  const last = lastItem(state);
68
208
  if (last && last.kind === "assistant" && last.streaming) {
69
209
  last.text += ev.delta;
@@ -89,7 +229,20 @@ export function applyEvent(state, ev) {
89
229
  break;
90
230
  }
91
231
  case "tool_call": {
92
- appendItem(state, { kind: "tool", name: ev.name, args: ev.args, ok: null, output: "", step: ev.step });
232
+ appendItem(state, {
233
+ kind: "tool",
234
+ name: ev.name,
235
+ args: ev.args,
236
+ callId: ev.id,
237
+ ok: null,
238
+ output: "",
239
+ step: ev.step,
240
+ startedAt: Date.now(),
241
+ });
242
+ // Phase tracking cho breadcrumb (không trùng phase liền trước).
243
+ const phase = toolPhase(ev.name);
244
+ if (phase && state.phases[state.phases.length - 1] !== phase)
245
+ state.phases.push(phase);
93
246
  break;
94
247
  }
95
248
  case "tool_output": {
@@ -109,13 +262,20 @@ export function applyEvent(state, ev) {
109
262
  break;
110
263
  }
111
264
  case "tool_result": {
265
+ // T6: correlate theo callId (parallel calls cùng tên vẫn đúng card);
266
+ // fallback theo name cho event cũ không id.
112
267
  for (let i = state.items.length - 1; i >= 0; i--) {
113
268
  const it = state.items[i];
114
- if (it.kind === "tool" && it.name === ev.name && it.ok === null) {
269
+ const match = it.kind === "tool" && it.ok === null && (ev.id ? it.callId === ev.id : it.name === ev.name);
270
+ if (match) {
115
271
  it.ok = ev.ok;
272
+ if (it.startedAt != null)
273
+ it.durationMs = Date.now() - it.startedAt;
116
274
  // Prefer the final (complete) result over accumulated live chunks.
117
275
  if (ev.output.length >= it.output.length)
118
276
  it.output = ev.output;
277
+ if (ev.image)
278
+ it.image = ev.image;
119
279
  break;
120
280
  }
121
281
  }
@@ -136,16 +296,41 @@ export function applyEvent(state, ev) {
136
296
  }
137
297
  case "build": {
138
298
  state.buildLamp = ev.success ? "ok" : "fail";
299
+ if (state.phases[state.phases.length - 1] !== "verify")
300
+ state.phases.push("verify");
139
301
  appendItem(state, { kind: "build", success: ev.success, command: ev.command });
140
302
  break;
141
303
  }
304
+ case "todo": {
305
+ // Plan updates render as a live checkbox list in the chat pane.
306
+ appendItem(state, { kind: "todo", items: ev.items.map((t) => ({ ...t })) });
307
+ break;
308
+ }
309
+ case "guardrail": {
310
+ applyGuardrail(state, ev);
311
+ break;
312
+ }
313
+ case "mcp_status": {
314
+ // Sidebar: structured event (server+state) → cập nhật map, không spam chat.
315
+ // onStatus riêng vẫn đẩy notice text đầy đủ cho transcript.
316
+ if (ev.server && ev.state) {
317
+ state.mcpServers = { ...state.mcpServers, [ev.server]: ev.state };
318
+ break;
319
+ }
320
+ appendItem(state, { kind: "notice", text: ev.line, level: ev.line.includes("unavailable") ? "warn" : "info" });
321
+ break;
322
+ }
142
323
  case "finish": {
143
324
  state.status = ev.success ? "success" : /interrupted|aborted/i.test(ev.summary) ? "aborted" : "failed";
144
325
  state.summary = ev.summary;
145
- // Close any dangling streaming assistant message.
146
- const last = lastItem(state);
147
- if (last && last.kind === "assistant")
148
- last.streaming = false;
326
+ state.endedAt = Date.now();
327
+ // Close ALL dangling streaming assistant messages — not just the last
328
+ // item: a notice/tool item appended after the assistant text would
329
+ // otherwise leave it stuck in raw streaming mode (invisible text).
330
+ for (const it of state.items) {
331
+ if (it.kind === "assistant" && it.streaming)
332
+ it.streaming = false;
333
+ }
149
334
  break;
150
335
  }
151
336
  case "error": {
@@ -159,6 +344,7 @@ export function applyEvent(state, ev) {
159
344
  export function markAborted(state) {
160
345
  state.status = "aborted";
161
346
  state.summary = "abort requested…";
347
+ state.endedAt = Date.now();
162
348
  const last = lastItem(state);
163
349
  if (last && last.kind === "assistant")
164
350
  last.streaming = false;
@@ -0,0 +1,90 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Text } from "ink";
3
+ import { readFileSync, existsSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ const MISSION = {
6
+ brand: "magenta", accent: "cyan", success: "green", error: "red", warn: "yellow",
7
+ user: "blue", dim: "gray", border: "gray",
8
+ mermaid: "magentaBright", thinking: "gray", code: "green", table: "cyan",
9
+ };
10
+ const PAPER = {
11
+ brand: "blue", accent: "blue", success: "green", error: "red", warn: "yellow",
12
+ user: "black", dim: "gray", border: "gray",
13
+ mermaid: "magentaBright", thinking: "gray", code: "green", table: "cyan",
14
+ };
15
+ const MONO = {
16
+ brand: "white", accent: "white", success: "white", error: "white", warn: "white",
17
+ user: "white", dim: "gray", border: "gray",
18
+ mermaid: "whiteBright", thinking: "white", code: "whiteBright", table: "whiteBright",
19
+ };
20
+ const LIGHT = {
21
+ brand: "magentaBright", accent: "cyanBright", success: "greenBright", error: "redBright",
22
+ warn: "yellowBright", user: "blueBright", dim: "blackBright", border: "blackBright",
23
+ mermaid: "magentaBright", thinking: "gray", code: "greenBright", table: "cyanBright",
24
+ };
25
+ const HIGH_CONTRAST = {
26
+ brand: "whiteBright", accent: "cyanBright", success: "greenBright", error: "redBright",
27
+ warn: "yellowBright", user: "whiteBright", dim: "white", border: "whiteBright",
28
+ mermaid: "magentaBright", thinking: "whiteBright", code: "greenBright", table: "cyanBright",
29
+ };
30
+ const SOLARIZED = {
31
+ brand: "magentaBright", accent: "cyanBright", success: "greenBright", error: "redBright",
32
+ warn: "yellowBright", user: "blueBright", dim: "gray", border: "gray",
33
+ mermaid: "magentaBright", thinking: "gray", code: "greenBright", table: "cyanBright",
34
+ };
35
+ export const THEMES = {
36
+ mission: MISSION, paper: PAPER, mono: MONO,
37
+ light: LIGHT, "high-contrast": HIGH_CONTRAST, solarized: SOLARIZED,
38
+ };
39
+ export function defaultThemeName(env = process.env) {
40
+ if (env.TERM === "dumb" || !env.TERM)
41
+ return "mono";
42
+ if (env.COLOR_COUNT && Number(env.COLOR_COUNT) < 16)
43
+ return "mono";
44
+ return "mission";
45
+ }
46
+ export function getTheme(workdir) {
47
+ const p = join(workdir, ".scoder", "theme.json");
48
+ if (existsSync(p)) {
49
+ try {
50
+ const raw = JSON.parse(readFileSync(p, "utf8"));
51
+ const base = raw.name ? THEMES[raw.name] ?? THEMES[defaultThemeName()] : THEMES[defaultThemeName()];
52
+ return { ...base, ...Object.fromEntries(Object.entries(raw).filter(([k]) => k in base)) };
53
+ }
54
+ catch { /* malformed → default */ }
55
+ }
56
+ return THEMES[defaultThemeName()];
57
+ }
58
+ let active = MISSION;
59
+ export function setActiveTheme(t) { active = t; }
60
+ export function useNamedTheme(name) {
61
+ if (name === "list")
62
+ return `themes: ${Object.keys(THEMES).join(", ")} — /theme <name>`;
63
+ const t = THEMES[name];
64
+ if (!t)
65
+ return `ERROR: unknown theme "${name}" — available: ${Object.keys(THEMES).join(", ")}`;
66
+ active = t;
67
+ return `theme → ${name}`;
68
+ }
69
+ export const theme = new Proxy({}, {
70
+ get(_t, prop) { return active[prop]; },
71
+ });
72
+ /** Icon per tool name — compact visual vocabulary for the flight recorder. */
73
+ const TOOL_ICONS = {
74
+ list_files: "🗂", read_file: "📖", write_file: "📝", edit_file: "✏️",
75
+ run_command: "⚡", web_fetch: "🌐", write_todos: "☑",
76
+ };
77
+ export function toolIcon(name) {
78
+ if (TOOL_ICONS[name])
79
+ return TOOL_ICONS[name];
80
+ if (name.startsWith("mcp__"))
81
+ return "🔌";
82
+ return "🔧";
83
+ }
84
+ export function fit(text, max) {
85
+ return text.length <= max ? text : text.slice(0, Math.max(0, max - 1)) + "…";
86
+ }
87
+ /** The agent's signature mark, used as an avatar on assistant output. */
88
+ export function AgentMark() {
89
+ return _jsx(Text, { color: theme.brand, bold: true, children: "\u25C6 " });
90
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * T5: vim-motion cho Input — mode normal/insert, word motions.
3
+ * Module thuần (test được); Input chỉ wire keymap.
4
+ */
5
+ /** w — đầu từ kế tiếp (vim: skip word chars rồi skip whitespace). */
6
+ export function wordForward(s, i) {
7
+ while (i < s.length && /\S/.test(s[i]))
8
+ i++;
9
+ while (i < s.length && /\s/.test(s[i]))
10
+ i++;
11
+ return Math.min(i, s.length);
12
+ }
13
+ /** b — đầu từ trước. */
14
+ export function wordBack(s, i) {
15
+ i--;
16
+ while (i > 0 && /\s/.test(s[i]))
17
+ i--;
18
+ while (i > 0 && /\S/.test(s[i - 1]))
19
+ i--;
20
+ return Math.max(0, i);
21
+ }
22
+ /** e — cuối từ hiện tại/kế tiếp. */
23
+ export function wordEnd(s, i) {
24
+ i++;
25
+ while (i < s.length - 1 && /\s/.test(s[i]))
26
+ i++;
27
+ while (i < s.length - 1 && /\S/.test(s[i + 1]))
28
+ i++;
29
+ return Math.min(i, Math.max(0, s.length - 1));
30
+ }
31
+ /** Áp dụng một phím normal-mode. Trả về unhandled=true khi không phải motion. */
32
+ export function applyNormalKey(value, cursor, input) {
33
+ const ok = (v, c, mode = "normal") => ({ value: v, cursor: c, mode, unhandled: false });
34
+ switch (input) {
35
+ case "h":
36
+ return ok(value, Math.max(0, cursor - 1));
37
+ case "l":
38
+ return ok(value, Math.min(value.length, cursor + 1));
39
+ case "w":
40
+ return ok(value, wordForward(value, cursor));
41
+ case "b":
42
+ return ok(value, wordBack(value, cursor));
43
+ case "e":
44
+ return ok(value, wordEnd(value, cursor));
45
+ case "0":
46
+ return ok(value, 0);
47
+ case "$":
48
+ return ok(value, value.length);
49
+ case "x":
50
+ return cursor < value.length ? ok(value.slice(0, cursor) + value.slice(cursor + 1), cursor) : ok(value, cursor);
51
+ case "i":
52
+ return ok(value, cursor, "insert");
53
+ case "a":
54
+ return ok(value, Math.min(value.length, cursor + 1), "insert");
55
+ case "A":
56
+ return ok(value, value.length, "insert");
57
+ default:
58
+ return { value, cursor, mode: "normal", unhandled: true };
59
+ }
60
+ }
package/dist/watch.js ADDED
@@ -0,0 +1,52 @@
1
+ /**
2
+ * M4: hot-reload — watch .scoderrc + .scoder/skills/ (fs.watch, debounce
3
+ * 500ms). onChange(kind) — Agent quyết định hành động (reload skills /
4
+ * restart MCP). Module thuần, không dependency.
5
+ */
6
+ import { watch, existsSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ const DEBOUNCE_MS = 500;
9
+ /** Watch rc file + skills dir. Fire onChange(kind) debounced (giữa các burst). */
10
+ export function watchWorkdir(workdir, onChange) {
11
+ const watchers = [];
12
+ const timers = new Map();
13
+ const fire = (kind) => {
14
+ const t = timers.get(kind);
15
+ if (t)
16
+ clearTimeout(t);
17
+ timers.set(kind, setTimeout(() => {
18
+ timers.delete(kind);
19
+ onChange(kind);
20
+ }, DEBOUNCE_MS));
21
+ };
22
+ // .scoderrc (hoặc .scoderrc.json) — config changes.
23
+ for (const name of [".scoderrc.json", ".scoderrc"]) {
24
+ const p = join(workdir, name);
25
+ if (!existsSync(p))
26
+ continue;
27
+ try {
28
+ watchers.push(watch(p, () => fire("config")));
29
+ }
30
+ catch { /* race — file removed between existsSync and watch */ }
31
+ }
32
+ // .scoder/skills/ — skill files added/removed/edited.
33
+ const skillsDir = join(workdir, ".scoder", "skills");
34
+ if (existsSync(skillsDir)) {
35
+ try {
36
+ watchers.push(watch(skillsDir, { recursive: true }, () => fire("skills")));
37
+ }
38
+ catch { /* platform không hỗ trợ recursive (hiếm trên macOS/Windows OK) */ }
39
+ }
40
+ return {
41
+ close() {
42
+ for (const w of watchers) {
43
+ try {
44
+ w.close();
45
+ }
46
+ catch { /* already closed */ }
47
+ }
48
+ for (const t of timers.values())
49
+ clearTimeout(t);
50
+ },
51
+ };
52
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "s-app-scoder-agent",
3
- "version": "0.6.0",
4
- "description": "Autonomous coding agent CLI - similar to opencode/cline/claude. Runs tasks until build succeeds.",
3
+ "version": "0.8.0",
4
+ "description": "Autonomous coding agent CLI — writes, edits, and builds code autonomously until the project compiles without errors.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "bin": {
@@ -10,17 +10,19 @@
10
10
  "scripts": {
11
11
  "build": "tsc",
12
12
  "prepare": "npm run build",
13
- "prepublishOnly": "npm run lint && npm run typecheck && npm test",
13
+ "prepublishOnly": "npm run lint && npm run typecheck",
14
14
  "start": "node dist/index.js",
15
15
  "dev": "tsc --watch",
16
16
  "typecheck": "tsc --noEmit",
17
17
  "lint": "eslint src/",
18
18
  "coverage": "npm run build && node --test --experimental-test-coverage test/*.test.mjs",
19
+ "eval": "node test/run-eval.mjs",
19
20
  "test": "npm run build && node --test test/*.test.mjs && node test/run-e2e.mjs && node test/run-mcp-test.mjs"
20
21
  },
21
22
  "engines": {
22
- "node": ">=18"
23
+ "node": ">=20"
23
24
  },
25
+ "author": "nbhson",
24
26
  "keywords": [
25
27
  "cli",
26
28
  "agent",
@@ -41,18 +43,24 @@
41
43
  "files": [
42
44
  "dist",
43
45
  "README.md",
44
- "LICENSE"
46
+ "LICENSE",
47
+ ".scoderrc.example.json"
45
48
  ],
46
49
  "devDependencies": {
47
50
  "@eslint/js": "^10.0.1",
48
51
  "@types/node": "^22.0.0",
49
- "@types/react": "^18.3.31",
52
+ "@types/react": "^19.3.0",
50
53
  "eslint": "^10.13.0",
51
54
  "typescript": "^5.6.0",
52
55
  "typescript-eslint": "^8.72.1"
53
56
  },
54
57
  "dependencies": {
55
- "ink": "^5.2.1",
56
- "react": "^18.3.1"
58
+ "fuzzysort": "^3.1.0",
59
+ "gpt-tokenizer": "^4.1.0",
60
+ "ink": "^8.0.0",
61
+ "react": "^19.3.1",
62
+ "react-dom": "^19.3.1",
63
+ "tree-sitter": "^0.25.1",
64
+ "tree-sitter-bash": "^0.25.1"
57
65
  }
58
66
  }
@@ -1,4 +0,0 @@
1
- import { runTui } from "./tui/app.js";
2
- console.log("calling runTui");
3
- await runTui({ workdir: process.cwd(), model: "m", provider: "openai", apiKey: "k", permissionMode: "auto", baseUrl: "http://127.0.0.1:1", maxSteps: 5, stream: false }, null);
4
- console.log("returned");