s-app-scoder-agent 0.6.0 → 0.7.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 +599 -468
  4. package/dist/agent.js +825 -175
  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 +409 -0
  11. package/dist/index.js +121 -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 +97 -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 +28 -1
  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 +498 -198
  31. package/dist/tui/at-complete.js +93 -0
  32. package/dist/tui/chat-views.js +145 -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 +101 -0
  44. package/dist/tui/state.js +182 -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,75 @@ 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
+ export function latestPlan(items) {
74
+ for (let i = items.length - 1; i >= 0; i--) {
75
+ const it = items[i];
76
+ if (it.kind === "todo")
77
+ return it;
78
+ }
79
+ return null;
80
+ }
19
81
  function appendItem(state, item) {
20
82
  state.items.push(item);
21
83
  if (state.items.length > MAX_ITEMS)
@@ -30,22 +92,71 @@ function capTail(text, max = MAX_OUTPUT_CHARS) {
30
92
  return text;
31
93
  return `…[truncated ${text.length - max} chars]\n${text.slice(text.length - max)}`;
32
94
  }
95
+ /**
96
+ * Map tên tool → phase của breadcrumb (plan→explore→edit→build→verify).
97
+ * Trả về null với tool không thuộc phase nào (mcp__*...).
98
+ */
99
+ export function toolPhase(name) {
100
+ if (name === "write_todos")
101
+ return "plan";
102
+ if (name === "list_files" || name === "read_file" || name === "web_fetch" || name === "load_skill")
103
+ return "explore";
104
+ if (name === "write_file" || name === "edit_file")
105
+ return "edit";
106
+ if (name === "run_command")
107
+ return "build";
108
+ return null;
109
+ }
110
+ /** Guardrail event: đếm + giữ decision gần nhất (ngắn, tự giải thích cho header). */
111
+ function applyGuardrail(state, ev) {
112
+ // history-trim là routine context management — đếm làm gì, hiện làm gì:
113
+ // header chỉ hiển thị những can thiệp THẬT sự đáng chú ý (inject/finish/verify).
114
+ if (ev.action === "trim")
115
+ return;
116
+ state.guardrailCount += 1;
117
+ // Thông điệp thân thiện theo rule — không dump raw "rule · reason" lên UI.
118
+ const HUMAN = {
119
+ "wrap-up": "sắp hết bước — yêu cầu agent chốt việc và chạy build",
120
+ "chat-done": "lượt chat thuần — kết thúc không cần build",
121
+ "completion-claim": "agent báo hoàn thành — buộc chạy build xác minh",
122
+ "text-only-streak": "agent chỉ nói không làm — dừng run",
123
+ "continue-nudge": "agent trả lời text mà không dùng tool — nhắc tiếp tục làm việc",
124
+ "doom-loop": "lặp lẫn lộn cùng tool call — yêu cầu đổi hướng",
125
+ "build-stop": "build pass — kết thúc run",
126
+ };
127
+ state.lastGuardrail = HUMAN[ev.rule] ?? `${ev.rule}: ${ev.reason}`;
128
+ if (state.lastGuardrail.length > 72)
129
+ state.lastGuardrail = state.lastGuardrail.slice(0, 69) + "…";
130
+ }
33
131
  /**
34
132
  * Fold one AgentEvent into the state (mutates `state`, returns it for
35
133
  * reducer-style chaining). Unknown events are ignored.
36
134
  */
37
135
  export function applyEvent(state, ev) {
38
136
  switch (ev.type) {
137
+ case "permission_request": {
138
+ // T-03: surfaced via a dedicated React state hook in TuiApp (not the
139
+ // chat items — the modal is modal, not transcript).
140
+ break;
141
+ }
39
142
  case "run_start": {
40
143
  state.model = ev.model;
41
144
  state.provider = ev.provider;
42
145
  state.workdir = ev.workdir;
43
146
  state.buildCmd = ev.buildCmd;
44
147
  state.buildLamp = "none";
148
+ state.permission = ev.permission;
149
+ state.startedAt = Date.now();
150
+ state.endedAt = null;
45
151
  state.status = "running";
46
152
  state.step = 0;
47
153
  state.summary = null;
48
154
  state.usage = { calls: 0, inputTokens: 0, outputTokens: 0, cachedTokens: 0 };
155
+ // Reset tracking của run mới.
156
+ state.guardrailCount = 0;
157
+ state.lastGuardrail = null;
158
+ state.phases = [];
159
+ state.streamingOutputChars = 0;
49
160
  // The TUI pushes the user's message itself when submitting; avoid a
50
161
  // duplicate when run_start carries the same task.
51
162
  const last = lastItem(state);
@@ -54,6 +165,22 @@ export function applyEvent(state, ev) {
54
165
  }
55
166
  break;
56
167
  }
168
+ case "llm_reasoning": {
169
+ // T-10: thinking stream — chỉ append khi thinkingVisible; vẫn giữ trong
170
+ // reasoning buffer riêng để toggle bật lại thấy (đơn giản: drop khi off).
171
+ if (!state.thinkingVisible)
172
+ break;
173
+ const last = lastItem(state);
174
+ if (last && last.kind === "thinking" && last.streaming) {
175
+ last.text += ev.delta;
176
+ if (last.text.length > MAX_TEXT_CHARS)
177
+ last.text = last.text.slice(-MAX_TEXT_CHARS);
178
+ }
179
+ else {
180
+ appendItem(state, { kind: "thinking", text: ev.delta, streaming: true });
181
+ }
182
+ break;
183
+ }
57
184
  case "step": {
58
185
  state.step = ev.step;
59
186
  state.maxSteps = ev.maxSteps;
@@ -61,9 +188,12 @@ export function applyEvent(state, ev) {
61
188
  const last = lastItem(state);
62
189
  if (last && last.kind === "assistant" && last.streaming)
63
190
  last.streaming = false;
191
+ if (last && last.kind === "thinking" && last.streaming)
192
+ last.streaming = false;
64
193
  break;
65
194
  }
66
195
  case "llm_delta": {
196
+ state.streamingOutputChars += ev.delta.length;
67
197
  const last = lastItem(state);
68
198
  if (last && last.kind === "assistant" && last.streaming) {
69
199
  last.text += ev.delta;
@@ -89,7 +219,20 @@ export function applyEvent(state, ev) {
89
219
  break;
90
220
  }
91
221
  case "tool_call": {
92
- appendItem(state, { kind: "tool", name: ev.name, args: ev.args, ok: null, output: "", step: ev.step });
222
+ appendItem(state, {
223
+ kind: "tool",
224
+ name: ev.name,
225
+ args: ev.args,
226
+ callId: ev.id,
227
+ ok: null,
228
+ output: "",
229
+ step: ev.step,
230
+ startedAt: Date.now(),
231
+ });
232
+ // Phase tracking cho breadcrumb (không trùng phase liền trước).
233
+ const phase = toolPhase(ev.name);
234
+ if (phase && state.phases[state.phases.length - 1] !== phase)
235
+ state.phases.push(phase);
93
236
  break;
94
237
  }
95
238
  case "tool_output": {
@@ -109,13 +252,20 @@ export function applyEvent(state, ev) {
109
252
  break;
110
253
  }
111
254
  case "tool_result": {
255
+ // T6: correlate theo callId (parallel calls cùng tên vẫn đúng card);
256
+ // fallback theo name cho event cũ không id.
112
257
  for (let i = state.items.length - 1; i >= 0; i--) {
113
258
  const it = state.items[i];
114
- if (it.kind === "tool" && it.name === ev.name && it.ok === null) {
259
+ const match = it.kind === "tool" && it.ok === null && (ev.id ? it.callId === ev.id : it.name === ev.name);
260
+ if (match) {
115
261
  it.ok = ev.ok;
262
+ if (it.startedAt != null)
263
+ it.durationMs = Date.now() - it.startedAt;
116
264
  // Prefer the final (complete) result over accumulated live chunks.
117
265
  if (ev.output.length >= it.output.length)
118
266
  it.output = ev.output;
267
+ if (ev.image)
268
+ it.image = ev.image;
119
269
  break;
120
270
  }
121
271
  }
@@ -136,16 +286,41 @@ export function applyEvent(state, ev) {
136
286
  }
137
287
  case "build": {
138
288
  state.buildLamp = ev.success ? "ok" : "fail";
289
+ if (state.phases[state.phases.length - 1] !== "verify")
290
+ state.phases.push("verify");
139
291
  appendItem(state, { kind: "build", success: ev.success, command: ev.command });
140
292
  break;
141
293
  }
294
+ case "todo": {
295
+ // Plan updates render as a live checkbox list in the chat pane.
296
+ appendItem(state, { kind: "todo", items: ev.items.map((t) => ({ ...t })) });
297
+ break;
298
+ }
299
+ case "guardrail": {
300
+ applyGuardrail(state, ev);
301
+ break;
302
+ }
303
+ case "mcp_status": {
304
+ // Sidebar: structured event (server+state) → cập nhật map, không spam chat.
305
+ // onStatus riêng vẫn đẩy notice text đầy đủ cho transcript.
306
+ if (ev.server && ev.state) {
307
+ state.mcpServers = { ...state.mcpServers, [ev.server]: ev.state };
308
+ break;
309
+ }
310
+ appendItem(state, { kind: "notice", text: ev.line, level: ev.line.includes("unavailable") ? "warn" : "info" });
311
+ break;
312
+ }
142
313
  case "finish": {
143
314
  state.status = ev.success ? "success" : /interrupted|aborted/i.test(ev.summary) ? "aborted" : "failed";
144
315
  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;
316
+ state.endedAt = Date.now();
317
+ // Close ALL dangling streaming assistant messages — not just the last
318
+ // item: a notice/tool item appended after the assistant text would
319
+ // otherwise leave it stuck in raw streaming mode (invisible text).
320
+ for (const it of state.items) {
321
+ if (it.kind === "assistant" && it.streaming)
322
+ it.streaming = false;
323
+ }
149
324
  break;
150
325
  }
151
326
  case "error": {
@@ -159,6 +334,7 @@ export function applyEvent(state, ev) {
159
334
  export function markAborted(state) {
160
335
  state.status = "aborted";
161
336
  state.summary = "abort requested…";
337
+ state.endedAt = Date.now();
162
338
  const last = lastItem(state);
163
339
  if (last && last.kind === "assistant")
164
340
  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.7.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");